diff --git a/lib/src/models/yust_image.dart b/lib/src/models/yust_image.dart index 8bd35a4d..83b5905d 100644 --- a/lib/src/models/yust_image.dart +++ b/lib/src/models/yust_image.dart @@ -76,6 +76,16 @@ class YustImage extends YustFile { : null; } + /// Allowed file extensions for image uploads. + static const List allowedExtensions = [ + 'jpg', + 'jpeg', + 'png', + 'gif', + 'tiff', + 'heic', + ]; + /// Type identifier for this class /// /// Used for local caching on mobile devices diff --git a/lib/src/util/yust_file_validation_options.dart b/lib/src/util/yust_file_validation_options.dart new file mode 100644 index 00000000..024d3641 --- /dev/null +++ b/lib/src/util/yust_file_validation_options.dart @@ -0,0 +1,174 @@ +import '../models/yust_file.dart'; +import 'yust_exception.dart'; + +/// Options for validating files before upload. +/// +/// Used by file/image pickers and share intent handling. +class YustFileValidationOptions { + final num numberOfFiles; + final bool overwriteSingleFile; + final num? maximumFileSizeInKiB; + final List? allowedExtensions; + + const YustFileValidationOptions({ + this.numberOfFiles = 1, + this.overwriteSingleFile = false, + this.maximumFileSizeInKiB, + this.allowedExtensions, + }); + + /// Validates a new file against all constraints. + /// + /// Throws typed [YustFileValidationException] subclasses on failure. + /// [newFile] is the file to validate. + /// [existingFiles] is the list of files already present. + /// [newFileSizeInKiB] is the size of the new file in KiB. If not provided, + /// the size is calculated from [newFile.bytes] or [newFile.file]. + void validateFile({ + required YustFile newFile, + required List existingFiles, + int? newFileSizeInKiB, + }) { + _checkFileCount(existingFiles); + _checkFileExtension(newFile); + _checkFileSize(newFile, newFileSizeInKiB); + _checkExistingFileNames(newFile, existingFiles); + } + + void _checkFileCount(List existingFiles) { + if (!overwriteSingleFile && existingFiles.length >= numberOfFiles) { + throw YustFileLimitExceededException( + 'File limit of $numberOfFiles reached.', + limit: numberOfFiles, + ); + } + + if (overwriteSingleFile && existingFiles.length > 1) { + throw YustFileLimitExceededException( + 'File limit of $numberOfFiles reached.', + limit: numberOfFiles, + ); + } + + if (overwriteSingleFile && existingFiles.isNotEmpty) { + throw YustFileOverwriteRequiredException( + 'A file already exists and will be overwritten.', + existingFile: existingFiles.first, + ); + } + } + + void _checkFileExtension(YustFile newFile) { + if (allowedExtensions == null || allowedExtensions!.isEmpty) return; + + final extension = newFile.getFilenameExtension().toLowerCase(); + if (!allowedExtensions!.contains(extension)) { + throw YustFileExtensionNotAllowedException( + 'File extension "$extension" is not allowed.', + extension: extension, + allowedExtensions: allowedExtensions!, + ); + } + } + + void _checkFileSize(YustFile newFile, int? newFileSizeInKiB) { + if (maximumFileSizeInKiB == null) return; + + final int fileSizeInKiB; + if (newFileSizeInKiB != null) { + fileSizeInKiB = newFileSizeInKiB; + } else if (newFile.bytes != null) { + fileSizeInKiB = newFile.bytes!.length ~/ 1024; + } else if (newFile.file != null) { + fileSizeInKiB = newFile.file!.lengthSync() ~/ 1024; + } else { + return; + } + + if (fileSizeInKiB > maximumFileSizeInKiB!) { + throw YustFileSizeExceededException( + 'File "${newFile.name}" exceeds the maximum size of $maximumFileSizeInKiB KiB.', + fileSizeInKiB: fileSizeInKiB, + maximumFileSizeInKiB: maximumFileSizeInKiB!, + ); + } + } + + void _checkExistingFileNames( + YustFile newFile, + List existingFiles, + ) { + if (existingFiles.any((file) => file.name == newFile.name)) { + throw YustFileAlreadyExistsException( + 'File "${newFile.name}" already exists.', + fileName: newFile.name ?? '', + ); + } + } +} + +/// Base exception for file validation failures. +class YustFileValidationException extends YustException { + YustFileValidationException(super.message); +} + +/// Thrown when the file size exceeds the maximum allowed size. +class YustFileSizeExceededException extends YustFileValidationException { + final int fileSizeInKiB; + final num maximumFileSizeInKiB; + + YustFileSizeExceededException( + super.message, { + required this.fileSizeInKiB, + required this.maximumFileSizeInKiB, + }); +} + +/// Thrown when the file extension is not in the allowed list. +class YustFileExtensionNotAllowedException + extends YustFileValidationException { + final String extension; + final List allowedExtensions; + + YustFileExtensionNotAllowedException( + super.message, { + required this.extension, + required this.allowedExtensions, + }); +} + +/// Thrown when the file limit has been reached. +class YustFileLimitExceededException extends YustFileValidationException { + final num limit; + + YustFileLimitExceededException( + super.message, { + required this.limit, + }); +} + +/// Thrown when a file with the same name already exists. +/// +/// This is a soft validation — callers may choose to confirm with the user +/// and proceed by deleting the existing file. +class YustFileAlreadyExistsException extends YustFileValidationException { + final String fileName; + + YustFileAlreadyExistsException( + super.message, { + required this.fileName, + }); +} + +/// Thrown when overwrite is enabled and an existing file needs to be replaced. +/// +/// This is a soft validation — callers should confirm with the user before +/// proceeding to overwrite. +class YustFileOverwriteRequiredException extends YustFileValidationException { + final YustFile existingFile; + + YustFileOverwriteRequiredException( + super.message, { + required this.existingFile, + }); +} diff --git a/lib/yust.dart b/lib/yust.dart index 18c1d78f..87b9da41 100644 --- a/lib/yust.dart +++ b/lib/yust.dart @@ -40,6 +40,7 @@ export 'src/util/yust_database_statistics.dart' export 'src/util/yust_dms_coordinates.dart'; export 'src/util/yust_exception.dart'; export 'src/util/yust_field_transform.dart'; +export 'src/util/yust_file_validation_options.dart'; export 'src/util/yust_helpers.dart'; export 'src/util/yust_location_appearance.dart'; export 'src/util/yust_retry_helper.dart';