Add support for attachment (system, settings)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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{}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user