From c4875688eb52c2094b44fb105504376e7e5c0646 Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Mon, 10 Sep 2018 18:55:05 +0200 Subject: [PATCH] Attachment serving --- sam/rest/attachment.go | 60 +++++++++--------- sam/rest/auth.go | 14 +++-- sam/rest/handlers/attachment_custom.go | 64 +++++++++++++++++++ sam/rest/handlers/auth_custom.go | 76 +++++++++++++++++++++++ sam/rest/router.go | 12 +++- sam/service/attachment.go | 25 +++++--- sam/websocket/outgoing/attachment.go | 19 ++++++ sam/websocket/outgoing/message.go | 12 ---- sam/websocket/session_incoming_message.go | 2 +- 9 files changed, 226 insertions(+), 58 deletions(-) create mode 100644 sam/rest/handlers/attachment_custom.go create mode 100644 sam/rest/handlers/auth_custom.go create mode 100644 sam/websocket/outgoing/attachment.go diff --git a/sam/rest/attachment.go b/sam/rest/attachment.go index 000541744..768de7caf 100644 --- a/sam/rest/attachment.go +++ b/sam/rest/attachment.go @@ -2,8 +2,10 @@ package rest import ( "context" + "github.com/crusttech/crust/sam/rest/handlers" "github.com/crusttech/crust/sam/rest/request" "github.com/crusttech/crust/sam/service" + "github.com/crusttech/crust/sam/types" "github.com/pkg/errors" "io" "time" @@ -16,12 +18,10 @@ type ( svc service.AttachmentService } - tempAttachmentPayload struct { - ThisIsATempSolutionForAttachmentPayload bool - - Name string - ModTime time.Time - Download bool + file struct { + *types.Attachment + content io.ReadSeeker + download bool } ) @@ -30,43 +30,47 @@ func (Attachment) New(svc service.AttachmentService) *Attachment { } func (ctrl *Attachment) Original(ctx context.Context, r *request.AttachmentOriginal) (interface{}, error) { - rval := tempAttachmentPayload{Download: r.Download} + return ctrl.get(r.AttachmentID, false, 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") + return ctrl.get(r.AttachmentID, true, false) } -func (ctrl Attachment) get(ID uint64, preview, download bool) (interface{}, error) { - rval := tempAttachmentPayload{Download: download} +func (ctrl Attachment) get(ID uint64, preview, download bool) (handlers.Downloadable, error) { + rval := &file{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 - + rval.Attachment = att if preview { - rs, err = ctrl.svc.OpenPreview(att) + rval.content, err = ctrl.svc.OpenPreview(att) } else { - rs, err = ctrl.svc.OpenOriginal(att) + rval.content, err = ctrl.svc.OpenOriginal(att) } - rval.Name = att.Name - rval.ModTime = att.CreatedAt - - // @todo do something with rs :) - _ = rs + if err != nil { + return nil, err + } } return rval, nil } + +func (f *file) Download() bool { + return f.download +} + +func (f *file) Name() string { + return f.Attachment.Name +} + +func (f *file) ModTime() time.Time { + return f.Attachment.CreatedAt +} + +func (f *file) Content() io.ReadSeeker { + return f.content +} diff --git a/sam/rest/auth.go b/sam/rest/auth.go index 95f2e1bd7..e73977d43 100644 --- a/sam/rest/auth.go +++ b/sam/rest/auth.go @@ -16,6 +16,11 @@ type ( token auth.TokenEncoder } + authPayload struct { + JWT string + User *types.User `json:"user"` + } + authUserBasics interface { ValidateCredentials(ctx context.Context, username, password string) (*types.User, error) Create(ctx context.Context, input *types.User) (user *types.User, err error) @@ -46,11 +51,12 @@ func (ctrl *Auth) tokenize(user *types.User, err error) (interface{}, error) { return nil, err } - return struct { - JWT string - User *types.User `json:"user"` - }{ + return &authPayload{ JWT: ctrl.token.Encode(user), User: user, }, nil } + +func (ap authPayload) Token() string { + return ap.JWT +} diff --git a/sam/rest/handlers/attachment_custom.go b/sam/rest/handlers/attachment_custom.go new file mode 100644 index 000000000..11d1b14a6 --- /dev/null +++ b/sam/rest/handlers/attachment_custom.go @@ -0,0 +1,64 @@ +package handlers + +import ( + "net/http" + + "github.com/crusttech/crust/sam/rest/request" + "io" + "net/url" + "time" +) + +// HTTP API interface +type AttachmentDownloadable struct { + Original func(http.ResponseWriter, *http.Request) + Preview func(http.ResponseWriter, *http.Request) +} + +type Downloadable interface { + Name() string + Download() bool + ModTime() time.Time + Content() io.ReadSeeker +} + +func NewAttachmentDownloadable(ah AttachmentAPI) *Attachment { + serve := func(f interface{}, err error, w http.ResponseWriter, r *http.Request) { + if err != nil { + switch true { + case err.Error() == "crust.sam.repository.AttachmentNotFound": + http.Error(w, "Attachment not found", 404) + default: + http.Error(w, err.Error(), 500) + } + } else if dl, ok := f.(Downloadable); ok { + if dl.Download() { + w.Header().Add("Content-Disposition", "attachment; filename="+url.QueryEscape(dl.Name())) + } + + http.ServeContent(w, r, dl.Name(), dl.ModTime(), dl.Content()) + } else { + http.Error(w, "Got incompatible type from controller", 500) + } + } + + return &Attachment{ + Original: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAttachmentOriginal() + params.Fill(r) + + f, err := ah.Original(r.Context(), params) + serve(f, err, w, r) + }, + + Preview: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAttachmentPreview() + params.Fill(r) + + f, err := ah.Preview(r.Context(), params) + serve(f, err, w, r) + }, + } +} diff --git a/sam/rest/handlers/auth_custom.go b/sam/rest/handlers/auth_custom.go new file mode 100644 index 000000000..962a8636a --- /dev/null +++ b/sam/rest/handlers/auth_custom.go @@ -0,0 +1,76 @@ +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 `auth.go`, `auth.util.go` or `auth_test.go` to + implement your API calls, helper functions and tests. The file `auth.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "net/http" + + "github.com/titpetric/factory/resputil" + + "github.com/crusttech/crust/sam/rest/request" + "net/url" + "time" +) + +type ( + authPayload interface { + Token() string + } +) + +// Initializies custom auth handler that attaches cookie info +// +// Cookie with JWT is added on successful login or user creation +// +func NewAuthCustom(ah AuthAPI, cookieExp int) *Auth { + setCookie := func(w http.ResponseWriter, reqUrl *url.URL) func(payload interface{}, err error) (interface{}, error) { + return func(payload interface{}, err error) (interface{}, error) { + if ap, ok := payload.(authPayload); ok && err == nil { + http.SetCookie(w, &http.Cookie{ + Name: "jwt", + Value: ap.Token(), + + HttpOnly: false, // we need this for attachments & ws! + Secure: reqUrl.Scheme == "https", + Path: "/", + //Domain: "localhost", + + // @todo read from the config file. + Expires: time.Now().Add(time.Duration(cookieExp) * time.Minute), + }) + } + + return payload, err + } + } + + return &Auth{ + Login: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAuthLogin() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return setCookie(w, r.URL)(ah.Login(r.Context(), params)) + }) + }, + Create: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAuthCreate() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return setCookie(w, r.URL)(ah.Create(r.Context(), params)) + }) + }, + } +} diff --git a/sam/rest/router.go b/sam/rest/router.go index fce35226b..d1b2e3129 100644 --- a/sam/rest/router.go +++ b/sam/rest/router.go @@ -12,7 +12,7 @@ import ( func MountRoutes(jwtAuth types.TokenEncoder) func(chi.Router) { // Initialize services - fs, err := store.New("/tmp/crust/messages") + fs, err := store.New("var/store") if err != nil { log.Fatalf("Failed to initialize stor: %v", err) } @@ -37,7 +37,14 @@ func MountRoutes(jwtAuth types.TokenEncoder) func(chi.Router) { // Initialize handers & controllers. return func(r chi.Router) { - handlers.NewAuth(Auth{}.New(userSvc, jwtAuth)).MountRoutes(r) + // Cookie expiration in minutes + // @todo pull this from auth/jwt config + var cookieExp = 3600 + + handlers.NewAuthCustom(Auth{}.New(userSvc, jwtAuth), cookieExp).MountRoutes(r) + + // @todo solve cookie issues ( + handlers.NewAttachmentDownloadable(attachment).MountRoutes(r) // Protect all _private_ routes r.Group(func(r chi.Router) { @@ -48,7 +55,6 @@ 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) }) } } diff --git a/sam/service/attachment.go b/sam/service/attachment.go index 35ff30df8..fce84b52c 100644 --- a/sam/service/attachment.go +++ b/sam/service/attachment.go @@ -9,6 +9,7 @@ import ( "github.com/crusttech/crust/store" "github.com/titpetric/factory" "io" + "log" "net/http" "net/url" "path" @@ -18,7 +19,7 @@ import ( type ( attachment struct { rpo attachmentRepository - sto attachmentStore + sto store.Store config struct { url string @@ -30,20 +31,19 @@ type ( 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) + OpenOriginal(att *types.Attachment) (io.ReadSeeker, error) + OpenPreview(att *types.Attachment) (io.ReadSeeker, error) } attachmentRepository interface { repository.Transactionable repository.Attachment } - - attachmentStore store.Store ) -func Attachment(store attachmentStore) *attachment { +func Attachment(store store.Store) *attachment { svc := &attachment{} + svc.config.url = "/attachment/%d/%s" svc.config.previewUrl = "/attachment/%d/%s/preview" svc.rpo = repository.New() @@ -56,13 +56,11 @@ 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 +func (svc attachment) OpenOriginal(att *types.Attachment) (io.ReadSeeker, error) { 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 +func (svc attachment) OpenPreview(att *types.Attachment) (io.ReadSeeker, error) { return svc.sto.Open(att.PreviewUrl) } @@ -113,9 +111,12 @@ func (svc attachment) Create(ctx context.Context, channelId uint64, name string, // @todo extract mimetype and update att.Mimetype + log.Printf("Processing uploaded file (name: %s, size: %d, mime: %s)", att.Name, att.Size, att.Mimetype) + if svc.sto != nil { att.Url = svc.sto.Original(att.ID, ext) if err = svc.sto.Save(att.Url, fh); err != nil { + log.Print(err.Error()) return } @@ -123,6 +124,8 @@ func (svc attachment) Create(ctx context.Context, channelId uint64, name string, svc.makePreview(att, fh) } + log.Printf("File %s stored as %s", att.Name, att.Url) + return att, svc.rpo.BeginWith(ctx, func(r repository.Interfaces) (err error) { if att, err = r.CreateAttachment(att); err != nil { @@ -145,6 +148,8 @@ func (svc attachment) Create(ctx context.Context, channelId uint64, name string, return } + log.Printf("File %s (id: %s) attached to message (id: %d)", att.Name, att.ID, msg.ID) + return }) } diff --git a/sam/websocket/outgoing/attachment.go b/sam/websocket/outgoing/attachment.go new file mode 100644 index 000000000..8967d70f6 --- /dev/null +++ b/sam/websocket/outgoing/attachment.go @@ -0,0 +1,19 @@ +package outgoing + +import ( + "time" +) + +type ( + Attachment struct { + ID string `json:"id"` + UserID string `json:"uid"` + Url string `json:"url"` + PreviewUrl string `json:"prw"` + Size int64 `json:"sze"` + Mimetype string `json:"typ"` + Name string `json:"nme"` + CreatedAt time.Time `json:"cat,omitempty"` + UpdatedAt *time.Time `json:"uat,omitempty"` + } +) diff --git a/sam/websocket/outgoing/message.go b/sam/websocket/outgoing/message.go index 1f42c6d75..a3d5f6677 100644 --- a/sam/websocket/outgoing/message.go +++ b/sam/websocket/outgoing/message.go @@ -32,18 +32,6 @@ type ( MessageDelete struct { ID string `json:"id"` } - - Attachment struct { - ID string `json:"id"` - UserID string `json:"uid"` - Url string `json:"url"` - PreviewUrl string `json:"prw"` - Size int64 `json:"sze"` - Mimetype string `json:"typ"` - Name string `json:"nme"` - CreatedAt time.Time `json:"cat,omitempty"` - UpdatedAt *time.Time `json:"uat,omitempty"` - } ) func (p *Message) EncodeMessage() ([]byte, error) { diff --git a/sam/websocket/session_incoming_message.go b/sam/websocket/session_incoming_message.go index 630a9129f..e44e24e70 100644 --- a/sam/websocket/session_incoming_message.go +++ b/sam/websocket/session_incoming_message.go @@ -11,7 +11,7 @@ import ( func messageService() service.MessageService { // @todo refactor, optimize this - store, _ := fstore.New("") + store, _ := fstore.New("var/store") attSvc := service.Attachment(store) msgSvc := service.Message(attSvc)