feat(storage): add S3 backend support and advanced upload limits
- Introduce S3-compatible storage backend support using minio-go. - Add configuration options for local storage limits, box limits, and rate limiting. - Implement storage backend selection (local vs S3) for anonymous and registered users. - Add an `/admin/storage` management interface. - Update documentation and environment examples with the new configuration variables.
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -143,12 +145,15 @@ func (a *App) Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
path := a.uploadService.ThumbnailPath(box, file)
|
||||
if path == "" {
|
||||
object, err := a.uploadService.OpenThumbnailObject(r.Context(), box, file)
|
||||
if err != nil {
|
||||
http.ServeFile(w, r, filepath.Join(a.cfg.StaticDir, "img", "file-placeholder.webp"))
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, path)
|
||||
defer object.Body.Close()
|
||||
w.Header().Set("Content-Type", "image/jpeg")
|
||||
w.Header().Set("Cache-Control", "public, max-age=604800, immutable")
|
||||
http.ServeContent(w, r, file.ID+"-thumbnail.jpg", object.ModTime, readSeekCloser(object.Body))
|
||||
}
|
||||
|
||||
func (a *App) UnlockBox(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -199,31 +204,40 @@ func (a *App) loadFileForRequest(w http.ResponseWriter, r *http.Request) (servic
|
||||
}
|
||||
|
||||
func (a *App) serveFileContent(w http.ResponseWriter, r *http.Request, box services.Box, file services.File, attachment bool) {
|
||||
path := a.uploadService.FilePath(box, file)
|
||||
source, err := os.Open(path)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
stat, err := source.Stat()
|
||||
object, err := a.uploadService.OpenFileObject(r.Context(), box, file)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
defer object.Body.Close()
|
||||
|
||||
w.Header().Set("Content-Type", file.ContentType)
|
||||
if attachment {
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", file.Name))
|
||||
}
|
||||
http.ServeContent(w, r, file.Name, stat.ModTime(), source)
|
||||
if seeker, ok := object.Body.(io.ReadSeeker); ok {
|
||||
http.ServeContent(w, r, file.Name, object.ModTime, seeker)
|
||||
} else {
|
||||
if object.Size > 0 {
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", object.Size))
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = io.Copy(w, object.Body)
|
||||
}
|
||||
|
||||
if err := a.uploadService.RecordDownload(box.ID); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
a.logger.Warn("failed to record file download", "source", "download", "severity", "warn", "code", 4002, "box_id", box.ID, "error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func readSeekCloser(source io.ReadCloser) io.ReadSeeker {
|
||||
data, err := io.ReadAll(source)
|
||||
if err != nil {
|
||||
return bytes.NewReader(nil)
|
||||
}
|
||||
return bytes.NewReader(data)
|
||||
}
|
||||
|
||||
func (a *App) DownloadZip(w http.ResponseWriter, r *http.Request) {
|
||||
box, err := a.uploadService.GetBox(r.PathValue("boxID"))
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user