feat(upload): support batching via header and update ShareX config
Introduce support for grouping multiple sequential file uploads into a single box using the `X-Warpbox-Batch` header. This is particularly useful for ShareX multi-file selections, which are sent as separate back-to-back requests.
Additionally, this change:
- Updates the ShareX configuration template to opt-in to batching by default.
- Switches ShareX configuration placeholders to the modern `{json:...}` format.
- Adds `thumbnailUrl` to the upload response schema and documents its usage.
This commit is contained in:
@@ -82,20 +82,22 @@ type File struct {
|
||||
}
|
||||
|
||||
type UploadResult struct {
|
||||
BoxID string `json:"boxId"`
|
||||
BoxURL string `json:"boxUrl"`
|
||||
ZipURL string `json:"zipUrl"`
|
||||
ManageURL string `json:"manageUrl"`
|
||||
DeleteURL string `json:"deleteUrl"`
|
||||
ExpiresAt string `json:"expiresAt"`
|
||||
Files []ResultFile `json:"files"`
|
||||
BoxID string `json:"boxId"`
|
||||
BoxURL string `json:"boxUrl"`
|
||||
ZipURL string `json:"zipUrl"`
|
||||
ThumbnailURL string `json:"thumbnailUrl"`
|
||||
ManageURL string `json:"manageUrl"`
|
||||
DeleteURL string `json:"deleteUrl"`
|
||||
ExpiresAt string `json:"expiresAt"`
|
||||
Files []ResultFile `json:"files"`
|
||||
}
|
||||
|
||||
type ResultFile struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Size string `json:"size"`
|
||||
URL string `json:"url"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Size string `json:"size"`
|
||||
URL string `json:"url"`
|
||||
ThumbnailURL string `json:"thumbnailUrl"`
|
||||
}
|
||||
|
||||
type AdminStats struct {
|
||||
@@ -226,15 +228,66 @@ func (s *UploadService) CreateBox(files []*multipart.FileHeader, opts UploadOpti
|
||||
box.PasswordHash = hash
|
||||
}
|
||||
|
||||
backend, err := s.storage.Backend(box.StorageBackendID)
|
||||
if err := s.writeFilesToBox(&box, files, opts); err != nil {
|
||||
return UploadResult{}, err
|
||||
}
|
||||
|
||||
if err := s.SaveBox(box); err != nil {
|
||||
return UploadResult{}, err
|
||||
}
|
||||
|
||||
s.logger.Info("upload complete",
|
||||
"source", "user-upload",
|
||||
"severity", "user_activity",
|
||||
"code", 2001,
|
||||
"box_id", box.ID,
|
||||
"file_count", len(box.Files),
|
||||
)
|
||||
|
||||
return s.resultForBox(box, deleteToken), nil
|
||||
}
|
||||
|
||||
// AppendFiles adds files to an existing box (used to group a ShareX multi-file
|
||||
// selection into a single box). The box keeps its original expiry, password and
|
||||
// other settings; only the new files are written.
|
||||
func (s *UploadService) AppendFiles(boxID string, files []*multipart.FileHeader, opts UploadOptions) (UploadResult, error) {
|
||||
if len(files) == 0 {
|
||||
return UploadResult{}, fmt.Errorf("no files were uploaded")
|
||||
}
|
||||
box, err := s.GetBox(boxID)
|
||||
if err != nil {
|
||||
return UploadResult{}, err
|
||||
}
|
||||
if err := s.writeFilesToBox(&box, files, opts); err != nil {
|
||||
return UploadResult{}, err
|
||||
}
|
||||
if err := s.SaveBox(box); err != nil {
|
||||
return UploadResult{}, err
|
||||
}
|
||||
s.logger.Info("upload appended",
|
||||
"source", "user-upload",
|
||||
"severity", "user_activity",
|
||||
"code", 2001,
|
||||
"box_id", box.ID,
|
||||
"added", len(files),
|
||||
"file_count", len(box.Files),
|
||||
)
|
||||
return s.resultForBox(box, ""), nil
|
||||
}
|
||||
|
||||
// writeFilesToBox streams each uploaded file into the box's storage backend and
|
||||
// appends the file metadata to box.Files. The box's StorageBackendID determines
|
||||
// where files land, so it works for both new and existing boxes.
|
||||
func (s *UploadService) writeFilesToBox(box *Box, files []*multipart.FileHeader, opts UploadOptions) error {
|
||||
backend, err := s.storage.Backend(box.StorageBackendID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, header := range files {
|
||||
if !opts.SkipSizeLimit {
|
||||
if err := s.ValidateSize(header.Size); err != nil {
|
||||
return UploadResult{}, err
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +298,7 @@ func (s *UploadService) CreateBox(files []*multipart.FileHeader, opts UploadOpti
|
||||
|
||||
file, err := header.Open()
|
||||
if err != nil {
|
||||
return UploadResult{}, err
|
||||
return err
|
||||
}
|
||||
|
||||
fileID := randomID(8)
|
||||
@@ -263,7 +316,7 @@ func (s *UploadService) CreateBox(files []*multipart.FileHeader, opts UploadOpti
|
||||
|
||||
if err := s.writeUploadedObject(context.Background(), backend, objectKey, file, header.Size, maxSize, contentType); err != nil {
|
||||
file.Close()
|
||||
return UploadResult{}, err
|
||||
return err
|
||||
}
|
||||
file.Close()
|
||||
|
||||
@@ -278,20 +331,7 @@ func (s *UploadService) CreateBox(files []*multipart.FileHeader, opts UploadOpti
|
||||
UploadedAt: time.Now().UTC(),
|
||||
})
|
||||
}
|
||||
|
||||
if err := s.SaveBox(box); err != nil {
|
||||
return UploadResult{}, err
|
||||
}
|
||||
|
||||
s.logger.Info("upload complete",
|
||||
"source", "user-upload",
|
||||
"severity", "user_activity",
|
||||
"code", 2001,
|
||||
"box_id", box.ID,
|
||||
"file_count", len(box.Files),
|
||||
)
|
||||
|
||||
return s.resultForBox(box, deleteToken), nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UploadService) GetBox(id string) (Box, error) {
|
||||
@@ -726,19 +766,28 @@ func (s *UploadService) resultForBox(box Box, deleteToken string) UploadResult {
|
||||
files := make([]ResultFile, 0, len(box.Files))
|
||||
for _, file := range box.Files {
|
||||
files = append(files, ResultFile{
|
||||
ID: file.ID,
|
||||
Name: file.Name,
|
||||
Size: helpers.FormatBytes(file.Size),
|
||||
URL: fmt.Sprintf("%s/d/%s/f/%s", s.baseURL, box.ID, file.ID),
|
||||
ID: file.ID,
|
||||
Name: file.Name,
|
||||
Size: helpers.FormatBytes(file.Size),
|
||||
URL: fmt.Sprintf("%s/d/%s/f/%s", s.baseURL, box.ID, file.ID),
|
||||
ThumbnailURL: fmt.Sprintf("%s/d/%s/thumb/%s", s.baseURL, box.ID, file.ID),
|
||||
})
|
||||
}
|
||||
|
||||
// The box-level thumbnail points at the most recently added file, so a
|
||||
// per-file ShareX upload previews the file it just sent.
|
||||
thumbnailURL := fmt.Sprintf("%s/d/%s/og-image.jpg", s.baseURL, box.ID)
|
||||
if len(files) > 0 {
|
||||
thumbnailURL = files[len(files)-1].ThumbnailURL
|
||||
}
|
||||
|
||||
result := UploadResult{
|
||||
BoxID: box.ID,
|
||||
BoxURL: fmt.Sprintf("%s/d/%s", s.baseURL, box.ID),
|
||||
ZipURL: fmt.Sprintf("%s/d/%s/zip", s.baseURL, box.ID),
|
||||
ExpiresAt: box.ExpiresAt.Format(time.RFC3339),
|
||||
Files: files,
|
||||
BoxID: box.ID,
|
||||
BoxURL: fmt.Sprintf("%s/d/%s", s.baseURL, box.ID),
|
||||
ZipURL: fmt.Sprintf("%s/d/%s/zip", s.baseURL, box.ID),
|
||||
ThumbnailURL: thumbnailURL,
|
||||
ExpiresAt: box.ExpiresAt.Format(time.RFC3339),
|
||||
Files: files,
|
||||
}
|
||||
if deleteToken != "" {
|
||||
result.ManageURL = fmt.Sprintf("%s/d/%s/manage/%s", s.baseURL, box.ID, deleteToken)
|
||||
|
||||
Reference in New Issue
Block a user