Attachment serving
This commit is contained in:
parent
e351c73a0a
commit
b04a681d81
@ -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)
|
||||
|
||||
@ -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",
|
||||
|
||||
47
sam/docs/src/spec/attachment.json
Normal file
47
sam/docs/src/spec/attachment.json
Normal 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
72
sam/rest/attachment.go
Normal 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
|
||||
}
|
||||
67
sam/rest/handlers/attachment.go
Normal file
67
sam/rest/handlers/attachment.go
Normal 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)
|
||||
})
|
||||
})
|
||||
}
|
||||
120
sam/rest/request/attachment.go
Normal file
120
sam/rest/request/attachment.go
Normal 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()
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user