feat(upload): add resumable chunk configuration and file validation
Some checks failed
Build and Publish Docker Image / deploy (push) Failing after 56s
Some checks failed
Build and Publish Docker Image / deploy (push) Failing after 56s
- Add `WARPBOX_RESUMABLE_CHUNK_MODE` and `WARPBOX_RESUMABLE_CHUNK_PATH` environment variables to configure temporary chunk storage. - Implement strict file validation for resuming uploads to ensure selected files match the pending session's metadata. - Add `PLANS.md` to document development stages, roadmap, and API specifications (including batching and resumable flows).
This commit is contained in:
@@ -130,6 +130,8 @@ type File struct {
|
||||
Thumbnail string `json:"thumbnail,omitempty"`
|
||||
ObjectKey string `json:"objectKey,omitempty"`
|
||||
ThumbnailObjectKey string `json:"thumbnailObjectKey,omitempty"`
|
||||
Processing bool `json:"processing,omitempty"`
|
||||
ProcessingError string `json:"processingError,omitempty"`
|
||||
UploadedAt time.Time `json:"uploadedAt"`
|
||||
}
|
||||
|
||||
@@ -150,6 +152,7 @@ type ResultFile struct {
|
||||
Size string `json:"size"`
|
||||
URL string `json:"url"`
|
||||
ThumbnailURL string `json:"thumbnailUrl"`
|
||||
Processing bool `json:"processing,omitempty"`
|
||||
}
|
||||
|
||||
type AdminStats struct {
|
||||
@@ -254,6 +257,10 @@ func (s *UploadService) CreateBox(files []*multipart.FileHeader, opts UploadOpti
|
||||
}
|
||||
|
||||
func (s *UploadService) CreateBoxFromIncoming(files []IncomingFile, opts UploadOptions) (UploadResult, error) {
|
||||
return s.CreateBoxFromIncomingContext(context.Background(), files, opts)
|
||||
}
|
||||
|
||||
func (s *UploadService) CreateBoxFromIncomingContext(ctx context.Context, files []IncomingFile, opts UploadOptions) (UploadResult, error) {
|
||||
if len(files) == 0 {
|
||||
return UploadResult{}, fmt.Errorf("no files were uploaded")
|
||||
}
|
||||
@@ -297,7 +304,7 @@ func (s *UploadService) CreateBoxFromIncoming(files []IncomingFile, opts UploadO
|
||||
box.PasswordHash = hash
|
||||
}
|
||||
|
||||
if err := s.writeIncomingFilesToBox(&box, files, opts); err != nil {
|
||||
if err := s.writeIncomingFilesToBox(ctx, &box, files, opts); err != nil {
|
||||
return UploadResult{}, err
|
||||
}
|
||||
|
||||
@@ -331,7 +338,7 @@ func (s *UploadService) AppendIncomingFiles(boxID string, files []IncomingFile,
|
||||
if err != nil {
|
||||
return UploadResult{}, err
|
||||
}
|
||||
if err := s.writeIncomingFilesToBox(&box, files, opts); err != nil {
|
||||
if err := s.writeIncomingFilesToBox(context.Background(), &box, files, opts); err != nil {
|
||||
return UploadResult{}, err
|
||||
}
|
||||
if err := s.SaveBox(box); err != nil {
|
||||
@@ -352,7 +359,7 @@ func (s *UploadService) AppendIncomingFiles(boxID string, files []IncomingFile,
|
||||
// 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 {
|
||||
return s.writeIncomingFilesToBox(box, multipartIncomingFiles(files), opts)
|
||||
return s.writeIncomingFilesToBox(context.Background(), box, multipartIncomingFiles(files), opts)
|
||||
}
|
||||
|
||||
func multipartIncomingFiles(files []*multipart.FileHeader) []IncomingFile {
|
||||
@@ -363,7 +370,7 @@ func multipartIncomingFiles(files []*multipart.FileHeader) []IncomingFile {
|
||||
return incoming
|
||||
}
|
||||
|
||||
func (s *UploadService) writeIncomingFilesToBox(box *Box, files []IncomingFile, opts UploadOptions) error {
|
||||
func (s *UploadService) writeIncomingFilesToBox(ctx context.Context, box *Box, files []IncomingFile, opts UploadOptions) error {
|
||||
backend, err := s.storage.Backend(box.StorageBackendID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -399,8 +406,9 @@ func (s *UploadService) writeIncomingFilesToBox(box *Box, files []IncomingFile,
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.writeUploadedObject(context.Background(), backend, objectKey, file, incoming.Size(), maxSize, contentType); err != nil {
|
||||
if err := s.writeUploadedObject(ctx, backend, objectKey, file, incoming.Size(), maxSize, contentType); err != nil {
|
||||
file.Close()
|
||||
_ = backend.Delete(context.Background(), objectKey)
|
||||
return err
|
||||
}
|
||||
file.Close()
|
||||
@@ -811,6 +819,9 @@ func (s *UploadService) ThumbnailObjectKey(box Box, file File) string {
|
||||
}
|
||||
|
||||
func (s *UploadService) OpenFileObject(ctx context.Context, box Box, file File) (StorageObject, error) {
|
||||
if file.Processing {
|
||||
return StorageObject{}, fmt.Errorf("file is still processing")
|
||||
}
|
||||
backend, err := s.storage.Backend(s.BoxStorageBackendID(box))
|
||||
if err != nil {
|
||||
return StorageObject{}, err
|
||||
@@ -932,6 +943,13 @@ func (s *UploadService) WriteZip(w io.Writer, box Box) error {
|
||||
}
|
||||
|
||||
func (s *UploadService) SaveBox(box Box) error {
|
||||
if err := s.saveBoxRecord(box); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.writeBoxMetadata(box)
|
||||
}
|
||||
|
||||
func (s *UploadService) saveBoxRecord(box Box) error {
|
||||
if box.StorageBackendID == "" {
|
||||
box.StorageBackendID = StorageBackendLocal
|
||||
}
|
||||
@@ -941,10 +959,7 @@ func (s *UploadService) SaveBox(box Box) error {
|
||||
}
|
||||
|
||||
return s.db.Update(func(tx *bbolt.Tx) error {
|
||||
if err := tx.Bucket(boxesBucket).Put([]byte(box.ID), data); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.writeBoxMetadata(box)
|
||||
return tx.Bucket(boxesBucket).Put([]byte(box.ID), data)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -957,6 +972,7 @@ func (s *UploadService) resultForBox(box Box, deleteToken string) UploadResult {
|
||||
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),
|
||||
Processing: file.Processing,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1016,9 +1032,26 @@ func (s *UploadService) writeUploadedObject(ctx context.Context, backend Storage
|
||||
reader = io.LimitReader(source, maxSize)
|
||||
putSize = size
|
||||
}
|
||||
if ctx != nil {
|
||||
reader = contextReader{ctx: ctx, reader: reader}
|
||||
}
|
||||
return backend.Put(ctx, key, reader, putSize, contentType)
|
||||
}
|
||||
|
||||
type contextReader struct {
|
||||
ctx context.Context
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
func (r contextReader) Read(p []byte) (int, error) {
|
||||
select {
|
||||
case <-r.ctx.Done():
|
||||
return 0, r.ctx.Err()
|
||||
default:
|
||||
return r.reader.Read(p)
|
||||
}
|
||||
}
|
||||
|
||||
func boxObjectKey(boxID, name string) string {
|
||||
return filepath.ToSlash(filepath.Join(boxID, name))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user