diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml deleted file mode 100644 index 02d6daa..0000000 --- a/.github/workflows/actions.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Actions - -on: - pull_request: - branches: - - main - -jobs: - - bb_checks: - name: BB Checks - uses: BinaryBirds/github-workflows/.github/workflows/extra_soundness.yml@main - with: - local_swift_dependencies_check_enabled : true - - swiftlang_checks: - name: Swiftlang Checks - uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main - with: - license_header_check_project_name: "Toucan" - format_check_enabled : true - broken_symlink_check_enabled : true - unacceptable_language_check_enabled : true - api_breakage_check_enabled : false - docs_check_enabled : false - license_header_check_enabled : false - shell_check_enabled : false - yamllint_check_enabled : false - python_lint_check_enabled : false - - swiftlang_tests: - name: Swiftlang Tests - uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main - with: - enable_windows_checks : false - linux_build_command: "swift test --parallel --enable-code-coverage" - linux_exclude_swift_versions: "[{\"swift_version\": \"5.8\"}, {\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}, {\"swift_version\": \"nightly\"}, {\"swift_version\": \"nightly-main\"}, {\"swift_version\": \"nightly-6.0\"}, {\"swift_version\": \"nightly-6.1\"}]" \ No newline at end of file diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml new file mode 100644 index 0000000..2015df9 --- /dev/null +++ b/.github/workflows/deployment.yml @@ -0,0 +1,15 @@ +name: Deployment + +on: + push: + tags: + - 'v*' + - '[0-9]*' + +jobs: + create-docc-and-deploy: + uses: BinaryBirds/github-workflows/.github/workflows/docc_deploy.yml@main + permissions: + contents: read + pages: write + id-token: write diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 0000000..8197ebb --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,38 @@ +name: Testing + +on: + pull_request: + branches: + - main + +jobs: + swiftlang_checks: + name: Swiftlang Checks + uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main + with: + license_header_check_project_name: 'project' + format_check_enabled: true + broken_symlink_check_enabled: true + unacceptable_language_check_enabled: true + shell_check_enabled: true + docs_check_enabled: false + api_breakage_check_enabled: false + license_header_check_enabled: false + yamllint_check_enabled: false + python_lint_check_enabled: false + + bb_checks: + name: BB Checks + uses: BinaryBirds/github-workflows/.github/workflows/extra_soundness.yml@main + with: + local_swift_dependencies_check_enabled: true + headers_check_enabled: true + docc_warnings_check_enabled: true + + swiftlang_tests: + name: Swiftlang Tests + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main + with: + enable_windows_checks: false + linux_build_command: 'swift test --parallel --enable-code-coverage' + linux_exclude_swift_versions: '[{"swift_version": "5.8"}, {"swift_version": "5.9"}, {"swift_version": "5.10"}, {"swift_version": "nightly"}, {"swift_version": "nightly-main"}, {"swift_version": "6.0"}, {"swift_version": "nightly-6.0"}, {"swift_version": "nightly-6.1"}, {"swift_version": "nightly-6.3"}]' diff --git a/.swiftheaderignore b/.swiftheaderignore index cd21e3d..5d3d156 100644 --- a/.swiftheaderignore +++ b/.swiftheaderignore @@ -8,4 +8,5 @@ Makefile LICENSE Package.swift Docker/** +docker/** scripts/** \ No newline at end of file diff --git a/LICENSE b/LICENSE index 705dd77..c9749d8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ MIT License Copyright (c) 2018-2022 Tibor Bödecs -Copyright (c) 2022-2025 Binary Birds Ltd. +Copyright (c) 2022-2026 Binary Birds Ltd. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/Makefile b/Makefile index c27df85..415f6a7 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ SHELL=/bin/bash baseUrl = https://raw.githubusercontent.com/BinaryBirds/github-workflows/refs/heads/main/scripts -check: symlinks language deps lint +check: symlinks language deps lint headers symlinks: curl -s $(baseUrl)/check-broken-symlinks.sh | bash @@ -18,9 +18,6 @@ deps: lint: curl -s $(baseUrl)/run-swift-format.sh | bash -fmt: - swiftformat . - format: curl -s $(baseUrl)/run-swift-format.sh | bash -s -- --fix @@ -29,24 +26,21 @@ headers: fix-headers: curl -s $(baseUrl)/check-swift-headers.sh | bash -s -- --fix + +docc-local: + curl -s $(baseUrl)/generate-docc.sh | bash -s -- --local -build: - swift build +run-docc: + curl -s $(baseUrl)/run-docc-docker.sh | bash -release: - swift build -c release +docc-warnings: + curl -s $(baseUrl)/check-docc-warnings.sh | bash test: swift test --parallel -test-with-coverage: - swift test --parallel --enable-code-coverage - -clean: - rm -rf .build - -docker-tests: - docker build -t file-manager-kit-tests . -f ./Docker/Dockerfile.testing && docker run --rm file-manager-kit-tests +docker-test: + docker build -t file-manager-kit-tests . -f ./docker/tests/dockerfile && docker run --rm file-manager-kit-tests docker-run: - docker run --rm -v $(pwd):/app -it swift:6.0 + docker run --rm -v $(pwd):/app -it swift:6.1 diff --git a/Package.swift b/Package.swift index 0d889fc..d1b4c17 100644 --- a/Package.swift +++ b/Package.swift @@ -1,14 +1,34 @@ -// swift-tools-version: 6.0 +// swift-tools-version:6.1 import PackageDescription +// NOTE: https://github.com/swift-server/swift-http-server/blob/main/Package.swift +var defaultSwiftSettings: [SwiftSetting] = +[ + // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0441-formalize-language-mode-terminology.md + .swiftLanguageMode(.v6), + // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md + .enableUpcomingFeature("MemberImportVisibility"), + // https://forums.swift.org/t/experimental-support-for-lifetime-dependencies-in-swift-6-2-and-beyond/78638 + .enableExperimentalFeature("Lifetimes"), + // https://github.com/swiftlang/swift/pull/65218 + .enableExperimentalFeature("AvailabilityMacro=featherDatabase 1.0:macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0"), +] + +#if compiler(>=6.2) +defaultSwiftSettings.append( + // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0461-async-function-isolation.md + .enableUpcomingFeature("NonisolatedNonsendingByDefault") +) +#endif + let package = Package( name: "file-manager-kit", platforms: [ - .macOS(.v14), - .iOS(.v17), - .tvOS(.v17), - .watchOS(.v10), - .visionOS(.v1), + .macOS(.v15), + .iOS(.v18), + .tvOS(.v18), + .watchOS(.v11), + .visionOS(.v2), ], products: [ .library( @@ -20,35 +40,38 @@ let package = Package( targets: ["FileManagerKitBuilder"] ), ], + dependencies: [ + // [docc-plugin-placeholder] + ], targets: [ .target( name: "FileManagerKit", - swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency=complete"), - ] + swiftSettings: defaultSwiftSettings + ), .target( name: "FileManagerKitBuilder", dependencies: [ .target(name: "FileManagerKit"), ], - swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency=complete"), - ] + swiftSettings: defaultSwiftSettings + ), .testTarget( name: "FileManagerKitTests", dependencies: [ .target(name: "FileManagerKit"), .target(name: "FileManagerKitBuilder") - ] + ], + swiftSettings: defaultSwiftSettings ), .testTarget( name: "FileManagerKitBuilderTests", dependencies: [ .target(name: "FileManagerKit"), .target(name: "FileManagerKitBuilder") - ] + ], + swiftSettings: defaultSwiftSettings ), ] ) diff --git a/README.md b/README.md index f30c4f5..0d53c18 100644 --- a/README.md +++ b/README.md @@ -2,23 +2,39 @@ Swift extensions and DSLs for filesystem testing, scripting, and inspection. +![Release: 0.5.0](https://img.shields.io/badge/Release-0%2E5%2E0-F05138) + +## Features + This package contains two products: - FileManagerKit – high-level extensions for FileManager - FileManagerKitBuilder – a DSL for creating filesystem layouts (ideal for tests) -Note: This repository is a work in progress. Expect breaking changes before v1.0.0. +## Requirements + +![Swift 6.1+](https://img.shields.io/badge/Swift-6%2E1%2B-F05138) +![Platforms: Linux, macOS, iOS, tvOS, watchOS, visionOS](https://img.shields.io/badge/Platforms-Linux_%7C_macOS_%7C_iOS_%7C_tvOS_%7C_watchOS_%7C_visionOS-F05138) +- Swift 6.1+ + +- Platforms: + - Linux + - macOS 15+ + - iOS 18+ + - tvOS 18+ + - watchOS 11+ + - visionOS 2+ ## Installation -Add the package to your `Package.swift` to the package dependencies section: +Use Swift Package Manager; add the dependency to your `Package.swift` file: ```swift -.package(url: "https://github.com/binarybirds/file-manager-kit", .upToNextMinor(from: "0.4.0")), +.package(url: "https://github.com/binarybirds/file-manager-kit", .exact: "1.0.0-beta.1"), ``` -Then add the library to the target dependencies: +Then add `FileManagerKit` to your target dependencies: ```swift .product(name: "FileManagerKit", package: "file-manager-kit"), @@ -30,12 +46,13 @@ Also add the other library too, if you need the builder: .product(name: "FileManagerKitBuilder", package: "file-manager-kit"), ``` - ## Usage +![DocC documentation](https://img.shields.io/badge/DocC-documentation-F05138) +Documentation is available at the following [link](https://binarybirds.github.io/file-manager-kit). Here are a few common use-cases. -### FileManagerKit +### FileManagerKit A set of ergonomic, safe extensions for working with FileManager. @@ -92,12 +109,10 @@ let size = try fileManager.size(at: URL(filePath: "/path/to/file")) print("\(size) bytes") ``` - ### FileManagerKitBuilder A Swift DSL to declaratively build, inspect, and tear down file system structures — great for testing. - Simple Example to create and clean up a file structure: ```swift @@ -151,3 +166,16 @@ try FileManagerPlayground { #expect(fileManager.fileExists(at: fileURL)) } ``` + +## Development + +- Build: `swift build` +- Test: + - local: `swift test` + - using Docker: `make docker-test` +- Format: `make format` +- Check: `make check` + +## Contributing + +[Pull requests](https://github.com/BinaryBirds/file-manager-kit/pulls) are welcome. Please keep changes focused and include tests for new logic. 🙏 diff --git a/Sources/FileManagerKit/FileManager+Kit.swift b/Sources/FileManagerKit/FileManager+Kit.swift index 21bd061..62d72eb 100644 --- a/Sources/FileManagerKit/FileManager+Kit.swift +++ b/Sources/FileManagerKit/FileManager+Kit.swift @@ -5,7 +5,11 @@ // Created by Viasz-Kádi Ferenc on 2025. 05. 30.. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif #if os(Linux) import Glibc @@ -41,6 +45,16 @@ private extension URL { } } +private extension String { + #if canImport(FoundationEssentials) + var fmkit_removingPercentEncoding: String { self } + #else + var fmkit_removingPercentEncoding: String { + self.removingPercentEncoding ?? self + } + #endif +} + extension FileManager: FileManagerKit { // MARK: - @@ -66,6 +80,15 @@ extension FileManager: FileManagerKit { public func directoryExists( at url: URL ) -> Bool { + #if canImport(FoundationEssentials) + var isDirectory = false + if fileExists( + atPath: url.path(percentEncoded: false), + isDirectory: &isDirectory + ) { + return isDirectory + } + #else var isDirectory = ObjCBool(false) if fileExists( atPath: url.path(percentEncoded: false), @@ -73,6 +96,7 @@ extension FileManager: FileManagerKit { ) { return isDirectory.boolValue } + #endif return false } @@ -83,6 +107,17 @@ extension FileManager: FileManagerKit { public func fileExists( at url: URL ) -> Bool { + #if canImport(FoundationEssentials) + var isDirectory = false + if fileExists( + atPath: url.path( + percentEncoded: false + ), + isDirectory: &isDirectory + ) { + return !isDirectory + } + #else var isDirectory = ObjCBool(false) if fileExists( atPath: url.path( @@ -92,6 +127,7 @@ extension FileManager: FileManagerKit { ) { return !isDirectory.boolValue } + #endif return false } @@ -129,15 +165,20 @@ extension FileManager: FileManagerKit { public func createDirectory( at url: URL, attributes: [FileAttributeKey: Any]? - ) throws { + ) throws(FileManagerKitError) { guard !directoryExists(at: url) else { return } - try createDirectory( - atPath: url.path(percentEncoded: false), - withIntermediateDirectories: true, - attributes: attributes - ) + do { + try createDirectory( + atPath: url.path(percentEncoded: false), + withIntermediateDirectories: true, + attributes: attributes + ) + } + catch { + throw .directoryCreateFailed(url: url, underlying: error) + } } /// Creates a file at the specified URL with optional contents and attributes. @@ -151,7 +192,7 @@ extension FileManager: FileManagerKit { at url: URL, contents: Data?, attributes: [FileAttributeKey: Any]? - ) throws { + ) throws(FileManagerKitError) { guard createFile( atPath: url.path(percentEncoded: false), @@ -159,7 +200,7 @@ extension FileManager: FileManagerKit { attributes: attributes ) else { - throw CocoaError(.fileWriteUnknown) + throw .fileCreateFailed(url: url) } } @@ -172,8 +213,17 @@ extension FileManager: FileManagerKit { public func copy( from source: URL, to destination: URL - ) throws { - try copyItem(at: source, to: destination) + ) throws(FileManagerKitError) { + do { + try copyItem(at: source, to: destination) + } + catch { + throw .copyFailed( + source: source, + destination: destination, + underlying: error + ) + } } /// Recursively copies a directory and its contents from a source URL to a destination URL. @@ -185,7 +235,7 @@ extension FileManager: FileManagerKit { public func copyRecursively( from inputURL: URL, to outputURL: URL - ) throws { + ) throws(FileManagerKitError) { guard directoryExists(at: inputURL) else { return } @@ -194,7 +244,7 @@ extension FileManager: FileManagerKit { } for item in listDirectory(at: inputURL) { - let path = item.removingPercentEncoding ?? item + let path = item.fmkit_removingPercentEncoding let itemSourceUrl = inputURL.appending(path: path) let itemDestinationUrl = outputURL.appending(path: path) if fileExists(at: itemSourceUrl) { @@ -218,8 +268,17 @@ extension FileManager: FileManagerKit { public func move( from source: URL, to destination: URL - ) throws { - try moveItem(at: source, to: destination) + ) throws(FileManagerKitError) { + do { + try moveItem(at: source, to: destination) + } + catch { + throw .moveFailed( + source: source, + destination: destination, + underlying: error + ) + } } /// Creates a symbolic (soft) link from a source path to a destination path. @@ -231,11 +290,20 @@ extension FileManager: FileManagerKit { public func softLink( from source: URL, to destination: URL - ) throws { - try createSymbolicLink( - at: destination, - withDestinationURL: source - ) + ) throws(FileManagerKitError) { + do { + try createSymbolicLink( + at: destination, + withDestinationURL: source + ) + } + catch { + throw .copyFailed( + source: source, + destination: destination, + underlying: error + ) + } } /// Creates a hard link from a source path to a destination path. @@ -247,16 +315,30 @@ extension FileManager: FileManagerKit { public func hardLink( from source: URL, to destination: URL - ) throws { - try linkItem(at: source, to: destination) + ) throws(FileManagerKitError) { + do { + try linkItem(at: source, to: destination) + } + catch { + throw .copyFailed( + source: source, + destination: destination, + underlying: error + ) + } } /// Deletes the file, directory, or symbolic link at the specified URL. /// /// - Parameter url: The URL of the item to delete. /// - Throws: An error if the item could not be deleted. - public func delete(at url: URL) throws { - try removeItem(at: url) + public func delete(at url: URL) throws(FileManagerKitError) { + do { + try removeItem(at: url) + } + catch { + throw .deleteFailed(url: url, underlying: error) + } } // MARK: - @@ -357,12 +439,17 @@ extension FileManager: FileManagerKit { /// - Throws: An error if attributes could not be retrieved. public func attributes( at url: URL - ) throws -> [FileAttributeKey: Any] { - try attributesOfItem( - atPath: url.path( - percentEncoded: false + ) throws(FileManagerKitError) -> [FileAttributeKey: Any] { + do { + return try attributesOfItem( + atPath: url.path( + percentEncoded: false + ) ) - ) + } + catch { + throw .attributesReadFailed(url: url, underlying: error) + } } /// Retrieves the POSIX permissions for the file or directory at the specified URL. @@ -372,9 +459,22 @@ extension FileManager: FileManagerKit { /// - Throws: An error if the permissions could not be retrieved. public func permissions( at url: URL - ) throws -> Int { - let attributes = try attributes(at: url) - return attributes[.posixPermissions] as! Int + ) throws(FileManagerKitError) -> Int { + let attrs = try attributes(at: url) + + guard let raw = attrs[.posixPermissions] else { + throw .missingAttribute(url: url, key: .posixPermissions) + } + if let int = raw as? Int { + return int + } + + throw .invalidAttributeType( + url: url, + key: .posixPermissions, + expected: "Int", + actual: String(describing: type(of: raw)) + ) } /// Returns the size of the file at the specified URL in bytes. @@ -384,37 +484,59 @@ extension FileManager: FileManagerKit { /// - Throws: An error if the size could not be retrieved. public func size( at url: URL - ) throws -> UInt64 { + ) throws(FileManagerKitError) -> UInt64 { if fileExists(at: url) { let attributes = try attributes(at: url) - let size = attributes[.size] as! NSNumber - return size.uint64Value - } - let keys: Set = [ - .isRegularFileKey, - .fileAllocatedSizeKey, - .totalFileAllocatedSizeKey, - ] - guard - let enumerator = enumerator( - at: url, - includingPropertiesForKeys: Array(keys) - ) - else { + if let intSize = attributes[.size] as? Int { + return UInt64(intSize) + } + if let int64Size = attributes[.size] as? Int64 { + return UInt64(int64Size) + } + #if !canImport(FoundationEssentials) + if let num = attributes[.size] as? NSNumber { + return num.uint64Value + } + #endif return 0 } - var size: UInt64 = 0 - for item in enumerator.compactMap({ $0 as? URL }) { - let values = try item.resourceValues(forKeys: keys) - guard values.isRegularFile ?? false else { - continue + var total: UInt64 = 0 + let all = listDirectoryRecursively(at: url) + for fileURL in all { + if fileExists(at: fileURL) { + #if !canImport(FoundationEssentials) + let keys: [URLResourceKey] = [ + .totalFileAllocatedSizeKey, .fileAllocatedSizeKey, + ] + if let values = try? fileURL.resourceValues(forKeys: Set(keys)) + { + if let s = values.totalFileAllocatedSize + ?? values.fileAllocatedSize + { + total += UInt64(s) + continue + } + } + #endif + if let attrs = try? attributes(at: fileURL) { + if let intSize = attrs[.size] as? Int { + total += UInt64(intSize) + } + else if let int64Size = attrs[.size] as? Int64 { + total += UInt64(int64Size) + } + else { + #if !canImport(FoundationEssentials) + if let num = attrs[.size] as? NSNumber { + total += num.uint64Value + } + #endif + } + } } - size += UInt64( - values.totalFileAllocatedSize ?? values.fileAllocatedSize ?? 0 - ) } - return size + return total } /// Retrieves the creation date of the item at the specified URL. @@ -424,10 +546,18 @@ extension FileManager: FileManagerKit { /// - Throws: An error if the creation date could not be retrieved. public func creationDate( at url: URL - ) throws -> Date { + ) throws(FileManagerKitError) -> Date { let attr = try attributes(at: url) + + if let d = attr[.creationDate] as? Date { + return d + } // On Linux, we return the modification date, since no .creationDate - return attr[.creationDate] as? Date ?? attr[.modificationDate] as! Date + if let d = attr[.modificationDate] as? Date { + return d + } + + throw .missingAttribute(url: url, key: .creationDate) } /// Retrieves the last modification date of the item at the specified URL. @@ -437,9 +567,22 @@ extension FileManager: FileManagerKit { /// - Throws: An error if the modification date could not be retrieved. public func modificationDate( at url: URL - ) throws -> Date { + ) throws(FileManagerKitError) -> Date { let attr = try attributes(at: url) - return attr[.modificationDate] as! Date + + guard let raw = attr[.modificationDate] else { + throw .missingAttribute(url: url, key: .modificationDate) + } + if let d = raw as? Date { + return d + } + + throw .invalidAttributeType( + url: url, + key: .modificationDate, + expected: "Date", + actual: String(describing: type(of: raw)) + ) } // MARK: - @@ -453,13 +596,18 @@ extension FileManager: FileManagerKit { public func setAttributes( _ attributes: [FileAttributeKey: Any], at url: URL - ) throws { - try setAttributes( - attributes, - ofItemAtPath: url.path( - percentEncoded: false + ) throws(FileManagerKitError) { + do { + try setAttributes( + attributes, + ofItemAtPath: url.path( + percentEncoded: false + ) ) - ) + } + catch { + throw .attributesWriteFailed(url: url, underlying: error) + } } /// Sets the POSIX file permissions at the specified URL. @@ -471,7 +619,7 @@ extension FileManager: FileManagerKit { public func setPermissions( _ permission: Int, at url: URL - ) throws { + ) throws(FileManagerKitError) { try setAttributes([.posixPermissions: permission], at: url) } diff --git a/Sources/FileManagerKit/FileManagerKit.swift b/Sources/FileManagerKit/FileManagerKit.swift index 9e2e3d5..1343c6d 100644 --- a/Sources/FileManagerKit/FileManagerKit.swift +++ b/Sources/FileManagerKit/FileManagerKit.swift @@ -5,7 +5,11 @@ // Created by Viasz-Kádi Ferenc on 2025. 05. 30.. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif /// A protocol that abstracts common file system operations, such as checking for file existence, /// creating directories or files, copying, moving, deleting, and querying file attributes. @@ -68,7 +72,7 @@ public protocol FileManagerKit { func createDirectory( at url: URL, attributes: [FileAttributeKey: Any]? - ) throws + ) throws(FileManagerKitError) /// Creates a file at the specified URL with optional contents and attributes. /// @@ -81,7 +85,7 @@ public protocol FileManagerKit { at url: URL, contents: Data?, attributes: [FileAttributeKey: Any]? - ) throws + ) throws(FileManagerKitError) /// Copies a file or directory from a source URL to a destination URL. /// @@ -92,7 +96,7 @@ public protocol FileManagerKit { func copy( from source: URL, to destination: URL - ) throws + ) throws(FileManagerKitError) /// Recursively copies a directory and its contents from a source URL to a destination URL. /// @@ -103,7 +107,7 @@ public protocol FileManagerKit { func copyRecursively( from inputURL: URL, to outputURL: URL - ) throws + ) throws(FileManagerKitError) /// Moves a file or directory from a source URL to a destination URL. /// @@ -114,7 +118,7 @@ public protocol FileManagerKit { func move( from source: URL, to destination: URL - ) throws + ) throws(FileManagerKitError) /// Creates a symbolic (soft) link from a source path to a destination path. /// @@ -125,7 +129,7 @@ public protocol FileManagerKit { func softLink( from source: URL, to destination: URL - ) throws + ) throws(FileManagerKitError) /// Creates a hard link from a source path to a destination path. /// @@ -136,7 +140,7 @@ public protocol FileManagerKit { func hardLink( from source: URL, to destination: URL - ) throws + ) throws(FileManagerKitError) /// Deletes the file, directory, or symbolic link at the specified URL. /// @@ -144,7 +148,7 @@ public protocol FileManagerKit { /// - Throws: An error if the item could not be deleted. func delete( at url: URL - ) throws + ) throws(FileManagerKitError) // MARK: - @@ -192,7 +196,7 @@ public protocol FileManagerKit { /// - Throws: An error if attributes could not be retrieved. func attributes( at url: URL - ) throws -> [FileAttributeKey: Any] + ) throws(FileManagerKitError) -> [FileAttributeKey: Any] /// Retrieves the POSIX permissions for the file or directory at the specified URL. /// @@ -201,7 +205,7 @@ public protocol FileManagerKit { /// - Throws: An error if the permissions could not be retrieved. func permissions( at url: URL - ) throws -> Int + ) throws(FileManagerKitError) -> Int /// Returns the size of the file at the specified URL in bytes. /// @@ -210,7 +214,7 @@ public protocol FileManagerKit { /// - Throws: An error if the size could not be retrieved. func size( at url: URL - ) throws -> UInt64 + ) throws(FileManagerKitError) -> UInt64 /// Retrieves the creation date of the item at the specified URL. /// @@ -219,7 +223,7 @@ public protocol FileManagerKit { /// - Throws: An error if the creation date could not be retrieved. func creationDate( at url: URL - ) throws -> Date + ) throws(FileManagerKitError) -> Date /// Retrieves the last modification date of the item at the specified URL. /// @@ -228,7 +232,7 @@ public protocol FileManagerKit { /// - Throws: An error if the modification date could not be retrieved. func modificationDate( at url: URL - ) throws -> Date + ) throws(FileManagerKitError) -> Date /// Sets the file attributes at the specified URL. /// @@ -239,7 +243,7 @@ public protocol FileManagerKit { func setAttributes( _ attributes: [FileAttributeKey: Any], at url: URL - ) throws + ) throws(FileManagerKitError) /// Sets the POSIX file permissions at the specified URL. /// @@ -250,5 +254,5 @@ public protocol FileManagerKit { func setPermissions( _ permission: Int, at url: URL - ) throws + ) throws(FileManagerKitError) } diff --git a/Sources/FileManagerKit/FileManagerKitError.swift b/Sources/FileManagerKit/FileManagerKitError.swift new file mode 100644 index 0000000..67181c1 --- /dev/null +++ b/Sources/FileManagerKit/FileManagerKitError.swift @@ -0,0 +1,41 @@ +// +// FileManagerKitError.swift +// file-manager-kit +// +// Created by Binary Birds on 2026. 01. 30.. + +import Foundation + +/// The single error type thrown by FileManagerKit APIs. +/// +/// Swift 6 typed throws is used throughout the module, so keep this error +/// stable and expressive. +public enum FileManagerKitError: Error, Sendable { + + // MARK: - Directory & file operations + + case directoryCreateFailed(url: URL, underlying: Error) + case fileCreateFailed(url: URL) + + case copyFailed(source: URL, destination: URL, underlying: Error) + case moveFailed(source: URL, destination: URL, underlying: Error) + case deleteFailed(url: URL, underlying: Error) + + // MARK: - Attributes + + case attributesReadFailed(url: URL, underlying: Error) + case attributesWriteFailed(url: URL, underlying: Error) + + case missingAttribute(url: URL, key: FileAttributeKey) + case invalidAttributeType( + url: URL, + key: FileAttributeKey, + expected: String, + actual: String + ) + + // MARK: - POSIX + + /// Use this when you call POSIX APIs and have an `errno` value. + case posixError(path: String, errno: Int32) +} diff --git a/Sources/FileManagerKitBuilder/Build/Buildable.swift b/Sources/FileManagerKitBuilder/Build/Buildable.swift index 356e2e5..d5a8064 100644 --- a/Sources/FileManagerKitBuilder/Build/Buildable.swift +++ b/Sources/FileManagerKitBuilder/Build/Buildable.swift @@ -5,7 +5,12 @@ // Created by Viasz-Kádi Ferenc on 2025. 05. 30.. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif + import FileManagerKit /// A protocol that defines the ability to create or assemble resources at a given file system path. diff --git a/Sources/FileManagerKitBuilder/FileManagerPlayground.swift b/Sources/FileManagerKitBuilder/FileManagerPlayground.swift index 5f8eabe..5cc8e85 100644 --- a/Sources/FileManagerKitBuilder/FileManagerPlayground.swift +++ b/Sources/FileManagerKitBuilder/FileManagerPlayground.swift @@ -5,7 +5,12 @@ // Created by Viasz-Kádi Ferenc on 2025. 05. 30.. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif + import FileManagerKit /// A utility type for creating, testing, and cleaning up temporary file system hierarchies using `FileManager`. diff --git a/Sources/FileManagerKitBuilder/Items/Directory.swift b/Sources/FileManagerKitBuilder/Items/Directory.swift index fab8aa9..5ab45c3 100644 --- a/Sources/FileManagerKitBuilder/Items/Directory.swift +++ b/Sources/FileManagerKitBuilder/Items/Directory.swift @@ -5,7 +5,12 @@ // Created by Viasz-Kádi Ferenc on 2025. 05. 30.. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif + import FileManagerKit /// A `Buildable` representation of a directory in the file system. diff --git a/Sources/FileManagerKitBuilder/Items/File.swift b/Sources/FileManagerKitBuilder/Items/File.swift index 5d7a24b..2861a9b 100644 --- a/Sources/FileManagerKitBuilder/Items/File.swift +++ b/Sources/FileManagerKitBuilder/Items/File.swift @@ -5,7 +5,12 @@ // Created by Viasz-Kádi Ferenc on 2025. 05. 30.. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif + import FileManagerKit /// A `Buildable` representation of a file in the file system. diff --git a/Sources/FileManagerKitBuilder/Items/Link.swift b/Sources/FileManagerKitBuilder/Items/Link.swift index b525fbd..667ac49 100644 --- a/Sources/FileManagerKitBuilder/Items/Link.swift +++ b/Sources/FileManagerKitBuilder/Items/Link.swift @@ -5,7 +5,12 @@ // Created by Viasz-Kádi Ferenc on 2025. 05. 30.. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif + import FileManagerKit /// A `Buildable` representation of a file system link, either symbolic or hard. diff --git a/Tests/FileManagerKitBuilderTests/DirectoryBuilderTestSuite.swift b/Tests/FileManagerKitBuilderTests/DirectoryBuilderTestSuite.swift index cab100e..2b62596 100644 --- a/Tests/FileManagerKitBuilderTests/DirectoryBuilderTestSuite.swift +++ b/Tests/FileManagerKitBuilderTests/DirectoryBuilderTestSuite.swift @@ -5,9 +5,14 @@ // Created by Viasz-Kádi Ferenc on 2025. 04. 01.. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation -import Testing +#endif +import Testing +import FileManagerKit @testable import FileManagerKitBuilder @Suite diff --git a/Tests/FileManagerKitBuilderTests/JSON.swift b/Tests/FileManagerKitBuilderTests/JSON.swift index dec3037..2108384 100644 --- a/Tests/FileManagerKitBuilderTests/JSON.swift +++ b/Tests/FileManagerKitBuilderTests/JSON.swift @@ -5,9 +5,14 @@ // Created by Viasz-Kádi Ferenc on 2025. 05. 30.. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + import FileManagerKit import FileManagerKitBuilder -import Foundation /// A `BuildableItem` that generates a `.json` file from any `Encodable` type. /// diff --git a/Tests/FileManagerKitTests/FileManagerKitTestSuite.swift b/Tests/FileManagerKitTests/FileManagerKitTestSuite.swift index 76fd4b1..3b5aea8 100644 --- a/Tests/FileManagerKitTests/FileManagerKitTestSuite.swift +++ b/Tests/FileManagerKitTests/FileManagerKitTestSuite.swift @@ -5,8 +5,13 @@ // Created by Viasz-Kádi Ferenc on 2025. 04. 01.. // -import FileManagerKitBuilder +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif + +import FileManagerKitBuilder import Testing @testable import FileManagerKit @@ -144,7 +149,7 @@ struct FileManagerKitTestSuite { let url = rootUrl.appending(path: "foo/bar/baz") #expect( - throws: CocoaError(.fileWriteUnknown), + throws: FileManagerKitError.self, performing: { try fileManager.createFile( at: url, @@ -249,11 +254,15 @@ struct FileManagerKitTestSuite { do { try $0.delete(at: url) - #expect(Bool(false)) + Issue.record("Expected error was not thrown") } - catch let error as NSError { - #expect(error.domain == NSCocoaErrorDomain) - #expect(error.code == 4) + catch let error as FileManagerKitError { + switch error { + case .deleteFailed: + return + default: + Issue.record("Unexpected error type") + } } } } @@ -347,11 +356,15 @@ struct FileManagerKitTestSuite { do { try $0.copy(from: source, to: destination) - #expect(Bool(false)) + Issue.record("Expected error was not thrown") } - catch let error as NSError { - #expect(error.domain == NSCocoaErrorDomain) - #expect(error.code == 260) + catch let error as FileManagerKitError { + switch error { + case .copyFailed: + return + default: + Issue.record("Unexpected error type") + } } } } @@ -368,11 +381,15 @@ struct FileManagerKitTestSuite { do { try $0.copy(from: source, to: destination) - #expect(Bool(false)) + Issue.record("Expected error was not thrown") } - catch let error as NSError { - #expect(error.domain == NSCocoaErrorDomain) - #expect(error.code == 516) + catch let error as FileManagerKitError { + switch error { + case .copyFailed: + return + default: + Issue.record("Unexpected error type") + } } } } @@ -404,11 +421,15 @@ struct FileManagerKitTestSuite { do { try $0.move(from: source, to: destination) - #expect(Bool(false)) + Issue.record("Expected error was not thrown") } - catch let error as NSError { - #expect(error.domain == NSCocoaErrorDomain) - #expect(error.code == 4) + catch let error as FileManagerKitError { + switch error { + case .moveFailed: + return + default: + Issue.record("Unexpected error type") + } } } } @@ -425,11 +446,15 @@ struct FileManagerKitTestSuite { do { try $0.move(from: source, to: destination) - #expect(Bool(false)) + Issue.record("Expected error was not thrown") } - catch let error as NSError { - #expect(error.domain == NSCocoaErrorDomain) - #expect(error.code == 516) + catch let error as FileManagerKitError { + switch error { + case .moveFailed: + return + default: + Issue.record("Unexpected error type") + } } } } @@ -495,11 +520,15 @@ struct FileManagerKitTestSuite { do { try $0.softLink(from: source, to: destination) - #expect(Bool(false)) + Issue.record("Expected error was not thrown") } - catch let error as NSError { - #expect(error.domain == NSCocoaErrorDomain) - #expect(error.code == 516) + catch let error as FileManagerKitError { + switch error { + case .copyFailed: + return + default: + Issue.record("Unexpected error type") + } } } } @@ -533,11 +562,15 @@ struct FileManagerKitTestSuite { do { _ = try $0.creationDate(at: file) - #expect(Bool(false)) + Issue.record("Expected error was not thrown") } - catch let error as NSError { - #expect(error.domain == NSCocoaErrorDomain) - #expect(error.code == 260) + catch let error as FileManagerKitError { + switch error { + case .attributesReadFailed: + return + default: + Issue.record("Unexpected error type") + } } } } @@ -567,11 +600,15 @@ struct FileManagerKitTestSuite { do { _ = try $0.modificationDate(at: file) - #expect(Bool(false)) + Issue.record("Expected error was not thrown") } - catch let error as NSError { - #expect(error.domain == NSCocoaErrorDomain) - #expect(error.code == 260) + catch let error as FileManagerKitError { + switch error { + case .attributesReadFailed: + return + default: + Issue.record("Unexpected error type") + } } } } @@ -637,11 +674,15 @@ struct FileManagerKitTestSuite { .modificationDate: Date() ] try $0.setAttributes(attributes, at: url) - #expect(Bool(false)) + Issue.record("Expected error was not thrown") } - catch let error as NSError { - #expect(error.domain == NSCocoaErrorDomain) - #expect(error.code == 4) + catch let error as FileManagerKitError { + switch error { + case .attributesWriteFailed: + return + default: + Issue.record("Unexpected error type") + } } } } @@ -672,11 +713,15 @@ struct FileManagerKitTestSuite { do { try $0.setPermissions(600, at: url) - #expect(Bool(false)) + Issue.record("Expected error was not thrown") } - catch let error as NSError { - #expect(error.domain == NSCocoaErrorDomain) - #expect(error.code == 4) + catch let error as FileManagerKitError { + switch error { + case .attributesWriteFailed: + return + default: + Issue.record("Unexpected error type") + } } } } diff --git a/Docker/Dockerfile.testing b/docker/tests/dockerfile similarity index 70% rename from Docker/Dockerfile.testing rename to docker/tests/dockerfile index 9f72ca5..a73434c 100644 --- a/Docker/Dockerfile.testing +++ b/docker/tests/dockerfile @@ -1,4 +1,4 @@ -FROM swift:6.0 +FROM swift:6.1 WORKDIR /app @@ -7,5 +7,4 @@ COPY . ./ RUN swift package resolve RUN swift package clean -# CMD ["swift", "build", "-c", "release"] CMD ["swift", "test", "--parallel", "--enable-code-coverage"]