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)) } */