diff --git a/packages/http/lib/foundation_file.dart b/packages/http/lib/foundation_file.dart index c0170e2..43a7c7e 100644 --- a/packages/http/lib/foundation_file.dart +++ b/packages/http/lib/foundation_file.dart @@ -9,4 +9,5 @@ library; +export "src/foundation/file/file.dart"; export 'src/foundation/file/upload_file.dart'; diff --git a/packages/http/lib/foundation_file_exception.dart b/packages/http/lib/foundation_file_exception.dart index ff8e781..d473df1 100644 --- a/packages/http/lib/foundation_file_exception.dart +++ b/packages/http/lib/foundation_file_exception.dart @@ -16,5 +16,5 @@ export 'src/foundation/file/exception/file_not_found_exception.dart'; export 'src/foundation/file/exception/form_size_file_exception.dart'; export 'src/foundation/file/exception/ini_size_file_exception.dart'; export 'src/foundation/file/exception/no_file_exception.dart'; -export 'src/foundation/file/exception/no_tmpdir_file_exception.dart'; +export 'src/foundation/file/exception/no_tmp_dir_file_exception.dart'; export 'src/foundation/file/exception/partial_file_exception.dart'; diff --git a/packages/http/lib/src/foundation/file/exception/cannot_write_file_exception.dart b/packages/http/lib/src/foundation/file/exception/cannot_write_file_exception.dart index 4cf88df..44df614 100644 --- a/packages/http/lib/src/foundation/file/exception/cannot_write_file_exception.dart +++ b/packages/http/lib/src/foundation/file/exception/cannot_write_file_exception.dart @@ -12,5 +12,5 @@ import 'package:protevus_http/foundation_file_exception.dart'; /// @author Florent Mata class CannotWriteFileException extends FileException { /// Creates a new [CannotWriteFileException] with the given [message]. - CannotWriteFileException([String message = '']) : super(message); + CannotWriteFileException([super.message]); } diff --git a/packages/http/lib/src/foundation/file/exception/extension_file_exception.dart b/packages/http/lib/src/foundation/file/exception/extension_file_exception.dart index e1bf5cf..17abe11 100644 --- a/packages/http/lib/src/foundation/file/exception/extension_file_exception.dart +++ b/packages/http/lib/src/foundation/file/exception/extension_file_exception.dart @@ -12,5 +12,5 @@ import 'package:protevus_http/foundation_file_exception.dart'; /// @author Florent Mata class ExtensionFileException extends FileException { // The constructor is empty as it inherits from FileException - ExtensionFileException([String message = '']) : super(message); + ExtensionFileException([super.message]); } diff --git a/packages/http/lib/src/foundation/file/exception/form_size_file_exception.dart b/packages/http/lib/src/foundation/file/exception/form_size_file_exception.dart index 24f99f4..cd1490d 100644 --- a/packages/http/lib/src/foundation/file/exception/form_size_file_exception.dart +++ b/packages/http/lib/src/foundation/file/exception/form_size_file_exception.dart @@ -11,5 +11,5 @@ import 'package:protevus_http/foundation_file_exception.dart'; /// /// @author Florent Mata class FormSizeFileException extends FileException { - FormSizeFileException([String message = '']) : super(message); + FormSizeFileException([super.message]); } diff --git a/packages/http/lib/src/foundation/file/exception/ini_size_file_exception.dart b/packages/http/lib/src/foundation/file/exception/ini_size_file_exception.dart index 683f0d6..3a7802d 100644 --- a/packages/http/lib/src/foundation/file/exception/ini_size_file_exception.dart +++ b/packages/http/lib/src/foundation/file/exception/ini_size_file_exception.dart @@ -11,5 +11,5 @@ import 'package:protevus_http/foundation_file_exception.dart'; /// /// @author Florent Mata class IniSizeFileException extends FileException { - // No additional functionality needed, as this exception is just a specific type of FileException + IniSizeFileException(super.message); } diff --git a/packages/http/lib/src/foundation/file/exception/no_file_exception.dart b/packages/http/lib/src/foundation/file/exception/no_file_exception.dart index 7c96426..4683ac2 100644 --- a/packages/http/lib/src/foundation/file/exception/no_file_exception.dart +++ b/packages/http/lib/src/foundation/file/exception/no_file_exception.dart @@ -11,6 +11,5 @@ import 'package:protevus_http/foundation_file_exception.dart'; /// /// @author Florent Mata class NoFileException extends FileException { - // In Dart, we don't need to explicitly declare an empty class body - // if there are no additional members or methods. + NoFileException(super.message); } diff --git a/packages/http/lib/src/foundation/file/exception/no_tmpdir_file_exception.dart b/packages/http/lib/src/foundation/file/exception/no_tmp_dir_file_exception.dart similarity index 73% rename from packages/http/lib/src/foundation/file/exception/no_tmpdir_file_exception.dart rename to packages/http/lib/src/foundation/file/exception/no_tmp_dir_file_exception.dart index f3261b1..c796066 100644 --- a/packages/http/lib/src/foundation/file/exception/no_tmpdir_file_exception.dart +++ b/packages/http/lib/src/foundation/file/exception/no_tmp_dir_file_exception.dart @@ -4,5 +4,5 @@ import 'package:protevus_http/foundation_file_exception.dart'; /// /// @author Florent Mata class NoTmpDirFileException extends FileException { - // The constructor is implicit in this case, as it doesn't require any additional setup + NoTmpDirFileException(super.message); } diff --git a/packages/http/lib/src/foundation/file/exception/partial_file_exception.dart b/packages/http/lib/src/foundation/file/exception/partial_file_exception.dart index bff6e99..c31d406 100644 --- a/packages/http/lib/src/foundation/file/exception/partial_file_exception.dart +++ b/packages/http/lib/src/foundation/file/exception/partial_file_exception.dart @@ -14,6 +14,5 @@ import 'package:protevus_http/foundation_file_exception.dart'; /// /// @author Florent Mata class PartialFileException extends FileException { - // In Dart, we don't need to declare an empty class body if there are no additional - // properties or methods. The class will inherit everything from FileException. + PartialFileException(super.message); } diff --git a/packages/http/lib/src/foundation/file/file.dart b/packages/http/lib/src/foundation/file/file.dart new file mode 100644 index 0000000..9eab7ca --- /dev/null +++ b/packages/http/lib/src/foundation/file/file.dart @@ -0,0 +1,174 @@ +import 'dart:io' as io; +import 'package:path/path.dart' as p; +import 'package:protevus_http/foundation_file_exception.dart'; +import 'package:protevus_mime/mime.dart'; + +/// A file in the file system. +class File extends io.FileSystemEntity { + late final io.File _dartFile; + @override + final String path; + + /// Constructs a new file from the given path. + /// + /// [path] The path to the file + /// [checkPath] Whether to check the path or not + /// + /// Throws [FileNotFoundException] If the given path is not a file + File(this.path, {bool checkPath = true}) { + if (checkPath && !io.FileSystemEntity.isFileSync(path)) { + throw FileNotFoundException(path); + } + _dartFile = io.File(path); + } + + /// Returns the extension based on the mime type. + /// + /// If the mime type is unknown, returns null. + /// + /// This method uses the mime type as guessed by getMimeType() + /// to guess the file extension. + Future guessExtension() async { + final mimeType = await getMimeType(); + if (mimeType == null) return null; + + final extensions = MimeTypes.getDefault().getExtensions(mimeType); + return extensions.isNotEmpty ? extensions.first : null; + } + + /// Returns the mime type of the file. + /// + /// The mime type is guessed using the MimeTypes class. + Future getMimeType() { + return MimeTypes.getDefault().guessMimeType(path); + } + + /// Moves the file to a new location. + /// + /// Throws [FileException] if the target file could not be created + File move(String directory, [String? name]) { + final target = getTargetFile(directory, name); + + try { + final newPath = target.path; + _dartFile.renameSync(newPath); + chmod(newPath, '0666'); + return target; + } catch (e) { + throw FileException( + 'Could not move the file "$path" to "${target.path}" ($e).'); + } + } + + /// Returns the content of the file. + String getContent() { + try { + return _dartFile.readAsStringSync(); + } catch (e) { + throw FileException('Could not get the content of the file "$path".'); + } + } + + /// Returns the target file for a move operation. + File getTargetFile(String directory, [String? name]) { + final dir = io.Directory(directory); + if (!dir.existsSync()) { + try { + dir.createSync(recursive: true); + } catch (e) { + throw FileException('Unable to create the "$directory" directory.'); + } + } else if (!dir.statSync().modeString().contains('w')) { + throw FileException('Unable to write in the "$directory" directory.'); + } + + final targetPath = p.join(directory, name ?? p.basename(path)); + return File(targetPath, checkPath: false); + } + + /// Returns locale independent base name of the given path. + String getName(String name) { + final normalizedName = name.replaceAll('\\', '/'); + final pos = normalizedName.lastIndexOf('/'); + return pos == -1 ? normalizedName : normalizedName.substring(pos + 1); + } + + /// Changes the file permissions. + /// + /// [filePath] is the path to the file whose permissions should be changed. + /// [mode] should be an octal string like '0644' for Unix-like systems. + /// For Windows, use 'read' for read-only, 'write' for read/write, or 'full' for full control. + static Future chmod(String filePath, String mode) async { + if (io.Platform.isWindows) { + await _chmodWindows(filePath, mode); + } else { + await _chmodUnix(filePath, mode); + } + } + + static Future _chmodUnix(String filePath, String mode) async { + try { + final result = await io.Process.run('chmod', [mode, filePath]); + if (result.exitCode != 0) { + throw FileException( + 'Failed to change permissions for $filePath: ${result.stderr}'); + } + } catch (e) { + if (e.toString().contains('Permission denied')) { + // Optionally, you could try with sudo here, but that requires user interaction + throw FileException( + 'Permission denied. You may need to run this with elevated privileges.'); + } else { + throw FileException('Failed to change permissions for $filePath: $e'); + } + } + } + + static Future _chmodWindows(String filePath, String mode) async { + String permission; + switch (mode.toLowerCase()) { + case 'read': + permission = '(R)'; + break; + case 'write': + permission = '(R,W)'; + break; + case 'full': + permission = '(F)'; + break; + default: + throw ArgumentError( + 'Invalid mode for Windows. Use "read", "write", or "full".'); + } + + final result = await io.Process.run( + 'icacls', [filePath, '/grant', '*S-1-1-0:$permission']); + if (result.exitCode != 0) { + throw FileException( + 'Failed to change permissions for $filePath: ${result.stderr}'); + } + } + + @override + io.FileSystemEntity get absolute => throw UnimplementedError(); + + @override + Future exists() { + throw UnimplementedError(); + } + + @override + bool existsSync() { + throw UnimplementedError(); + } + + @override + Future rename(String newPath) { + throw UnimplementedError(); + } + + @override + io.FileSystemEntity renameSync(String newPath) { + throw UnimplementedError(); + } +} diff --git a/packages/http/lib/src/foundation/file/upload_file.dart b/packages/http/lib/src/foundation/file/upload_file.dart index e69de29..4f8a134 100644 --- a/packages/http/lib/src/foundation/file/upload_file.dart +++ b/packages/http/lib/src/foundation/file/upload_file.dart @@ -0,0 +1,131 @@ +import 'package:path/path.dart' as p; + +import 'package:protevus_http/foundation_file_exception.dart'; +import 'package:protevus_http/foundation_file.dart'; +import 'package:protevus_mime/mime.dart'; + +/// A file uploaded through a form. +class UploadedFile extends File { + late String _originalName; + late String _mimeType; + late int _error; + late String _originalPath; + late bool _test; + + UploadedFile( + super.path, + String originalName, + String? mimeType, + int? error, + bool test, + ) { + _originalName = _getName(originalName); + _originalPath = originalName.replaceAll('\\', '/'); + _mimeType = mimeType ?? 'application/octet-stream'; + _error = error ?? 0; // UPLOAD_ERR_OK + _test = test; + } + + String getClientOriginalName() { + return _originalName; + } + + String getClientOriginalExtension() { + return p.extension(_originalName); + } + + String getClientOriginalPath() { + return _originalPath; + } + + String getClientMimeType() => _mimeType; + + String? guessClientExtension() { + try { + List extensions = + MimeTypes.getDefault().getExtensions(getClientMimeType()); + return extensions.isNotEmpty ? extensions[0] : null; + } catch (e) { + return null; + } + } + + int getError() { + return _error; + } + + bool isValid() { + bool isOk = _error == 0; // UPLOAD_ERR_OK + return _test ? isOk : isOk && File(path).existsSync(); + } + + @override + File move(String directory, [String? name]) { + if (isValid()) { + if (_test) { + return File(p.join(directory, name ?? p.basename(path))); + } + + File target = _getTargetFile(directory, name); + try { + return super.move(target.path); + } catch (e) { + throw FileException( + 'Could not move the file "$path" to "${target.path}" ($e).'); + } + } + + switch (_error) { + case 1: // UPLOAD_ERR_INI_SIZE + throw IniSizeFileException(_getErrorMessage()); + case 2: // UPLOAD_ERR_FORM_SIZE + throw FormSizeFileException(_getErrorMessage()); + case 3: // UPLOAD_ERR_PARTIAL + throw PartialFileException(_getErrorMessage()); + case 4: // UPLOAD_ERR_NO_FILE + throw NoFileException(_getErrorMessage()); + case 6: // UPLOAD_ERR_NO_TMP_DIR + throw NoTmpDirFileException(_getErrorMessage()); + case 7: // UPLOAD_ERR_CANT_WRITE + throw CannotWriteFileException(_getErrorMessage()); + case 8: // UPLOAD_ERR_EXTENSION + throw ExtensionFileException(_getErrorMessage()); + default: + throw FileException(_getErrorMessage()); + } + } + + static int getMaxFilesize() { + // Note: This is a placeholder. Implement according to your needs. + return 2 * 1024 * 1024; // 2MB as an example + } + + String _getErrorMessage() { + Map errors = { + 1: 'The file "%s" exceeds your upload_max_filesize ini directive (limit is %d KiB).', + 2: 'The file "%s" exceeds the upload limit defined in your form.', + 3: 'The file "%s" was only partially uploaded.', + 4: 'No file was uploaded.', + 6: 'File could not be uploaded: missing temporary directory.', + 7: 'The file "%s" could not be written on disk.', + 8: 'File upload was stopped by a PHP extension.', + }; + + int maxFilesize = _error == 1 ? getMaxFilesize() ~/ 1024 : 0; + String message = errors[_error] ?? + 'The file "%s" was not uploaded due to an unknown error.'; + + return message + .replaceAll('%s', _originalName) + .replaceAll('%d', maxFilesize.toString()); + } + + String _getName(String name) { + return p.basename(name); + } + + File _getTargetFile(String directory, String? name) { + name ??= p.basename(path); + return File(p.join(directory, name)); + } +} diff --git a/packages/http/pubspec.yaml b/packages/http/pubspec.yaml index a0b56b3..ed20f1a 100644 --- a/packages/http/pubspec.yaml +++ b/packages/http/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: email_validator: ^3.0.0 validator_dart: ^0.1.0 protevus_mime: ^0.0.1 + path: ^1.9.0 dev_dependencies: lints: ^3.0.0