3
0

Attachment serving

This commit is contained in:
Denis Arh 2018-09-10 12:25:36 +02:00
parent e351c73a0a
commit b04a681d81
10 changed files with 459 additions and 33 deletions

View File

@ -552,6 +552,38 @@ The following event types may be sent with a message event:
# Attachments
## Serves attached file
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/attachment/{attachmentID}/{name}` | HTTP/S | GET | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| download | bool | GET | Force file download | N/A | NO |
## Serves preview of an attached file
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/attachment/{attachmentID}/{name}/preview` | HTTP/S | GET | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
# Users
## Search users (Directory)

View File

@ -468,6 +468,38 @@
}
]
},
{
"title": "Attachments",
"package": "sam",
"path": "/attachment/{attachmentID}",
"parameters": {
"path": [
{ "name": "attachmentID", "type": "uint64", "required": true, "title": "Attachment ID" }
]
},
"entrypoint": "attachment",
"authentication": ["Client ID", "Session ID"],
"apis": [
{
"name": "original",
"path": "/{name}",
"method": "GET",
"title": "Serves attached file",
"parameters": {
"GET": [
{ "type": "bool", "name": "download", "required": false, "title": "Force file download" }
]
}
},
{
"name": "preview",
"path": "/{name}/preview",
"method": "GET",
"title": "Serves preview of an attached file"
}
]
},
{
"title": "Users",
"package": "sam",

View File

@ -0,0 +1,47 @@
{
"Title": "Attachments",
"Package": "sam",
"Interface": "Attachment",
"Struct": null,
"Parameters": {
"path": [
{
"name": "attachmentID",
"required": true,
"title": "Attachment ID",
"type": "uint64"
}
]
},
"Protocol": "",
"Authentication": [
"Client ID",
"Session ID"
],
"Path": "/attachment/{attachmentID}",
"APIs": [
{
"Name": "original",
"Method": "GET",
"Title": "Serves attached file",
"Path": "/{name}",
"Parameters": {
"GET": [
{
"name": "download",
"required": false,
"title": "Force file download",
"type": "bool"
}
]
}
},
{
"Name": "preview",
"Method": "GET",
"Title": "Serves preview of an attached file",
"Path": "/{name}/preview",
"Parameters": null
}
]
}

72
sam/rest/attachment.go Normal file
View File

@ -0,0 +1,72 @@
package rest
import (
"context"
"github.com/crusttech/crust/sam/rest/request"
"github.com/crusttech/crust/sam/service"
"github.com/pkg/errors"
"io"
"time"
)
var _ = errors.Wrap
type (
Attachment struct {
svc service.AttachmentService
}
tempAttachmentPayload struct {
ThisIsATempSolutionForAttachmentPayload bool
Name string
ModTime time.Time
Download bool
}
)
func (Attachment) New(svc service.AttachmentService) *Attachment {
return &Attachment{svc: svc}
}
func (ctrl *Attachment) Original(ctx context.Context, r *request.AttachmentOriginal) (interface{}, error) {
rval := tempAttachmentPayload{Download: r.Download}
if att, err := ctrl.svc.FindByID(r.AttachmentID); err != nil {
return nil, err
} else {
rval.Name = att.Name
rval.ModTime = att.CreatedAt
}
return rval, nil
}
func (ctrl *Attachment) Preview(ctx context.Context, r *request.AttachmentPreview) (interface{}, error) {
return nil, errors.New("Not implemented: Attachment.preview")
}
func (ctrl Attachment) get(ID uint64, preview, download bool) (interface{}, error) {
rval := tempAttachmentPayload{Download: download}
if att, err := ctrl.svc.FindByID(ID); err != nil {
return nil, err
} else {
// @todo update this to io.ReadSeeker when store's Open() func rval is updated
var rs io.Reader
if preview {
rs, err = ctrl.svc.OpenPreview(att)
} else {
rs, err = ctrl.svc.OpenOriginal(att)
}
rval.Name = att.Name
rval.ModTime = att.CreatedAt
// @todo do something with rs :)
_ = rs
}
return rval, nil
}

View File

@ -0,0 +1,67 @@
package handlers
/*
Hello! This file is auto-generated from `docs/src/spec.json`.
For development:
In order to update the generated files, edit this file under the location,
add your struct fields, imports, API definitions and whatever you want, and:
1. run [spec](https://github.com/titpetric/spec) in the same folder,
2. run `./_gen.php` in this folder.
You may edit `attachment.go`, `attachment.util.go` or `attachment_test.go` to
implement your API calls, helper functions and tests. The file `attachment.go`
is only generated the first time, and will not be overwritten if it exists.
*/
import (
"context"
"github.com/go-chi/chi"
"net/http"
"github.com/titpetric/factory/resputil"
"github.com/crusttech/crust/sam/rest/request"
)
// Internal API interface
type AttachmentAPI interface {
Original(context.Context, *request.AttachmentOriginal) (interface{}, error)
Preview(context.Context, *request.AttachmentPreview) (interface{}, error)
}
// HTTP API interface
type Attachment struct {
Original func(http.ResponseWriter, *http.Request)
Preview func(http.ResponseWriter, *http.Request)
}
func NewAttachment(ah AttachmentAPI) *Attachment {
return &Attachment{
Original: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAttachmentOriginal()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return ah.Original(r.Context(), params)
})
},
Preview: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAttachmentPreview()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return ah.Preview(r.Context(), params)
})
},
}
}
func (ah *Attachment) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Route("/attachment/{attachmentID}", func(r chi.Router) {
r.Get("/{name}", ah.Original)
r.Get("/{name}/preview", ah.Preview)
})
})
}

View File

@ -0,0 +1,120 @@
package request
/*
Hello! This file is auto-generated from `docs/src/spec.json`.
For development:
In order to update the generated files, edit this file under the location,
add your struct fields, imports, API definitions and whatever you want, and:
1. run [spec](https://github.com/titpetric/spec) in the same folder,
2. run `./_gen.php` in this folder.
You may edit `attachment.go`, `attachment.util.go` or `attachment_test.go` to
implement your API calls, helper functions and tests. The file `attachment.go`
is only generated the first time, and will not be overwritten if it exists.
*/
import (
"encoding/json"
"github.com/go-chi/chi"
"github.com/jmoiron/sqlx/types"
"github.com/pkg/errors"
"io"
"mime/multipart"
"net/http"
"strings"
)
var _ = chi.URLParam
var _ = types.JSONText{}
var _ = multipart.FileHeader{}
// Attachment original request parameters
type AttachmentOriginal struct {
Download bool
AttachmentID uint64
}
func NewAttachmentOriginal() *AttachmentOriginal {
return &AttachmentOriginal{}
}
func (a *AttachmentOriginal) Fill(r *http.Request) error {
var err error
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(a)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
r.ParseForm()
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := get["download"]; ok {
a.Download = parseBool(val)
}
a.AttachmentID = parseUInt64(chi.URLParam(r, "attachmentID"))
return err
}
var _ RequestFiller = NewAttachmentOriginal()
// Attachment preview request parameters
type AttachmentPreview struct {
AttachmentID uint64
}
func NewAttachmentPreview() *AttachmentPreview {
return &AttachmentPreview{}
}
func (a *AttachmentPreview) Fill(r *http.Request) error {
var err error
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(a)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
r.ParseForm()
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
a.AttachmentID = parseUInt64(chi.URLParam(r, "attachmentID"))
return err
}
var _ RequestFiller = NewAttachmentPreview()

View File

@ -20,7 +20,7 @@ func MountRoutes(jwtAuth types.TokenEncoder) func(chi.Router) {
var (
channelSvc = service.Channel()
attachmentSvc = service.Attachment(fs)
messageSvc = service.Message()
messageSvc = service.Message(attachmentSvc)
organisationSvc = service.Organisation()
teamSvc = service.Team()
userSvc = service.User()
@ -32,6 +32,7 @@ func MountRoutes(jwtAuth types.TokenEncoder) func(chi.Router) {
organisation = Organisation{}.New(organisationSvc)
team = Team{}.New(teamSvc)
user = User{}.New(userSvc, messageSvc)
attachment = Attachment{}.New(attachmentSvc)
)
// Initialize handers & controllers.
@ -47,6 +48,7 @@ func MountRoutes(jwtAuth types.TokenEncoder) func(chi.Router) {
handlers.NewOrganisation(organisation).MountRoutes(r)
handlers.NewTeam(team).MountRoutes(r)
handlers.NewUser(user).MountRoutes(r)
handlers.NewAttachment(attachment).MountRoutes(r)
})
}
}

View File

@ -2,12 +2,14 @@ package service
import (
"context"
"fmt"
"github.com/crusttech/crust/auth"
"github.com/crusttech/crust/sam/repository"
"github.com/crusttech/crust/sam/types"
"github.com/titpetric/factory"
"io"
"net/http"
"net/url"
"path"
"strings"
)
@ -16,10 +18,19 @@ type (
attachment struct {
rpo attachmentRepository
sto attachmentStore
config struct {
url string
previewUrl string
}
}
AttachmentService interface {
FindByID(id uint64) (*types.Attachment, error)
Create(ctx context.Context, channelId uint64, name string, size int64, fh io.ReadSeeker) (*types.Attachment, error)
LoadFromMessages(ctx context.Context, mm types.MessageSet) (err error)
OpenOriginal(att *types.Attachment) (io.Reader, error)
OpenPreview(att *types.Attachment) (io.Reader, error)
}
attachmentRepository interface {
@ -39,13 +50,56 @@ type (
func Attachment(store attachmentStore) *attachment {
svc := &attachment{}
svc.config.url = "/attachment/%d/%s"
svc.config.previewUrl = "/attachment/%d/%s/preview"
svc.rpo = repository.New()
svc.sto = store
// @todo bind file store
return svc
}
func (svc attachment) FindByID(id uint64) (*types.Attachment, error) {
return svc.rpo.FindAttachmentByID(id)
}
func (svc attachment) OpenOriginal(att *types.Attachment) (io.Reader, error) {
// @todo update this to io.ReadSeeker when store's Open() func rval is updated
return svc.sto.Open(att.Url)
}
func (svc attachment) OpenPreview(att *types.Attachment) (io.Reader, error) {
// @todo update this to io.ReadSeeker when store's Open() func rval is updated
return svc.sto.Open(att.PreviewUrl)
}
func (svc attachment) LoadFromMessages(ctx context.Context, mm types.MessageSet) (err error) {
var ids []uint64
mm.Walk(func(m *types.Message) error {
if m.Type == "attachment" {
ids = append(ids, m.ID)
}
return nil
})
if set, err := svc.rpo.FindAttachmentByMessageID(ids...); err != nil {
return err
} else {
return set.Walk(func(a *types.MessageAttachment) error {
if a.MessageID > 0 {
if m := mm.FindById(a.MessageID); m != nil {
m.Attachment = &a.Attachment
m.Attachment.Url = svc.url(&a.Attachment)
m.Attachment.PreviewUrl = svc.previewUrl(&a.Attachment)
}
}
return nil
})
}
}
func (svc attachment) Create(ctx context.Context, channelId uint64, name string, size int64, fh io.ReadSeeker) (att *types.Attachment, err error) {
var currentUserID uint64 = auth.GetIdentityFromContext(ctx).Identity()
@ -101,6 +155,16 @@ func (svc attachment) Create(ctx context.Context, channelId uint64, name string,
})
}
// Generates URL to a location
func (svc attachment) url(att *types.Attachment) string {
return fmt.Sprintf(svc.config.url, att.ID, url.PathEscape(att.Name))
}
// Generates URL to a location
func (svc attachment) previewUrl(att *types.Attachment) string {
return fmt.Sprintf(svc.config.previewUrl, att.ID, url.PathEscape(att.Name))
}
func (svc attachment) extractMeta(att *types.Attachment, file io.ReadSeeker) (err error) {
if _, err = file.Seek(0, 0); err != nil {
return err

View File

@ -11,6 +11,7 @@ import (
type (
message struct {
rpo messageRepository
att AttachmentService
}
MessageService interface {
@ -42,8 +43,11 @@ type (
}
)
func Message() *message {
m := &message{rpo: repository.New()}
func Message(attSvc AttachmentService) *message {
m := &message{
att: attSvc,
rpo: repository.New(),
}
return m
}
@ -60,31 +64,7 @@ func (svc message) Find(ctx context.Context, filter *types.MessageFilter) (mm ty
return nil, err
}
{
var ids []uint64
mm.Walk(func(m *types.Message) error {
if m.Type == "attachment" {
ids = append(ids, m.ID)
}
return nil
})
if set, err := svc.rpo.FindAttachmentByMessageID(ids...); err != nil {
return nil, err
} else {
set.Walk(func(a *types.MessageAttachment) error {
if a.MessageID > 0 {
if m := mm.FindById(a.MessageID); m != nil {
m.Attachment = &a.Attachment
}
}
return nil
})
}
}
return
return mm, svc.att.LoadFromMessages(ctx, mm)
}
func (svc message) Direct(ctx context.Context, recipientID uint64, in *types.Message) (out *types.Message, err error) {

View File

@ -6,8 +6,18 @@ import (
"github.com/crusttech/crust/sam/types"
"github.com/crusttech/crust/sam/websocket/incoming"
"github.com/crusttech/crust/sam/websocket/outgoing"
fstore "github.com/crusttech/crust/store"
)
func messageService() service.MessageService {
// @todo refactor, optimize this
store, _ := fstore.New("")
attSvc := service.Attachment(store)
msgSvc := service.Message(attSvc)
return msgSvc
}
func (s *Session) messageCreate(ctx context.Context, p *incoming.MessageCreate) error {
var (
msg = &types.Message{
@ -16,7 +26,7 @@ func (s *Session) messageCreate(ctx context.Context, p *incoming.MessageCreate)
}
)
msg, err := service.Message().Create(ctx, msg)
msg, err := messageService().Create(ctx, msg)
if err != nil {
return err
}
@ -31,7 +41,7 @@ func (s *Session) messageUpdate(ctx context.Context, p *incoming.MessageUpdate)
Message: p.Message,
}
)
msg, err := service.Message().Update(ctx, msg)
msg, err := messageService().Update(ctx, msg)
if err != nil {
return err
}
@ -50,7 +60,7 @@ func (s *Session) messageDelete(ctx context.Context, p *incoming.MessageDelete)
id = parseUInt64(p.ID)
)
if err := service.Message().Delete(ctx, id); err != nil {
if err := messageService().Delete(ctx, id); err != nil {
return err
}
@ -69,7 +79,7 @@ func (s *Session) messageHistory(ctx context.Context, p *incoming.MessageHistory
}
)
messages, err := service.Message().Find(ctx, filter)
messages, err := messageService().Find(ctx, filter)
if err != nil {
return err
}