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"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -62,7 +63,7 @@ func (a *App) Upload(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
files := uploadFiles(r)
|
files := uploadIncomingFiles(r)
|
||||||
totalBytes := totalUploadBytes(files)
|
totalBytes := totalUploadBytes(files)
|
||||||
var ownerID string
|
var ownerID string
|
||||||
var collectionID 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
|
// 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
|
// 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.
|
// 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))
|
batch := strings.TrimSpace(r.Header.Get(uploadBatchHeader))
|
||||||
if batch == "" {
|
if batch == "" {
|
||||||
if enforceBoxLimits {
|
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
|
return services.UploadResult{}, 0, status, message, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result, err := a.uploadService.CreateBox(files, opts)
|
result, err := a.uploadService.CreateBoxFromIncoming(files, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return services.UploadResult{}, 0, 0, "", err
|
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 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 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
|
// Re-attach the manage/delete URLs from the box's creation so every
|
||||||
// upload in the batch returns a working deletion URL.
|
// upload in the batch returns a working deletion URL.
|
||||||
result.ManageURL = entry.manageURL
|
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
|
return services.UploadResult{}, 0, status, message, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result, err := a.uploadService.CreateBox(files, opts)
|
result, err := a.uploadService.CreateBoxFromIncoming(files, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return services.UploadResult{}, 0, 0, "", err
|
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)
|
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 {
|
if len(files) == 0 {
|
||||||
return 0, ""
|
return 0, ""
|
||||||
}
|
}
|
||||||
sizes := make([]int64, 0, len(files))
|
sizes := make([]int64, 0, len(files))
|
||||||
for _, file := range 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)
|
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)
|
return "ip:" + uploadClientIP(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func totalUploadBytes(files []*multipart.FileHeader) int64 {
|
func totalUploadBytes(files []services.IncomingFile) int64 {
|
||||||
var total int64
|
var total int64
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
total += file.Size
|
total += file.Size()
|
||||||
}
|
}
|
||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
@@ -409,13 +410,48 @@ func statusForDownloadError(err error) int {
|
|||||||
return http.StatusForbidden
|
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 {
|
if r.MultipartForm == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
files := make([]*multipart.FileHeader, 0)
|
fileHeaders := r.MultipartForm.File["file"]
|
||||||
files = append(files, r.MultipartForm.File["file"]...)
|
shareXHeaders := r.MultipartForm.File["sharex"]
|
||||||
files = append(files, 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
|
return files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"warpbox.dev/backend/libs/config"
|
"warpbox.dev/backend/libs/config"
|
||||||
|
"warpbox.dev/backend/libs/jobs"
|
||||||
"warpbox.dev/backend/libs/services"
|
"warpbox.dev/backend/libs/services"
|
||||||
"warpbox.dev/backend/libs/web"
|
"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+`"`) {
|
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)
|
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)
|
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)
|
t.Fatalf("NewBanService returned error: %v", err)
|
||||||
}
|
}
|
||||||
return NewApp(cfg, logger, renderer, service, authService, settingsService, reactionService, banService), func() {
|
return NewApp(cfg, logger, renderer, service, authService, settingsService, reactionService, banService), func() {
|
||||||
|
jobs.WaitForThumbnailJobs()
|
||||||
if err := service.Close(); err != nil {
|
if err := service.Close(); err != nil {
|
||||||
t.Fatalf("Close returned error: %v", err)
|
t.Fatalf("Close returned error: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/image/font"
|
"golang.org/x/image/font"
|
||||||
@@ -38,8 +39,12 @@ type ThumbnailJobResult struct {
|
|||||||
Failed int
|
Failed int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var thumbnailJobs sync.WaitGroup
|
||||||
|
|
||||||
func GenerateThumbnailsForBoxAsync(uploadService *services.UploadService, logger *slog.Logger, boxID string) {
|
func GenerateThumbnailsForBoxAsync(uploadService *services.UploadService, logger *slog.Logger, boxID string) {
|
||||||
|
thumbnailJobs.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
|
defer thumbnailJobs.Done()
|
||||||
box, err := uploadService.GetBox(boxID)
|
box, err := uploadService.GetBox(boxID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("thumbnail box lookup failed", "source", "thumbnail", "severity", "warn", "code", 4204, "box_id", boxID, "error", err.Error())
|
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 {
|
func newThumbnailsJob(cfg config.Config, logger *slog.Logger, uploadService *services.UploadService) job {
|
||||||
return job{
|
return job{
|
||||||
name: "thumbnail",
|
name: "thumbnail",
|
||||||
|
|||||||
@@ -411,7 +411,7 @@
|
|||||||
data: { url: window.Warpbox.absoluteURL(url || "/") },
|
data: { url: window.Warpbox.absoluteURL(url || "/") },
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const registration = await navigator.serviceWorker?.ready;
|
const registration = navigator.serviceWorker ? await navigator.serviceWorker.ready : null;
|
||||||
if (registration && registration.showNotification) {
|
if (registration && registration.showNotification) {
|
||||||
await registration.showNotification(title, options);
|
await registration.showNotification(title, options);
|
||||||
return;
|
return;
|
||||||
@@ -419,14 +419,18 @@
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
/* fall through to page notification */
|
/* fall through to page notification */
|
||||||
}
|
}
|
||||||
const notification = new Notification(title, options);
|
try {
|
||||||
notification.onclick = () => {
|
const notification = new Notification(title, options);
|
||||||
window.focus();
|
notification.onclick = () => {
|
||||||
if (url) {
|
window.focus();
|
||||||
window.location.href = window.Warpbox.absoluteURL(url);
|
if (url) {
|
||||||
}
|
window.location.href = window.Warpbox.absoluteURL(url);
|
||||||
notification.close();
|
}
|
||||||
};
|
notification.close();
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
/* notifications are best-effort */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function notify(variant, message, options) {
|
function notify(variant, message, options) {
|
||||||
@@ -1410,8 +1414,10 @@
|
|||||||
function uploadFormData() {
|
function uploadFormData() {
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
formData.delete("file");
|
formData.delete("file");
|
||||||
|
formData.delete("file_path");
|
||||||
selectedFiles.forEach((file) => {
|
selectedFiles.forEach((file) => {
|
||||||
formData.append("file", file, uploadName(file));
|
formData.append("file", file, uploadName(file));
|
||||||
|
formData.append("file_path", uploadName(file));
|
||||||
});
|
});
|
||||||
return formData;
|
return formData;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user