3
0

Add upload/attachment capabilities

This commit is contained in:
Denis Arh 2018-09-08 21:17:03 +02:00
parent bf9ca1ac1e
commit 0400451823
25 changed files with 357 additions and 184 deletions

View File

@ -21,12 +21,14 @@ import (
"github.com/jmoiron/sqlx/types"
"github.com/pkg/errors"
"io"
"mime/multipart"
"net/http"
"strings"
)
var _ = chi.URLParam
var _ = types.JSONText{}
var _ = multipart.FileHeader{}
// Auth login request parameters
type AuthLogin struct {

View File

@ -9,11 +9,13 @@ import (
"github.com/go-chi/chi"
"github.com/pkg/errors"
"github.com/jmoiron/sqlx/types"
"mime/multipart"
"strings"
)
var _ = chi.URLParam
var _ = types.JSONText{}
var _ = multipart.FileHeader{}
{foreach $calls as $call}
// {name} {call.name} request parameters
@ -58,14 +60,18 @@ func ({self} *{name|expose}{call.name|capitalize}) Fill(r *http.Request) error {
{foreach $params as $param}
{if strtolower($method) === "path"}
{self}.{param.name|expose} = {if ($param.type !== "string")}{$parsers[$param.type]}({/if}chi.URLParam(r, "{param.name}"){if ($param.type !== "string")}){/if}
{elseif $param.type === "*multipart.FileHeader"}
if _, {self}.{param.name|expose}, err = r.FormFile("{$param.name}"); err != nil {
return errors.Wrap(err, "error procesing uploaded file")
}
{elseif substr($param.type, 0, 2) !== '[]'}
if val, ok := {method|strtolower}["{param.name}"]; ok {
{if $param.type === "types.JSONText"}
if {self}.{param.name|expose}, err = {$parsers[$param.type]}(val); err != nil {
return err
}
if {self}.{param.name|expose}, err = {$parsers[$param.type]}(val); err != nil {
return err
}
{else}
{self}.{param.name|expose} = {if ($param.type !== "string")}{$parsers[$param.type]}(val){else}val{/if}
{self}.{param.name|expose} = {if ($param.type !== "string")}{$parsers[$param.type]}(val){else}val{/if}
{/if}
}{/if}
{/foreach}

View File

@ -21,12 +21,14 @@ import (
"github.com/jmoiron/sqlx/types"
"github.com/pkg/errors"
"io"
"mime/multipart"
"net/http"
"strings"
)
var _ = chi.URLParam
var _ = types.JSONText{}
var _ = multipart.FileHeader{}
// Field list request parameters
type FieldList struct {

View File

@ -21,12 +21,14 @@ import (
"github.com/jmoiron/sqlx/types"
"github.com/pkg/errors"
"io"
"mime/multipart"
"net/http"
"strings"
)
var _ = chi.URLParam
var _ = types.JSONText{}
var _ = multipart.FileHeader{}
// Module list request parameters
type ModuleList struct {

View File

@ -10,7 +10,7 @@ CREATE TABLE organisations (
deleted_at DATETIME NULL, -- organisation soft delete
PRIMARY KEY (id)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Keeps all known teams
CREATE TABLE teams (
@ -24,7 +24,7 @@ CREATE TABLE teams (
deleted_at DATETIME NULL, -- team soft delete
PRIMARY KEY (id)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Keeps all known channels
CREATE TABLE channels (
@ -46,7 +46,7 @@ CREATE TABLE channels (
rel_last_message BIGINT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (id)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Keeps all known users, home and external organisation
-- changes are stored in audit log
@ -64,7 +64,7 @@ CREATE TABLE users (
deleted_at DATETIME NULL, -- user soft delete
PRIMARY KEY (id)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Keeps team memberships
CREATE TABLE team_members (
@ -72,7 +72,7 @@ CREATE TABLE team_members (
rel_user BIGINT UNSIGNED NOT NULL REFERENCES users(id),
PRIMARY KEY (rel_team, rel_user)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- handles channel membership
CREATE TABLE channel_members (
@ -85,7 +85,7 @@ CREATE TABLE channel_members (
updated_at DATETIME NULL,
PRIMARY KEY (rel_channel, rel_user)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE channel_views (
rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),
@ -98,7 +98,7 @@ CREATE TABLE channel_views (
new_since INT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (rel_user, rel_channel)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE channel_pins (
rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),
@ -108,7 +108,7 @@ CREATE TABLE channel_pins (
created_at DATETIME NOT NULL DEFAULT NOW(),
PRIMARY KEY (rel_channel, rel_message)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE messages (
id BIGINT UNSIGNED NOT NULL,
@ -124,7 +124,7 @@ CREATE TABLE messages (
deleted_at DATETIME NULL,
PRIMARY KEY (id)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE reactions (
id BIGINT UNSIGNED NOT NULL,
@ -136,26 +136,32 @@ CREATE TABLE reactions (
created_at DATETIME NOT NULL DEFAULT NOW(),
PRIMARY KEY (id)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE attachments (
id BIGINT UNSIGNED NOT NULL,
rel_user BIGINT UNSIGNED NOT NULL REFERENCES users(id),
rel_message BIGINT UNSIGNED NOT NULL REFERENCES messages(id),
rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),
url TEXT,
preview_url TEXT,
url VARCHAR(512),
preview_url VARCHAR(512),
size INT UNSIGNED,
mimetype TEXT,
mimetype VARCHAR(255),
name TEXT,
attachment JSON NOT NULL,
created_at DATETIME NOT NULL DEFAULT NOW(),
updated_at DATETIME NULL,
deleted_at DATETIME NULL,
PRIMARY KEY (id)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE message_attachment (
rel_message BIGINT UNSIGNED NOT NULL REFERENCES messages(id),
rel_attachment BIGINT UNSIGNED NOT NULL REFERENCES attachment(id),
PRIMARY KEY (rel_message)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE event_queue (
id BIGINT UNSIGNED NOT NULL,
@ -164,11 +170,11 @@ CREATE TABLE event_queue (
payload JSON,
PRIMARY KEY (id)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE event_queue_synced (
origin BIGINT UNSIGNED NOT NULL,
rel_last BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (origin)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

View File

@ -351,6 +351,21 @@ A channel is a representation of a sequence of messages. It has meta data like c
| channelID | uint64 | PATH | Channel ID | N/A | YES |
| userID | []uint64 | POST | User ID | N/A | NO |
## Attach file to channel
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/channels/{channelID}/attach` | HTTP/S | POST | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| channelID | uint64 | PATH | Channel ID | N/A | YES |
| upload | *multipart.FileHeader | POST | File to upload | N/A | YES |
@ -433,19 +448,6 @@ The following event types may be sent with a message event:
| --------- | ---- | ------ | ----------- | ------- | --------- |
| messageID | uint64 | PATH | Message ID | N/A | YES |
## Attach file to message
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/channels/{channelID}/messages/{messageID}/attach` | HTTP/S | PUT | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
## Search messages
#### Method

View File

@ -304,7 +304,21 @@
{ "name": "userID", "type": "[]uint64", "required": false, "title": "User ID" }
]
}
}
},
{
"name": "attach",
"path": "/{channelID}/attach",
"method": "POST",
"title": "Attach file to channel",
"parameters": {
"path": [
{ "name": "channelID", "type": "uint64", "required": true, "title": "Channel ID" }
],
"post": [
{ "name": "upload", "type": "*multipart.FileHeader", "required": true, "title": "File to upload" }
]
}
}
]
},
{
@ -366,20 +380,6 @@
]
}
},
{
"name": "attach",
"path": "/{messageID}/attach",
"method": "PUT",
"title": "Attach file to message",
"params": {
"path": [
{ "name": "messageID", "type": "uint64", "required": true, "title": "Message ID" }
],
"post": [
{ "name": "name", "type": "string", "title": "File name to drop" }
]
}
},
{
"name": "search",
"path": "/search",

View File

@ -201,6 +201,30 @@
}
]
}
},
{
"Name": "attach",
"Method": "POST",
"Title": "Attach file to channel",
"Path": "/{channelID}/attach",
"Parameters": {
"path": [
{
"name": "channelID",
"required": true,
"title": "Channel ID",
"type": "uint64"
}
],
"post": [
{
"name": "upload",
"required": true,
"title": "File to upload",
"type": "*multipart.FileHeader"
}
]
}
}
]
}

View File

@ -92,13 +92,6 @@
]
}
},
{
"Name": "attach",
"Method": "PUT",
"Title": "Attach file to message",
"Path": "/{messageID}/attach",
"Parameters": null
},
{
"Name": "search",
"Method": "GET",

View File

@ -9,10 +9,9 @@ import (
type (
Attachment interface {
FindAttachmentByID(id uint64) (*types.Attachment, error)
FindAttachmentByRange(channelID, fromAttachmentID, toAttachmentID uint64) ([]*types.Attachment, error)
CreateAttachment(mod *types.Attachment) (*types.Attachment, error)
UpdateAttachment(mod *types.Attachment) (*types.Attachment, error)
DeleteAttachmentByID(id uint64) error
BindAttachment(attachmentId, messageId uint64) error
}
)
@ -31,6 +30,16 @@ func (r *repository) FindAttachmentByID(id uint64) (*types.Attachment, error) {
return mod, isFound(r.db().Get(mod, sql, id), mod.ID > 0, ErrAttachmentNotFound)
}
func (r *repository) FindAttachmentByMessageID(id uint64) (*types.Attachment, error) {
sql := "SELECT a.* " +
" FROM attachments AS a" +
" INNER JOIN message_attachment AS ma ON a.id = ma.rel_attachment " +
" WHERE ma.rel_message = ? AND " + sqlAttachmentScope
mod := &types.Attachment{}
return mod, isFound(r.db().Get(mod, sql, id), mod.ID > 0, ErrAttachmentNotFound)
}
func (r *repository) FindAttachmentByRange(channelID, fromAttachmentID, toAttachmentID uint64) ([]*types.Attachment, error) {
rval := make([]*types.Attachment, 0)
@ -45,21 +54,24 @@ func (r *repository) FindAttachmentByRange(channelID, fromAttachmentID, toAttach
}
func (r *repository) CreateAttachment(mod *types.Attachment) (*types.Attachment, error) {
mod.ID = factory.Sonyflake.NextID()
if mod.ID == 0 {
mod.ID = factory.Sonyflake.NextID()
}
mod.CreatedAt = time.Now()
mod.Attachment = coalesceJson(mod.Attachment, []byte("{}"))
return mod, r.db().Insert("attachments", mod)
}
func (r *repository) UpdateAttachment(mod *types.Attachment) (*types.Attachment, error) {
mod.UpdatedAt = timeNowPtr()
mod.Attachment = coalesceJson(mod.Attachment, []byte("{}"))
return mod, r.db().Replace("attachments", mod)
}
func (r *repository) DeleteAttachmentByID(id uint64) error {
return r.updateColumnByID("attachments", "deleted_at", nil, id)
}
func (r *repository) BindAttachment(attachmentId, messageId uint64) error {
bond := struct {
RelAttachment uint64 `db:"rel_attachment"`
RelMessage uint64 `db:"rel_message"`
}{attachmentId, messageId}
return r.db().Insert("message_attachment", bond)
}

View File

@ -25,14 +25,6 @@ func TestAttachment(t *testing.T) {
assert(t, err == nil, "CreateAttachment error: %v", err)
assert(t, att.ChannelID == 1, "Changes were not stored")
{
att.ChannelID = 2
att, err = rpo.UpdateAttachment(att)
assert(t, err == nil, "UpdateAttachment error: %v", err)
assert(t, att.ChannelID == 2, "Changes were not stored")
}
{
att, err = rpo.FindAttachmentByID(att.ID)
assert(t, err == nil, "FindAttachmentByID error: %v", err)

View File

@ -7,18 +7,30 @@ import (
"github.com/crusttech/crust/sam/service"
"github.com/crusttech/crust/sam/types"
"github.com/pkg/errors"
"io"
)
var _ = errors.Wrap
type (
Channel struct {
svc service.ChannelService
svc struct {
ch service.ChannelService
at channelAttachmentService
}
}
channelAttachmentService interface {
Create(ctx context.Context, channelID uint64, name string, size int64, fh io.ReadSeeker) (*types.Attachment, error)
}
)
func (Channel) New(channel service.ChannelService) *Channel {
return &Channel{channel}
func (Channel) New(chSvc service.ChannelService, atSvc service.AttachmentService) *Channel {
ctrl := &Channel{}
ctrl.svc.ch = chSvc
ctrl.svc.at = atSvc
return ctrl
}
func (ctrl *Channel) Create(ctx context.Context, r *request.ChannelCreate) (interface{}, error) {
@ -27,7 +39,7 @@ func (ctrl *Channel) Create(ctx context.Context, r *request.ChannelCreate) (inte
Topic: r.Topic,
}
return ctrl.svc.Create(ctx, channel)
return ctrl.svc.ch.Create(ctx, channel)
}
func (ctrl *Channel) Edit(ctx context.Context, r *request.ChannelEdit) (interface{}, error) {
@ -36,20 +48,20 @@ func (ctrl *Channel) Edit(ctx context.Context, r *request.ChannelEdit) (interfac
Topic: r.Topic,
}
return ctrl.svc.Update(ctx, channel)
return ctrl.svc.ch.Update(ctx, channel)
}
func (ctrl *Channel) Delete(ctx context.Context, r *request.ChannelDelete) (interface{}, error) {
return nil, ctrl.svc.Delete(ctx, r.ChannelID)
return nil, ctrl.svc.ch.Delete(ctx, r.ChannelID)
}
func (ctrl *Channel) Read(ctx context.Context, r *request.ChannelRead) (interface{}, error) {
return ctrl.svc.FindByID(ctx, r.ChannelID)
return ctrl.svc.ch.FindByID(ctx, r.ChannelID)
}
func (ctrl *Channel) List(ctx context.Context, r *request.ChannelList) (interface{}, error) {
return ctrl.svc.Find(ctx, &types.ChannelFilter{Query: r.Query})
return ctrl.svc.ch.Find(ctx, &types.ChannelFilter{Query: r.Query})
}
func (ctrl *Channel) Members(ctx context.Context, r *request.ChannelMembers) (interface{}, error) {
@ -67,3 +79,19 @@ func (ctrl *Channel) Part(ctx context.Context, r *request.ChannelPart) (interfac
func (ctrl *Channel) Invite(ctx context.Context, r *request.ChannelInvite) (interface{}, error) {
return nil, nil
}
func (ctrl *Channel) Attach(ctx context.Context, r *request.ChannelAttach) (interface{}, error) {
file, err := r.Upload.Open()
if err != nil {
return nil, err
}
defer file.Close()
return ctrl.svc.at.Create(
ctx,
r.ChannelID,
r.Upload.Filename,
r.Upload.Size,
file)
}

View File

@ -36,6 +36,7 @@ type ChannelAPI interface {
Join(context.Context, *request.ChannelJoin) (interface{}, error)
Part(context.Context, *request.ChannelPart) (interface{}, error)
Invite(context.Context, *request.ChannelInvite) (interface{}, error)
Attach(context.Context, *request.ChannelAttach) (interface{}, error)
}
// HTTP API interface
@ -49,6 +50,7 @@ type Channel struct {
Join func(http.ResponseWriter, *http.Request)
Part func(http.ResponseWriter, *http.Request)
Invite func(http.ResponseWriter, *http.Request)
Attach func(http.ResponseWriter, *http.Request)
}
func NewChannel(ch ChannelAPI) *Channel {
@ -116,6 +118,13 @@ func NewChannel(ch ChannelAPI) *Channel {
return ch.Invite(r.Context(), params)
})
},
Attach: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewChannelAttach()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return ch.Attach(r.Context(), params)
})
},
}
}
@ -132,6 +141,7 @@ func (ch *Channel) MountRoutes(r chi.Router, middlewares ...func(http.Handler) h
r.Post("/{channelID}/members/{userID}", ch.Join)
r.Delete("/{channelID}/members/{userID}", ch.Part)
r.Post("/{channelID}/invite", ch.Invite)
r.Post("/{channelID}/attach", ch.Attach)
})
})
}

View File

@ -31,7 +31,6 @@ type MessageAPI interface {
History(context.Context, *request.MessageHistory) (interface{}, error)
Edit(context.Context, *request.MessageEdit) (interface{}, error)
Delete(context.Context, *request.MessageDelete) (interface{}, error)
Attach(context.Context, *request.MessageAttach) (interface{}, error)
Search(context.Context, *request.MessageSearch) (interface{}, error)
Pin(context.Context, *request.MessagePin) (interface{}, error)
Unpin(context.Context, *request.MessageUnpin) (interface{}, error)
@ -47,7 +46,6 @@ type Message struct {
History func(http.ResponseWriter, *http.Request)
Edit func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
Attach func(http.ResponseWriter, *http.Request)
Search func(http.ResponseWriter, *http.Request)
Pin func(http.ResponseWriter, *http.Request)
Unpin func(http.ResponseWriter, *http.Request)
@ -87,13 +85,6 @@ func NewMessage(mh MessageAPI) *Message {
return mh.Delete(r.Context(), params)
})
},
Attach: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewMessageAttach()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.Attach(r.Context(), params)
})
},
Search: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewMessageSearch()
@ -154,7 +145,6 @@ func (mh *Message) MountRoutes(r chi.Router, middlewares ...func(http.Handler) h
r.Get("/", mh.History)
r.Put("/{messageID}", mh.Edit)
r.Delete("/{messageID}", mh.Delete)
r.Put("/{messageID}/attach", mh.Attach)
r.Get("/search", mh.Search)
r.Post("/{messageID}/pin", mh.Pin)
r.Delete("/{messageID}/pin", mh.Unpin)

View File

@ -46,10 +46,6 @@ func (ctrl *Message) Delete(ctx context.Context, r *request.MessageDelete) (inte
return nil, ctrl.svc.Delete(ctx, r.MessageID)
}
func (ctrl *Message) Attach(ctx context.Context, r *request.MessageAttach) (interface{}, error) {
return ctrl.svc.Attach(ctx)
}
func (ctrl *Message) Search(ctx context.Context, r *request.MessageSearch) (interface{}, error) {
return ctrl.svc.Find(ctx, &types.MessageFilter{
ChannelID: r.ChannelID,

View File

@ -21,12 +21,14 @@ import (
"github.com/jmoiron/sqlx/types"
"github.com/pkg/errors"
"io"
"mime/multipart"
"net/http"
"strings"
)
var _ = chi.URLParam
var _ = types.JSONText{}
var _ = multipart.FileHeader{}
// Auth login request parameters
type AuthLogin struct {

View File

@ -21,12 +21,14 @@ import (
"github.com/jmoiron/sqlx/types"
"github.com/pkg/errors"
"io"
"mime/multipart"
"net/http"
"strings"
)
var _ = chi.URLParam
var _ = types.JSONText{}
var _ = multipart.FileHeader{}
// Channel list request parameters
type ChannelList struct {
@ -439,3 +441,49 @@ func (c *ChannelInvite) Fill(r *http.Request) error {
}
var _ RequestFiller = NewChannelInvite()
// Channel attach request parameters
type ChannelAttach struct {
ChannelID uint64
Upload *multipart.FileHeader
}
func NewChannelAttach() *ChannelAttach {
return &ChannelAttach{}
}
func (c *ChannelAttach) Fill(r *http.Request) error {
var err error
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(c)
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])
}
c.ChannelID = parseUInt64(chi.URLParam(r, "channelID"))
if _, c.Upload, err = r.FormFile("upload"); err != nil {
return errors.Wrap(err, "error procesing uploaded file")
}
return err
}
var _ RequestFiller = NewChannelAttach()

View File

@ -21,12 +21,14 @@ import (
"github.com/jmoiron/sqlx/types"
"github.com/pkg/errors"
"io"
"mime/multipart"
"net/http"
"strings"
)
var _ = chi.URLParam
var _ = types.JSONText{}
var _ = multipart.FileHeader{}
// Message create request parameters
type MessageCreate struct {
@ -215,48 +217,6 @@ func (m *MessageDelete) Fill(r *http.Request) error {
var _ RequestFiller = NewMessageDelete()
// Message attach request parameters
type MessageAttach struct {
ChannelID uint64
}
func NewMessageAttach() *MessageAttach {
return &MessageAttach{}
}
func (m *MessageAttach) Fill(r *http.Request) error {
var err error
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(m)
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])
}
m.ChannelID = parseUInt64(chi.URLParam(r, "channelID"))
return err
}
var _ RequestFiller = NewMessageAttach()
// Message search request parameters
type MessageSearch struct {
Query string

View File

@ -21,12 +21,14 @@ import (
"github.com/jmoiron/sqlx/types"
"github.com/pkg/errors"
"io"
"mime/multipart"
"net/http"
"strings"
)
var _ = chi.URLParam
var _ = types.JSONText{}
var _ = multipart.FileHeader{}
// Organisation list request parameters
type OrganisationList struct {

View File

@ -21,12 +21,14 @@ import (
"github.com/jmoiron/sqlx/types"
"github.com/pkg/errors"
"io"
"mime/multipart"
"net/http"
"strings"
)
var _ = chi.URLParam
var _ = types.JSONText{}
var _ = multipart.FileHeader{}
// Team list request parameters
type TeamList struct {

View File

@ -21,12 +21,14 @@ import (
"github.com/jmoiron/sqlx/types"
"github.com/pkg/errors"
"io"
"mime/multipart"
"net/http"
"strings"
)
var _ = chi.URLParam
var _ = types.JSONText{}
var _ = multipart.FileHeader{}
// User search request parameters
type UserSearch struct {

View File

@ -5,13 +5,21 @@ import (
"github.com/crusttech/crust/auth/types"
"github.com/crusttech/crust/sam/rest/handlers"
"github.com/crusttech/crust/sam/service"
"github.com/crusttech/crust/store"
"github.com/go-chi/chi"
"log"
)
func MountRoutes(jwtAuth types.TokenEncoder) func(chi.Router) {
// Initialize services
fs, err := store.New("/tmp/crust/messages")
if err != nil {
log.Fatalf("Failed to initialize stor: %v", err)
}
var (
channelSvc = service.Channel()
attachmentSvc = service.Attachment(fs)
messageSvc = service.Message()
organisationSvc = service.Organisation()
teamSvc = service.Team()
@ -19,7 +27,7 @@ func MountRoutes(jwtAuth types.TokenEncoder) func(chi.Router) {
)
var (
channel = Channel{}.New(channelSvc)
channel = Channel{}.New(channelSvc, attachmentSvc)
message = Message{}.New(messageSvc)
organisation = Organisation{}.New(organisationSvc)
team = Team{}.New(teamSvc)

115
sam/service/attachment.go Normal file
View File

@ -0,0 +1,115 @@
package service
import (
"context"
"github.com/crusttech/crust/auth"
"github.com/crusttech/crust/sam/repository"
"github.com/crusttech/crust/sam/types"
"github.com/titpetric/factory"
"io"
"path"
"strings"
)
type (
attachment struct {
rpo attachmentRepository
sto attachmentStore
}
AttachmentService interface {
Create(ctx context.Context, channelId uint64, name string, size int64, fh io.ReadSeeker) (*types.Attachment, error)
}
attachmentRepository interface {
repository.Transactionable
repository.Attachment
}
attachmentStore interface {
Original(id uint64, ext string) string
Preview(id uint64, ext string) string
Save(filename string, contents io.Reader) error
Remove(filename string) error
Open(filename string) (io.Reader, error)
}
)
func Attachment(store attachmentStore) *attachment {
svc := &attachment{}
svc.rpo = repository.New()
svc.sto = store
// @todo bind file store
return svc
}
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()
// @todo verify if current user can access this channel
// @todo verify if current user can upload to this channel
att = &types.Attachment{
ID: factory.Sonyflake.NextID(),
UserID: currentUserID,
Name: strings.TrimSpace(name),
Mimetype: "application/octet-stream",
Size: size,
}
// Extract extension but make sure path.Ext is not confused by any leading/trailing dots
var ext = strings.Trim(path.Ext(strings.Trim(name, ".")), ".")
// @todo extract mimetype and update att.Mimetype
if svc.sto != nil {
att.Url = svc.sto.Original(att.ID, ext)
if err = svc.sto.Save(att.Url, fh); err != nil {
return
}
// Try to make preview
svc.makePreview(att, fh)
}
return att, svc.rpo.BeginWith(ctx, func(r repository.Interfaces) (err error) {
if att, err = r.CreateAttachment(att); err != nil {
return
}
msg := &types.Message{
Message: name,
Type: "attachment",
ChannelID: channelId,
}
// Create the first message, doing this directly with repository to circumvent
// message service constraints
if msg, err = r.CreateMessage(msg); err != nil {
return
}
if err = r.BindAttachment(att.ID, msg.ID); err != nil {
return
}
return
})
}
func (svc attachment) makePreview(att *types.Attachment, originalFh io.ReadSeeker) (err error) {
if true {
return
}
// Can and how we make a preview of this attachment?
var ext = "jpg"
att.PreviewUrl = svc.sto.Preview(att.ID, ext)
return svc.sto.Save(att.PreviewUrl, originalFh)
}
var _ AttachmentService = &attachment{}

View File

@ -28,9 +28,6 @@ type (
Flag(ctx context.Context, messageID uint64) error
Unflag(ctx context.Context, messageID uint64) error
Attach(ctx context.Context) (*types.Attachment, error)
Detach(ctx context.Context, messageID uint64) error
Direct(ctx context.Context, recipientID uint64, in *types.Message) (out *types.Message, err error)
deleter
@ -226,28 +223,4 @@ func (svc message) Unflag(ctx context.Context, messageID uint64) error {
return nil
}
func (svc message) Attach(ctx context.Context) (*types.Attachment, error) {
// @todo define func signature
// @todo get user from context
var currentUserID uint64 = auth.GetIdentityFromContext(ctx).Identity()
// @todo verify if current user can access & write to this channel
_ = currentUserID
return nil, nil
}
func (svc message) Detach(ctx context.Context, attachmentID uint64) error {
// @todo get user from context
var currentUserID uint64 = auth.GetIdentityFromContext(ctx).Identity()
// @todo verify if current user can access & write to this channel
_ = currentUserID
// @todo verify if current user can remove this attachment
return svc.rpo.DeleteAttachmentByID(attachmentID)
}
var _ MessageService = &message{}

View File

@ -1,24 +1,20 @@
package types
import (
"encoding/json"
"time"
)
type (
Attachment struct {
ID uint64 `db:"id"`
UserID uint64 `db:"rel_user"`
MessageID uint64 `db:"rel_message"`
ChannelID uint64 `db:"rel_channel"`
Attachment json.RawMessage `db:"attachment"`
Url string `db:"url"`
PreviewUrl string `db:"preview_url"`
Size uint `db:"size"`
Mimetype string `db:"mimetype"`
Name string `db:"name"`
CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"`
UpdatedAt *time.Time `json:"updatedAt,omitempty" db:"updated_at"`
DeletedAt *time.Time `json:"deletedAt,omitempty" db:"deleted_at"`
ID uint64 `db:"id" json:"id,omitempty"`
UserID uint64 `db:"rel_user" json:"userID,omitempty"`
Url string `db:"url" json:"url,omitempty"`
PreviewUrl string `db:"preview_url"json:"previewUrl,omitempty"`
Size int64 `db:"size" json:"size,omitempty"`
Mimetype string `db:"mimetype" json:"mimetype,omitempty"`
Name string `db:"name" json:"name,omitempty"`
CreatedAt time.Time `db:"created_at" json:"createdAt,omitempty"`
UpdatedAt *time.Time `db:"updated_at" json:"updatedAt,omitempty"`
DeletedAt *time.Time `db:"deleted_at" json:"deletedAt,omitempty"`
}
)