refactor(upload): use IncomingFile interface instead of multipart headers
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m58s
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m58s
Refactors the upload handler to use the `services.IncomingFile` interface instead of concrete `*multipart.FileHeader` pointers. This decouples the core upload logic from the HTTP multipart implementation, allowing for more flexible file sources. Changes include: - Introducing `namedMultipartFile` to adapt multipart headers to the new interface. - Updating `createOrAppendBox`, `checkUploadPolicy`, and `totalUploadBytes` to accept `IncomingFile`. - Renaming service calls to `CreateBoxFromIncoming` and `AppendIncomingFiles`.
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -62,7 +63,7 @@ func (a *App) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
files := uploadFiles(r)
|
||||
files := uploadIncomingFiles(r)
|
||||
totalBytes := totalUploadBytes(files)
|
||||
var ownerID string
|
||||
var collectionID string
|
||||
@@ -164,7 +165,7 @@ func (a *App) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
// uploadGroupWindow are folded into one box. Without the header the behaviour is
|
||||
// identical to creating a fresh box every time. Returns the result and how many
|
||||
// boxes were created (1 for a new box, 0 for an append) for usage accounting.
|
||||
func (a *App) createOrAppendBox(r *http.Request, user services.User, loggedIn bool, policy services.EffectiveUploadPolicy, files []*multipart.FileHeader, opts services.UploadOptions, enforceBoxLimits bool) (services.UploadResult, int, int, string, error) {
|
||||
func (a *App) createOrAppendBox(r *http.Request, user services.User, loggedIn bool, policy services.EffectiveUploadPolicy, files []services.IncomingFile, opts services.UploadOptions, enforceBoxLimits bool) (services.UploadResult, int, int, string, error) {
|
||||
batch := strings.TrimSpace(r.Header.Get(uploadBatchHeader))
|
||||
if batch == "" {
|
||||
if enforceBoxLimits {
|
||||
@@ -172,7 +173,7 @@ func (a *App) createOrAppendBox(r *http.Request, user services.User, loggedIn bo
|
||||
return services.UploadResult{}, 0, status, message, nil
|
||||
}
|
||||
}
|
||||
result, err := a.uploadService.CreateBox(files, opts)
|
||||
result, err := a.uploadService.CreateBoxFromIncoming(files, opts)
|
||||
if err != nil {
|
||||
return services.UploadResult{}, 0, 0, "", err
|
||||
}
|
||||
@@ -193,7 +194,7 @@ func (a *App) createOrAppendBox(r *http.Request, user services.User, loggedIn bo
|
||||
|
||||
if entry.boxID != "" && time.Since(entry.at) < uploadGroupWindow {
|
||||
if box, err := a.uploadService.GetBox(entry.boxID); err == nil && a.batchBoxMatches(box, user, loggedIn, r) && a.uploadService.CanDownload(box) == nil {
|
||||
if result, err := a.uploadService.AppendFiles(entry.boxID, files, opts); err == nil {
|
||||
if result, err := a.uploadService.AppendIncomingFiles(entry.boxID, files, opts); err == nil {
|
||||
// Re-attach the manage/delete URLs from the box's creation so every
|
||||
// upload in the batch returns a working deletion URL.
|
||||
result.ManageURL = entry.manageURL
|
||||
@@ -209,7 +210,7 @@ func (a *App) createOrAppendBox(r *http.Request, user services.User, loggedIn bo
|
||||
return services.UploadResult{}, 0, status, message, nil
|
||||
}
|
||||
}
|
||||
result, err := a.uploadService.CreateBox(files, opts)
|
||||
result, err := a.uploadService.CreateBoxFromIncoming(files, opts)
|
||||
if err != nil {
|
||||
return services.UploadResult{}, 0, 0, "", err
|
||||
}
|
||||
@@ -229,13 +230,13 @@ func (a *App) batchBoxMatches(box services.Box, user services.User, loggedIn boo
|
||||
return box.OwnerID == "" && box.CreatorIP == uploadClientIP(r)
|
||||
}
|
||||
|
||||
func (a *App) checkUploadPolicy(r *http.Request, user services.User, loggedIn bool, settings services.UploadPolicySettings, policy services.EffectiveUploadPolicy, files []*multipart.FileHeader, totalBytes int64) (int, string) {
|
||||
func (a *App) checkUploadPolicy(r *http.Request, user services.User, loggedIn bool, settings services.UploadPolicySettings, policy services.EffectiveUploadPolicy, files []services.IncomingFile, totalBytes int64) (int, string) {
|
||||
if len(files) == 0 {
|
||||
return 0, ""
|
||||
}
|
||||
sizes := make([]int64, 0, len(files))
|
||||
for _, file := range files {
|
||||
sizes = append(sizes, file.Size)
|
||||
sizes = append(sizes, file.Size())
|
||||
}
|
||||
return a.checkUploadPolicyForSizes(r, user, loggedIn, settings, policy, sizes, totalBytes)
|
||||
}
|
||||
@@ -383,10 +384,10 @@ func uploadRateKey(r *http.Request, user services.User, loggedIn bool) string {
|
||||
return "ip:" + uploadClientIP(r)
|
||||
}
|
||||
|
||||
func totalUploadBytes(files []*multipart.FileHeader) int64 {
|
||||
func totalUploadBytes(files []services.IncomingFile) int64 {
|
||||
var total int64
|
||||
for _, file := range files {
|
||||
total += file.Size
|
||||
total += file.Size()
|
||||
}
|
||||
return total
|
||||
}
|
||||
@@ -409,13 +410,48 @@ func statusForDownloadError(err error) int {
|
||||
return http.StatusForbidden
|
||||
}
|
||||
|
||||
func uploadFiles(r *http.Request) []*multipart.FileHeader {
|
||||
type namedMultipartFile struct {
|
||||
header *multipart.FileHeader
|
||||
name string
|
||||
}
|
||||
|
||||
func (f namedMultipartFile) Name() string {
|
||||
if strings.TrimSpace(f.name) != "" {
|
||||
return f.name
|
||||
}
|
||||
return f.header.Filename
|
||||
}
|
||||
|
||||
func (f namedMultipartFile) Size() int64 {
|
||||
return f.header.Size
|
||||
}
|
||||
|
||||
func (f namedMultipartFile) ContentType() string {
|
||||
return f.header.Header.Get("Content-Type")
|
||||
}
|
||||
|
||||
func (f namedMultipartFile) Open() (io.ReadCloser, error) {
|
||||
return f.header.Open()
|
||||
}
|
||||
|
||||
func uploadIncomingFiles(r *http.Request) []services.IncomingFile {
|
||||
if r.MultipartForm == nil {
|
||||
return nil
|
||||
}
|
||||
files := make([]*multipart.FileHeader, 0)
|
||||
files = append(files, r.MultipartForm.File["file"]...)
|
||||
files = append(files, r.MultipartForm.File["sharex"]...)
|
||||
fileHeaders := r.MultipartForm.File["file"]
|
||||
shareXHeaders := r.MultipartForm.File["sharex"]
|
||||
paths := r.MultipartForm.Value["file_path"]
|
||||
files := make([]services.IncomingFile, 0, len(fileHeaders)+len(shareXHeaders))
|
||||
for index, header := range fileHeaders {
|
||||
name := ""
|
||||
if index < len(paths) {
|
||||
name = paths[index]
|
||||
}
|
||||
files = append(files, namedMultipartFile{header: header, name: name})
|
||||
}
|
||||
for _, header := range shareXHeaders {
|
||||
files = append(files, namedMultipartFile{header: header})
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"time"
|
||||
|
||||
"warpbox.dev/backend/libs/config"
|
||||
"warpbox.dev/backend/libs/jobs"
|
||||
"warpbox.dev/backend/libs/services"
|
||||
"warpbox.dev/backend/libs/web"
|
||||
)
|
||||
@@ -127,7 +128,7 @@ func TestSocialPreviewBotGetsCardForSingleNonMediaBox(t *testing.T) {
|
||||
if !strings.Contains(body, `class="file-thumb" src="/d/`+payload.BoxID+`/thumb/`+payload.Files[0].ID+`"`) {
|
||||
t.Fatalf("download page did not render text thumbnail image: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, "Click to preview or download") && !strings.Contains(body, "click to preview or download") {
|
||||
if !strings.Contains(body, "Open to preview or download") {
|
||||
t.Fatalf("social preview body missing preview/download description: %s", body)
|
||||
}
|
||||
}
|
||||
@@ -817,6 +818,7 @@ func newTestApp(t *testing.T) (*App, func()) {
|
||||
t.Fatalf("NewBanService returned error: %v", err)
|
||||
}
|
||||
return NewApp(cfg, logger, renderer, service, authService, settingsService, reactionService, banService), func() {
|
||||
jobs.WaitForThumbnailJobs()
|
||||
if err := service.Close(); err != nil {
|
||||
t.Fatalf("Close returned error: %v", err)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/image/font"
|
||||
@@ -38,8 +39,12 @@ type ThumbnailJobResult struct {
|
||||
Failed int
|
||||
}
|
||||
|
||||
var thumbnailJobs sync.WaitGroup
|
||||
|
||||
func GenerateThumbnailsForBoxAsync(uploadService *services.UploadService, logger *slog.Logger, boxID string) {
|
||||
thumbnailJobs.Add(1)
|
||||
go func() {
|
||||
defer thumbnailJobs.Done()
|
||||
box, err := uploadService.GetBox(boxID)
|
||||
if err != nil {
|
||||
logger.Warn("thumbnail box lookup failed", "source", "thumbnail", "severity", "warn", "code", 4204, "box_id", boxID, "error", err.Error())
|
||||
@@ -61,6 +66,10 @@ func GenerateThumbnailsForBoxAsync(uploadService *services.UploadService, logger
|
||||
}()
|
||||
}
|
||||
|
||||
func WaitForThumbnailJobs() {
|
||||
thumbnailJobs.Wait()
|
||||
}
|
||||
|
||||
func newThumbnailsJob(cfg config.Config, logger *slog.Logger, uploadService *services.UploadService) job {
|
||||
return job{
|
||||
name: "thumbnail",
|
||||
|
||||
@@ -411,7 +411,7 @@
|
||||
data: { url: window.Warpbox.absoluteURL(url || "/") },
|
||||
};
|
||||
try {
|
||||
const registration = await navigator.serviceWorker?.ready;
|
||||
const registration = navigator.serviceWorker ? await navigator.serviceWorker.ready : null;
|
||||
if (registration && registration.showNotification) {
|
||||
await registration.showNotification(title, options);
|
||||
return;
|
||||
@@ -419,6 +419,7 @@
|
||||
} catch (error) {
|
||||
/* fall through to page notification */
|
||||
}
|
||||
try {
|
||||
const notification = new Notification(title, options);
|
||||
notification.onclick = () => {
|
||||
window.focus();
|
||||
@@ -427,6 +428,9 @@
|
||||
}
|
||||
notification.close();
|
||||
};
|
||||
} catch (error) {
|
||||
/* notifications are best-effort */
|
||||
}
|
||||
}
|
||||
|
||||
function notify(variant, message, options) {
|
||||
@@ -1410,8 +1414,10 @@
|
||||
function uploadFormData() {
|
||||
const formData = new FormData(form);
|
||||
formData.delete("file");
|
||||
formData.delete("file_path");
|
||||
selectedFiles.forEach((file) => {
|
||||
formData.append("file", file, uploadName(file));
|
||||
formData.append("file_path", uploadName(file));
|
||||
});
|
||||
return formData;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user