server: implement local file management
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func initFileStore() FileStore {
|
||||
val, ok := os.LookupEnv("FILE_BACKEND")
|
||||
if !ok {
|
||||
slog.Error("FILE_BACKEND environment variable not set")
|
||||
os.Exit(1)
|
||||
}
|
||||
switch FileBackend(val) {
|
||||
case FileBackendLocal:
|
||||
localFilePath, ok := os.LookupEnv("LOCAL_FILEPATH")
|
||||
if !ok {
|
||||
slog.Error("LOCAL_FILEPATH environment variable not set")
|
||||
os.Exit(1)
|
||||
}
|
||||
return &LocalFileStore{BaseDir: localFilePath}
|
||||
}
|
||||
slog.Error("unsupported FILE_BACKEND", "value", val)
|
||||
os.Exit(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
type File struct {
|
||||
ID uuid.UUID
|
||||
Name string
|
||||
Created time.Time
|
||||
Backend FileBackend
|
||||
Path string
|
||||
}
|
||||
|
||||
type FileBackend string
|
||||
|
||||
const (
|
||||
FileBackendLocal FileBackend = "local"
|
||||
FileBackendS3 FileBackend = "s3"
|
||||
)
|
||||
|
||||
var Store FileStore
|
||||
|
||||
type FileStore interface {
|
||||
Save(name string, r io.Reader) (*File, error)
|
||||
URL(file *File) (string, error)
|
||||
}
|
||||
|
||||
type LocalFileStore struct {
|
||||
BaseDir string
|
||||
}
|
||||
|
||||
func (s *LocalFileStore) Save(name string, r io.Reader) (*File, error) {
|
||||
id := uuid.New()
|
||||
path := filepath.Join(s.BaseDir, id.String())
|
||||
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("file(local): failed to create file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := io.Copy(f, r); err != nil {
|
||||
os.Remove(path)
|
||||
return nil, fmt.Errorf("file(local): failed to write file: %w", err)
|
||||
}
|
||||
|
||||
return &File{
|
||||
ID: id,
|
||||
Name: name,
|
||||
Created: time.Now(),
|
||||
Backend: FileBackendLocal,
|
||||
Path: path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *LocalFileStore) URL(file *File) (string, error) {
|
||||
return "/files/" + file.ID.String(), nil
|
||||
}
|
||||
|
||||
func ServeFile(w http.ResponseWriter, r *http.Request) {
|
||||
slog.Debug("file: entering ServeFile handler")
|
||||
|
||||
fileID := chi.URLParam(r, "fileID")
|
||||
parsed, err := uuid.Parse(fileID)
|
||||
if err != nil {
|
||||
render.Render(w, r, ErrInvalidRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
file, err := dbGetFile(parsed.String())
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrFileNotFound) {
|
||||
render.Render(w, r, ErrNotFound)
|
||||
} else {
|
||||
slog.Error("file: failed to fetch file", "fileid", parsed.String(), "error", err)
|
||||
render.Render(w, r, ErrInternal(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
f, err := os.Open(file.Path)
|
||||
if err != nil {
|
||||
slog.Error("file: failed to open file", "fileid", file.ID, "error", err)
|
||||
render.Render(w, r, ErrInternal(err))
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
http.ServeContent(w, r, file.Name, file.Created, f)
|
||||
}
|
||||
|
||||
// UploadFile is a temporary handler for testing file uploads.
|
||||
/*
|
||||
func UploadFile(w http.ResponseWriter, r *http.Request) {
|
||||
slog.Debug("file: entering UploadFile handler")
|
||||
|
||||
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
||||
render.Render(w, r, ErrInvalidRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
f, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
render.Render(w, r, ErrInvalidRequest(err))
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
file, err := Store.Save(header.Filename, f)
|
||||
if err != nil {
|
||||
slog.Error("file: failed to save file", "error", err)
|
||||
render.Render(w, r, ErrInternal(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := dbAddFile(file); err != nil {
|
||||
render.Render(w, r, ErrInternal(err))
|
||||
return
|
||||
}
|
||||
|
||||
slog.Debug("file: uploaded file", "fileid", file.ID, "filename", file.Name)
|
||||
render.Render(w, r, NewFilePayloadResponse(file))
|
||||
}
|
||||
*/
|
||||
Reference in New Issue
Block a user