3
0

Attachment serving

This commit is contained in:
Denis Arh
2018-09-10 18:55:05 +02:00
parent cef9af9903
commit c4875688eb
9 changed files with 226 additions and 58 deletions
+32 -28
View File
@@ -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
}
+10 -4
View File
@@ -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
}
+64
View File
@@ -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)
},
}
}
+76
View File
@@ -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))
})
},
}
}
+9 -3
View File
@@ -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)
})
}
}
+15 -10
View File
@@ -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
})
}
+19
View File
@@ -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"`
}
)
-12
View File
@@ -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) {
+1 -1
View File
@@ -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)