mirror of
https://github.com/hay-kot/homebox.git
synced 2025-08-03 08:10:28 +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": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -1242,9 +1279,6 @@ const docTemplate = `{
|
|||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"groupId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -1271,9 +1305,6 @@ const docTemplate = `{
|
|||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"groupId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"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": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -1234,9 +1271,6 @@
|
|||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"groupId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -1263,9 +1297,6 @@
|
|||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"groupId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
@ -266,8 +266,6 @@ definitions:
|
|||
type: string
|
||||
description:
|
||||
type: string
|
||||
groupId:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
items:
|
||||
|
@ -285,8 +283,6 @@ definitions:
|
|||
type: string
|
||||
description:
|
||||
type: string
|
||||
groupId:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
|
@ -540,6 +536,29 @@ paths:
|
|||
summary: imports items into the database
|
||||
tags:
|
||||
- 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:
|
||||
post:
|
||||
parameters:
|
||||
|
|
|
@ -83,6 +83,7 @@ func (a *app) newRouter(repos *repo.AllRepos) *chi.Mux {
|
|||
r.Delete(v1Base("/items/{id}"), v1Ctrl.HandleItemDelete())
|
||||
|
||||
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 (
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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/internal/services"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
|
@ -91,8 +95,8 @@ func (ctrl *V1Controller) HandleItemDelete() http.HandlerFunc {
|
|||
// @Summary Gets a item and fields
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Success 200 {object} types.ItemOut
|
||||
// @Param id path string true "Item ID"
|
||||
// @Success 200 {object} types.ItemOut
|
||||
// @Router /v1/items/{id} [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemGet() http.HandlerFunc {
|
||||
|
@ -196,12 +200,12 @@ func (ctrl *V1Controller) HandleItemsImport() http.HandlerFunc {
|
|||
// @Summary imports items into the database
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param file formData file true "File attachment"
|
||||
// @Param type formData string true "Type of file"
|
||||
// @Param name formData string true "name of the file including extension"
|
||||
// @Success 200 {object} types.ItemOut
|
||||
// @Router /v1/items/{id}/attachment [Post]
|
||||
// @Success 200 {object} types.ItemOut
|
||||
// @Router /v1/items/{id}/attachment [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -252,3 +256,40 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
|
|||
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/services/mappers"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/pkgs/pathlib"
|
||||
"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 {
|
||||
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
|
||||
|
|
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 {
|
||||
createdAt: Date;
|
||||
description: string;
|
||||
groupId: string;
|
||||
id: string;
|
||||
items: ItemSummary[];
|
||||
name: string;
|
||||
|
@ -202,7 +201,6 @@ export interface LabelOut {
|
|||
export interface LabelSummary {
|
||||
createdAt: Date;
|
||||
description: string;
|
||||
groupId: string;
|
||||
id: string;
|
||||
name: string;
|
||||
updatedAt: Date;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue