Files

129 lines
3.4 KiB
Go

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
}