diff --git a/jam-ui/src/services/attachmentValidation.js b/jam-ui/src/services/attachmentValidation.js new file mode 100644 index 000000000..e165ecccb --- /dev/null +++ b/jam-ui/src/services/attachmentValidation.js @@ -0,0 +1,127 @@ +/** + * Attachment Validation Service + * + * Client-side file validation for session attachment uploads + * - Validates file size (10 MB limit) + * - Validates file type (extension whitelist) + * - Determines attachment type (audio vs notation) + * - Formats file sizes for display + */ + +/** + * Maximum file size: 10 MB + * Matches legacy client-side validation + */ +export const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB in bytes + +/** + * Allowed file extensions + * Note: .mp3 support pending backend decision (see Phase 12 docs) + */ +export const ALLOWED_EXTENSIONS = [ + '.pdf', '.xml', '.mxl', '.txt', // Notation + '.png', '.jpg', '.jpeg', '.gif', // Images + '.mp3', '.wav' // Audio (pending backend mp3 support) +]; + +/** + * Backend-supported extensions (from MusicNotationUploader) + * Used for warning messages if frontend allows extensions backend doesn't + */ +export const BACKEND_SUPPORTED_EXTENSIONS = [ + '.pdf', '.xml', '.mxl', '.txt', + '.png', '.jpg', '.jpeg', '.gif', + '.wav', '.flac', '.ogg', '.aiff', '.aifc', '.au' +]; + +/** + * Audio file extensions (for attachment_type determination) + */ +export const AUDIO_EXTENSIONS = ['.mp3', '.wav', '.flac', '.ogg', '.aiff', '.aifc', '.au']; + +/** + * Validate file size + * @param {File} file - File object from input + * @param {number} [maxSizeBytes=MAX_FILE_SIZE] - Maximum allowed size + * @returns {Object} { valid: boolean, error: string|null } + */ +export const validateFileSize = (file, maxSizeBytes = MAX_FILE_SIZE) => { + if (file.size > maxSizeBytes) { + const maxSizeMB = Math.floor(maxSizeBytes / (1024 * 1024)); + return { + valid: false, + error: `File exceeds ${maxSizeMB} MB limit` + }; + } + return { valid: true, error: null }; +}; + +/** + * Validate file type by extension + * @param {File} file - File object from input + * @param {string[]} [allowedTypes=ALLOWED_EXTENSIONS] - Array of allowed extensions + * @returns {Object} { valid: boolean, error: string|null } + */ +export const validateFileType = (file, allowedTypes = ALLOWED_EXTENSIONS) => { + const fileName = file.name.toLowerCase(); + const hasAllowedExtension = allowedTypes.some(ext => fileName.endsWith(ext)); + + if (!hasAllowedExtension) { + return { + valid: false, + error: `File type not allowed. Supported: ${allowedTypes.join(', ')}` + }; + } + + return { valid: true, error: null }; +}; + +/** + * Get attachment type from filename extension + * @param {string} filename - File name with extension + * @returns {string} 'notation' | 'audio' + */ +export const getAttachmentType = (filename) => { + const ext = '.' + filename.split('.').pop().toLowerCase(); + return AUDIO_EXTENSIONS.includes(ext) ? 'audio' : 'notation'; +}; + +/** + * Comprehensive file validation (size + type) + * @param {File} file - File object from input + * @returns {Object} { valid: boolean, error: string|null, warnings: string[] } + */ +export const validateFile = (file) => { + const warnings = []; + + // Validate size first (fail fast) + const sizeValidation = validateFileSize(file); + if (!sizeValidation.valid) { + return { valid: false, error: sizeValidation.error, warnings }; + } + + // Validate type + const typeValidation = validateFileType(file); + if (!typeValidation.valid) { + return { valid: false, error: typeValidation.error, warnings }; + } + + // Check if backend supports this type (warning only) + const ext = '.' + file.name.split('.').pop().toLowerCase(); + if (!BACKEND_SUPPORTED_EXTENSIONS.includes(ext)) { + warnings.push(`File type ${ext} may not be supported by server`); + } + + return { valid: true, error: null, warnings }; +}; + +/** + * Format file size for display + * @param {number} bytes - File size in bytes + * @returns {string} Formatted size (e.g., "2.5 MB") + */ +export const formatFileSize = (bytes) => { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +};