feat: item-attachments CRUD (#22)

* change /content/ -> /homebox/

* add cache to code generators

* update env variables to set data storage

* update env variables

* set env variables in prod container

* implement attachment post route (WIP)

* get attachment endpoint

* attachment download

* implement string utilities lib

* implement generic drop zone

* use explicit truncate

* remove clean dir

* drop strings composable for lib

* update item types and add attachments

* add attachment API

* implement service context

* consolidate API code

* implement editing attachments

* implement upload limit configuration

* improve error handling

* add docs for max upload size

* fix test cases
This commit is contained in:
Hayden 2022-09-24 11:33:38 -08:00 committed by GitHub
parent 852d312ba7
commit 31b34241e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
165 changed files with 2509 additions and 664 deletions

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 {
filename := strings.ToLower(filepath.Base(path))
for _, n := range neighbors {
if strings.ToLower(n) == filename {
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,94 @@
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{"bar.pdf", "bar (1).pdf", "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

@ -4,22 +4,47 @@ import (
"net/http"
)
type ValidationError struct {
Field string `json:"field"`
Reason string `json:"reason"`
}
type ValidationErrors []ValidationError
func (ve *ValidationErrors) HasErrors() bool {
if (ve == nil) || (len(*ve) == 0) {
return false
}
for _, err := range *ve {
if err.Field != "" {
return true
}
}
return false
}
func (ve ValidationErrors) Append(field, reasons string) ValidationErrors {
return append(ve, ValidationError{
Field: field,
Reason: reasons,
})
}
// ErrorBuilder is a helper type to build a response that contains an array of errors.
// Typical use cases are for returning an array of validation errors back to the user.
//
// Example:
//
//
// {
// "errors": [
// "invalid id",
// "invalid name",
// "invalid description"
// ],
// "message": "Unprocessable Entity",
// "status": 422
// }
//
// {
// "errors": [
// "invalid id",
// "invalid name",
// "invalid description"
// ],
// "message": "Unprocessable Entity",
// "status": 422
// }
type ErrorBuilder struct {
errs []string
}

View file

@ -7,7 +7,7 @@ import (
"net/http/httptest"
"testing"
"github.com/hay-kot/content/backend/pkgs/faker"
"github.com/hay-kot/homebox/backend/pkgs/faker"
"github.com/stretchr/testify/assert"
)