get attachment endpoint

This commit is contained in:
Hayden 2022-09-19 13:12:07 -08:00
parent 48036eabce
commit c61ac295ba
9 changed files with 326 additions and 24 deletions

View file

@ -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"
},

View file

@ -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"
},

View file

@ -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:

View file

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

View file

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

View file

@ -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

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

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

View file

@ -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;