Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/src/models/yust_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ class YustImage extends YustFile {
: null;
}

/// Allowed file extensions for image uploads.
static const List<String> allowedExtensions = [
'jpg',
'jpeg',
'png',
'gif',
'tiff',
'heic',
];

/// Type identifier for this class
///
/// Used for local caching on mobile devices
Expand Down
174 changes: 174 additions & 0 deletions lib/src/util/yust_file_validation_options.dart
Original file line number Diff line number Diff line change
@@ -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<String>? 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<YustFile> existingFiles,
int? newFileSizeInKiB,
}) {
_checkFileCount(existingFiles);
_checkFileExtension(newFile);
_checkFileSize(newFile, newFileSizeInKiB);
_checkExistingFileNames(newFile, existingFiles);
}

void _checkFileCount(List<YustFile> 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<YustFile> 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<String> 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,
});
}
1 change: 1 addition & 0 deletions lib/yust.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down