Add support for attachment (system, settings)

This commit is contained in:
Denis Arh
2020-04-20 12:27:57 +02:00
parent 4a5914627d
commit c7df11af0b
20 changed files with 2383 additions and 2 deletions
+148
View File
@@ -271,6 +271,34 @@
}]
}
},
{
"name": "set",
"path": "/{key}",
"method": "POST",
"title": "Set value for specific setting",
"parameters": {
"path": [
{
"name": "key",
"type": "string",
"title": "Key",
"required": true
}
],
"post": [
{
"name": "upload",
"type": "*multipart.FileHeader",
"title": "File to upload"
},
{
"name": "ownerID",
"type": "uint64",
"title": "Owner ID"
}
]
}
},
{
"name": "current",
"method": "GET",
@@ -1612,6 +1640,126 @@
}
]
},
{
"title": "Attachments",
"path": "/attachment/{kind}",
"entrypoint": "attachment",
"authentication": [
"Client ID",
"Session ID"
],
"parameters": {
"path": [
{
"name": "kind",
"type": "string",
"required": true,
"title": "Kind"
}
],
"get": [
{
"type": "string",
"name": "sign",
"required": false,
"title": "Signature"
},
{
"type": "uint64",
"name": "userID",
"required": false,
"title": "User ID"
}
]
},
"apis": [
{
"name": "read",
"path": "/{attachmentID}",
"method": "GET",
"title": "Attachment details",
"parameters": {
"path": [
{
"name": "attachmentID",
"type": "uint64",
"required": true,
"title": "Attachment ID"
}
]
}
},
{
"name": "delete",
"path": "/{attachmentID}",
"method": "DELETE",
"title": "Delete attachment",
"parameters": {
"path": [
{
"name": "attachmentID",
"type": "uint64",
"required": true,
"title": "Attachment ID"
}
]
}
},
{
"name": "original",
"path": "/{attachmentID}/original/{name}",
"method": "GET",
"title": "Serves attached file",
"parameters": {
"path": [
{
"name": "attachmentID",
"type": "uint64",
"required": true,
"title": "Attachment ID"
},
{
"name": "name",
"type": "string",
"required": true,
"title": "File name"
}
],
"get": [
{
"type": "bool",
"name": "download",
"required": false,
"title": "Force file download"
}
]
}
},
{
"name": "preview",
"path": "/{attachmentID}/preview.{ext}",
"method": "GET",
"title": "Serves preview of an attached file",
"parameters": {
"path": [
{
"name": "attachmentID",
"type": "uint64",
"required": true,
"title": "Attachment ID"
},
{
"name": "ext",
"type": "string",
"required": true,
"title": "Preview extension/format"
}
]
}
}
]
},
{
"title": "Statistics",
"entrypoint": "stats",
+121
View File
@@ -0,0 +1,121 @@
{
"Title": "Attachments",
"Interface": "Attachment",
"Struct": null,
"Parameters": {
"get": [
{
"name": "sign",
"required": false,
"title": "Signature",
"type": "string"
},
{
"name": "userID",
"required": false,
"title": "User ID",
"type": "uint64"
}
],
"path": [
{
"name": "kind",
"required": true,
"title": "Kind",
"type": "string"
}
]
},
"Protocol": "",
"Authentication": [
"Client ID",
"Session ID"
],
"Path": "/attachment/{kind}",
"APIs": [
{
"Name": "read",
"Method": "GET",
"Title": "Attachment details",
"Path": "/{attachmentID}",
"Parameters": {
"path": [
{
"name": "attachmentID",
"required": true,
"title": "Attachment ID",
"type": "uint64"
}
]
}
},
{
"Name": "delete",
"Method": "DELETE",
"Title": "Delete attachment",
"Path": "/{attachmentID}",
"Parameters": {
"path": [
{
"name": "attachmentID",
"required": true,
"title": "Attachment ID",
"type": "uint64"
}
]
}
},
{
"Name": "original",
"Method": "GET",
"Title": "Serves attached file",
"Path": "/{attachmentID}/original/{name}",
"Parameters": {
"get": [
{
"name": "download",
"required": false,
"title": "Force file download",
"type": "bool"
}
],
"path": [
{
"name": "attachmentID",
"required": true,
"title": "Attachment ID",
"type": "uint64"
},
{
"name": "name",
"required": true,
"title": "File name",
"type": "string"
}
]
}
},
{
"Name": "preview",
"Method": "GET",
"Title": "Serves preview of an attached file",
"Path": "/{attachmentID}/preview.{ext}",
"Parameters": {
"path": [
{
"name": "attachmentID",
"required": true,
"title": "Attachment ID",
"type": "uint64"
},
{
"name": "ext",
"required": true,
"title": "Preview extension/format",
"type": "string"
}
]
}
}
]
}
+28
View File
@@ -67,6 +67,34 @@
]
}
},
{
"Name": "set",
"Method": "POST",
"Title": "Set value for specific setting",
"Path": "/{key}",
"Parameters": {
"path": [
{
"name": "key",
"required": true,
"title": "Key",
"type": "string"
}
],
"post": [
{
"name": "upload",
"title": "File to upload",
"type": "*multipart.FileHeader"
},
{
"name": "ownerID",
"title": "Owner ID",
"type": "uint64"
}
]
}
},
{
"Name": "current",
"Method": "GET",
+2
View File
@@ -82,6 +82,7 @@ function types {
./build/gen-type-set --types Organisation --output system/types/organisation.gen.go
./build/gen-type-set --types Credentials --output system/types/credentials.gen.go
./build/gen-type-set --types Reminder --output system/types/reminder.gen.go
./build/gen-type-set --types Attachment --output system/types/attachment.gen.go
./build/gen-type-set-test --types User --output system/types/user.gen_test.go
./build/gen-type-set-test --types Application --output system/types/application.gen_test.go
@@ -89,6 +90,7 @@ function types {
./build/gen-type-set-test --types Organisation --output system/types/organisation.gen_test.go
./build/gen-type-set-test --types Credentials --output system/types/credentials.gen_test.go
./build/gen-type-set-test --types Reminder --output system/types/reminder.gen_test.go
./build/gen-type-set-test --types Attachment --output system/types/attachment.gen_test.go
./build/gen-type-set --types Value --output pkg/settings/types.gen.go --with-primary-key=false --package settings
./build/gen-type-set-test --types Value --output pkg/settings/types.gen_test.go --with-primary-key=false --package settings
+102
View File
@@ -128,6 +128,91 @@
# Attachments
| Method | Endpoint | Purpose |
| ------ | -------- | ------- |
| `GET` | `/attachment/{kind}/{attachmentID}` | Attachment details |
| `DELETE` | `/attachment/{kind}/{attachmentID}` | Delete attachment |
| `GET` | `/attachment/{kind}/{attachmentID}/original/{name}` | Serves attached file |
| `GET` | `/attachment/{kind}/{attachmentID}/preview.{ext}` | Serves preview of an attached file |
## Attachment details
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/attachment/{kind}/{attachmentID}` | HTTP/S | GET | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| attachmentID | uint64 | PATH | Attachment ID | N/A | YES |
| kind | string | PATH | Kind | N/A | YES |
| sign | string | GET | Signature | N/A | NO |
| userID | uint64 | GET | User ID | N/A | NO |
## Delete attachment
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/attachment/{kind}/{attachmentID}` | HTTP/S | DELETE | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| attachmentID | uint64 | PATH | Attachment ID | N/A | YES |
| kind | string | PATH | Kind | N/A | YES |
| sign | string | GET | Signature | N/A | NO |
| userID | uint64 | GET | User ID | N/A | NO |
## Serves attached file
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/attachment/{kind}/{attachmentID}/original/{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 |
| sign | string | GET | Signature | N/A | NO |
| userID | uint64 | GET | User ID | N/A | NO |
| attachmentID | uint64 | PATH | Attachment ID | N/A | YES |
| name | string | PATH | File name | N/A | YES |
| kind | string | PATH | Kind | N/A | YES |
## Serves preview of an attached file
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/attachment/{kind}/{attachmentID}/preview.{ext}` | HTTP/S | GET | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| attachmentID | uint64 | PATH | Attachment ID | N/A | YES |
| ext | string | PATH | Preview extension/format | N/A | YES |
| kind | string | PATH | Kind | N/A | YES |
| sign | string | GET | Signature | N/A | NO |
| userID | uint64 | GET | User ID | N/A | NO |
---
# Authentication
| Method | Endpoint | Purpose |
@@ -953,6 +1038,7 @@ An organisation may have many roles. Roles may have many channels available. Acc
| `GET` | `/settings/` | List settings |
| `PATCH` | `/settings/` | Update settings |
| `GET` | `/settings/{key}` | Get a value for a key |
| `POST` | `/settings/{key}` | Set value for specific setting |
| `GET` | `/settings/current` | Current compose settings |
## List settings
@@ -998,6 +1084,22 @@ An organisation may have many roles. Roles may have many channels available. Acc
| ownerID | uint64 | GET | Owner ID | N/A | NO |
| key | string | PATH | Setting key | N/A | YES |
## Set value for specific setting
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/settings/{key}` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| key | string | PATH | Key | N/A | YES |
| upload | *multipart.FileHeader | POST | File to upload | N/A | NO |
| ownerID | uint64 | POST | Owner ID | N/A | NO |
## Current compose settings
#### Method
File diff suppressed because one or more lines are too long
@@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS sys_attachment (
id BIGINT UNSIGNED NOT NULL,
rel_owner BIGINT UNSIGNED NOT NULL,
kind VARCHAR(32) NOT NULL,
url VARCHAR(512),
preview_url VARCHAR(512),
size INT UNSIGNED,
mimetype VARCHAR(255),
name TEXT,
meta JSON,
created_at DATETIME NOT NULL DEFAULT NOW(),
updated_at DATETIME NULL,
deleted_at DATETIME NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+139
View File
@@ -0,0 +1,139 @@
package repository
import (
"context"
"time"
"github.com/Masterminds/squirrel"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"github.com/cortezaproject/corteza-server/pkg/rh"
"github.com/cortezaproject/corteza-server/system/types"
)
type (
AttachmentRepository interface {
With(ctx context.Context, db *factory.DB) AttachmentRepository
Find(filter types.AttachmentFilter) (types.AttachmentSet, types.AttachmentFilter, error)
FindByID(attachmentID uint64) (*types.Attachment, error)
Create(mod *types.Attachment) (*types.Attachment, error)
DeleteByID(attachmentID uint64) error
}
attachment struct {
*repository
}
)
const (
ErrAttachmentNotFound = repositoryError("AttachmentNotFound")
)
func Attachment(ctx context.Context, db *factory.DB) AttachmentRepository {
return (&attachment{}).With(ctx, db)
}
func (r attachment) With(ctx context.Context, db *factory.DB) AttachmentRepository {
return &attachment{
repository: r.repository.With(ctx, db),
}
}
func (r attachment) table() string {
return "sys_attachment"
}
func (r attachment) columns() []string {
return []string{
"a.id",
"a.rel_owner",
"a.kind",
"a.url",
"a.preview_url",
"a.name",
"a.meta",
"a.created_at",
"a.updated_at",
"a.deleted_at",
}
}
func (r attachment) query() squirrel.SelectBuilder {
return squirrel.
Select(r.columns()...).
From(r.table() + " AS a").
Where("a.deleted_at IS NULL")
}
func (r attachment) FindByID(attachmentID uint64) (*types.Attachment, error) {
return r.findOneBy("id", attachmentID)
}
func (r attachment) findOneBy(field string, value interface{}) (*types.Attachment, error) {
var (
p = &types.Attachment{}
q = r.query().
Where(squirrel.Eq{field: value})
err = rh.FetchOne(r.db(), q, p)
)
if err != nil {
return nil, err
} else if p.ID == 0 {
return nil, ErrAttachmentNotFound
}
return p, nil
}
func (r attachment) Find(filter types.AttachmentFilter) (set types.AttachmentSet, f types.AttachmentFilter, err error) {
f = filter
if f.Sort == "" {
f.Sort = "id ASC"
}
query := r.query().
Where(squirrel.Eq{"a.kind": f.Kind})
if f.Filter != "" {
err = errors.New("filtering by filter not implemented")
return
}
var orderBy []string
if orderBy, err = rh.ParseOrder(f.Sort, r.columns()...); err != nil {
return
} else {
query = query.OrderBy(orderBy...)
}
if f.Count, err = rh.Count(r.db(), query); err != nil || f.Count == 0 {
return
}
return set, f, rh.FetchPaged(r.db(), query, f.PageFilter, &set)
}
func (r attachment) Create(mod *types.Attachment) (*types.Attachment, error) {
if mod.ID == 0 {
mod.ID = factory.Sonyflake.NextID()
}
mod.CreatedAt = time.Now()
return mod, r.db().Insert(r.table(), mod)
}
func (r attachment) DeleteByID(attachmentID uint64) error {
_, err := r.db().Exec(
"UPDATE "+r.table()+" SET deleted_at = NOW() WHERE id = ?",
attachmentID,
)
return err
}
+189
View File
@@ -0,0 +1,189 @@
package rest
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"github.com/titpetric/factory/resputil"
"github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/system/rest/request"
"github.com/cortezaproject/corteza-server/system/service"
"github.com/cortezaproject/corteza-server/system/types"
"github.com/pkg/errors"
)
var _ = errors.Wrap
type (
attachmentPayload struct {
*types.Attachment
}
attachmentSetPayload struct {
Filter types.AttachmentFilter `json:"filter"`
Set []*attachmentPayload `json:"set"`
}
Attachment struct {
attachment service.AttachmentService
}
)
func (Attachment) New() *Attachment {
return &Attachment{
attachment: service.DefaultAttachment,
}
}
func (ctrl Attachment) Read(ctx context.Context, r *request.AttachmentRead) (interface{}, error) {
if !auth.GetIdentityFromContext(ctx).Valid() {
return nil, errors.New("Unauthorized")
}
a, err := ctrl.attachment.With(ctx).FindByID(r.AttachmentID)
return makeAttachmentPayload(ctx, a, err)
}
func (ctrl Attachment) Delete(ctx context.Context, r *request.AttachmentDelete) (interface{}, error) {
if !auth.GetIdentityFromContext(ctx).Valid() {
return nil, errors.New("Unauthorized")
}
_, err := ctrl.attachment.With(ctx).FindByID(r.AttachmentID)
if err != nil {
return nil, err
}
return resputil.OK(), ctrl.attachment.With(ctx).DeleteByID(r.AttachmentID)
}
func (ctrl Attachment) Original(ctx context.Context, r *request.AttachmentOriginal) (interface{}, error) {
if err := ctrl.isAccessible(r.Kind, r.AttachmentID, r.UserID, r.Sign); err != nil {
return nil, err
}
return ctrl.serve(ctx, r.AttachmentID, false, r.Download)
}
func (ctrl Attachment) Preview(ctx context.Context, r *request.AttachmentPreview) (interface{}, error) {
if err := ctrl.isAccessible(r.Kind, r.AttachmentID, r.UserID, r.Sign); err != nil {
return nil, err
}
return ctrl.serve(ctx, r.AttachmentID, true, false)
}
func (ctrl Attachment) isAccessible(kind string, attachmentID, userID uint64, signature string) error {
if kind == types.AttachmentKindSettings {
// Attachments on settings are public
return nil
}
if signature == "" {
return errors.New("Unauthorized")
}
if userID == 0 {
return errors.New("missing or invalid user ID")
}
if attachmentID == 0 {
return errors.New("missing or invalid attachment ID")
}
if !auth.DefaultSigner.Verify(signature, userID, attachmentID) {
return errors.New("missing or invalid signature")
}
return nil
}
func (ctrl Attachment) serve(ctx context.Context, attachmentID uint64, preview, download bool) (interface{}, error) {
return func(w http.ResponseWriter, req *http.Request) {
att, err := ctrl.attachment.With(ctx).FindByID(attachmentID)
if err != nil {
// Simplify error handling for now
w.WriteHeader(http.StatusNotFound)
return
}
var fh io.ReadSeeker
if preview {
fh, err = ctrl.attachment.OpenPreview(att)
} else {
fh, err = ctrl.attachment.OpenOriginal(att)
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
name := url.QueryEscape(att.Name)
if download {
w.Header().Add("Content-Disposition", "attachment; filename="+name)
} else {
w.Header().Add("Content-Disposition", "inline; filename="+name)
}
http.ServeContent(w, req, name, att.CreatedAt, fh)
}, nil
}
func (ctrl Attachment) makeFilterPayload(ctx context.Context, aa types.AttachmentSet, f types.AttachmentFilter, err error) (*attachmentSetPayload, error) {
if err != nil {
return nil, err
}
asp := &attachmentSetPayload{Filter: f, Set: make([]*attachmentPayload, len(aa))}
for i := range aa {
asp.Set[i], _ = makeAttachmentPayload(ctx, aa[i], nil)
}
return asp, nil
}
func makeAttachmentPayload(ctx context.Context, a *types.Attachment, err error) (*attachmentPayload, error) {
if err != nil || a == nil {
return nil, err
}
var (
userID = auth.GetIdentityFromContext(ctx).Identity()
signParams = ""
preview string
baseURL = fmt.Sprintf("/attachment/%s/%d/", a.Kind, a.ID)
)
if a.Meta.Preview != nil {
var ext = a.Meta.Preview.Extension
if ext == "" {
ext = "jpg"
}
preview = baseURL + fmt.Sprintf("preview.%s", ext)
}
switch a.Kind {
case types.AttachmentKindSettings:
// No URL signing, settings attachments are public.
default:
signParams = fmt.Sprintf("?sign=%s&userID=%d", auth.DefaultSigner.Sign(userID, a.ID), userID)
}
ap := &attachmentPayload{a}
ap.Url = baseURL + fmt.Sprintf("original/%s", url.PathEscape(a.Name)) + signParams
ap.PreviewUrl = preview + signParams
return ap, nil
}
+139
View File
@@ -0,0 +1,139 @@
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"
"net/http"
"github.com/go-chi/chi"
"github.com/titpetric/factory/resputil"
"github.com/cortezaproject/corteza-server/pkg/logger"
"github.com/cortezaproject/corteza-server/system/rest/request"
)
// Internal API interface
type AttachmentAPI interface {
Read(context.Context, *request.AttachmentRead) (interface{}, error)
Delete(context.Context, *request.AttachmentDelete) (interface{}, error)
Original(context.Context, *request.AttachmentOriginal) (interface{}, error)
Preview(context.Context, *request.AttachmentPreview) (interface{}, error)
}
// HTTP API interface
type Attachment struct {
Read func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
Original func(http.ResponseWriter, *http.Request)
Preview func(http.ResponseWriter, *http.Request)
}
func NewAttachment(h AttachmentAPI) *Attachment {
return &Attachment{
Read: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAttachmentRead()
if err := params.Fill(r); err != nil {
logger.LogParamError("Attachment.Read", r, err)
resputil.JSON(w, err)
return
}
value, err := h.Read(r.Context(), params)
if err != nil {
logger.LogControllerError("Attachment.Read", r, err, params.Auditable())
resputil.JSON(w, err)
return
}
logger.LogControllerCall("Attachment.Read", r, params.Auditable())
if !serveHTTP(value, w, r) {
resputil.JSON(w, value)
}
},
Delete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAttachmentDelete()
if err := params.Fill(r); err != nil {
logger.LogParamError("Attachment.Delete", r, err)
resputil.JSON(w, err)
return
}
value, err := h.Delete(r.Context(), params)
if err != nil {
logger.LogControllerError("Attachment.Delete", r, err, params.Auditable())
resputil.JSON(w, err)
return
}
logger.LogControllerCall("Attachment.Delete", r, params.Auditable())
if !serveHTTP(value, w, r) {
resputil.JSON(w, value)
}
},
Original: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAttachmentOriginal()
if err := params.Fill(r); err != nil {
logger.LogParamError("Attachment.Original", r, err)
resputil.JSON(w, err)
return
}
value, err := h.Original(r.Context(), params)
if err != nil {
logger.LogControllerError("Attachment.Original", r, err, params.Auditable())
resputil.JSON(w, err)
return
}
logger.LogControllerCall("Attachment.Original", r, params.Auditable())
if !serveHTTP(value, w, r) {
resputil.JSON(w, value)
}
},
Preview: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAttachmentPreview()
if err := params.Fill(r); err != nil {
logger.LogParamError("Attachment.Preview", r, err)
resputil.JSON(w, err)
return
}
value, err := h.Preview(r.Context(), params)
if err != nil {
logger.LogControllerError("Attachment.Preview", r, err, params.Auditable())
resputil.JSON(w, err)
return
}
logger.LogControllerCall("Attachment.Preview", r, params.Auditable())
if !serveHTTP(value, w, r) {
resputil.JSON(w, value)
}
},
}
}
func (h Attachment) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/attachment/{kind}/{attachmentID}", h.Read)
r.Delete("/attachment/{kind}/{attachmentID}", h.Delete)
r.Get("/attachment/{kind}/{attachmentID}/original/{name}", h.Original)
r.Get("/attachment/{kind}/{attachmentID}/preview.{ext}", h.Preview)
})
}
+23
View File
@@ -32,6 +32,7 @@ type SettingsAPI interface {
List(context.Context, *request.SettingsList) (interface{}, error)
Update(context.Context, *request.SettingsUpdate) (interface{}, error)
Get(context.Context, *request.SettingsGet) (interface{}, error)
Set(context.Context, *request.SettingsSet) (interface{}, error)
Current(context.Context, *request.SettingsCurrent) (interface{}, error)
}
@@ -40,6 +41,7 @@ type Settings struct {
List func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
Get func(http.ResponseWriter, *http.Request)
Set func(http.ResponseWriter, *http.Request)
Current func(http.ResponseWriter, *http.Request)
}
@@ -105,6 +107,26 @@ func NewSettings(h SettingsAPI) *Settings {
resputil.JSON(w, value)
}
},
Set: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewSettingsSet()
if err := params.Fill(r); err != nil {
logger.LogParamError("Settings.Set", r, err)
resputil.JSON(w, err)
return
}
value, err := h.Set(r.Context(), params)
if err != nil {
logger.LogControllerError("Settings.Set", r, err, params.Auditable())
resputil.JSON(w, err)
return
}
logger.LogControllerCall("Settings.Set", r, params.Auditable())
if !serveHTTP(value, w, r) {
resputil.JSON(w, value)
}
},
Current: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewSettingsCurrent()
@@ -134,6 +156,7 @@ func (h Settings) MountRoutes(r chi.Router, middlewares ...func(http.Handler) ht
r.Get("/settings/", h.List)
r.Patch("/settings/", h.Update)
r.Get("/settings/{key}", h.Get)
r.Post("/settings/{key}", h.Set)
r.Get("/settings/current", h.Current)
})
}
+686
View File
@@ -0,0 +1,686 @@
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 (
"io"
"strings"
"encoding/json"
"mime/multipart"
"net/http"
"github.com/go-chi/chi"
"github.com/pkg/errors"
)
var _ = chi.URLParam
var _ = multipart.FileHeader{}
// AttachmentRead request parameters
type AttachmentRead struct {
hasAttachmentID bool
rawAttachmentID string
AttachmentID uint64 `json:",string"`
hasKind bool
rawKind string
Kind string
hasSign bool
rawSign string
Sign string
hasUserID bool
rawUserID string
UserID uint64 `json:",string"`
}
// NewAttachmentRead request
func NewAttachmentRead() *AttachmentRead {
return &AttachmentRead{}
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentRead) Auditable() map[string]interface{} {
var out = map[string]interface{}{}
out["attachmentID"] = r.AttachmentID
out["kind"] = r.Kind
out["sign"] = r.Sign
out["userID"] = r.UserID
return out
}
// Fill processes request and fills internal variables
func (r *AttachmentRead) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = req.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := req.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := req.Form
for name, param := range postVars {
post[name] = string(param[0])
}
r.hasAttachmentID = true
r.rawAttachmentID = chi.URLParam(req, "attachmentID")
r.AttachmentID = parseUInt64(chi.URLParam(req, "attachmentID"))
r.hasKind = true
r.rawKind = chi.URLParam(req, "kind")
r.Kind = chi.URLParam(req, "kind")
if val, ok := get["sign"]; ok {
r.hasSign = true
r.rawSign = val
r.Sign = val
}
if val, ok := get["userID"]; ok {
r.hasUserID = true
r.rawUserID = val
r.UserID = parseUInt64(val)
}
return err
}
var _ RequestFiller = NewAttachmentRead()
// AttachmentDelete request parameters
type AttachmentDelete struct {
hasAttachmentID bool
rawAttachmentID string
AttachmentID uint64 `json:",string"`
hasKind bool
rawKind string
Kind string
hasSign bool
rawSign string
Sign string
hasUserID bool
rawUserID string
UserID uint64 `json:",string"`
}
// NewAttachmentDelete request
func NewAttachmentDelete() *AttachmentDelete {
return &AttachmentDelete{}
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentDelete) Auditable() map[string]interface{} {
var out = map[string]interface{}{}
out["attachmentID"] = r.AttachmentID
out["kind"] = r.Kind
out["sign"] = r.Sign
out["userID"] = r.UserID
return out
}
// Fill processes request and fills internal variables
func (r *AttachmentDelete) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = req.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := req.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := req.Form
for name, param := range postVars {
post[name] = string(param[0])
}
r.hasAttachmentID = true
r.rawAttachmentID = chi.URLParam(req, "attachmentID")
r.AttachmentID = parseUInt64(chi.URLParam(req, "attachmentID"))
r.hasKind = true
r.rawKind = chi.URLParam(req, "kind")
r.Kind = chi.URLParam(req, "kind")
if val, ok := get["sign"]; ok {
r.hasSign = true
r.rawSign = val
r.Sign = val
}
if val, ok := get["userID"]; ok {
r.hasUserID = true
r.rawUserID = val
r.UserID = parseUInt64(val)
}
return err
}
var _ RequestFiller = NewAttachmentDelete()
// AttachmentOriginal request parameters
type AttachmentOriginal struct {
hasDownload bool
rawDownload string
Download bool
hasSign bool
rawSign string
Sign string
hasUserID bool
rawUserID string
UserID uint64 `json:",string"`
hasAttachmentID bool
rawAttachmentID string
AttachmentID uint64 `json:",string"`
hasName bool
rawName string
Name string
hasKind bool
rawKind string
Kind string
}
// NewAttachmentOriginal request
func NewAttachmentOriginal() *AttachmentOriginal {
return &AttachmentOriginal{}
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentOriginal) Auditable() map[string]interface{} {
var out = map[string]interface{}{}
out["download"] = r.Download
out["sign"] = r.Sign
out["userID"] = r.UserID
out["attachmentID"] = r.AttachmentID
out["name"] = r.Name
out["kind"] = r.Kind
return out
}
// Fill processes request and fills internal variables
func (r *AttachmentOriginal) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = req.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := req.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := req.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := get["download"]; ok {
r.hasDownload = true
r.rawDownload = val
r.Download = parseBool(val)
}
if val, ok := get["sign"]; ok {
r.hasSign = true
r.rawSign = val
r.Sign = val
}
if val, ok := get["userID"]; ok {
r.hasUserID = true
r.rawUserID = val
r.UserID = parseUInt64(val)
}
r.hasAttachmentID = true
r.rawAttachmentID = chi.URLParam(req, "attachmentID")
r.AttachmentID = parseUInt64(chi.URLParam(req, "attachmentID"))
r.hasName = true
r.rawName = chi.URLParam(req, "name")
r.Name = chi.URLParam(req, "name")
r.hasKind = true
r.rawKind = chi.URLParam(req, "kind")
r.Kind = chi.URLParam(req, "kind")
return err
}
var _ RequestFiller = NewAttachmentOriginal()
// AttachmentPreview request parameters
type AttachmentPreview struct {
hasAttachmentID bool
rawAttachmentID string
AttachmentID uint64 `json:",string"`
hasExt bool
rawExt string
Ext string
hasKind bool
rawKind string
Kind string
hasSign bool
rawSign string
Sign string
hasUserID bool
rawUserID string
UserID uint64 `json:",string"`
}
// NewAttachmentPreview request
func NewAttachmentPreview() *AttachmentPreview {
return &AttachmentPreview{}
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentPreview) Auditable() map[string]interface{} {
var out = map[string]interface{}{}
out["attachmentID"] = r.AttachmentID
out["ext"] = r.Ext
out["kind"] = r.Kind
out["sign"] = r.Sign
out["userID"] = r.UserID
return out
}
// Fill processes request and fills internal variables
func (r *AttachmentPreview) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = req.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := req.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := req.Form
for name, param := range postVars {
post[name] = string(param[0])
}
r.hasAttachmentID = true
r.rawAttachmentID = chi.URLParam(req, "attachmentID")
r.AttachmentID = parseUInt64(chi.URLParam(req, "attachmentID"))
r.hasExt = true
r.rawExt = chi.URLParam(req, "ext")
r.Ext = chi.URLParam(req, "ext")
r.hasKind = true
r.rawKind = chi.URLParam(req, "kind")
r.Kind = chi.URLParam(req, "kind")
if val, ok := get["sign"]; ok {
r.hasSign = true
r.rawSign = val
r.Sign = val
}
if val, ok := get["userID"]; ok {
r.hasUserID = true
r.rawUserID = val
r.UserID = parseUInt64(val)
}
return err
}
var _ RequestFiller = NewAttachmentPreview()
// HasAttachmentID returns true if attachmentID was set
func (r *AttachmentRead) HasAttachmentID() bool {
return r.hasAttachmentID
}
// RawAttachmentID returns raw value of attachmentID parameter
func (r *AttachmentRead) RawAttachmentID() string {
return r.rawAttachmentID
}
// GetAttachmentID returns casted value of attachmentID parameter
func (r *AttachmentRead) GetAttachmentID() uint64 {
return r.AttachmentID
}
// HasKind returns true if kind was set
func (r *AttachmentRead) HasKind() bool {
return r.hasKind
}
// RawKind returns raw value of kind parameter
func (r *AttachmentRead) RawKind() string {
return r.rawKind
}
// GetKind returns casted value of kind parameter
func (r *AttachmentRead) GetKind() string {
return r.Kind
}
// HasSign returns true if sign was set
func (r *AttachmentRead) HasSign() bool {
return r.hasSign
}
// RawSign returns raw value of sign parameter
func (r *AttachmentRead) RawSign() string {
return r.rawSign
}
// GetSign returns casted value of sign parameter
func (r *AttachmentRead) GetSign() string {
return r.Sign
}
// HasUserID returns true if userID was set
func (r *AttachmentRead) HasUserID() bool {
return r.hasUserID
}
// RawUserID returns raw value of userID parameter
func (r *AttachmentRead) RawUserID() string {
return r.rawUserID
}
// GetUserID returns casted value of userID parameter
func (r *AttachmentRead) GetUserID() uint64 {
return r.UserID
}
// HasAttachmentID returns true if attachmentID was set
func (r *AttachmentDelete) HasAttachmentID() bool {
return r.hasAttachmentID
}
// RawAttachmentID returns raw value of attachmentID parameter
func (r *AttachmentDelete) RawAttachmentID() string {
return r.rawAttachmentID
}
// GetAttachmentID returns casted value of attachmentID parameter
func (r *AttachmentDelete) GetAttachmentID() uint64 {
return r.AttachmentID
}
// HasKind returns true if kind was set
func (r *AttachmentDelete) HasKind() bool {
return r.hasKind
}
// RawKind returns raw value of kind parameter
func (r *AttachmentDelete) RawKind() string {
return r.rawKind
}
// GetKind returns casted value of kind parameter
func (r *AttachmentDelete) GetKind() string {
return r.Kind
}
// HasSign returns true if sign was set
func (r *AttachmentDelete) HasSign() bool {
return r.hasSign
}
// RawSign returns raw value of sign parameter
func (r *AttachmentDelete) RawSign() string {
return r.rawSign
}
// GetSign returns casted value of sign parameter
func (r *AttachmentDelete) GetSign() string {
return r.Sign
}
// HasUserID returns true if userID was set
func (r *AttachmentDelete) HasUserID() bool {
return r.hasUserID
}
// RawUserID returns raw value of userID parameter
func (r *AttachmentDelete) RawUserID() string {
return r.rawUserID
}
// GetUserID returns casted value of userID parameter
func (r *AttachmentDelete) GetUserID() uint64 {
return r.UserID
}
// HasDownload returns true if download was set
func (r *AttachmentOriginal) HasDownload() bool {
return r.hasDownload
}
// RawDownload returns raw value of download parameter
func (r *AttachmentOriginal) RawDownload() string {
return r.rawDownload
}
// GetDownload returns casted value of download parameter
func (r *AttachmentOriginal) GetDownload() bool {
return r.Download
}
// HasSign returns true if sign was set
func (r *AttachmentOriginal) HasSign() bool {
return r.hasSign
}
// RawSign returns raw value of sign parameter
func (r *AttachmentOriginal) RawSign() string {
return r.rawSign
}
// GetSign returns casted value of sign parameter
func (r *AttachmentOriginal) GetSign() string {
return r.Sign
}
// HasUserID returns true if userID was set
func (r *AttachmentOriginal) HasUserID() bool {
return r.hasUserID
}
// RawUserID returns raw value of userID parameter
func (r *AttachmentOriginal) RawUserID() string {
return r.rawUserID
}
// GetUserID returns casted value of userID parameter
func (r *AttachmentOriginal) GetUserID() uint64 {
return r.UserID
}
// HasAttachmentID returns true if attachmentID was set
func (r *AttachmentOriginal) HasAttachmentID() bool {
return r.hasAttachmentID
}
// RawAttachmentID returns raw value of attachmentID parameter
func (r *AttachmentOriginal) RawAttachmentID() string {
return r.rawAttachmentID
}
// GetAttachmentID returns casted value of attachmentID parameter
func (r *AttachmentOriginal) GetAttachmentID() uint64 {
return r.AttachmentID
}
// HasName returns true if name was set
func (r *AttachmentOriginal) HasName() bool {
return r.hasName
}
// RawName returns raw value of name parameter
func (r *AttachmentOriginal) RawName() string {
return r.rawName
}
// GetName returns casted value of name parameter
func (r *AttachmentOriginal) GetName() string {
return r.Name
}
// HasKind returns true if kind was set
func (r *AttachmentOriginal) HasKind() bool {
return r.hasKind
}
// RawKind returns raw value of kind parameter
func (r *AttachmentOriginal) RawKind() string {
return r.rawKind
}
// GetKind returns casted value of kind parameter
func (r *AttachmentOriginal) GetKind() string {
return r.Kind
}
// HasAttachmentID returns true if attachmentID was set
func (r *AttachmentPreview) HasAttachmentID() bool {
return r.hasAttachmentID
}
// RawAttachmentID returns raw value of attachmentID parameter
func (r *AttachmentPreview) RawAttachmentID() string {
return r.rawAttachmentID
}
// GetAttachmentID returns casted value of attachmentID parameter
func (r *AttachmentPreview) GetAttachmentID() uint64 {
return r.AttachmentID
}
// HasExt returns true if ext was set
func (r *AttachmentPreview) HasExt() bool {
return r.hasExt
}
// RawExt returns raw value of ext parameter
func (r *AttachmentPreview) RawExt() string {
return r.rawExt
}
// GetExt returns casted value of ext parameter
func (r *AttachmentPreview) GetExt() string {
return r.Ext
}
// HasKind returns true if kind was set
func (r *AttachmentPreview) HasKind() bool {
return r.hasKind
}
// RawKind returns raw value of kind parameter
func (r *AttachmentPreview) RawKind() string {
return r.rawKind
}
// GetKind returns casted value of kind parameter
func (r *AttachmentPreview) GetKind() string {
return r.Kind
}
// HasSign returns true if sign was set
func (r *AttachmentPreview) HasSign() bool {
return r.hasSign
}
// RawSign returns raw value of sign parameter
func (r *AttachmentPreview) RawSign() string {
return r.rawSign
}
// GetSign returns casted value of sign parameter
func (r *AttachmentPreview) GetSign() string {
return r.Sign
}
// HasUserID returns true if userID was set
func (r *AttachmentPreview) HasUserID() bool {
return r.hasUserID
}
// RawUserID returns raw value of userID parameter
func (r *AttachmentPreview) RawUserID() string {
return r.rawUserID
}
// GetUserID returns casted value of userID parameter
func (r *AttachmentPreview) GetUserID() uint64 {
return r.UserID
}
+124
View File
@@ -214,6 +214,85 @@ func (r *SettingsGet) Fill(req *http.Request) (err error) {
var _ RequestFiller = NewSettingsGet()
// SettingsSet request parameters
type SettingsSet struct {
hasKey bool
rawKey string
Key string
hasUpload bool
rawUpload string
Upload *multipart.FileHeader
hasOwnerID bool
rawOwnerID string
OwnerID uint64 `json:",string"`
}
// NewSettingsSet request
func NewSettingsSet() *SettingsSet {
return &SettingsSet{}
}
// Auditable returns all auditable/loggable parameters
func (r SettingsSet) Auditable() map[string]interface{} {
var out = map[string]interface{}{}
out["key"] = r.Key
out["upload.size"] = r.Upload.Size
out["upload.filename"] = r.Upload.Filename
out["ownerID"] = r.OwnerID
return out
}
// Fill processes request and fills internal variables
func (r *SettingsSet) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = req.ParseMultipartForm(32 << 20); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := req.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := req.Form
for name, param := range postVars {
post[name] = string(param[0])
}
r.hasKey = true
r.rawKey = chi.URLParam(req, "key")
r.Key = chi.URLParam(req, "key")
if _, r.Upload, err = req.FormFile("upload"); err != nil {
return errors.Wrap(err, "error processing uploaded file")
}
if val, ok := post["ownerID"]; ok {
r.hasOwnerID = true
r.rawOwnerID = val
r.OwnerID = parseUInt64(val)
}
return err
}
var _ RequestFiller = NewSettingsSet()
// SettingsCurrent request parameters
type SettingsCurrent struct {
}
@@ -322,3 +401,48 @@ func (r *SettingsGet) RawKey() string {
func (r *SettingsGet) GetKey() string {
return r.Key
}
// HasKey returns true if key was set
func (r *SettingsSet) HasKey() bool {
return r.hasKey
}
// RawKey returns raw value of key parameter
func (r *SettingsSet) RawKey() string {
return r.rawKey
}
// GetKey returns casted value of key parameter
func (r *SettingsSet) GetKey() string {
return r.Key
}
// HasUpload returns true if upload was set
func (r *SettingsSet) HasUpload() bool {
return r.hasUpload
}
// RawUpload returns raw value of upload parameter
func (r *SettingsSet) RawUpload() string {
return r.rawUpload
}
// GetUpload returns casted value of upload parameter
func (r *SettingsSet) GetUpload() *multipart.FileHeader {
return r.Upload
}
// HasOwnerID returns true if ownerID was set
func (r *SettingsSet) HasOwnerID() bool {
return r.hasOwnerID
}
// RawOwnerID returns raw value of ownerID parameter
func (r *SettingsSet) RawOwnerID() string {
return r.rawOwnerID
}
// GetOwnerID returns casted value of ownerID parameter
func (r *SettingsSet) GetOwnerID() uint64 {
return r.OwnerID
}
+1
View File
@@ -12,6 +12,7 @@ func MountRoutes(r chi.Router) {
NewExternalAuth().ApiServerRoutes(r)
r.Group(func(r chi.Router) {
handlers.NewAttachment(Attachment{}.New()).MountRoutes(r)
handlers.NewAuth((Auth{}).New()).MountRoutes(r)
handlers.NewAuthInternal((AuthInternal{}).New()).MountRoutes(r)
+35 -1
View File
@@ -2,16 +2,18 @@ package rest
import (
"context"
"fmt"
"github.com/cortezaproject/corteza-server/pkg/settings"
"github.com/cortezaproject/corteza-server/system/rest/request"
"github.com/cortezaproject/corteza-server/system/service"
"strconv"
)
type (
Settings struct {
svc struct {
settings settings.Service
att service.AttachmentService
}
}
)
@@ -19,6 +21,7 @@ type (
func (Settings) New() *Settings {
ctrl := &Settings{}
ctrl.svc.settings = service.DefaultSettings
ctrl.svc.att = service.DefaultAttachment
return ctrl
}
@@ -47,6 +50,37 @@ func (ctrl *Settings) Get(ctx context.Context, r *request.SettingsGet) (interfac
}
}
func (ctrl *Settings) Set(ctx context.Context, r *request.SettingsSet) (interface{}, error) {
if r.Upload != nil {
file, err := r.Upload.Open()
if err != nil {
return nil, err
}
defer file.Close()
// @todo this whole attachment + settings logic must be moved to settings service
// this can be done when we generalize attachment handling
// and move that our of sys/msg/cmp to pkg
att, err := ctrl.svc.att.With(ctx).CreateSettingsAttachment(
r.Upload.Filename,
r.Upload.Size,
file,
map[string]string{"key": r.Key, "ownedBy": strconv.FormatUint(r.OwnerID, 10)},
)
s := &settings.Value{Name: r.Key, OwnedBy: r.OwnerID}
if err = s.SetValue(fmt.Sprintf("attachment:%d", att.ID)); err != nil {
return nil, err
}
return s, ctrl.svc.settings.Set(ctx, s)
}
return nil, nil
}
// Current settings, structured
//
// This is available to all authenticated users
+308
View File
@@ -0,0 +1,308 @@
package service
import (
"bytes"
"context"
"github.com/cortezaproject/corteza-server/pkg/settings"
"image"
"image/gif"
"io"
"net/http"
"path"
"strings"
"github.com/disintegration/imaging"
"github.com/edwvee/exiffix"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
intAuth "github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/logger"
"github.com/cortezaproject/corteza-server/pkg/store"
"github.com/cortezaproject/corteza-server/system/repository"
"github.com/cortezaproject/corteza-server/system/types"
)
const (
attachmentPreviewMaxWidth = 320
attachmentPreviewMaxHeight = 180
)
type (
attachment struct {
db *factory.DB
ctx context.Context
logger *zap.Logger
store store.Store
ac attachmentAccessController
settingsSvc settings.Service
attachment repository.AttachmentRepository
}
attachmentAccessController interface {
CanManageSettings(context.Context) bool
}
AttachmentService interface {
With(ctx context.Context) AttachmentService
FindByID(attachmentID uint64) (*types.Attachment, error)
Find(filter types.AttachmentFilter) (types.AttachmentSet, types.AttachmentFilter, error)
CreateSettingsAttachment(name string, size int64, fh io.ReadSeeker, labels map[string]string) (*types.Attachment, error)
OpenOriginal(att *types.Attachment) (io.ReadSeeker, error)
OpenPreview(att *types.Attachment) (io.ReadSeeker, error)
DeleteByID(attachmentID uint64) error
}
)
func Attachment(store store.Store) AttachmentService {
return (&attachment{
logger: DefaultLogger.Named("attachment"),
store: store,
ac: DefaultAccessControl,
settingsSvc: DefaultSettings,
}).With(context.Background())
}
func (svc attachment) With(ctx context.Context) AttachmentService {
db := repository.DB(ctx)
return &attachment{
db: db,
ctx: ctx,
logger: svc.logger,
ac: svc.ac,
settingsSvc: svc.settingsSvc,
store: svc.store,
attachment: repository.Attachment(ctx, db),
}
}
// log() returns zap's logger with requestID from current context and fields.
func (svc attachment) log(fields ...zapcore.Field) *zap.Logger {
return logger.AddRequestID(svc.ctx, svc.logger).With(fields...)
}
func (svc attachment) FindByID(attachmentID uint64) (*types.Attachment, error) {
return svc.attachment.FindByID(attachmentID)
}
func (svc attachment) DeleteByID(attachmentID uint64) error {
return svc.attachment.DeleteByID(attachmentID)
}
func (svc attachment) Find(filter types.AttachmentFilter) (types.AttachmentSet, types.AttachmentFilter, error) {
return svc.attachment.Find(filter)
}
func (svc attachment) OpenOriginal(att *types.Attachment) (io.ReadSeeker, error) {
if len(att.Url) == 0 {
return nil, nil
}
return svc.store.Open(att.Url)
}
func (svc attachment) OpenPreview(att *types.Attachment) (io.ReadSeeker, error) {
if len(att.PreviewUrl) == 0 {
return nil, nil
}
return svc.store.Open(att.PreviewUrl)
}
func (svc attachment) CreateSettingsAttachment(name string, size int64, fh io.ReadSeeker, labels map[string]string) (att *types.Attachment, err error) {
var (
currentUserID uint64 = intAuth.GetIdentityFromContext(svc.ctx).Identity()
)
if !svc.ac.CanManageSettings(svc.ctx) {
return nil, ErrNoUpdatePermissions.withStack()
}
att = &types.Attachment{
ID: factory.Sonyflake.NextID(),
OwnerID: currentUserID,
Name: strings.TrimSpace(name),
Kind: types.AttachmentKindSettings,
}
if labels != nil {
att.Meta.Labels = labels
}
if err = svc.create(name, size, fh, att); err != nil {
return nil, err
}
return att, err
}
func (svc attachment) create(name string, size int64, fh io.ReadSeeker, att *types.Attachment) (err error) {
if svc.store == nil {
return errors.New("Can not create attachment: store handler not set")
}
log := svc.log(
zap.String("name", att.Name),
zap.Int64("size", att.Meta.Original.Size),
)
// Extract extension but make sure path.Ext is not confused by any leading/trailing dots
att.Meta.Original.Extension = strings.Trim(path.Ext(strings.Trim(name, ".")), ".")
att.Meta.Original.Size = size
if att.Meta.Original.Mimetype, err = svc.extractMimetype(fh); err != nil {
log.Error("could not extract mime-type", zap.Error(err))
return
}
att.Url = svc.store.Original(att.ID, att.Meta.Original.Extension)
log = log.With(zap.String("url", att.Url))
if err = svc.store.Save(att.Url, fh); err != nil {
log.Error("could not store file", zap.Error(err))
return
}
// Process image: extract width, height, make preview
err = svc.processImage(fh, att)
if err != nil {
log.Error("could not process image", zap.Error(err))
}
return svc.db.Transaction(func() (err error) {
if att, err = svc.attachment.Create(att); err != nil {
return
}
return nil
})
}
func (svc attachment) extractMimetype(file io.ReadSeeker) (mimetype string, err error) {
if _, err = file.Seek(0, 0); err != nil {
return
}
// Make sure we rewind when we're done
defer file.Seek(0, 0)
// See http.DetectContentType about 512 bytes
var buf = make([]byte, 512)
if _, err = file.Read(buf); err != nil {
return
}
return http.DetectContentType(buf), nil
}
func (svc attachment) processImage(original io.ReadSeeker, att *types.Attachment) (err error) {
if !strings.HasPrefix(att.Meta.Original.Mimetype, "image/") {
// Only supporting previews from images (for now)
return
}
var (
preview image.Image
opts []imaging.EncodeOption
format imaging.Format
previewFormat imaging.Format
animated bool
f2m = map[imaging.Format]string{
imaging.JPEG: "image/jpeg",
imaging.GIF: "image/gif",
}
f2e = map[imaging.Format]string{
imaging.JPEG: "jpg",
imaging.GIF: "gif",
}
)
if _, err = original.Seek(0, 0); err != nil {
return
}
if format, err = imaging.FormatFromExtension(att.Meta.Original.Extension); err != nil {
return errors.Wrapf(err, "Could not get format from extension '%s'", att.Meta.Original.Extension)
}
previewFormat = format
if imaging.JPEG == format {
// Rotate image if needed
// if preview, _, err = exiffix.Decode(original); err != nil {
// return errors.Wrapf(err, "Could not decode EXIF from JPEG")
// }
preview, _, _ = exiffix.Decode(original)
}
if imaging.GIF == format {
// Decode all and check loops & delay to determine if GIF is animated or not
if cfg, err := gif.DecodeAll(original); err == nil {
animated = cfg.LoopCount > 0 || len(cfg.Delay) > 1
// Use first image for the preview
preview = cfg.Image[0]
} else {
return errors.Wrapf(err, "Could not decode gif config")
}
} else {
// Use GIF preview for GIFs and JPEG for everything else!
previewFormat = imaging.JPEG
// Store with a bit lower quality
opts = append(opts, imaging.JPEGQuality(85))
}
// In case of JPEG we decode the image and rotate it beforehand
// other cases are handled here
if preview == nil {
if preview, err = imaging.Decode(original); err != nil {
return errors.Wrapf(err, "Could not decode original image")
}
}
var width, height = preview.Bounds().Max.X, preview.Bounds().Max.Y
att.SetOriginalImageMeta(width, height, animated)
if width > attachmentPreviewMaxWidth && width > height {
// Landscape does not fit
preview = imaging.Resize(preview, attachmentPreviewMaxWidth, 0, imaging.Lanczos)
} else if height > attachmentPreviewMaxHeight {
// Height does not fit
preview = imaging.Resize(preview, 0, attachmentPreviewMaxHeight, imaging.Lanczos)
}
// Get dimensions from the preview
width, height = preview.Bounds().Max.X, preview.Bounds().Max.Y
var buf = &bytes.Buffer{}
if err = imaging.Encode(buf, preview, previewFormat, opts...); err != nil {
return
}
meta := att.SetPreviewImageMeta(width, height, false)
meta.Size = int64(buf.Len())
meta.Mimetype = f2m[previewFormat]
meta.Extension = f2e[previewFormat]
// Can and how we make a preview of this attachment?
att.PreviewUrl = svc.store.Preview(att.ID, meta.Extension)
return svc.store.Save(att.PreviewUrl, buf)
}
var _ AttachmentService = &attachment{}
+42
View File
@@ -2,6 +2,9 @@ package service
import (
"context"
"github.com/cortezaproject/corteza-server/pkg/store"
"github.com/cortezaproject/corteza-server/pkg/store/minio"
"github.com/cortezaproject/corteza-server/pkg/store/plain"
"go.uber.org/zap"
@@ -42,6 +45,8 @@ type (
)
var (
DefaultStore store.Store
DefaultLogger *zap.Logger
// CurrentSubscription holds current subscription info,
@@ -77,6 +82,7 @@ var (
DefaultOrganisation OrganisationService
DefaultApplication ApplicationService
DefaultReminder ReminderService
DefaultAttachment AttachmentService
DefaultStatistics *statistics
)
@@ -99,6 +105,41 @@ func Initialize(ctx context.Context, log *zap.Logger, c Config) (err error) {
CurrentSettings,
)
if DefaultStore == nil {
const svcPath = "compose"
if c.Storage.MinioEndpoint != "" {
var bucket = svcPath
if c.Storage.MinioBucket != "" {
bucket = c.Storage.MinioBucket + "/" + svcPath
}
DefaultStore, err = minio.New(bucket, minio.Options{
Endpoint: c.Storage.MinioEndpoint,
Secure: c.Storage.MinioSecure,
Strict: c.Storage.MinioStrict,
AccessKeyID: c.Storage.MinioAccessKey,
SecretAccessKey: c.Storage.MinioSecretKey,
ServerSideEncryptKey: []byte(c.Storage.MinioSSECKey),
})
log.Info("initializing minio",
zap.String("bucket", bucket),
zap.String("endpoint", c.Storage.MinioEndpoint),
zap.Error(err))
} else {
path := c.Storage.Path + "/" + svcPath
DefaultStore, err = plain.New(path)
log.Info("initializing store",
zap.String("path", path),
zap.Error(err))
}
if err != nil {
return err
}
}
DefaultAuthNotification = AuthNotification(ctx)
DefaultAuth = Auth(ctx)
DefaultUser = User(ctx)
@@ -108,6 +149,7 @@ func Initialize(ctx context.Context, log *zap.Logger, c Config) (err error) {
DefaultReminder = Reminder(ctx)
DefaultSink = Sink()
DefaultStatistics = Statistics(ctx)
DefaultAttachment = Attachment(DefaultStore)
return
}
+67
View File
@@ -0,0 +1,67 @@
package types
// Hello! This file is auto-generated.
type (
// AttachmentSet slice of Attachment
//
// This type is auto-generated.
AttachmentSet []*Attachment
)
// Walk iterates through every slice item and calls w(Attachment) err
//
// This function is auto-generated.
func (set AttachmentSet) Walk(w func(*Attachment) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(Attachment) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set AttachmentSet) Filter(f func(*Attachment) (bool, error)) (out AttachmentSet, err error) {
var ok bool
out = AttachmentSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}
// FindByID finds items from slice by its ID property
//
// This function is auto-generated.
func (set AttachmentSet) FindByID(ID uint64) *Attachment {
for i := range set {
if set[i].ID == ID {
return set[i]
}
}
return nil
}
// IDs returns a slice of uint64s from all items in the set
//
// This function is auto-generated.
func (set AttachmentSet) IDs() (IDs []uint64) {
IDs = make([]uint64, len(set))
for i := range set {
IDs[i] = set[i].ID
}
return
}
+102
View File
@@ -0,0 +1,102 @@
package types
import (
"testing"
"errors"
"github.com/stretchr/testify/require"
)
// Hello! This file is auto-generated.
func TestAttachmentSetWalk(t *testing.T) {
var (
value = make(AttachmentSet, 3)
req = require.New(t)
)
// check walk with no errors
{
err := value.Walk(func(*Attachment) error {
return nil
})
req.NoError(err)
}
// check walk with error
req.Error(value.Walk(func(*Attachment) error { return errors.New("walk error") }))
}
func TestAttachmentSetFilter(t *testing.T) {
var (
value = make(AttachmentSet, 3)
req = require.New(t)
)
// filter nothing
{
set, err := value.Filter(func(*Attachment) (bool, error) {
return true, nil
})
req.NoError(err)
req.Equal(len(set), len(value))
}
// filter one item
{
found := false
set, err := value.Filter(func(*Attachment) (bool, error) {
if !found {
found = true
return found, nil
}
return false, nil
})
req.NoError(err)
req.Len(set, 1)
}
// filter error
{
_, err := value.Filter(func(*Attachment) (bool, error) {
return false, errors.New("filter error")
})
req.Error(err)
}
}
func TestAttachmentSetIDs(t *testing.T) {
var (
value = make(AttachmentSet, 3)
req = require.New(t)
)
// construct objects
value[0] = new(Attachment)
value[1] = new(Attachment)
value[2] = new(Attachment)
// set ids
value[0].ID = 1
value[1].ID = 2
value[2].ID = 3
// Find existing
{
val := value.FindByID(2)
req.Equal(uint64(2), val.ID)
}
// Find non-existing
{
val := value.FindByID(4)
req.Nil(val)
}
// List IDs from set
{
val := value.IDs()
req.Equal(len(val), len(value))
}
}
+104
View File
@@ -0,0 +1,104 @@
package types
import (
"database/sql/driver"
"encoding/json"
"time"
"github.com/pkg/errors"
"github.com/cortezaproject/corteza-server/pkg/rh"
)
type (
Attachment struct {
ID uint64 `db:"id" json:"attachmentID,string"`
OwnerID uint64 `db:"rel_owner" json:"ownerID,string"`
Kind string `db:"kind" json:"-"`
Url string `db:"url" json:"url,omitempty"`
PreviewUrl string `db:"preview_url" json:"previewUrl,omitempty"`
Name string `db:"name" json:"name,omitempty"`
Meta attachmentMeta `db:"meta" json:"meta"`
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"`
}
// AttachmentFilter is used for filtering and as a return value from Find
AttachmentFilter struct {
Kind string `json:"kind,omitempty"`
Filter string `json:"filter"`
Sort string `json:"sort"`
// Standard paging fields & helpers
rh.PageFilter
}
attachmentImageMeta struct {
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Animated bool `json:"animated"`
}
attachmentFileMeta struct {
Size int64 `json:"size"`
Extension string `json:"ext"`
Mimetype string `json:"mimetype"`
Image *attachmentImageMeta `json:"image,omitempty"`
}
attachmentMeta struct {
Original attachmentFileMeta `json:"original"`
Preview *attachmentFileMeta `json:"preview,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
}
)
const (
AttachmentKindSettings string = "settings"
)
func (a *Attachment) SetOriginalImageMeta(width, height int, animated bool) *attachmentFileMeta {
a.imageMeta(&a.Meta.Original, width, height, animated)
return &a.Meta.Original
}
func (a *Attachment) SetPreviewImageMeta(width, height int, animated bool) *attachmentFileMeta {
if a.Meta.Preview == nil {
a.Meta.Preview = &attachmentFileMeta{}
}
a.imageMeta(a.Meta.Preview, width, height, animated)
return a.Meta.Preview
}
func (a *Attachment) imageMeta(in *attachmentFileMeta, width, height int, animated bool) {
if in.Image == nil {
in.Image = &attachmentImageMeta{}
}
if width > 0 && height > 0 {
in.Image.Animated = animated
in.Image.Width = width
in.Image.Height = height
}
}
func (meta *attachmentMeta) Scan(value interface{}) error {
//lint:ignore S1034 This typecast is intentional, we need to get []byte out of a []uint8
switch value.(type) {
case nil:
*meta = attachmentMeta{}
case []uint8:
if err := json.Unmarshal(value.([]byte), meta); err != nil {
return errors.Wrapf(err, "Can not scan '%v' into attachmentMeta", value)
}
}
return nil
}
func (meta attachmentMeta) Value() (driver.Value, error) {
return json.Marshal(meta)
}