mirror of
https://github.com/hay-kot/homebox.git
synced 2025-08-05 09:10:26 +00:00
get attachment endpoint
This commit is contained in:
parent
48036eabce
commit
c61ac295ba
9 changed files with 326 additions and 24 deletions
|
@ -278,6 +278,43 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v1/items/{id}/attachment/{attachment_id}": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/octet-stream"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Items"
|
||||||
|
],
|
||||||
|
"summary": "retrieves an attachment for an item",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Item ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Attachment ID",
|
||||||
|
"name": "attachment_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v1/labels": {
|
"/v1/labels": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
@ -1242,9 +1279,6 @@ const docTemplate = `{
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"groupId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -1271,9 +1305,6 @@ const docTemplate = `{
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"groupId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
|
@ -270,6 +270,43 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v1/items/{id}/attachment/{attachment_id}": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/octet-stream"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Items"
|
||||||
|
],
|
||||||
|
"summary": "retrieves an attachment for an item",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Item ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Attachment ID",
|
||||||
|
"name": "attachment_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v1/labels": {
|
"/v1/labels": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
@ -1234,9 +1271,6 @@
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"groupId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -1263,9 +1297,6 @@
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"groupId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
|
@ -266,8 +266,6 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
description:
|
description:
|
||||||
type: string
|
type: string
|
||||||
groupId:
|
|
||||||
type: string
|
|
||||||
id:
|
id:
|
||||||
type: string
|
type: string
|
||||||
items:
|
items:
|
||||||
|
@ -285,8 +283,6 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
description:
|
description:
|
||||||
type: string
|
type: string
|
||||||
groupId:
|
|
||||||
type: string
|
|
||||||
id:
|
id:
|
||||||
type: string
|
type: string
|
||||||
name:
|
name:
|
||||||
|
@ -540,6 +536,29 @@ paths:
|
||||||
summary: imports items into the database
|
summary: imports items into the database
|
||||||
tags:
|
tags:
|
||||||
- Items
|
- Items
|
||||||
|
/v1/items/{id}/attachment/{attachment_id}:
|
||||||
|
get:
|
||||||
|
parameters:
|
||||||
|
- description: Item ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Attachment ID
|
||||||
|
in: path
|
||||||
|
name: attachment_id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/octet-stream
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: ""
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
summary: retrieves an attachment for an item
|
||||||
|
tags:
|
||||||
|
- Items
|
||||||
/v1/items/import:
|
/v1/items/import:
|
||||||
post:
|
post:
|
||||||
parameters:
|
parameters:
|
||||||
|
|
|
@ -83,6 +83,7 @@ func (a *app) newRouter(repos *repo.AllRepos) *chi.Mux {
|
||||||
r.Delete(v1Base("/items/{id}"), v1Ctrl.HandleItemDelete())
|
r.Delete(v1Base("/items/{id}"), v1Ctrl.HandleItemDelete())
|
||||||
|
|
||||||
r.Post(v1Base("/items/{id}/attachment"), v1Ctrl.HandleItemAttachmentCreate())
|
r.Post(v1Base("/items/{id}/attachment"), v1Ctrl.HandleItemAttachmentCreate())
|
||||||
|
r.Get(v1Base("/items/{id}/attachment/{attachment_id}"), v1Ctrl.HandleItemAttachmentGet())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,12 @@ package v1
|
||||||
import (
|
import (
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/hay-kot/homebox/backend/ent/attachment"
|
"github.com/hay-kot/homebox/backend/ent/attachment"
|
||||||
"github.com/hay-kot/homebox/backend/internal/services"
|
"github.com/hay-kot/homebox/backend/internal/services"
|
||||||
"github.com/hay-kot/homebox/backend/internal/types"
|
"github.com/hay-kot/homebox/backend/internal/types"
|
||||||
|
@ -91,8 +95,8 @@ func (ctrl *V1Controller) HandleItemDelete() http.HandlerFunc {
|
||||||
// @Summary Gets a item and fields
|
// @Summary Gets a item and fields
|
||||||
// @Tags Items
|
// @Tags Items
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "Item ID"
|
// @Param id path string true "Item ID"
|
||||||
// @Success 200 {object} types.ItemOut
|
// @Success 200 {object} types.ItemOut
|
||||||
// @Router /v1/items/{id} [GET]
|
// @Router /v1/items/{id} [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemGet() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemGet() http.HandlerFunc {
|
||||||
|
@ -196,12 +200,12 @@ func (ctrl *V1Controller) HandleItemsImport() http.HandlerFunc {
|
||||||
// @Summary imports items into the database
|
// @Summary imports items into the database
|
||||||
// @Tags Items
|
// @Tags Items
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "Item ID"
|
// @Param id path string true "Item ID"
|
||||||
// @Param file formData file true "File attachment"
|
// @Param file formData file true "File attachment"
|
||||||
// @Param type formData string true "Type of file"
|
// @Param type formData string true "Type of file"
|
||||||
// @Param name formData string true "name of the file including extension"
|
// @Param name formData string true "name of the file including extension"
|
||||||
// @Success 200 {object} types.ItemOut
|
// @Success 200 {object} types.ItemOut
|
||||||
// @Router /v1/items/{id}/attachment [Post]
|
// @Router /v1/items/{id}/attachment [POST]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -252,3 +256,40 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
|
||||||
server.Respond(w, http.StatusOK, item)
|
server.Respond(w, http.StatusOK, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleItemAttachmentGet godocs
|
||||||
|
// @Summary retrieves an attachment for an item
|
||||||
|
// @Tags Items
|
||||||
|
// @Produce application/octet-stream
|
||||||
|
// @Param id path string true "Item ID"
|
||||||
|
// @Param attachment_id path string true "Attachment ID"
|
||||||
|
// @Success 200
|
||||||
|
// @Router /v1/items/{id}/attachment/{attachment_id} [GET]
|
||||||
|
// @Security Bearer
|
||||||
|
func (ctrl *V1Controller) HandleItemAttachmentGet() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
uid, user, err := ctrl.partialParseIdAndUser(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentId, err := uuid.Parse(chi.URLParam(r, "attachment_id"))
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("failed to parse attachment_id param")
|
||||||
|
server.RespondError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := ctrl.svc.Items.GetAttachment(r.Context(), user.GroupID, uid, attachmentId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("failed to get attachment")
|
||||||
|
server.RespondServerError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filepath.Base(path)))
|
||||||
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||||||
|
http.ServeFile(w, r, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||||
"github.com/hay-kot/homebox/backend/internal/services/mappers"
|
"github.com/hay-kot/homebox/backend/internal/services/mappers"
|
||||||
"github.com/hay-kot/homebox/backend/internal/types"
|
"github.com/hay-kot/homebox/backend/internal/types"
|
||||||
|
"github.com/hay-kot/homebox/backend/pkgs/pathlib"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -95,7 +96,28 @@ func (svc *ItemService) Update(ctx context.Context, gid uuid.UUID, data types.It
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *ItemService) attachmentPath(gid, itemId uuid.UUID, filename string) string {
|
func (svc *ItemService) attachmentPath(gid, itemId uuid.UUID, filename string) string {
|
||||||
return filepath.Join(svc.filepath, gid.String(), itemId.String(), filename)
|
path := filepath.Join(svc.filepath, gid.String(), itemId.String(), filename)
|
||||||
|
return pathlib.Safe(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ItemService) GetAttachment(ctx context.Context, gid, itemId, attachmentId uuid.UUID) (string, error) {
|
||||||
|
// Get the Item
|
||||||
|
item, err := svc.repo.Items.GetOne(ctx, itemId)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.Edges.Group.ID != gid {
|
||||||
|
return "", ErrNotOwner
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the attachment
|
||||||
|
attachment, err := svc.repo.Attachments.Get(ctx, attachmentId)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return attachment.Edges.Document.Path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddAttachment adds an attachment to an item by creating an entry in the Documents table and linking it to the Attachment
|
// AddAttachment adds an attachment to an item by creating an entry in the Documents table and linking it to the Attachment
|
||||||
|
|
64
backend/pkgs/pathlib/pathlib.go
Normal file
64
backend/pkgs/pathlib/pathlib.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package pathlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dirReaderFunc func(name string) []string
|
||||||
|
|
||||||
|
var dirReader dirReaderFunc = func(directory string) []string {
|
||||||
|
f, err := os.Open(directory)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
names, err := f.Readdirnames(-1)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasConflict(path string, neighbors []string) bool {
|
||||||
|
path = strings.ToLower(path)
|
||||||
|
|
||||||
|
for _, n := range neighbors {
|
||||||
|
if strings.ToLower(n) == path {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safe will take a destination path and return a validated path that is safe to use.
|
||||||
|
// without overwriting any existing files. If a conflict exists, it will append a number
|
||||||
|
// to the end of the file name. If the parent directory does not exist this function will
|
||||||
|
// return the original path.
|
||||||
|
func Safe(path string) string {
|
||||||
|
parent := filepath.Dir(path)
|
||||||
|
|
||||||
|
neighbors := dirReader(parent)
|
||||||
|
if neighbors == nil {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasConflict(path, neighbors) {
|
||||||
|
ext := filepath.Ext(path)
|
||||||
|
|
||||||
|
name := strings.TrimSuffix(filepath.Base(path), ext)
|
||||||
|
|
||||||
|
for i := 1; i < 1000; i++ {
|
||||||
|
newName := fmt.Sprintf("%s (%d)%s", name, i, ext)
|
||||||
|
newPath := filepath.Join(parent, newName)
|
||||||
|
if !hasConflict(newPath, neighbors) {
|
||||||
|
return newPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
95
backend/pkgs/pathlib/pathlib_test.go
Normal file
95
backend/pkgs/pathlib/pathlib_test.go
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
package pathlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_hasConflict(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
path string
|
||||||
|
neighbors []string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no conflict",
|
||||||
|
args: args{
|
||||||
|
path: "foo",
|
||||||
|
neighbors: []string{"bar", "baz"},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "conflict",
|
||||||
|
args: args{
|
||||||
|
path: "foo",
|
||||||
|
neighbors: []string{"bar", "foo"},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "conflict with different case",
|
||||||
|
args: args{
|
||||||
|
path: "foo",
|
||||||
|
neighbors: []string{"bar", "Foo"},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := hasConflict(tt.args.path, tt.args.neighbors); got != tt.want {
|
||||||
|
t.Errorf("hasConflict() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSafePath(t *testing.T) {
|
||||||
|
// override dirReader
|
||||||
|
|
||||||
|
dirReader = func(name string) []string {
|
||||||
|
return []string{"/foo/bar.pdf", "/foo/bar (1).pdf", "/foo/bar (2).pdf"}
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no conflict",
|
||||||
|
args: args{
|
||||||
|
path: "/foo/foo.pdf",
|
||||||
|
},
|
||||||
|
want: "/foo/foo.pdf",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "conflict",
|
||||||
|
args: args{
|
||||||
|
path: "/foo/bar.pdf",
|
||||||
|
},
|
||||||
|
want: "/foo/bar (3).pdf",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "conflict with different case",
|
||||||
|
args: args{
|
||||||
|
path: "/foo/BAR.pdf",
|
||||||
|
},
|
||||||
|
want: "/foo/BAR (3).pdf",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := Safe(tt.args.path); got != tt.want {
|
||||||
|
t.Errorf("SafePath() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -192,7 +192,6 @@ export interface LabelCreate {
|
||||||
export interface LabelOut {
|
export interface LabelOut {
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
description: string;
|
description: string;
|
||||||
groupId: string;
|
|
||||||
id: string;
|
id: string;
|
||||||
items: ItemSummary[];
|
items: ItemSummary[];
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -202,7 +201,6 @@ export interface LabelOut {
|
||||||
export interface LabelSummary {
|
export interface LabelSummary {
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
description: string;
|
description: string;
|
||||||
groupId: string;
|
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue