package filestore import ( "errors" "mime" "mime/multipart" "path/filepath" "strings" ) const ( // MaxUploadSize is the maximum allowed size per file (100 MB). MaxUploadSize = 100 * 1024 * 1024 // MaxBatchUploadFiles is the maximum number of files per batch request. MaxBatchUploadFiles = 20 ) // UploadedFileResult holds metadata for a successfully stored file. type UploadedFileResult struct { FileID string `json:"file_id"` Filename string `json:"filename"` ContentType string `json:"content_type"` Size int64 `json:"size"` } // FailedUploadResult describes a file that could not be uploaded. type FailedUploadResult struct { Filename string `json:"filename"` Error string `json:"error"` Code string `json:"code,omitempty"` } // UploadFromMultipartFile validates and stores a single multipart file. func UploadFromMultipartFile(service FileStoreService, file *multipart.FileHeader, opts UploadOptions) (UploadedFileResult, error) { if file == nil { return UploadedFileResult{}, NewError(ErrCodeInvalidInput, "file is required") } contentType := detectContentType(file) validator := NewFileValidator(MaxUploadSize, SupportedMimeTypes) if err := validator.ValidateFile(file.Filename, contentType, file.Size); err != nil { return UploadedFileResult{}, err } src, err := file.Open() if err != nil { return UploadedFileResult{}, NewErrorWithCause(ErrCodeUploadFailed, "failed to open uploaded file", err) } defer src.Close() if opts.Filename == "" { opts.Filename = file.Filename } if opts.ContentType == "" { opts.ContentType = contentType } if opts.MaxSize == 0 { opts.MaxSize = MaxUploadSize } fileID, err := service.Upload("", file.Filename, src, opts) if err != nil { return UploadedFileResult{}, err } return UploadedFileResult{ FileID: fileID, Filename: file.Filename, ContentType: contentType, Size: file.Size, }, nil } // UploadManyFromMultipart uploads multiple files and returns successes and failures. func UploadManyFromMultipart(service FileStoreService, files []*multipart.FileHeader, opts UploadOptions) ([]UploadedFileResult, []FailedUploadResult) { uploaded := make([]UploadedFileResult, 0, len(files)) failed := make([]FailedUploadResult, 0) for _, file := range files { result, err := UploadFromMultipartFile(service, file, opts) if err != nil { code := ErrCodeUploadFailed var fsErr *FileStoreError if errors.As(err, &fsErr) { code = fsErr.Code } failed = append(failed, FailedUploadResult{ Filename: file.Filename, Error: err.Error(), Code: code, }) continue } uploaded = append(uploaded, result) } return uploaded, failed } func detectContentType(file *multipart.FileHeader) string { contentType := file.Header.Get("Content-Type") if contentType == "" { ext := filepath.Ext(file.Filename) if detected := mime.TypeByExtension(ext); detected != "" { contentType = detected } else { contentType = "application/octet-stream" } } return strings.TrimSpace(strings.Split(contentType, ";")[0]) } // CollectMultipartFiles returns file headers from the "files" field, or a single "file" if present. func CollectMultipartFiles(form *multipart.Form) []*multipart.FileHeader { if form == nil { return nil } files := form.File["files"] if len(files) > 0 { return files } if file := form.File["file"]; len(file) > 0 { return file } return nil }