3
0

Merge branch 'master' of ssh://github.com/crusttech/crust

This commit is contained in:
Tit Petric 2018-09-12 10:55:48 +02:00
commit 91db6b37f7
11 changed files with 286 additions and 63 deletions

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
}

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
}

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)
},
}
}

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))
})
},
}
}

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)
})
}
}

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)
}
@ -70,7 +68,7 @@ func (svc attachment) OpenPreview(att *types.Attachment) (io.Reader, error) {
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" {
if m.Type == types.MessageTypeAttachment || m.Type == types.MessageTypeInlineImage {
ids = append(ids, m.ID)
}
return nil
@ -111,11 +109,16 @@ func (svc attachment) Create(ctx context.Context, channelId uint64, name string,
// 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 err := svc.extractMeta(att, fh); err != nil {
// @todo logmeta extraction failure
}
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 +126,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 {
@ -131,8 +136,13 @@ func (svc attachment) Create(ctx context.Context, channelId uint64, name string,
msg := &types.Message{
Message: name,
Type: "attachment",
Type: types.MessageTypeAttachment,
ChannelID: channelId,
UserID: currentUserID,
}
if strings.HasPrefix(att.Mimetype, "image/") {
msg.Type = types.MessageTypeInlineImage
}
// Create the first message, doing this directly with repository to circumvent
@ -145,6 +155,8 @@ func (svc attachment) Create(ctx context.Context, channelId uint64, name string,
return
}
log.Printf("File %s (id: %d) attached to message (id: %d)", att.Name, att.ID, msg.ID)
return
})
}

View File

@ -1,13 +1,14 @@
package types
import (
"database/sql/driver"
"time"
)
type (
Message struct {
ID uint64 `json:"id" db:"id"`
Type string `json:"type" db:"type"`
Type MessageType `json:"type" db:"type"`
Message string `json:"message" db:"message"`
UserID uint64 `json:"userId" db:"rel_user"`
ChannelID uint64 `json:"channelId" db:"rel_channel"`
@ -27,6 +28,15 @@ type (
UntilMessageID uint64
Limit uint
}
MessageType string
)
const (
MessageTypeSimpleMessage MessageType = ""
MessageTypeChannelEvent MessageType = "channelEvent"
MessageTypeInlineImage MessageType = "inlineImage"
MessageTypeAttachment MessageType = "attachment"
)
func (mm MessageSet) Walk(w func(*Message) error) (err error) {
@ -48,3 +58,41 @@ func (mm MessageSet) FindById(ID uint64) *Message {
return nil
}
func (mtype MessageType) String() string {
return string(mtype)
}
func (mtype MessageType) IsValid() bool {
switch mtype {
case MessageTypeSimpleMessage,
MessageTypeChannelEvent,
MessageTypeInlineImage,
MessageTypeAttachment:
return true
}
return false
}
//func (mtype *MessageType) Scan(value interface{}) error {
// switch value.(type) {
// case nil:
// *mtype = MessageTypeSimpleMessage
// case []uint8:
// *mtype = MessageType(string(value.([]uint8)))
// if !mtype.IsValid() {
// return errors.Errorf("Can not scan %v into MessageType", value)
// }
// }
//
// return nil
//}
func (mtype MessageType) Value() (driver.Value, error) {
if mtype == MessageTypeSimpleMessage {
return nil, nil
}
return mtype.String(), nil
}

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"`
}
)

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) {

View File

@ -10,7 +10,7 @@ func payloadFromMessage(msg *types.Message) *outgoing.Message {
Message: msg.Message,
ID: uint64toa(msg.ID),
ChannelID: uint64toa(msg.ChannelID),
Type: msg.Type,
Type: string(msg.Type),
UserID: uint64toa(msg.UserID),
ReplyTo: uint64toa(msg.ReplyTo),

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)