package file import ( "fmt" "strings" dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection" ) // File size limits (in bytes) const ( // MaxFileSizeGeneral is the maximum file size for general folders (500MB) MaxFileSizeGeneral = 500 * 1024 * 1024 // MaxFileSizeAlbum is the maximum file size for album uploads (100MB) // Albums are for photos/videos, so we use a more restrictive limit MaxFileSizeAlbum = 100 * 1024 * 1024 // MaxThumbnailSize is the maximum thumbnail size (10MB) MaxThumbnailSize = 10 * 1024 * 1024 ) // Allowed content types for albums (photos and videos only) var AlbumAllowedContentTypes = []string{ // Image formats "image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp", "image/heic", "image/heif", "image/bmp", "image/tiff", "image/svg+xml", // Video formats "video/mp4", "video/mpeg", "video/quicktime", // .mov files "video/x-msvideo", // .avi files "video/x-matroska", // .mkv files "video/webm", "video/3gpp", "video/x-flv", } // FileValidator provides file upload validation based on collection type type FileValidator struct{} // NewFileValidator creates a new file validator func NewFileValidator() *FileValidator { return &FileValidator{} } // ValidateFileUpload validates a file upload based on collection type and file properties // CWE-434: Unrestricted Upload of File with Dangerous Type // OWASP A01:2021: Broken Access Control - File upload restrictions func (v *FileValidator) ValidateFileUpload( collectionType string, fileSize int64, thumbnailSize int64, contentType string, ) error { // Validate file size based on collection type if err := v.validateFileSize(collectionType, fileSize); err != nil { return err } // Validate thumbnail size if provided if thumbnailSize > 0 { if err := v.validateThumbnailSize(thumbnailSize); err != nil { return err } } // Validate content type for albums (photos/videos only) if collectionType == dom_collection.CollectionTypeAlbum { if err := v.validateContentType(contentType); err != nil { return err } } // For folders (non-albums), no content-type restrictions // Users can upload any file type to regular folders return nil } // validateFileSize checks if the file size is within limits func (v *FileValidator) validateFileSize(collectionType string, fileSize int64) error { if fileSize <= 0 { return fmt.Errorf("file size must be greater than 0") } var maxSize int64 var collectionTypeName string if collectionType == dom_collection.CollectionTypeAlbum { maxSize = MaxFileSizeAlbum collectionTypeName = "album" } else { maxSize = MaxFileSizeGeneral collectionTypeName = "folder" } if fileSize > maxSize { return fmt.Errorf( "file size (%s) exceeds maximum allowed size for %s (%s)", formatBytes(fileSize), collectionTypeName, formatBytes(maxSize), ) } return nil } // validateThumbnailSize checks if the thumbnail size is within limits func (v *FileValidator) validateThumbnailSize(thumbnailSize int64) error { if thumbnailSize <= 0 { return nil // Thumbnail is optional } if thumbnailSize > MaxThumbnailSize { return fmt.Errorf( "thumbnail size (%s) exceeds maximum allowed size (%s)", formatBytes(thumbnailSize), formatBytes(MaxThumbnailSize), ) } return nil } // validateContentType checks if the content type is allowed for albums func (v *FileValidator) validateContentType(contentType string) error { if contentType == "" { return fmt.Errorf("content type is required for album uploads") } // Normalize content type (lowercase and trim) normalizedType := strings.ToLower(strings.TrimSpace(contentType)) // Check if content type is in allowed list for _, allowedType := range AlbumAllowedContentTypes { if normalizedType == allowedType { return nil } } return fmt.Errorf( "content type '%s' is not allowed in albums. Only photos and videos are permitted", contentType, ) } // GetAllowedContentTypes returns the list of allowed content types for albums func (v *FileValidator) GetAllowedContentTypes() []string { return AlbumAllowedContentTypes } // GetMaxFileSize returns the maximum file size for a collection type func (v *FileValidator) GetMaxFileSize(collectionType string) int64 { if collectionType == dom_collection.CollectionTypeAlbum { return MaxFileSizeAlbum } return MaxFileSizeGeneral } // GetMaxThumbnailSize returns the maximum thumbnail size func (v *FileValidator) GetMaxThumbnailSize() int64 { return MaxThumbnailSize } // formatBytes formats byte size into human-readable format func formatBytes(bytes int64) string { const unit = 1024 if bytes < unit { return fmt.Sprintf("%d B", bytes) } div, exp := int64(unit), 0 for n := bytes / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) }