From 9cf191f4e5375ee9fecb9ca3287d29ce0a35f3fc Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Fri, 10 Jan 2025 17:43:05 +0000 Subject: [PATCH 01/31] feat: create directory Since it's the first feature to be implemented, it includes adding the new files. --- OSFilesystemLib.xcodeproj/project.pbxproj | 34 +++++----- OSFilesystemLib/OSFLSTManager.swift | 19 ++++++ OSFilesystemLib/OSFilesystemLib.swift | 12 ---- OSFilesystemLibTests/MockFileManager.swift | 27 ++++++++ OSFilesystemLibTests/OSFLSTManagerTests.swift | 62 +++++++++++++++++++ OSFilesystemLibTests/OSFilesystemLib.swift | 11 ---- 6 files changed, 127 insertions(+), 38 deletions(-) create mode 100644 OSFilesystemLib/OSFLSTManager.swift delete mode 100644 OSFilesystemLib/OSFilesystemLib.swift create mode 100644 OSFilesystemLibTests/MockFileManager.swift create mode 100644 OSFilesystemLibTests/OSFLSTManagerTests.swift delete mode 100644 OSFilesystemLibTests/OSFilesystemLib.swift diff --git a/OSFilesystemLib.xcodeproj/project.pbxproj b/OSFilesystemLib.xcodeproj/project.pbxproj index 5b259b9..408807e 100644 --- a/OSFilesystemLib.xcodeproj/project.pbxproj +++ b/OSFilesystemLib.xcodeproj/project.pbxproj @@ -7,9 +7,10 @@ objects = { /* Begin PBXBuildFile section */ - 756346652C00F21000685AA3 /* OSFilesystemLib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 756346612C00F21000685AA3 /* OSFilesystemLib.swift */; }; + 751328D52D3175170031BDD0 /* OSFLSTManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D42D3175170031BDD0 /* OSFLSTManager.swift */; }; + 751328DA2D318DBA0031BDD0 /* OSFLSTManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D92D318DBA0031BDD0 /* OSFLSTManagerTests.swift */; }; + 751328DB2D318E770031BDD0 /* MockFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D82D3179430031BDD0 /* MockFileManager.swift */; }; 7575CF6A2BFCEE6F008F3FD0 /* OSFilesystemLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7575CF612BFCEE6F008F3FD0 /* OSFilesystemLib.framework */; }; - 7575CF802BFCEEEA008F3FD0 /* OSFilesystemLib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7575CF7D2BFCEEEA008F3FD0 /* OSFilesystemLib.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -23,10 +24,11 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 756346612C00F21000685AA3 /* OSFilesystemLib.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSFilesystemLib.swift; sourceTree = ""; }; + 751328D42D3175170031BDD0 /* OSFLSTManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFLSTManager.swift; sourceTree = ""; }; + 751328D82D3179430031BDD0 /* MockFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFileManager.swift; sourceTree = ""; }; + 751328D92D318DBA0031BDD0 /* OSFLSTManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFLSTManagerTests.swift; sourceTree = ""; }; 7575CF612BFCEE6F008F3FD0 /* OSFilesystemLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OSFilesystemLib.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7575CF692BFCEE6F008F3FD0 /* OSFilesystemLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OSFilesystemLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 7575CF7D2BFCEEEA008F3FD0 /* OSFilesystemLib.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSFilesystemLib.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -69,7 +71,7 @@ 7575CF632BFCEE6F008F3FD0 /* OSFilesystemLib */ = { isa = PBXGroup; children = ( - 756346612C00F21000685AA3 /* OSFilesystemLib.swift */, + 751328D42D3175170031BDD0 /* OSFLSTManager.swift */, ); path = OSFilesystemLib; sourceTree = ""; @@ -77,7 +79,8 @@ 7575CF6D2BFCEE6F008F3FD0 /* OSFilesystemLibTests */ = { isa = PBXGroup; children = ( - 7575CF7D2BFCEEEA008F3FD0 /* OSFilesystemLib.swift */, + 751328D82D3179430031BDD0 /* MockFileManager.swift */, + 751328D92D318DBA0031BDD0 /* OSFLSTManagerTests.swift */, ); path = OSFilesystemLibTests; sourceTree = ""; @@ -140,15 +143,15 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1510; - LastUpgradeCheck = 1510; + LastUpgradeCheck = 1600; TargetAttributes = { 7575CF602BFCEE6F008F3FD0 = { CreatedOnToolsVersion = 15.1; - LastSwiftMigration = 1510; + LastSwiftMigration = 1600; }; 7575CF682BFCEE6F008F3FD0 = { CreatedOnToolsVersion = 15.1; - LastSwiftMigration = 1510; + LastSwiftMigration = 1600; }; }; }; @@ -217,7 +220,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 756346652C00F21000685AA3 /* OSFilesystemLib.swift in Sources */, + 751328D52D3175170031BDD0 /* OSFLSTManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -225,7 +228,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7575CF802BFCEEEA008F3FD0 /* OSFilesystemLib.swift in Sources */, + 751328DA2D318DBA0031BDD0 /* OSFLSTManagerTests.swift in Sources */, + 751328DB2D318E770031BDD0 /* MockFileManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -374,6 +378,7 @@ buildSettings = { BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; @@ -384,7 +389,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -408,6 +413,7 @@ buildSettings = { BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; @@ -418,7 +424,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -439,7 +445,6 @@ 7575CF772BFCEE6F008F3FD0 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -461,7 +466,6 @@ 7575CF782BFCEE6F008F3FD0 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; diff --git a/OSFilesystemLib/OSFLSTManager.swift b/OSFilesystemLib/OSFLSTManager.swift new file mode 100644 index 0000000..bce0b5c --- /dev/null +++ b/OSFilesystemLib/OSFLSTManager.swift @@ -0,0 +1,19 @@ +import Foundation + +public protocol OSFLSTDirectoryManager { + func createDirectory(atPathURL: URL, includeIntermediateDirectories: Bool) throws +} + +public struct OSFLSTManager { + private let fileManager: FileManager + + public init(fileManager: FileManager) { + self.fileManager = fileManager + } +} + +extension OSFLSTManager: OSFLSTDirectoryManager { + public func createDirectory(atPathURL pathURL: URL, includeIntermediateDirectories: Bool) throws { + try fileManager.createDirectory(at: pathURL, withIntermediateDirectories: includeIntermediateDirectories) + } +} diff --git a/OSFilesystemLib/OSFilesystemLib.swift b/OSFilesystemLib/OSFilesystemLib.swift deleted file mode 100644 index de9a61e..0000000 --- a/OSFilesystemLib/OSFilesystemLib.swift +++ /dev/null @@ -1,12 +0,0 @@ - -public struct OSFilesystemLib { - /// Constructor method. - public init() { - // Empty constructor - // This is required for the library's callers. - } - - public func ping(_ input: String) -> String { - return "PONG_" + input - } -} diff --git a/OSFilesystemLibTests/MockFileManager.swift b/OSFilesystemLibTests/MockFileManager.swift new file mode 100644 index 0000000..5bc75bb --- /dev/null +++ b/OSFilesystemLibTests/MockFileManager.swift @@ -0,0 +1,27 @@ +import Foundation + +class MockFileManager: FileManager { + var shouldThrowError: Bool + + private(set) var capturedPathURL: URL? + private(set) var capturedIntermediateDirectories: Bool = false + + init(shouldThrowError: Bool = false) { + self.shouldThrowError = shouldThrowError + } +} + +enum MockFileManagerError: Error { + case genericError +} + +extension MockFileManager { + override func createDirectory(at url: URL, withIntermediateDirectories createIntermediates: Bool, attributes: [FileAttributeKey: Any]? = nil) throws { + capturedPathURL = url + capturedIntermediateDirectories = createIntermediates + + if shouldThrowError { + throw MockFileManagerError.genericError + } + } +} diff --git a/OSFilesystemLibTests/OSFLSTManagerTests.swift b/OSFilesystemLibTests/OSFLSTManagerTests.swift new file mode 100644 index 0000000..01ef367 --- /dev/null +++ b/OSFilesystemLibTests/OSFLSTManagerTests.swift @@ -0,0 +1,62 @@ +import OSFilesystemLib +import XCTest + +final class OSFLSTManagerTests: XCTestCase { + private var sut: OSFLSTManager! + + // MARK: - 'createDirectory' tests + func test_createDirectory_withoutIntermediateDictionaries_shouldBeSuccessful() throws { + // Given + let fileManager = createFileManager() + + let testDirectoryURL = URL(fileURLWithPath: "/test/directory") + let shouldIncludeIntermediateDirectories = false + + // When + try sut.createDirectory(atPathURL: testDirectoryURL, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) + + // Then + XCTAssertEqual(fileManager.capturedPathURL, testDirectoryURL) + XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories) + } + + func test_createDirectory_withIntermediateDictionaries_shouldBeSuccessful() throws { + // Given + let fileManager = createFileManager() + + let testDirectoryURL = URL(fileURLWithPath: "/test/directory") + let shouldIncludeIntermediateDirectories = true + + // When + try sut.createDirectory(atPathURL: testDirectoryURL, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) + + // Then + XCTAssertEqual(fileManager.capturedPathURL, testDirectoryURL) + XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories) + } + + func test_createDictionary_butFails_shouldReturnAnError() { + // Given + createFileManager(shouldThrowError: true) + + let testDirectoryURL = URL(fileURLWithPath: "/test/directory") + let shouldIncludeIntermediateDirectories = false + + // When + XCTAssertThrowsError( + try sut.createDirectory(atPathURL: testDirectoryURL, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) + ) { + // Then + XCTAssertNotNil($0) + } + } +} + +private extension OSFLSTManagerTests { + @discardableResult func createFileManager(shouldThrowError: Bool = false) -> MockFileManager { + let fileManager = MockFileManager(shouldThrowError: shouldThrowError) + sut = OSFLSTManager(fileManager: fileManager) + + return fileManager + } +} diff --git a/OSFilesystemLibTests/OSFilesystemLib.swift b/OSFilesystemLibTests/OSFilesystemLib.swift deleted file mode 100644 index d63b7d5..0000000 --- a/OSFilesystemLibTests/OSFilesystemLib.swift +++ /dev/null @@ -1,11 +0,0 @@ -import OSFilesystemLib -import SafariServices -import XCTest - -final class OSFilesystemLib: XCTestCase { - -} - -extension OSFilesystemLib { - func makeSUT() -> OSFilesystemLib { .init() } -} From 378450d7b49f7866e1e58ee466d7db1916e59a71 Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Mon, 13 Jan 2025 10:41:05 +0000 Subject: [PATCH 02/31] feat: delete directory Includes a few refactors on create directory to keep structure as similar as possible. --- OSFilesystemLib/OSFLSTManager.swift | 22 ++++- OSFilesystemLibTests/MockFileManager.swift | 41 ++++++-- OSFilesystemLibTests/OSFLSTManagerTests.swift | 95 +++++++++++++++---- 3 files changed, 129 insertions(+), 29 deletions(-) diff --git a/OSFilesystemLib/OSFLSTManager.swift b/OSFilesystemLib/OSFLSTManager.swift index bce0b5c..3547596 100644 --- a/OSFilesystemLib/OSFLSTManager.swift +++ b/OSFilesystemLib/OSFLSTManager.swift @@ -1,7 +1,12 @@ import Foundation public protocol OSFLSTDirectoryManager { - func createDirectory(atPathURL: URL, includeIntermediateDirectories: Bool) throws + func createDirectory(atPath: String, includeIntermediateDirectories: Bool) throws + func removeDirectory(atPath: String, includeIntermediateDirectories: Bool) throws +} + +enum OSFLSTDirectoryManagerError: Error { + case notEmpty } public struct OSFLSTManager { @@ -13,7 +18,20 @@ public struct OSFLSTManager { } extension OSFLSTManager: OSFLSTDirectoryManager { - public func createDirectory(atPathURL pathURL: URL, includeIntermediateDirectories: Bool) throws { + public func createDirectory(atPath path: String, includeIntermediateDirectories: Bool) throws { + let pathURL = URL(fileURLWithPath: path) try fileManager.createDirectory(at: pathURL, withIntermediateDirectories: includeIntermediateDirectories) } + + public func removeDirectory(atPath path: String, includeIntermediateDirectories: Bool) throws { + let pathURL = URL(fileURLWithPath: path) + if !includeIntermediateDirectories { + let directoryContents = try fileManager.contentsOfDirectory(at: pathURL, includingPropertiesForKeys: nil) + if !directoryContents.isEmpty { + throw OSFLSTDirectoryManagerError.notEmpty + } + } + + try fileManager.removeItem(at: pathURL) + } } diff --git a/OSFilesystemLibTests/MockFileManager.swift b/OSFilesystemLibTests/MockFileManager.swift index 5bc75bb..bba1f74 100644 --- a/OSFilesystemLibTests/MockFileManager.swift +++ b/OSFilesystemLibTests/MockFileManager.swift @@ -1,27 +1,52 @@ import Foundation class MockFileManager: FileManager { - var shouldThrowError: Bool + var error: MockFileManagerError? + var shouldDirectoryHaveContent: Bool - private(set) var capturedPathURL: URL? + private(set) var capturedPath: String? private(set) var capturedIntermediateDirectories: Bool = false - init(shouldThrowError: Bool = false) { - self.shouldThrowError = shouldThrowError + init(error: MockFileManagerError? = nil, shouldDirectoryHaveContent: Bool = false) { + self.error = error + self.shouldDirectoryHaveContent = shouldDirectoryHaveContent } } enum MockFileManagerError: Error { - case genericError + case createDirectoryError + case readDirectoryError + case deleteDirectoryError } extension MockFileManager { override func createDirectory(at url: URL, withIntermediateDirectories createIntermediates: Bool, attributes: [FileAttributeKey: Any]? = nil) throws { - capturedPathURL = url + capturedPath = url.relativePath capturedIntermediateDirectories = createIntermediates - if shouldThrowError { - throw MockFileManagerError.genericError + if let error, error == .createDirectoryError { + throw error + } + } + + override func contentsOfDirectory(at url: URL, includingPropertiesForKeys keys: [URLResourceKey]?, options mask: FileManager.DirectoryEnumerationOptions = []) throws -> [URL] { + var urls = [URL]() + if shouldDirectoryHaveContent { + urls += [url] + } + + if let error, error == .readDirectoryError { + throw error + } + + return urls + } + + override func removeItem(at url: URL) throws { + capturedPath = url.relativePath + + if let error, error == .deleteDirectoryError { + throw error } } } diff --git a/OSFilesystemLibTests/OSFLSTManagerTests.swift b/OSFilesystemLibTests/OSFLSTManagerTests.swift index 01ef367..28c1bd7 100644 --- a/OSFilesystemLibTests/OSFLSTManagerTests.swift +++ b/OSFilesystemLibTests/OSFLSTManagerTests.swift @@ -1,60 +1,117 @@ -import OSFilesystemLib import XCTest +@testable import OSFilesystemLib + final class OSFLSTManagerTests: XCTestCase { private var sut: OSFLSTManager! // MARK: - 'createDirectory' tests - func test_createDirectory_withoutIntermediateDictionaries_shouldBeSuccessful() throws { + func test_createDirectory_shouldBeSuccessful() throws { // Given let fileManager = createFileManager() - - let testDirectoryURL = URL(fileURLWithPath: "/test/directory") + let testDirectory = "/test/directory" let shouldIncludeIntermediateDirectories = false // When - try sut.createDirectory(atPathURL: testDirectoryURL, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) + try sut.createDirectory(atPath: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) // Then - XCTAssertEqual(fileManager.capturedPathURL, testDirectoryURL) + XCTAssertEqual(fileManager.capturedPath, testDirectory) XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories) } - func test_createDirectory_withIntermediateDictionaries_shouldBeSuccessful() throws { + func test_createDirectory_butFails_shouldReturnAnError() { // Given - let fileManager = createFileManager() + let error = MockFileManagerError.createDirectoryError + createFileManager(with: error) + let testDirectory = "/test/directory" + let shouldIncludeIntermediateDirectories = false + + // When + XCTAssertThrowsError( + try sut.createDirectory(atPath: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) + ) { + // Then + XCTAssertEqual($0 as? MockFileManagerError, error) + } + } + + // MARK: - 'removeDirectory' tests + func test_removeDirectory_butFails_shouldReturnAnError() { + let error = MockFileManagerError.deleteDirectoryError + createFileManager(with: error) + let testDirectory = "/test/directory" + let shouldIncludeIntermediateDirectories = true + + // When + XCTAssertThrowsError( + try sut.removeDirectory(atPath: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) + ) { + // Then + XCTAssertEqual($0 as? MockFileManagerError, error) + } + } - let testDirectoryURL = URL(fileURLWithPath: "/test/directory") + func test_removeDirectory_includingIntermediateDirectories_shouldBeSuccessful() throws { + // Given + let fileManager = createFileManager() + let testDirectory = "/test/directory" let shouldIncludeIntermediateDirectories = true // When - try sut.createDirectory(atPathURL: testDirectoryURL, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) + try sut.removeDirectory(atPath: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) // Then - XCTAssertEqual(fileManager.capturedPathURL, testDirectoryURL) - XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories) + XCTAssertEqual(fileManager.capturedPath, testDirectory) + } + + func test_removeDirectory_excludingIntermediateDirectories_directoryDoesntHaveContent_shouldBeSuccessful() throws { + // Given + let fileManager = createFileManager() + let testDirectory = "/test/directory" + let shouldIncludeIntermediateDirectories = false + + // When + try sut.removeDirectory(atPath: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) + + // Then + XCTAssertEqual(fileManager.capturedPath, testDirectory) } - func test_createDictionary_butFails_shouldReturnAnError() { + func test_removeDirectory_excludingIntermediateDirectories_butFailsOnReadingDirectory_shouldReturnAnError() { // Given - createFileManager(shouldThrowError: true) + let error = MockFileManagerError.readDirectoryError + createFileManager(with: error) + let testDirectory = "/test/directory" + let shouldIncludeIntermediateDirectories = false + + // When + XCTAssertThrowsError( + try sut.removeDirectory(atPath: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) + ) { + // Then + XCTAssertEqual($0 as? MockFileManagerError, error) + } + } - let testDirectoryURL = URL(fileURLWithPath: "/test/directory") + func test_removeDirectory_excludingIntermediateDirectories_directoryHasContent_shouldReturnAnError() { + createFileManager(shouldDirectoryHaveContent: true) + let testDirectory = "/test/directory" let shouldIncludeIntermediateDirectories = false // When XCTAssertThrowsError( - try sut.createDirectory(atPathURL: testDirectoryURL, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) + try sut.removeDirectory(atPath: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) ) { // Then - XCTAssertNotNil($0) + XCTAssertEqual($0 as? OSFLSTDirectoryManagerError, .notEmpty) } } } private extension OSFLSTManagerTests { - @discardableResult func createFileManager(shouldThrowError: Bool = false) -> MockFileManager { - let fileManager = MockFileManager(shouldThrowError: shouldThrowError) + @discardableResult func createFileManager(with error: MockFileManagerError? = nil, shouldDirectoryHaveContent: Bool = false) -> MockFileManager { + let fileManager = MockFileManager(error: error, shouldDirectoryHaveContent: shouldDirectoryHaveContent) sut = OSFLSTManager(fileManager: fileManager) return fileManager From 1509ebfb9f7addfcfc4ce71fb9ea64d9bc1da88d Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Mon, 13 Jan 2025 11:52:29 +0000 Subject: [PATCH 03/31] chore: manage to be deprecated method Create a URLFactory that, depending on the current build iOS version, uses the new or current (to be deprecated) version of creating an URL based on a path string. --- OSFilesystemLib/OSFLSTManager.swift | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/OSFilesystemLib/OSFLSTManager.swift b/OSFilesystemLib/OSFLSTManager.swift index 3547596..e4043e4 100644 --- a/OSFilesystemLib/OSFLSTManager.swift +++ b/OSFilesystemLib/OSFLSTManager.swift @@ -19,12 +19,12 @@ public struct OSFLSTManager { extension OSFLSTManager: OSFLSTDirectoryManager { public func createDirectory(atPath path: String, includeIntermediateDirectories: Bool) throws { - let pathURL = URL(fileURLWithPath: path) + let pathURL = URLFactory.create(with: path) try fileManager.createDirectory(at: pathURL, withIntermediateDirectories: includeIntermediateDirectories) } public func removeDirectory(atPath path: String, includeIntermediateDirectories: Bool) throws { - let pathURL = URL(fileURLWithPath: path) + let pathURL = URLFactory.create(with: path) if !includeIntermediateDirectories { let directoryContents = try fileManager.contentsOfDirectory(at: pathURL, includingPropertiesForKeys: nil) if !directoryContents.isEmpty { @@ -35,3 +35,17 @@ extension OSFLSTManager: OSFLSTDirectoryManager { try fileManager.removeItem(at: pathURL) } } + +private struct URLFactory { + static func create(with path: String) -> URL { + let url: URL + + if #available(iOS 16.0, *) { + url = .init(filePath: path) + } else { + url = .init(fileURLWithPath: path) + } + + return url + } +} From 6b600f35812af8762466b8085abbf8f89fc64ed7 Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Mon, 13 Jan 2025 11:55:49 +0000 Subject: [PATCH 04/31] feat: list directory Refactor existing implementation to use the new method. --- OSFilesystemLib/OSFLSTManager.swift | 8 +++- OSFilesystemLibTests/MockFileManager.swift | 2 + OSFilesystemLibTests/OSFLSTManagerTests.swift | 47 +++++++++++++++---- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/OSFilesystemLib/OSFLSTManager.swift b/OSFilesystemLib/OSFLSTManager.swift index e4043e4..4e712fa 100644 --- a/OSFilesystemLib/OSFLSTManager.swift +++ b/OSFilesystemLib/OSFLSTManager.swift @@ -3,6 +3,7 @@ import Foundation public protocol OSFLSTDirectoryManager { func createDirectory(atPath: String, includeIntermediateDirectories: Bool) throws func removeDirectory(atPath: String, includeIntermediateDirectories: Bool) throws + func listDirectory(atPath: String) throws -> [URL] } enum OSFLSTDirectoryManagerError: Error { @@ -26,7 +27,7 @@ extension OSFLSTManager: OSFLSTDirectoryManager { public func removeDirectory(atPath path: String, includeIntermediateDirectories: Bool) throws { let pathURL = URLFactory.create(with: path) if !includeIntermediateDirectories { - let directoryContents = try fileManager.contentsOfDirectory(at: pathURL, includingPropertiesForKeys: nil) + let directoryContents = try listDirectory(atPath: path) if !directoryContents.isEmpty { throw OSFLSTDirectoryManagerError.notEmpty } @@ -34,6 +35,11 @@ extension OSFLSTManager: OSFLSTDirectoryManager { try fileManager.removeItem(at: pathURL) } + + public func listDirectory(atPath path: String) throws -> [URL] { + let pathURL = URLFactory.create(with: path) + return try fileManager.contentsOfDirectory(at: pathURL, includingPropertiesForKeys: nil) + } } private struct URLFactory { diff --git a/OSFilesystemLibTests/MockFileManager.swift b/OSFilesystemLibTests/MockFileManager.swift index bba1f74..ea33e14 100644 --- a/OSFilesystemLibTests/MockFileManager.swift +++ b/OSFilesystemLibTests/MockFileManager.swift @@ -30,6 +30,8 @@ extension MockFileManager { } override func contentsOfDirectory(at url: URL, includingPropertiesForKeys keys: [URLResourceKey]?, options mask: FileManager.DirectoryEnumerationOptions = []) throws -> [URL] { + capturedPath = url.relativePath + var urls = [URL]() if shouldDirectoryHaveContent { urls += [url] diff --git a/OSFilesystemLibTests/OSFLSTManagerTests.swift b/OSFilesystemLibTests/OSFLSTManagerTests.swift index 28c1bd7..e021ace 100644 --- a/OSFilesystemLibTests/OSFLSTManagerTests.swift +++ b/OSFilesystemLibTests/OSFLSTManagerTests.swift @@ -78,10 +78,8 @@ final class OSFLSTManagerTests: XCTestCase { XCTAssertEqual(fileManager.capturedPath, testDirectory) } - func test_removeDirectory_excludingIntermediateDirectories_butFailsOnReadingDirectory_shouldReturnAnError() { - // Given - let error = MockFileManagerError.readDirectoryError - createFileManager(with: error) + func test_removeDirectory_excludingIntermediateDirectories_directoryHasContent_shouldReturnAnError() { + createFileManager(shouldDirectoryHaveContent: true) let testDirectory = "/test/directory" let shouldIncludeIntermediateDirectories = false @@ -90,21 +88,50 @@ final class OSFLSTManagerTests: XCTestCase { try sut.removeDirectory(atPath: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) ) { // Then - XCTAssertEqual($0 as? MockFileManagerError, error) + XCTAssertEqual($0 as? OSFLSTDirectoryManagerError, .notEmpty) } } - func test_removeDirectory_excludingIntermediateDirectories_directoryHasContent_shouldReturnAnError() { - createFileManager(shouldDirectoryHaveContent: true) + // MARK: - 'listDirectory' tests + func test_listDirectory_withNoContent_shouldReturnEmptyArray() throws { + // Given + let fileManager = createFileManager() + let testDirectory = "/test/directory" + + // When + let directoryContent = try sut.listDirectory(atPath: testDirectory) + + // Then + XCTAssertEqual(fileManager.capturedPath, testDirectory) + XCTAssertTrue(directoryContent.isEmpty) + } + + // MARK: - 'listDirectory' tests + func test_listDirectory_withContent_shouldReturnEmptyArray() throws { + // Given + let fileManager = createFileManager(shouldDirectoryHaveContent: true) + let testDirectory = "/test/directory" + + // When + let directoryContent = try sut.listDirectory(atPath: testDirectory) + + // Then + XCTAssertEqual(fileManager.capturedPath, testDirectory) + XCTAssertEqual(directoryContent.map { $0.relativePath }, [testDirectory]) + } + + func test_listDirectory_butFails_shouldReturnAnError() { + // Given + let error = MockFileManagerError.readDirectoryError + createFileManager(with: error) let testDirectory = "/test/directory" - let shouldIncludeIntermediateDirectories = false // When XCTAssertThrowsError( - try sut.removeDirectory(atPath: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) + try sut.listDirectory(atPath: testDirectory) ) { // Then - XCTAssertEqual($0 as? OSFLSTDirectoryManagerError, .notEmpty) + XCTAssertEqual($0 as? MockFileManagerError, error) } } } From 27b8636e09712edae1e7faba0150195c8f4d4640 Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Tue, 14 Jan 2025 11:42:37 +0000 Subject: [PATCH 05/31] feat: read file Refactor project to make it easier to navigate. Add new swiftlint disabled rule. --- .swiftlint.yml | 1 + OSFilesystemLib.xcodeproj/project.pbxproj | 24 +++++-- OSFilesystemLib/OSFLSTManager+Assets.swift | 34 ++++++++++ OSFilesystemLib/OSFLSTManager.swift | 46 ++++++++------ OSFilesystemLib/URLFactory.swift | 15 +++++ ...wift => OSFLSTDirectoryManagerTests.swift} | 4 +- .../OSFLSTFileManagerTests.swift | 62 +++++++++++++++++++ OSFilesystemLibTests/file.txt | 1 + 8 files changed, 162 insertions(+), 25 deletions(-) create mode 100644 OSFilesystemLib/OSFLSTManager+Assets.swift create mode 100644 OSFilesystemLib/URLFactory.swift rename OSFilesystemLibTests/{OSFLSTManagerTests.swift => OSFLSTDirectoryManagerTests.swift} (98%) create mode 100644 OSFilesystemLibTests/OSFLSTFileManagerTests.swift create mode 100644 OSFilesystemLibTests/file.txt diff --git a/.swiftlint.yml b/.swiftlint.yml index f670eca..66bf042 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,5 +1,6 @@ disabled_rules: - trailing_whitespace +- switch_case_alignment opt_in_rules: - empty_count - empty_string diff --git a/OSFilesystemLib.xcodeproj/project.pbxproj b/OSFilesystemLib.xcodeproj/project.pbxproj index 408807e..c1b188f 100644 --- a/OSFilesystemLib.xcodeproj/project.pbxproj +++ b/OSFilesystemLib.xcodeproj/project.pbxproj @@ -8,9 +8,13 @@ /* Begin PBXBuildFile section */ 751328D52D3175170031BDD0 /* OSFLSTManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D42D3175170031BDD0 /* OSFLSTManager.swift */; }; - 751328DA2D318DBA0031BDD0 /* OSFLSTManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D92D318DBA0031BDD0 /* OSFLSTManagerTests.swift */; }; + 751328DA2D318DBA0031BDD0 /* OSFLSTDirectoryManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D92D318DBA0031BDD0 /* OSFLSTDirectoryManagerTests.swift */; }; 751328DB2D318E770031BDD0 /* MockFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D82D3179430031BDD0 /* MockFileManager.swift */; }; 7575CF6A2BFCEE6F008F3FD0 /* OSFilesystemLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7575CF612BFCEE6F008F3FD0 /* OSFilesystemLib.framework */; }; + 75FEB2B02D3546D7007C2686 /* OSFLSTManager+Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FEB2AF2D3546D7007C2686 /* OSFLSTManager+Assets.swift */; }; + 75FEB2B22D35470C007C2686 /* URLFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FEB2B12D35470C007C2686 /* URLFactory.swift */; }; + 75FEB2B42D35479B007C2686 /* OSFLSTFileManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FEB2B32D35479B007C2686 /* OSFLSTFileManagerTests.swift */; }; + 75FEB2B72D355F21007C2686 /* file.txt in Resources */ = {isa = PBXBuildFile; fileRef = 75FEB2B62D355F21007C2686 /* file.txt */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -26,9 +30,13 @@ /* Begin PBXFileReference section */ 751328D42D3175170031BDD0 /* OSFLSTManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFLSTManager.swift; sourceTree = ""; }; 751328D82D3179430031BDD0 /* MockFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFileManager.swift; sourceTree = ""; }; - 751328D92D318DBA0031BDD0 /* OSFLSTManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFLSTManagerTests.swift; sourceTree = ""; }; + 751328D92D318DBA0031BDD0 /* OSFLSTDirectoryManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFLSTDirectoryManagerTests.swift; sourceTree = ""; }; 7575CF612BFCEE6F008F3FD0 /* OSFilesystemLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OSFilesystemLib.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7575CF692BFCEE6F008F3FD0 /* OSFilesystemLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OSFilesystemLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 75FEB2AF2D3546D7007C2686 /* OSFLSTManager+Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSFLSTManager+Assets.swift"; sourceTree = ""; }; + 75FEB2B12D35470C007C2686 /* URLFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLFactory.swift; sourceTree = ""; }; + 75FEB2B32D35479B007C2686 /* OSFLSTFileManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFLSTFileManagerTests.swift; sourceTree = ""; }; + 75FEB2B62D355F21007C2686 /* file.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = file.txt; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -72,6 +80,8 @@ isa = PBXGroup; children = ( 751328D42D3175170031BDD0 /* OSFLSTManager.swift */, + 75FEB2AF2D3546D7007C2686 /* OSFLSTManager+Assets.swift */, + 75FEB2B12D35470C007C2686 /* URLFactory.swift */, ); path = OSFilesystemLib; sourceTree = ""; @@ -80,7 +90,9 @@ isa = PBXGroup; children = ( 751328D82D3179430031BDD0 /* MockFileManager.swift */, - 751328D92D318DBA0031BDD0 /* OSFLSTManagerTests.swift */, + 751328D92D318DBA0031BDD0 /* OSFLSTDirectoryManagerTests.swift */, + 75FEB2B32D35479B007C2686 /* OSFLSTFileManagerTests.swift */, + 75FEB2B62D355F21007C2686 /* file.txt */, ); path = OSFilesystemLibTests; sourceTree = ""; @@ -189,6 +201,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 75FEB2B72D355F21007C2686 /* file.txt in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -220,7 +233,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 75FEB2B02D3546D7007C2686 /* OSFLSTManager+Assets.swift in Sources */, 751328D52D3175170031BDD0 /* OSFLSTManager.swift in Sources */, + 75FEB2B22D35470C007C2686 /* URLFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -228,8 +243,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 751328DA2D318DBA0031BDD0 /* OSFLSTManagerTests.swift in Sources */, + 751328DA2D318DBA0031BDD0 /* OSFLSTDirectoryManagerTests.swift in Sources */, 751328DB2D318E770031BDD0 /* MockFileManager.swift in Sources */, + 75FEB2B42D35479B007C2686 /* OSFLSTFileManagerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/OSFilesystemLib/OSFLSTManager+Assets.swift b/OSFilesystemLib/OSFLSTManager+Assets.swift new file mode 100644 index 0000000..acea215 --- /dev/null +++ b/OSFilesystemLib/OSFLSTManager+Assets.swift @@ -0,0 +1,34 @@ +import Foundation + +public protocol OSFLSTDirectoryManager { + func createDirectory(atPath: String, includeIntermediateDirectories: Bool) throws + func removeDirectory(atPath: String, includeIntermediateDirectories: Bool) throws + func listDirectory(atPath: String) throws -> [URL] +} + +public protocol OSFLSTFileManager { + func readFile(atPath: String, withEncoding: OSFLSTEncoding) throws -> String +} + +enum OSFLSTDirectoryManagerError: Error { + case notEmpty +} + +public enum OSFLSTEncoding { + case byteBuffer + case string(encoding: OSFLSTStringEncoding) +} + +public enum OSFLSTStringEncoding { + case ascii + case utf8 + case utf16 + + var stringEncoding: String.Encoding { + switch self { + case .ascii: .ascii + case .utf8: .utf8 + case .utf16: .utf16 + } + } +} diff --git a/OSFilesystemLib/OSFLSTManager.swift b/OSFilesystemLib/OSFLSTManager.swift index 4e712fa..acce4ae 100644 --- a/OSFilesystemLib/OSFLSTManager.swift +++ b/OSFilesystemLib/OSFLSTManager.swift @@ -1,15 +1,5 @@ import Foundation -public protocol OSFLSTDirectoryManager { - func createDirectory(atPath: String, includeIntermediateDirectories: Bool) throws - func removeDirectory(atPath: String, includeIntermediateDirectories: Bool) throws - func listDirectory(atPath: String) throws -> [URL] -} - -enum OSFLSTDirectoryManagerError: Error { - case notEmpty -} - public struct OSFLSTManager { private let fileManager: FileManager @@ -32,7 +22,7 @@ extension OSFLSTManager: OSFLSTDirectoryManager { throw OSFLSTDirectoryManagerError.notEmpty } } - + try fileManager.removeItem(at: pathURL) } @@ -42,16 +32,34 @@ extension OSFLSTManager: OSFLSTDirectoryManager { } } -private struct URLFactory { - static func create(with path: String) -> URL { - let url: URL +extension OSFLSTManager: OSFLSTFileManager { + public func readFile(atPath path: String, withEncoding encoding: OSFLSTEncoding) throws -> String { + let fileURL = URLFactory.create(with: path) + + // Check if the URL requires security-scoped access + let requiresSecurityScope = fileURL.startAccessingSecurityScopedResource() - if #available(iOS 16.0, *) { - url = .init(filePath: path) - } else { - url = .init(fileURLWithPath: path) + // Use defer to ensure we stop accessing the security-scoped resource + // only if we started accessing it + defer { + if requiresSecurityScope { + fileURL.stopAccessingSecurityScopedResource() + } + } + + return switch encoding { + case .byteBuffer: + try readFileAsBase64EncodedString(from: fileURL) + case .string(let stringEncoding): + try readFileAsString(from: fileURL, using: stringEncoding.stringEncoding) } + } + + private func readFileAsBase64EncodedString(from fileURL: URL) throws -> String { + try Data(contentsOf: fileURL).base64EncodedString() + } - return url + private func readFileAsString(from fileURL: URL, using stringEncoding: String.Encoding) throws -> String { + try String(contentsOf: fileURL, encoding: stringEncoding) } } diff --git a/OSFilesystemLib/URLFactory.swift b/OSFilesystemLib/URLFactory.swift new file mode 100644 index 0000000..3168ba1 --- /dev/null +++ b/OSFilesystemLib/URLFactory.swift @@ -0,0 +1,15 @@ +import Foundation + +struct URLFactory { + static func create(with path: String) -> URL { + let url: URL + + if #available(iOS 16.0, *) { + url = .init(filePath: path) + } else { + url = .init(fileURLWithPath: path) + } + + return url + } +} diff --git a/OSFilesystemLibTests/OSFLSTManagerTests.swift b/OSFilesystemLibTests/OSFLSTDirectoryManagerTests.swift similarity index 98% rename from OSFilesystemLibTests/OSFLSTManagerTests.swift rename to OSFilesystemLibTests/OSFLSTDirectoryManagerTests.swift index e021ace..1e61cb0 100644 --- a/OSFilesystemLibTests/OSFLSTManagerTests.swift +++ b/OSFilesystemLibTests/OSFLSTDirectoryManagerTests.swift @@ -2,7 +2,7 @@ import XCTest @testable import OSFilesystemLib -final class OSFLSTManagerTests: XCTestCase { +final class OSFLSTDirectoryManagerTests: XCTestCase { private var sut: OSFLSTManager! // MARK: - 'createDirectory' tests @@ -136,7 +136,7 @@ final class OSFLSTManagerTests: XCTestCase { } } -private extension OSFLSTManagerTests { +private extension OSFLSTDirectoryManagerTests { @discardableResult func createFileManager(with error: MockFileManagerError? = nil, shouldDirectoryHaveContent: Bool = false) -> MockFileManager { let fileManager = MockFileManager(error: error, shouldDirectoryHaveContent: shouldDirectoryHaveContent) sut = OSFLSTManager(fileManager: fileManager) diff --git a/OSFilesystemLibTests/OSFLSTFileManagerTests.swift b/OSFilesystemLibTests/OSFLSTFileManagerTests.swift new file mode 100644 index 0000000..517a73f --- /dev/null +++ b/OSFilesystemLibTests/OSFLSTFileManagerTests.swift @@ -0,0 +1,62 @@ +import XCTest + +import OSFilesystemLib + +final class OSFLSTFileManagerTests: XCTestCase { + private var sut: OSFLSTManager! + + // MARK: - 'readFile` tests + func test_readFile_withStringEncoding_returnsContentSuccessfully() throws { + // Given + createFileManager() + + // When + let fileContent = try fetchContent(forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .string(encoding: .utf8)) + + // Then + XCTAssertEqual(fileContent, Configuration.fileContent) + } + + func test_readFile_withByteBufferEncoding_returnsContentSuccessfully() throws { + // Given + createFileManager() + + // When + let fileContent = try fetchContent(forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .byteBuffer) + + // Then + XCTAssertEqual(fileContent, Configuration.fileContent) + } +} + +private extension OSFLSTFileManagerTests { + struct Configuration { + static let fileName = "file" + static let fileExtension = "txt" + static let fileContent = "Hello, world!" + } + + @discardableResult func createFileManager() -> MockFileManager { + let fileManager = MockFileManager(error: nil, shouldDirectoryHaveContent: false) + sut = OSFLSTManager(fileManager: fileManager) + + return fileManager + } + + func fetchContent(forFile file: (name: String, extension: String), withEncoding encoding: OSFLSTEncoding) throws -> String { + let fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: file.name, withExtension: file.extension)) + let fileURLContent = try sut.readFile(atPath: fileURL.path(), withEncoding: encoding) + + var fileURLUnicodeScalars: String.UnicodeScalarView + if case .byteBuffer = encoding { + let fileURLData = try XCTUnwrap(Data(base64Encoded: fileURLContent)) + let fileURLDataString = try XCTUnwrap(String(data: fileURLData, encoding: .utf8)) + fileURLUnicodeScalars = fileURLDataString.unicodeScalars + } else { + fileURLUnicodeScalars = fileURLContent.unicodeScalars + } + + fileURLUnicodeScalars.removeAll(where: CharacterSet.newlines.contains) + return String(fileURLUnicodeScalars) + } +} diff --git a/OSFilesystemLibTests/file.txt b/OSFilesystemLibTests/file.txt new file mode 100644 index 0000000..af5626b --- /dev/null +++ b/OSFilesystemLibTests/file.txt @@ -0,0 +1 @@ +Hello, world! From 0a41e6370861d2ae6579b13174a8cefe2d3c9331 Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Tue, 14 Jan 2025 13:03:49 +0000 Subject: [PATCH 06/31] feat: get file url Exchange URLFactory with an extension as there are now 2 methods being used and it makes sense to group it all. --- OSFilesystemLib.xcodeproj/project.pbxproj | 8 +- OSFilesystemLib/OSFLSTManager+Assets.swift | 29 +++++- OSFilesystemLib/OSFLSTManager.swift | 32 ++++++- .../{URLFactory.swift => URL+Extension.swift} | 10 +- OSFilesystemLibTests/MockFileManager.swift | 11 ++- .../OSFLSTFileManagerTests.swift | 91 ++++++++++++++++++- 6 files changed, 166 insertions(+), 15 deletions(-) rename OSFilesystemLib/{URLFactory.swift => URL+Extension.swift} (54%) diff --git a/OSFilesystemLib.xcodeproj/project.pbxproj b/OSFilesystemLib.xcodeproj/project.pbxproj index c1b188f..f81853f 100644 --- a/OSFilesystemLib.xcodeproj/project.pbxproj +++ b/OSFilesystemLib.xcodeproj/project.pbxproj @@ -12,7 +12,7 @@ 751328DB2D318E770031BDD0 /* MockFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D82D3179430031BDD0 /* MockFileManager.swift */; }; 7575CF6A2BFCEE6F008F3FD0 /* OSFilesystemLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7575CF612BFCEE6F008F3FD0 /* OSFilesystemLib.framework */; }; 75FEB2B02D3546D7007C2686 /* OSFLSTManager+Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FEB2AF2D3546D7007C2686 /* OSFLSTManager+Assets.swift */; }; - 75FEB2B22D35470C007C2686 /* URLFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FEB2B12D35470C007C2686 /* URLFactory.swift */; }; + 75FEB2B22D35470C007C2686 /* URL+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FEB2B12D35470C007C2686 /* URL+Extension.swift */; }; 75FEB2B42D35479B007C2686 /* OSFLSTFileManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FEB2B32D35479B007C2686 /* OSFLSTFileManagerTests.swift */; }; 75FEB2B72D355F21007C2686 /* file.txt in Resources */ = {isa = PBXBuildFile; fileRef = 75FEB2B62D355F21007C2686 /* file.txt */; }; /* End PBXBuildFile section */ @@ -34,7 +34,7 @@ 7575CF612BFCEE6F008F3FD0 /* OSFilesystemLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OSFilesystemLib.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7575CF692BFCEE6F008F3FD0 /* OSFilesystemLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OSFilesystemLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 75FEB2AF2D3546D7007C2686 /* OSFLSTManager+Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSFLSTManager+Assets.swift"; sourceTree = ""; }; - 75FEB2B12D35470C007C2686 /* URLFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLFactory.swift; sourceTree = ""; }; + 75FEB2B12D35470C007C2686 /* URL+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extension.swift"; sourceTree = ""; }; 75FEB2B32D35479B007C2686 /* OSFLSTFileManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFLSTFileManagerTests.swift; sourceTree = ""; }; 75FEB2B62D355F21007C2686 /* file.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = file.txt; sourceTree = ""; }; /* End PBXFileReference section */ @@ -81,7 +81,7 @@ children = ( 751328D42D3175170031BDD0 /* OSFLSTManager.swift */, 75FEB2AF2D3546D7007C2686 /* OSFLSTManager+Assets.swift */, - 75FEB2B12D35470C007C2686 /* URLFactory.swift */, + 75FEB2B12D35470C007C2686 /* URL+Extension.swift */, ); path = OSFilesystemLib; sourceTree = ""; @@ -235,7 +235,7 @@ files = ( 75FEB2B02D3546D7007C2686 /* OSFLSTManager+Assets.swift in Sources */, 751328D52D3175170031BDD0 /* OSFLSTManager.swift in Sources */, - 75FEB2B22D35470C007C2686 /* URLFactory.swift in Sources */, + 75FEB2B22D35470C007C2686 /* URL+Extension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/OSFilesystemLib/OSFLSTManager+Assets.swift b/OSFilesystemLib/OSFLSTManager+Assets.swift index acea215..71e0400 100644 --- a/OSFilesystemLib/OSFLSTManager+Assets.swift +++ b/OSFilesystemLib/OSFLSTManager+Assets.swift @@ -6,12 +6,18 @@ public protocol OSFLSTDirectoryManager { func listDirectory(atPath: String) throws -> [URL] } +enum OSFLSTDirectoryManagerError: Error { + case notEmpty +} + public protocol OSFLSTFileManager { func readFile(atPath: String, withEncoding: OSFLSTEncoding) throws -> String + func getFileURL(atPath: String, withSearchPath: OSFLSTSearchPath) throws -> URL } -enum OSFLSTDirectoryManagerError: Error { - case notEmpty +enum OSFLSTFileManagerError: Error { + case cantCreateURL + case directoryNotFound } public enum OSFLSTEncoding { @@ -32,3 +38,22 @@ public enum OSFLSTStringEncoding { } } } + +public enum OSFLSTSearchPath { + case directory(searchPath: OSFLSTSearchPathDirectory) + case raw +} + +public enum OSFLSTSearchPathDirectory { + case cache + case document + case library + + var fileManagerSearchPathDirectory: FileManager.SearchPathDirectory { + switch self { + case .cache: .cachesDirectory + case .document: .documentDirectory + case .library: .libraryDirectory + } + } +} diff --git a/OSFilesystemLib/OSFLSTManager.swift b/OSFilesystemLib/OSFLSTManager.swift index acce4ae..dd8eb52 100644 --- a/OSFilesystemLib/OSFLSTManager.swift +++ b/OSFilesystemLib/OSFLSTManager.swift @@ -10,12 +10,12 @@ public struct OSFLSTManager { extension OSFLSTManager: OSFLSTDirectoryManager { public func createDirectory(atPath path: String, includeIntermediateDirectories: Bool) throws { - let pathURL = URLFactory.create(with: path) + let pathURL = URL.create(with: path) try fileManager.createDirectory(at: pathURL, withIntermediateDirectories: includeIntermediateDirectories) } public func removeDirectory(atPath path: String, includeIntermediateDirectories: Bool) throws { - let pathURL = URLFactory.create(with: path) + let pathURL = URL.create(with: path) if !includeIntermediateDirectories { let directoryContents = try listDirectory(atPath: path) if !directoryContents.isEmpty { @@ -27,14 +27,14 @@ extension OSFLSTManager: OSFLSTDirectoryManager { } public func listDirectory(atPath path: String) throws -> [URL] { - let pathURL = URLFactory.create(with: path) + let pathURL = URL.create(with: path) return try fileManager.contentsOfDirectory(at: pathURL, includingPropertiesForKeys: nil) } } extension OSFLSTManager: OSFLSTFileManager { public func readFile(atPath path: String, withEncoding encoding: OSFLSTEncoding) throws -> String { - let fileURL = URLFactory.create(with: path) + let fileURL = URL.create(with: path) // Check if the URL requires security-scoped access let requiresSecurityScope = fileURL.startAccessingSecurityScopedResource() @@ -55,6 +55,15 @@ extension OSFLSTManager: OSFLSTFileManager { } } + public func getFileURL(atPath path: String, withSearchPath searchPath: OSFLSTSearchPath) throws -> URL { + return switch searchPath { + case .directory(let directorySearchPath): + try resolveDirectoryURL(for: directorySearchPath.fileManagerSearchPathDirectory, with: path) + case .raw: + try resolveRawURL(from: path) + } + } + private func readFileAsBase64EncodedString(from fileURL: URL) throws -> String { try Data(contentsOf: fileURL).base64EncodedString() } @@ -62,4 +71,19 @@ extension OSFLSTManager: OSFLSTFileManager { private func readFileAsString(from fileURL: URL, using stringEncoding: String.Encoding) throws -> String { try String(contentsOf: fileURL, encoding: stringEncoding) } + + private func resolveDirectoryURL(for searchPath: FileManager.SearchPathDirectory, with path: String) throws -> URL { + guard let directoryURL = fileManager.urls(for: searchPath, in: .userDomainMask).first else { + throw OSFLSTFileManagerError.directoryNotFound + } + + return path.isEmpty ? directoryURL : directoryURL.appendingPathComponent(path) + } + + private func resolveRawURL(from path: String) throws -> URL { + guard let rawURL = URL(string: path) else { + throw OSFLSTFileManagerError.cantCreateURL + } + return rawURL + } } diff --git a/OSFilesystemLib/URLFactory.swift b/OSFilesystemLib/URL+Extension.swift similarity index 54% rename from OSFilesystemLib/URLFactory.swift rename to OSFilesystemLib/URL+Extension.swift index 3168ba1..217f618 100644 --- a/OSFilesystemLib/URLFactory.swift +++ b/OSFilesystemLib/URL+Extension.swift @@ -1,6 +1,6 @@ import Foundation -struct URLFactory { +extension URL { static func create(with path: String) -> URL { let url: URL @@ -12,4 +12,12 @@ struct URLFactory { return url } + + func urlWithAppendingPath(_ path: String) -> URL { + if #available(iOS 16.0, *) { + self.appending(path: path) + } else { + self.appendingPathComponent(path) + } + } } diff --git a/OSFilesystemLibTests/MockFileManager.swift b/OSFilesystemLibTests/MockFileManager.swift index ea33e14..24eddf6 100644 --- a/OSFilesystemLibTests/MockFileManager.swift +++ b/OSFilesystemLibTests/MockFileManager.swift @@ -3,13 +3,16 @@ import Foundation class MockFileManager: FileManager { var error: MockFileManagerError? var shouldDirectoryHaveContent: Bool + var urlsWithinDirectory: [URL] private(set) var capturedPath: String? private(set) var capturedIntermediateDirectories: Bool = false + private(set) var capturedSearchPathDirectory: FileManager.SearchPathDirectory? - init(error: MockFileManagerError? = nil, shouldDirectoryHaveContent: Bool = false) { + init(error: MockFileManagerError? = nil, shouldDirectoryHaveContent: Bool = false, urlsWithinDirectory: [URL] = []) { self.error = error self.shouldDirectoryHaveContent = shouldDirectoryHaveContent + self.urlsWithinDirectory = urlsWithinDirectory } } @@ -51,4 +54,10 @@ extension MockFileManager { throw error } } + + override func urls(for directory: FileManager.SearchPathDirectory, in domainMask: FileManager.SearchPathDomainMask) -> [URL] { + capturedSearchPathDirectory = directory + + return urlsWithinDirectory + } } diff --git a/OSFilesystemLibTests/OSFLSTFileManagerTests.swift b/OSFilesystemLibTests/OSFLSTFileManagerTests.swift index 517a73f..64c06f3 100644 --- a/OSFilesystemLibTests/OSFLSTFileManagerTests.swift +++ b/OSFilesystemLibTests/OSFLSTFileManagerTests.swift @@ -1,6 +1,6 @@ import XCTest -import OSFilesystemLib +@testable import OSFilesystemLib final class OSFLSTFileManagerTests: XCTestCase { private var sut: OSFLSTManager! @@ -27,6 +27,91 @@ final class OSFLSTFileManagerTests: XCTestCase { // Then XCTAssertEqual(fileContent, Configuration.fileContent) } + + // MARK: - 'getFileURL' tests + func test_getFileURL_fromDirectorySearchPath_containingSingleFile_returnsFileSuccessfully() throws { + // Given + let fileURL: URL = try XCTUnwrap(.init(string: "/file/directory")) + let fileManager = createFileManager(urlsWithinDirectory: [fileURL]) + let filePath = "/test/directory" + let searchPathDirectory = OSFLSTSearchPathDirectory.cache + + // When + let returnedURL = try sut.getFileURL(atPath: filePath, withSearchPath: .directory(searchPath: searchPathDirectory)) + + // Then + XCTAssertEqual(fileManager.capturedSearchPathDirectory, searchPathDirectory.fileManagerSearchPathDirectory) + XCTAssertEqual(fileURL.urlWithAppendingPath(filePath), returnedURL) + } + + func test_getFileURL_fromDirectorySearchPath_containingMultipleFiles_returnsFirstFileSuccessfully() throws { + // Given + let fileURL: URL = try XCTUnwrap(.init(string: "/file/directory")) + let ignoredFileURL: URL = try XCTUnwrap(.init(string: "another_file/directory")) + let fileManager = createFileManager(urlsWithinDirectory: [fileURL, ignoredFileURL]) + let filePath = "/test/directory" + let searchPathDirectory = OSFLSTSearchPathDirectory.cache + + // When + let returnedURL = try sut.getFileURL(atPath: filePath, withSearchPath: .directory(searchPath: searchPathDirectory)) + + // Then + XCTAssertEqual(fileManager.capturedSearchPathDirectory, searchPathDirectory.fileManagerSearchPathDirectory) + XCTAssertEqual(fileURL.urlWithAppendingPath(filePath), returnedURL) + } + + func test_getFileURL_fromDirectorySearchPath_containingNoFiles_returnsError() { + // Given + createFileManager() + let filePath = "/test/directory" + let searchPathDirectory = OSFLSTSearchPathDirectory.cache + + // When + XCTAssertThrowsError(try sut.getFileURL(atPath: filePath, withSearchPath: .directory(searchPath: searchPathDirectory))) { + // Then + XCTAssertEqual($0 as? OSFLSTFileManagerError, .directoryNotFound) + + } + } + + func test_getFileURL_fromDirectorySearchPath_withNoPath_returnsFileSuccessfully() throws { + // Given + let fileURL: URL = try XCTUnwrap(.init(string: "/file/directory")) + let fileManager = createFileManager(urlsWithinDirectory: [fileURL]) + let emptyFilePath = "" + let searchPathDirectory = OSFLSTSearchPathDirectory.cache + + // When + let returnedURL = try sut.getFileURL(atPath: emptyFilePath, withSearchPath: .directory(searchPath: searchPathDirectory)) + + // Then + XCTAssertEqual(fileManager.capturedSearchPathDirectory, searchPathDirectory.fileManagerSearchPathDirectory) + XCTAssertEqual(fileURL, returnedURL) + } + + func test_getFileURL_rawFile_returnsFileSuccessfully() throws { + // Given + createFileManager() + let filePath = "/test/directory" + + // When + let returnedURL = try sut.getFileURL(atPath: filePath, withSearchPath: .raw) + + // Then + XCTAssertEqual(filePath, returnedURL.path()) + } + + func test_getFileURL_rawFile_fromInvalidPath_returnsError() { + // Given + createFileManager() + let emptyFilePath = "" + + // When + XCTAssertThrowsError(try sut.getFileURL(atPath: emptyFilePath, withSearchPath: .raw)) { + // Then + XCTAssertEqual($0 as? OSFLSTFileManagerError, .cantCreateURL) + } + } } private extension OSFLSTFileManagerTests { @@ -36,8 +121,8 @@ private extension OSFLSTFileManagerTests { static let fileContent = "Hello, world!" } - @discardableResult func createFileManager() -> MockFileManager { - let fileManager = MockFileManager(error: nil, shouldDirectoryHaveContent: false) + @discardableResult func createFileManager(urlsWithinDirectory: [URL] = []) -> MockFileManager { + let fileManager = MockFileManager(urlsWithinDirectory: urlsWithinDirectory) sut = OSFLSTManager(fileManager: fileManager) return fileManager From b671e03bf12adb8998ac8db19a2d0978d9d1f3f1 Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Tue, 14 Jan 2025 15:21:47 +0000 Subject: [PATCH 07/31] feat: delete file --- OSFilesystemLib/OSFLSTManager+Assets.swift | 2 + OSFilesystemLib/OSFLSTManager.swift | 8 ++++ OSFilesystemLibTests/MockFileManager.swift | 17 +++++++- .../OSFLSTFileManagerTests.swift | 42 ++++++++++++++++++- 4 files changed, 66 insertions(+), 3 deletions(-) diff --git a/OSFilesystemLib/OSFLSTManager+Assets.swift b/OSFilesystemLib/OSFLSTManager+Assets.swift index 71e0400..4aaa205 100644 --- a/OSFilesystemLib/OSFLSTManager+Assets.swift +++ b/OSFilesystemLib/OSFLSTManager+Assets.swift @@ -13,11 +13,13 @@ enum OSFLSTDirectoryManagerError: Error { public protocol OSFLSTFileManager { func readFile(atPath: String, withEncoding: OSFLSTEncoding) throws -> String func getFileURL(atPath: String, withSearchPath: OSFLSTSearchPath) throws -> URL + func deleteFile(atPath: String) throws } enum OSFLSTFileManagerError: Error { case cantCreateURL case directoryNotFound + case fileNotFound } public enum OSFLSTEncoding { diff --git a/OSFilesystemLib/OSFLSTManager.swift b/OSFilesystemLib/OSFLSTManager.swift index dd8eb52..447cd17 100644 --- a/OSFilesystemLib/OSFLSTManager.swift +++ b/OSFilesystemLib/OSFLSTManager.swift @@ -64,6 +64,14 @@ extension OSFLSTManager: OSFLSTFileManager { } } + public func deleteFile(atPath path: String) throws { + guard fileManager.fileExists(atPath: path) else { + throw OSFLSTFileManagerError.fileNotFound + } + + try fileManager.removeItem(atPath: path) + } + private func readFileAsBase64EncodedString(from fileURL: URL) throws -> String { try Data(contentsOf: fileURL).base64EncodedString() } diff --git a/OSFilesystemLibTests/MockFileManager.swift b/OSFilesystemLibTests/MockFileManager.swift index 24eddf6..1e29efe 100644 --- a/OSFilesystemLibTests/MockFileManager.swift +++ b/OSFilesystemLibTests/MockFileManager.swift @@ -4,15 +4,17 @@ class MockFileManager: FileManager { var error: MockFileManagerError? var shouldDirectoryHaveContent: Bool var urlsWithinDirectory: [URL] + var fileExists: Bool private(set) var capturedPath: String? private(set) var capturedIntermediateDirectories: Bool = false private(set) var capturedSearchPathDirectory: FileManager.SearchPathDirectory? - init(error: MockFileManagerError? = nil, shouldDirectoryHaveContent: Bool = false, urlsWithinDirectory: [URL] = []) { + init(error: MockFileManagerError? = nil, shouldDirectoryHaveContent: Bool = false, urlsWithinDirectory: [URL] = [], fileExists: Bool = true) { self.error = error self.shouldDirectoryHaveContent = shouldDirectoryHaveContent self.urlsWithinDirectory = urlsWithinDirectory + self.fileExists = fileExists } } @@ -20,6 +22,7 @@ enum MockFileManagerError: Error { case createDirectoryError case readDirectoryError case deleteDirectoryError + case deleteFileError } extension MockFileManager { @@ -60,4 +63,16 @@ extension MockFileManager { return urlsWithinDirectory } + + override func fileExists(atPath path: String) -> Bool { + fileExists + } + + override func removeItem(atPath path: String) throws { + capturedPath = path + + if let error, error == .deleteFileError { + throw error + } + } } diff --git a/OSFilesystemLibTests/OSFLSTFileManagerTests.swift b/OSFilesystemLibTests/OSFLSTFileManagerTests.swift index 64c06f3..89278bd 100644 --- a/OSFilesystemLibTests/OSFLSTFileManagerTests.swift +++ b/OSFilesystemLibTests/OSFLSTFileManagerTests.swift @@ -112,6 +112,44 @@ final class OSFLSTFileManagerTests: XCTestCase { XCTAssertEqual($0 as? OSFLSTFileManagerError, .cantCreateURL) } } + + // MARK: - 'deleteFile' tests + func test_deleteFile_shouldBeSuccessful() throws { + // Given + let fileManager = createFileManager() + let filePath = "/test/directory" + + // When + try sut.deleteFile(atPath: filePath) + + // Then + XCTAssertEqual(fileManager.capturedPath, filePath) + } + + func test_deleteFile_thatDoesntExist_shouldReturnError() { + // Given + createFileManager(fileExists: false) + let filePath = "/test/directory" + + // When + XCTAssertThrowsError(try sut.deleteFile(atPath: filePath)) { + // Then + XCTAssertEqual($0 as? OSFLSTFileManagerError, .fileNotFound) + } + } + + func test_deleteFile_thatFailsWhileDeleting_shouldReturnError() { + // Given + let error = MockFileManagerError.deleteFileError + createFileManager(error: error) + let filePath = "/test/directory" + + // When + XCTAssertThrowsError(try sut.deleteFile(atPath: filePath)) { + // Then + XCTAssertEqual($0 as? MockFileManagerError, error) + } + } } private extension OSFLSTFileManagerTests { @@ -121,8 +159,8 @@ private extension OSFLSTFileManagerTests { static let fileContent = "Hello, world!" } - @discardableResult func createFileManager(urlsWithinDirectory: [URL] = []) -> MockFileManager { - let fileManager = MockFileManager(urlsWithinDirectory: urlsWithinDirectory) + @discardableResult func createFileManager(error: MockFileManagerError? = nil, urlsWithinDirectory: [URL] = [], fileExists: Bool = true) -> MockFileManager { + let fileManager = MockFileManager(error: error, urlsWithinDirectory: urlsWithinDirectory, fileExists: fileExists) sut = OSFLSTManager(fileManager: fileManager) return fileManager From e3e80108bfcc59f3f07660cb8f1b3842341995a0 Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Wed, 15 Jan 2025 11:13:08 +0000 Subject: [PATCH 08/31] feat: save file --- OSFilesystemLib/OSFLSTManager+Assets.swift | 7 + OSFilesystemLib/OSFLSTManager.swift | 22 +++ OSFilesystemLib/URL+Extension.swift | 12 +- .../OSFLSTFileManagerTests.swift | 150 +++++++++++++++++- 4 files changed, 187 insertions(+), 4 deletions(-) diff --git a/OSFilesystemLib/OSFLSTManager+Assets.swift b/OSFilesystemLib/OSFLSTManager+Assets.swift index 4aaa205..7211e51 100644 --- a/OSFilesystemLib/OSFLSTManager+Assets.swift +++ b/OSFilesystemLib/OSFLSTManager+Assets.swift @@ -14,12 +14,14 @@ public protocol OSFLSTFileManager { func readFile(atPath: String, withEncoding: OSFLSTEncoding) throws -> String func getFileURL(atPath: String, withSearchPath: OSFLSTSearchPath) throws -> URL func deleteFile(atPath: String) throws + func saveFile(atPath: String, withEncodingAndData: OSFLSTEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL } enum OSFLSTFileManagerError: Error { case cantCreateURL case directoryNotFound case fileNotFound + case missingParentFolder } public enum OSFLSTEncoding { @@ -27,6 +29,11 @@ public enum OSFLSTEncoding { case string(encoding: OSFLSTStringEncoding) } +public enum OSFLSTEncodingValueMapper { + case byteBuffer(value: Data) + case string(encoding: OSFLSTStringEncoding, value: String) +} + public enum OSFLSTStringEncoding { case ascii case utf8 diff --git a/OSFilesystemLib/OSFLSTManager.swift b/OSFilesystemLib/OSFLSTManager.swift index 447cd17..8482dcf 100644 --- a/OSFilesystemLib/OSFLSTManager.swift +++ b/OSFilesystemLib/OSFLSTManager.swift @@ -72,6 +72,28 @@ extension OSFLSTManager: OSFLSTFileManager { try fileManager.removeItem(atPath: path) } + public func saveFile(atPath path: String, withEncodingAndData encodingMapper: OSFLSTEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL { + let fileURL = URL.create(with: path) + let fileDirectoryURL = fileURL.deletingLastPathComponent() + + if !fileManager.fileExists(atPath: fileDirectoryURL.urlPath) { + if includeIntermediateDirectories { + try createDirectory(atPath: fileDirectoryURL.urlPath, includeIntermediateDirectories: true) + } else { + throw OSFLSTFileManagerError.missingParentFolder + } + } + + switch encodingMapper { + case .byteBuffer(let value): + try value.write(to: fileURL) + case .string(let encoding, let value): + try value.write(to: fileURL, atomically: false, encoding: encoding.stringEncoding) + } + + return fileURL + } + private func readFileAsBase64EncodedString(from fileURL: URL) throws -> String { try Data(contentsOf: fileURL).base64EncodedString() } diff --git a/OSFilesystemLib/URL+Extension.swift b/OSFilesystemLib/URL+Extension.swift index 217f618..e1a7381 100644 --- a/OSFilesystemLib/URL+Extension.swift +++ b/OSFilesystemLib/URL+Extension.swift @@ -1,6 +1,14 @@ import Foundation extension URL { + var urlPath: String { + if #available(iOS 16.0, *) { + path() + } else { + path + } + } + static func create(with path: String) -> URL { let url: URL @@ -15,9 +23,9 @@ extension URL { func urlWithAppendingPath(_ path: String) -> URL { if #available(iOS 16.0, *) { - self.appending(path: path) + appending(path: path) } else { - self.appendingPathComponent(path) + appendingPathComponent(path) } } } diff --git a/OSFilesystemLibTests/OSFLSTFileManagerTests.swift b/OSFilesystemLibTests/OSFLSTFileManagerTests.swift index 89278bd..81efeea 100644 --- a/OSFilesystemLibTests/OSFLSTFileManagerTests.swift +++ b/OSFilesystemLibTests/OSFLSTFileManagerTests.swift @@ -11,7 +11,9 @@ final class OSFLSTFileManagerTests: XCTestCase { createFileManager() // When - let fileContent = try fetchContent(forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .string(encoding: .utf8)) + let fileContent = try fetchContent( + forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .string(encoding: .utf8) + ) // Then XCTAssertEqual(fileContent, Configuration.fileContent) @@ -22,7 +24,9 @@ final class OSFLSTFileManagerTests: XCTestCase { createFileManager() // When - let fileContent = try fetchContent(forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .byteBuffer) + let fileContent = try fetchContent( + forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .byteBuffer + ) // Then XCTAssertEqual(fileContent, Configuration.fileContent) @@ -150,13 +154,151 @@ final class OSFLSTFileManagerTests: XCTestCase { XCTAssertEqual($0 as? MockFileManagerError, error) } } + + // MARK: - 'saveFile' tests + func test_saveFile_withStringEncoding_savesFileSuccessfullyAndReturnsItsURL() throws { + // Given + let fileManager = createFileManager() + let fileURL = try XCTUnwrap(fetchConfigurationFile()) + .deletingLastPathComponent() + .appending(path: "\(Configuration.newFileName).\(Configuration.fileExtension)") + let stringEncoding = OSFLSTStringEncoding.ascii + let contentToSave = Configuration.stringEncodedFileContent + let shouldIncludeIntermediateDirectories = false + + // When + let savedFileURL = try sut.saveFile( + atPath: fileURL.path(), + withEncodingAndData: .string(encoding: stringEncoding, value: contentToSave), + includeIntermediateDirectories: shouldIncludeIntermediateDirectories + ) + + // Then + XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories) + XCTAssertEqual(savedFileURL, fileURL) + + let savedFileContent = try fetchContent( + forFile: (Configuration.newFileName, Configuration.fileExtension), withEncoding: .string(encoding: stringEncoding) + ) + XCTAssertEqual(savedFileContent, contentToSave) + + try sut.deleteFile(atPath: fileURL.absoluteString) // keep things clean by deleting created file + } + + func test_saveFile_withByteBufferEncoding_savesFileSuccessfullyAndReturnsItsURL() throws { + // Given + let fileManager = createFileManager() + let fileURL = try XCTUnwrap(fetchConfigurationFile()) + .deletingLastPathComponent() + .appending(path: "\(Configuration.newFileName).\(Configuration.fileExtension)") + let contentToSave = Configuration.byteBufferEncodedFileContent + let contentToSaveData = try XCTUnwrap(contentToSave.data(using: .utf8)) + let shouldIncludeIntermediateDirectories = false + + // When + let savedFileURL = try sut.saveFile( + atPath: fileURL.path(), + withEncodingAndData: .byteBuffer(value: contentToSaveData), + includeIntermediateDirectories: shouldIncludeIntermediateDirectories + ) + + // Then + XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories) + XCTAssertEqual(savedFileURL, fileURL) + + let savedFileContent = try fetchContent( + forFile: (Configuration.newFileName, Configuration.fileExtension), withEncoding: .byteBuffer + ) + XCTAssertEqual(savedFileContent, contentToSave) + + try sut.deleteFile(atPath: fileURL.absoluteString) // keep things clean by deleting created file + } + + func test_saveFile_parentFolderMissing_shouldCreateIt_savesFileSuccessfullyAndReturnsItsURL() throws { + // Given + let fileManager = createFileManager(fileExists: false) + let parentFolderURL = try XCTUnwrap(fetchConfigurationFile()) + .deletingLastPathComponent() + let fileURL = parentFolderURL + .appending(path: "\(Configuration.newFileName).\(Configuration.fileExtension)") + let stringEncoding = OSFLSTStringEncoding.ascii + let contentToSave = Configuration.stringEncodedFileContent + let shouldIncludeIntermediateDirectories = true + + // When + let savedFileURL = try sut.saveFile( + atPath: fileURL.path(), + withEncodingAndData: .string(encoding: stringEncoding, value: contentToSave), + includeIntermediateDirectories: shouldIncludeIntermediateDirectories + ) + + // Then + XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories) + XCTAssertEqual(fileManager.capturedPath, parentFolderURL.relativePath) + XCTAssertEqual(savedFileURL, fileURL) + + let savedFileContent = try fetchContent( + forFile: (Configuration.newFileName, Configuration.fileExtension), withEncoding: .string(encoding: stringEncoding) + ) + XCTAssertEqual(savedFileContent, contentToSave) + + fileManager.fileExists = true + try sut.deleteFile(atPath: fileURL.absoluteString) // keep things clean by deleting created file + } + + func test_saveFile_parentFolderMissing_failsCreatingIt_returnsError() throws { + // Given + createFileManager(error: .createDirectoryError, fileExists: false) + let parentFolderURL = try XCTUnwrap(fetchConfigurationFile()) + .deletingLastPathComponent() + let fileURL = parentFolderURL + .appending(path: "\(Configuration.newFileName).\(Configuration.fileExtension)") + let stringEncoding = OSFLSTStringEncoding.ascii + let contentToSave = Configuration.stringEncodedFileContent + let shouldIncludeIntermediateDirectories = true + + // When + XCTAssertThrowsError(try sut.saveFile( + atPath: fileURL.path(), + withEncodingAndData: .string(encoding: stringEncoding, value: contentToSave), + includeIntermediateDirectories: shouldIncludeIntermediateDirectories) + ) { + // Then + XCTAssertEqual($0 as? MockFileManagerError, .createDirectoryError) + } + } + + func test_saveFile_parentFolderMissing_shouldntCreateIt_returnsError() throws { + // Given + createFileManager(fileExists: false) + let parentFolderURL = try XCTUnwrap(fetchConfigurationFile()) + .deletingLastPathComponent() + let fileURL = parentFolderURL + .appending(path: "\(Configuration.newFileName).\(Configuration.fileExtension)") + let stringEncoding = OSFLSTStringEncoding.ascii + let contentToSave = Configuration.stringEncodedFileContent + let shouldIncludeIntermediateDirectories = false + + // When + XCTAssertThrowsError(try sut.saveFile( + atPath: fileURL.path(), + withEncodingAndData: .string(encoding: stringEncoding, value: contentToSave), + includeIntermediateDirectories: shouldIncludeIntermediateDirectories) + ) { + // Then + XCTAssertEqual($0 as? OSFLSTFileManagerError, .missingParentFolder) + } + } } private extension OSFLSTFileManagerTests { struct Configuration { static let fileName = "file" + static let newFileName = "new_file" static let fileExtension = "txt" static let fileContent = "Hello, world!" + static let stringEncodedFileContent = "Hello, string-encoded world!" + static let byteBufferEncodedFileContent = "Hello, byte buffer-encoded world!" } @discardableResult func createFileManager(error: MockFileManagerError? = nil, urlsWithinDirectory: [URL] = [], fileExists: Bool = true) -> MockFileManager { @@ -182,4 +324,8 @@ private extension OSFLSTFileManagerTests { fileURLUnicodeScalars.removeAll(where: CharacterSet.newlines.contains) return String(fileURLUnicodeScalars) } + + func fetchConfigurationFile() -> URL? { + Bundle(for: type(of: self)).url(forResource: Configuration.fileName, withExtension: Configuration.fileExtension) + } } From f21cacb2542cbfa55bf18c4b1fd79638aa5d8e6e Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Wed, 15 Jan 2025 12:02:51 +0000 Subject: [PATCH 09/31] feat: append data to file --- OSFilesystemLib/OSFLSTManager+Assets.swift | 2 + OSFilesystemLib/OSFLSTManager.swift | 21 +++- .../OSFLSTFileManagerTests.swift | 112 ++++++++++++++++-- 3 files changed, 121 insertions(+), 14 deletions(-) diff --git a/OSFilesystemLib/OSFLSTManager+Assets.swift b/OSFilesystemLib/OSFLSTManager+Assets.swift index 7211e51..1740c36 100644 --- a/OSFilesystemLib/OSFLSTManager+Assets.swift +++ b/OSFilesystemLib/OSFLSTManager+Assets.swift @@ -15,10 +15,12 @@ public protocol OSFLSTFileManager { func getFileURL(atPath: String, withSearchPath: OSFLSTSearchPath) throws -> URL func deleteFile(atPath: String) throws func saveFile(atPath: String, withEncodingAndData: OSFLSTEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL + func appendData(_ data: OSFLSTEncodingValueMapper, atPath: String, includeIntermediateDirectories: Bool) throws } enum OSFLSTFileManagerError: Error { case cantCreateURL + case cantDecodeData case directoryNotFound case fileNotFound case missingParentFolder diff --git a/OSFilesystemLib/OSFLSTManager.swift b/OSFilesystemLib/OSFLSTManager.swift index 8482dcf..a045418 100644 --- a/OSFilesystemLib/OSFLSTManager.swift +++ b/OSFilesystemLib/OSFLSTManager.swift @@ -72,7 +72,7 @@ extension OSFLSTManager: OSFLSTFileManager { try fileManager.removeItem(atPath: path) } - public func saveFile(atPath path: String, withEncodingAndData encodingMapper: OSFLSTEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL { + @discardableResult public func saveFile(atPath path: String, withEncodingAndData encodingMapper: OSFLSTEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL { let fileURL = URL.create(with: path) let fileDirectoryURL = fileURL.deletingLastPathComponent() @@ -94,6 +94,25 @@ extension OSFLSTManager: OSFLSTFileManager { return fileURL } + public func appendData(_ encodingMapper: OSFLSTEncodingValueMapper, atPath path: String, includeIntermediateDirectories: Bool) throws { + guard fileManager.fileExists(atPath: path) else { + try saveFile(atPath: path, withEncodingAndData: encodingMapper, includeIntermediateDirectories: includeIntermediateDirectories) + return + } + + guard let dataToAppend = switch encodingMapper { + case .byteBuffer(let value): value + case .string(let encoding, let value): value.data(using: encoding.stringEncoding) + } else { + throw OSFLSTFileManagerError.cantDecodeData + } + + let fileHandle = FileHandle(forWritingAtPath: path) + try fileHandle?.seekToEnd() + try fileHandle?.write(contentsOf: dataToAppend) + try fileHandle?.close() + } + private func readFileAsBase64EncodedString(from fileURL: URL) throws -> String { try Data(contentsOf: fileURL).base64EncodedString() } diff --git a/OSFilesystemLibTests/OSFLSTFileManagerTests.swift b/OSFilesystemLibTests/OSFLSTFileManagerTests.swift index 81efeea..b9898e0 100644 --- a/OSFilesystemLibTests/OSFLSTFileManagerTests.swift +++ b/OSFilesystemLibTests/OSFLSTFileManagerTests.swift @@ -74,7 +74,6 @@ final class OSFLSTFileManagerTests: XCTestCase { XCTAssertThrowsError(try sut.getFileURL(atPath: filePath, withSearchPath: .directory(searchPath: searchPathDirectory))) { // Then XCTAssertEqual($0 as? OSFLSTFileManagerError, .directoryNotFound) - } } @@ -246,16 +245,16 @@ final class OSFLSTFileManagerTests: XCTestCase { try sut.deleteFile(atPath: fileURL.absoluteString) // keep things clean by deleting created file } - func test_saveFile_parentFolderMissing_failsCreatingIt_returnsError() throws { + func test_saveFile_parentFolderMissing_shouldntCreateIt_returnsError() throws { // Given - createFileManager(error: .createDirectoryError, fileExists: false) + createFileManager(fileExists: false) let parentFolderURL = try XCTUnwrap(fetchConfigurationFile()) .deletingLastPathComponent() let fileURL = parentFolderURL .appending(path: "\(Configuration.newFileName).\(Configuration.fileExtension)") let stringEncoding = OSFLSTStringEncoding.ascii let contentToSave = Configuration.stringEncodedFileContent - let shouldIncludeIntermediateDirectories = true + let shouldIncludeIntermediateDirectories = false // When XCTAssertThrowsError(try sut.saveFile( @@ -264,29 +263,114 @@ final class OSFLSTFileManagerTests: XCTestCase { includeIntermediateDirectories: shouldIncludeIntermediateDirectories) ) { // Then - XCTAssertEqual($0 as? MockFileManagerError, .createDirectoryError) + XCTAssertEqual($0 as? OSFLSTFileManagerError, .missingParentFolder) } } - func test_saveFile_parentFolderMissing_shouldntCreateIt_returnsError() throws { + // MARK: - 'appendData' tests + func test_appendData_withStringEncoding_savesFileSuccessfully() throws { // Given - createFileManager(fileExists: false) + createFileManager() + let fileURL = try XCTUnwrap(fetchConfigurationFile()) + let stringEncoding = OSFLSTStringEncoding.ascii + let contentToAdd = Configuration.fileExtendedContent + + // When + try sut.appendData( + .string(encoding: stringEncoding, value: contentToAdd), + atPath: fileURL.path(), + includeIntermediateDirectories: false + ) + + // Then + let savedFileContent = try fetchContent( + forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .string(encoding: stringEncoding) + ) + + XCTAssertEqual(savedFileContent, Configuration.fileContent + contentToAdd) + + try sut.saveFile( // keep things clean by resetting file + atPath: fileURL.path(), + withEncodingAndData: .string(encoding: stringEncoding, value: Configuration.fileContent), + includeIntermediateDirectories: false + ) + } + + func test_appendData_withByteBufferEncoding_savesFileSuccessfully() throws { + // Given + createFileManager() + let fileURL = try XCTUnwrap(fetchConfigurationFile()) + let contentToAdd = Configuration.byteBufferEncodedFileContent + let contentToAddData = try XCTUnwrap(contentToAdd.data(using: .utf8)) + + // When + try sut.appendData( + .byteBuffer(value: contentToAddData), + atPath: fileURL.path(), + includeIntermediateDirectories: false + ) + + // Then + let savedFileContent = try fetchContent( + forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .byteBuffer + ) + + XCTAssertEqual(savedFileContent, Configuration.fileContent + contentToAdd) + + try sut.saveFile( // keep things clean by resetting file + atPath: fileURL.path(), + withEncodingAndData: .string(encoding: .ascii, value: Configuration.fileContent), + includeIntermediateDirectories: false + ) + } + + func test_appendData_fileDoesntExist_createsNewFileSuccessfully() throws { + // Given + let fileManager = createFileManager(fileExists: false) let parentFolderURL = try XCTUnwrap(fetchConfigurationFile()) .deletingLastPathComponent() let fileURL = parentFolderURL .appending(path: "\(Configuration.newFileName).\(Configuration.fileExtension)") let stringEncoding = OSFLSTStringEncoding.ascii - let contentToSave = Configuration.stringEncodedFileContent - let shouldIncludeIntermediateDirectories = false + let contentToAdd = Configuration.stringEncodedFileContent + let shouldIncludeIntermediateDirectories = true // When - XCTAssertThrowsError(try sut.saveFile( + try sut.appendData( + .string(encoding: stringEncoding, value: contentToAdd), atPath: fileURL.path(), - withEncodingAndData: .string(encoding: stringEncoding, value: contentToSave), - includeIntermediateDirectories: shouldIncludeIntermediateDirectories) + includeIntermediateDirectories: shouldIncludeIntermediateDirectories + ) + + XCTAssertEqual(fileManager.capturedPath, parentFolderURL.relativePath) + XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories) + + // Then + let savedFileContent = try fetchContent( + forFile: (Configuration.newFileName, Configuration.fileExtension), withEncoding: .string(encoding: stringEncoding) + ) + + XCTAssertEqual(savedFileContent, contentToAdd) + + fileManager.fileExists = true + try sut.deleteFile(atPath: fileURL.absoluteString) // keep things clean by deleting created file + } + + func test_appendData_withStringEncoding_textCantBeDecoded_returnsError() throws { + // Given + createFileManager() + let fileURL = try XCTUnwrap(fetchConfigurationFile()) + let stringEncoding = OSFLSTStringEncoding.ascii + let contentToAdd = Configuration.emojiContent // ASCII can't represent emoji so the conversion will fail. + + // When + XCTAssertThrowsError(try sut.appendData( + .string(encoding: stringEncoding, value: contentToAdd), + atPath: fileURL.path(), + includeIntermediateDirectories: false) ) { // Then - XCTAssertEqual($0 as? OSFLSTFileManagerError, .missingParentFolder) + XCTAssertEqual($0 as? OSFLSTFileManagerError, .cantDecodeData) } } } @@ -299,6 +383,8 @@ private extension OSFLSTFileManagerTests { static let fileContent = "Hello, world!" static let stringEncodedFileContent = "Hello, string-encoded world!" static let byteBufferEncodedFileContent = "Hello, byte buffer-encoded world!" + static let fileExtendedContent = " How are you?" + static let emojiContent = "🙃" } @discardableResult func createFileManager(error: MockFileManagerError? = nil, urlsWithinDirectory: [URL] = [], fileExists: Bool = true) -> MockFileManager { From 6d474957b3abf18b42ec7c4e47b00e50ed29faf2 Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Wed, 15 Jan 2025 15:43:03 +0000 Subject: [PATCH 10/31] feat: get item attributes --- OSFilesystemLib.xcodeproj/project.pbxproj | 4 + .../OSFLSTItemAttributeModel.swift | 43 ++++++ OSFilesystemLib/OSFLSTManager+Assets.swift | 1 + OSFilesystemLib/OSFLSTManager.swift | 5 + OSFilesystemLibTests/MockFileManager.swift | 15 ++- .../OSFLSTFileManagerTests.swift | 127 +++++++++++++++++- 6 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 OSFilesystemLib/OSFLSTItemAttributeModel.swift diff --git a/OSFilesystemLib.xcodeproj/project.pbxproj b/OSFilesystemLib.xcodeproj/project.pbxproj index f81853f..a7f55a8 100644 --- a/OSFilesystemLib.xcodeproj/project.pbxproj +++ b/OSFilesystemLib.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 751328DA2D318DBA0031BDD0 /* OSFLSTDirectoryManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D92D318DBA0031BDD0 /* OSFLSTDirectoryManagerTests.swift */; }; 751328DB2D318E770031BDD0 /* MockFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D82D3179430031BDD0 /* MockFileManager.swift */; }; 7575CF6A2BFCEE6F008F3FD0 /* OSFilesystemLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7575CF612BFCEE6F008F3FD0 /* OSFilesystemLib.framework */; }; + 75F8380B2D37E42000FCE044 /* OSFLSTItemAttributeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F8380A2D37E42000FCE044 /* OSFLSTItemAttributeModel.swift */; }; 75FEB2B02D3546D7007C2686 /* OSFLSTManager+Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FEB2AF2D3546D7007C2686 /* OSFLSTManager+Assets.swift */; }; 75FEB2B22D35470C007C2686 /* URL+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FEB2B12D35470C007C2686 /* URL+Extension.swift */; }; 75FEB2B42D35479B007C2686 /* OSFLSTFileManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FEB2B32D35479B007C2686 /* OSFLSTFileManagerTests.swift */; }; @@ -33,6 +34,7 @@ 751328D92D318DBA0031BDD0 /* OSFLSTDirectoryManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFLSTDirectoryManagerTests.swift; sourceTree = ""; }; 7575CF612BFCEE6F008F3FD0 /* OSFilesystemLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OSFilesystemLib.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7575CF692BFCEE6F008F3FD0 /* OSFilesystemLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OSFilesystemLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 75F8380A2D37E42000FCE044 /* OSFLSTItemAttributeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFLSTItemAttributeModel.swift; sourceTree = ""; }; 75FEB2AF2D3546D7007C2686 /* OSFLSTManager+Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSFLSTManager+Assets.swift"; sourceTree = ""; }; 75FEB2B12D35470C007C2686 /* URL+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extension.swift"; sourceTree = ""; }; 75FEB2B32D35479B007C2686 /* OSFLSTFileManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFLSTFileManagerTests.swift; sourceTree = ""; }; @@ -82,6 +84,7 @@ 751328D42D3175170031BDD0 /* OSFLSTManager.swift */, 75FEB2AF2D3546D7007C2686 /* OSFLSTManager+Assets.swift */, 75FEB2B12D35470C007C2686 /* URL+Extension.swift */, + 75F8380A2D37E42000FCE044 /* OSFLSTItemAttributeModel.swift */, ); path = OSFilesystemLib; sourceTree = ""; @@ -233,6 +236,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 75F8380B2D37E42000FCE044 /* OSFLSTItemAttributeModel.swift in Sources */, 75FEB2B02D3546D7007C2686 /* OSFLSTManager+Assets.swift in Sources */, 751328D52D3175170031BDD0 /* OSFLSTManager.swift in Sources */, 75FEB2B22D35470C007C2686 /* URL+Extension.swift in Sources */, diff --git a/OSFilesystemLib/OSFLSTItemAttributeModel.swift b/OSFilesystemLib/OSFLSTItemAttributeModel.swift new file mode 100644 index 0000000..cfe22f4 --- /dev/null +++ b/OSFilesystemLib/OSFLSTItemAttributeModel.swift @@ -0,0 +1,43 @@ +import Foundation + +public enum OSFLSTItemType: Encodable { + case directory + case file + + static func create(from fileAttributeType: String?) -> OSFLSTItemType { + fileAttributeType == FileAttributeKey.FileTypeDirectoryValue ? .directory : .file + } +} + +public struct OSFLSTItemAttributeModel { + private(set) public var creationDateTimestamp: Double + private(set) public var modificationDateTimestamp: Double + private(set) public var size: UInt64 + private(set) public var type: OSFLSTItemType +} + +public extension OSFLSTItemAttributeModel { + static func create(from attributeDictionary: [FileAttributeKey: Any]) -> OSFLSTItemAttributeModel { + let creationDate = attributeDictionary[.creationDate] as? Date + let modificationDate = attributeDictionary[.modificationDate] as? Date + let size = attributeDictionary[.size] as? UInt64 ?? 0 + let type = attributeDictionary[.type] as? String + + return .init( + creationDateTimestamp: creationDate?.millisecondsSinceUnixEpoch ?? 0, + modificationDateTimestamp: modificationDate?.millisecondsSinceUnixEpoch ?? 0, + size: size, + type: .create(from: type) + ) + } +} + +extension Date { + var millisecondsSinceUnixEpoch: Double { + timeIntervalSince1970 * 1000 + } +} + +extension FileAttributeKey { + static var FileTypeDirectoryValue = "NSFileTypeDirectory" +} diff --git a/OSFilesystemLib/OSFLSTManager+Assets.swift b/OSFilesystemLib/OSFLSTManager+Assets.swift index 1740c36..fd267de 100644 --- a/OSFilesystemLib/OSFLSTManager+Assets.swift +++ b/OSFilesystemLib/OSFLSTManager+Assets.swift @@ -16,6 +16,7 @@ public protocol OSFLSTFileManager { func deleteFile(atPath: String) throws func saveFile(atPath: String, withEncodingAndData: OSFLSTEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL func appendData(_ data: OSFLSTEncodingValueMapper, atPath: String, includeIntermediateDirectories: Bool) throws + func getItemAttributes(atPath: String) throws -> OSFLSTItemAttributeModel } enum OSFLSTFileManagerError: Error { diff --git a/OSFilesystemLib/OSFLSTManager.swift b/OSFilesystemLib/OSFLSTManager.swift index a045418..265dee0 100644 --- a/OSFilesystemLib/OSFLSTManager.swift +++ b/OSFilesystemLib/OSFLSTManager.swift @@ -113,6 +113,11 @@ extension OSFLSTManager: OSFLSTFileManager { try fileHandle?.close() } + public func getItemAttributes(atPath path: String) throws -> OSFLSTItemAttributeModel { + let attributesDictionary = try fileManager.attributesOfItem(atPath: path) + return .create(from: attributesDictionary) + } + private func readFileAsBase64EncodedString(from fileURL: URL) throws -> String { try Data(contentsOf: fileURL).base64EncodedString() } diff --git a/OSFilesystemLibTests/MockFileManager.swift b/OSFilesystemLibTests/MockFileManager.swift index 1e29efe..5869b34 100644 --- a/OSFilesystemLibTests/MockFileManager.swift +++ b/OSFilesystemLibTests/MockFileManager.swift @@ -5,16 +5,18 @@ class MockFileManager: FileManager { var shouldDirectoryHaveContent: Bool var urlsWithinDirectory: [URL] var fileExists: Bool + var fileAttributes: [FileAttributeKey: Any] private(set) var capturedPath: String? private(set) var capturedIntermediateDirectories: Bool = false private(set) var capturedSearchPathDirectory: FileManager.SearchPathDirectory? - init(error: MockFileManagerError? = nil, shouldDirectoryHaveContent: Bool = false, urlsWithinDirectory: [URL] = [], fileExists: Bool = true) { + init(error: MockFileManagerError? = nil, shouldDirectoryHaveContent: Bool = false, urlsWithinDirectory: [URL] = [], fileExists: Bool = true, fileAttributes: [FileAttributeKey: Any] = [:]) { self.error = error self.shouldDirectoryHaveContent = shouldDirectoryHaveContent self.urlsWithinDirectory = urlsWithinDirectory self.fileExists = fileExists + self.fileAttributes = fileAttributes } } @@ -23,6 +25,7 @@ enum MockFileManagerError: Error { case readDirectoryError case deleteDirectoryError case deleteFileError + case itemAttributesError } extension MockFileManager { @@ -75,4 +78,14 @@ extension MockFileManager { throw error } } + + override func attributesOfItem(atPath path: String) throws -> [FileAttributeKey: Any] { + capturedPath = path + + if let error, error == .itemAttributesError { + throw error + } + + return fileAttributes + } } diff --git a/OSFilesystemLibTests/OSFLSTFileManagerTests.swift b/OSFilesystemLibTests/OSFLSTFileManagerTests.swift index b9898e0..cef3c66 100644 --- a/OSFilesystemLibTests/OSFLSTFileManagerTests.swift +++ b/OSFilesystemLibTests/OSFLSTFileManagerTests.swift @@ -373,6 +373,101 @@ final class OSFLSTFileManagerTests: XCTestCase { XCTAssertEqual($0 as? OSFLSTFileManagerError, .cantDecodeData) } } + + // MARK: - 'getItemAttributes' tests + func test_getItemAttributes_forFile_returnsFileAttributeModelSuccessfully() throws { + // Given + let currentDate = Date() + let createHourDifference = 2 + let modificationHourDifference = 1 + let fileSize: UInt64 = 128 + let fileAttributes = Configuration.fileAttributes( + consideringDate: currentDate, andDifference: (createHourDifference, modificationHourDifference), size: fileSize, isDirectoryType: false + ) + let fileManager = createFileManager(fileAttributes: fileAttributes) + let testDirectory = "/test/directory" + + // When + let fileAttributesModel = try sut.getItemAttributes(atPath: testDirectory) + + // Then + XCTAssertEqual(fileManager.capturedPath, testDirectory) + XCTAssertEqual(fileAttributesModel.creationDateTimestamp, applyHourDifference( + createHourDifference, toTimestamp: currentDate.millisecondsSinceUnixEpoch + )) + XCTAssertEqual(fileAttributesModel.modificationDateTimestamp, applyHourDifference( + modificationHourDifference, toTimestamp: currentDate.millisecondsSinceUnixEpoch + )) + XCTAssertEqual(fileAttributesModel.size, fileSize) + XCTAssertEqual(fileAttributesModel.type, .file) + } + + // MARK: - 'getItemAttributes' tests + func test_getItemAttributes_omittingValues_returnsFileAttributeModelSuccessfully() throws { + // Given + let fileAttributes = Configuration.fileAttributes( + isDirectoryType: false + ) + let fileManager = createFileManager(fileAttributes: fileAttributes) + let testDirectory = "/test/directory" + + // When + let fileAttributesModel = try sut.getItemAttributes(atPath: testDirectory) + + // Then + XCTAssertEqual(fileManager.capturedPath, testDirectory) + XCTAssertEqual(fileAttributesModel.creationDateTimestamp, 0) + XCTAssertEqual(fileAttributesModel.modificationDateTimestamp, 0) + XCTAssertEqual(fileAttributesModel.size, 0) + XCTAssertEqual(fileAttributesModel.type, .file) + } + + func test_getItemAttributes_forDirectory_returnsFileAttributeModelSuccessfully() throws { + // Given + let currentDate = Date() + let createHourDifference = 2 + let modificationHourDifference = 1 + let fileSize: UInt64 = 128 + let fileAttributes = Configuration.fileAttributes( + consideringDate: currentDate, andDifference: (createHourDifference, modificationHourDifference), size: fileSize, isDirectoryType: true + ) + let fileManager = createFileManager(fileAttributes: fileAttributes) + let testDirectory = "/test/directory" + + // When + let fileAttributesModel = try sut.getItemAttributes(atPath: testDirectory) + + // Then + XCTAssertEqual(fileManager.capturedPath, testDirectory) + XCTAssertEqual(fileAttributesModel.creationDateTimestamp, applyHourDifference( + createHourDifference, toTimestamp: currentDate.millisecondsSinceUnixEpoch + )) + XCTAssertEqual(fileAttributesModel.modificationDateTimestamp, applyHourDifference( + modificationHourDifference, toTimestamp: currentDate.millisecondsSinceUnixEpoch + )) + XCTAssertEqual(fileAttributesModel.size, fileSize) + XCTAssertEqual(fileAttributesModel.type, .directory) + } + + func test_getItemAttributes_errorWhileRetrieving_returnsError() { + // Given + let error = MockFileManagerError.itemAttributesError + let currentDate = Date() + let createHourDifference = 2 + let modificationHourDifference = 1 + let fileSize: UInt64 = 128 + let fileAttributes = Configuration.fileAttributes( + consideringDate: currentDate, andDifference: (createHourDifference, modificationHourDifference), size: fileSize, isDirectoryType: false + ) + createFileManager(error: error, fileAttributes: fileAttributes) + let testDirectory = "/test/directory" + + // When + XCTAssertThrowsError(try sut.getItemAttributes(atPath: testDirectory)) { + // Then + XCTAssertEqual($0 as? MockFileManagerError, error) + } + } } private extension OSFLSTFileManagerTests { @@ -385,10 +480,34 @@ private extension OSFLSTFileManagerTests { static let byteBufferEncodedFileContent = "Hello, byte buffer-encoded world!" static let fileExtendedContent = " How are you?" static let emojiContent = "🙃" + + static func fileAttributes(consideringDate date: Date? = nil, andDifference dateDifference: (creation: Int, modification: Int)? = nil, size: UInt64? = nil, isDirectoryType: Bool) -> [FileAttributeKey: Any] { + var result: [FileAttributeKey: Any] = [.type: isDirectoryType ? FileAttributeKey.FileTypeDirectoryValue : Configuration.fileName] + + if let date { + let removeDifferenceToDate: (Int) -> Date? = { + Calendar.current.date(byAdding: .hour, value: $0, to: date) + } + if let difference = dateDifference?.creation { + result[.creationDate] = removeDifferenceToDate(-difference) + } + if let difference = dateDifference?.modification { + result[.modificationDate] = removeDifferenceToDate(-difference) + } + } + + if let size { + result[.size] = size + } + + return result + } } - @discardableResult func createFileManager(error: MockFileManagerError? = nil, urlsWithinDirectory: [URL] = [], fileExists: Bool = true) -> MockFileManager { - let fileManager = MockFileManager(error: error, urlsWithinDirectory: urlsWithinDirectory, fileExists: fileExists) + @discardableResult func createFileManager(error: MockFileManagerError? = nil, urlsWithinDirectory: [URL] = [], fileExists: Bool = true, fileAttributes: [FileAttributeKey: Any] = [:]) -> MockFileManager { + let fileManager = MockFileManager( + error: error, urlsWithinDirectory: urlsWithinDirectory, fileExists: fileExists, fileAttributes: fileAttributes + ) sut = OSFLSTManager(fileManager: fileManager) return fileManager @@ -414,4 +533,8 @@ private extension OSFLSTFileManagerTests { func fetchConfigurationFile() -> URL? { Bundle(for: type(of: self)).url(forResource: Configuration.fileName, withExtension: Configuration.fileExtension) } + + func applyHourDifference(_ hour: Int, toTimestamp timestamp: Double) -> Double { + timestamp - Double(hour) * 60.0 * 60.0 * 1000.0 + } } From 613458c5b290951e94b27982a1fad948b79fac7c Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Wed, 15 Jan 2025 17:24:15 +0000 Subject: [PATCH 11/31] feat: rename and copy item Split OSFLSTFileManagerTests file into different extensions to avoid SwiftLint warnings. --- OSFilesystemLib/OSFLSTManager+Assets.swift | 2 + OSFilesystemLib/OSFLSTManager.swift | 27 +++ OSFilesystemLibTests/MockFileManager.swift | 22 ++- .../OSFLSTFileManagerTests.swift | 155 ++++++++++++++++-- 4 files changed, 195 insertions(+), 11 deletions(-) diff --git a/OSFilesystemLib/OSFLSTManager+Assets.swift b/OSFilesystemLib/OSFLSTManager+Assets.swift index fd267de..0773eaf 100644 --- a/OSFilesystemLib/OSFLSTManager+Assets.swift +++ b/OSFilesystemLib/OSFLSTManager+Assets.swift @@ -17,6 +17,8 @@ public protocol OSFLSTFileManager { func saveFile(atPath: String, withEncodingAndData: OSFLSTEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL func appendData(_ data: OSFLSTEncodingValueMapper, atPath: String, includeIntermediateDirectories: Bool) throws func getItemAttributes(atPath: String) throws -> OSFLSTItemAttributeModel + func renameItem(fromPath: String, toPath: String) throws + func copyItem(fromPath: String, toPath: String) throws } enum OSFLSTFileManagerError: Error { diff --git a/OSFilesystemLib/OSFLSTManager.swift b/OSFilesystemLib/OSFLSTManager.swift index 265dee0..1c9f299 100644 --- a/OSFilesystemLib/OSFLSTManager.swift +++ b/OSFilesystemLib/OSFLSTManager.swift @@ -118,6 +118,18 @@ extension OSFLSTManager: OSFLSTFileManager { return .create(from: attributesDictionary) } + public func renameItem(fromPath origin: String, toPath destination: String) throws { + try copy(fromPath: origin, toPath: destination) { + try fileManager.moveItem(atPath: origin, toPath: destination) + } + } + + public func copyItem(fromPath origin: String, toPath destination: String) throws { + try copy(fromPath: origin, toPath: destination) { + try fileManager.copyItem(atPath: origin, toPath: destination) + } + } + private func readFileAsBase64EncodedString(from fileURL: URL) throws -> String { try Data(contentsOf: fileURL).base64EncodedString() } @@ -140,4 +152,19 @@ extension OSFLSTManager: OSFLSTFileManager { } return rawURL } + + private func copy(fromPath origin: String, toPath destination: String, performOperation: () throws -> Void) throws { + guard origin != destination else { + return + } + + var isDirectory: ObjCBool = false + if fileManager.fileExists(atPath: destination, isDirectory: &isDirectory) { + if !isDirectory.boolValue { + try deleteFile(atPath: destination) + } + } + + try performOperation() + } } diff --git a/OSFilesystemLibTests/MockFileManager.swift b/OSFilesystemLibTests/MockFileManager.swift index 5869b34..4b1c1a7 100644 --- a/OSFilesystemLibTests/MockFileManager.swift +++ b/OSFilesystemLibTests/MockFileManager.swift @@ -6,17 +6,21 @@ class MockFileManager: FileManager { var urlsWithinDirectory: [URL] var fileExists: Bool var fileAttributes: [FileAttributeKey: Any] + var shouldBeDirectory: ObjCBool private(set) var capturedPath: String? + private(set) var capturedOriginPath: String? + private(set) var capturedDestinationPath: String? private(set) var capturedIntermediateDirectories: Bool = false private(set) var capturedSearchPathDirectory: FileManager.SearchPathDirectory? - init(error: MockFileManagerError? = nil, shouldDirectoryHaveContent: Bool = false, urlsWithinDirectory: [URL] = [], fileExists: Bool = true, fileAttributes: [FileAttributeKey: Any] = [:]) { + init(error: MockFileManagerError? = nil, shouldDirectoryHaveContent: Bool = false, urlsWithinDirectory: [URL] = [], fileExists: Bool = true, fileAttributes: [FileAttributeKey: Any] = [:], shouldBeDirectory: ObjCBool = true) { self.error = error self.shouldDirectoryHaveContent = shouldDirectoryHaveContent self.urlsWithinDirectory = urlsWithinDirectory self.fileExists = fileExists self.fileAttributes = fileAttributes + self.shouldBeDirectory = shouldBeDirectory } } @@ -71,6 +75,12 @@ extension MockFileManager { fileExists } + override func fileExists(atPath path: String, isDirectory: UnsafeMutablePointer?) -> Bool { + isDirectory?.pointee = shouldBeDirectory + + return fileExists + } + override func removeItem(atPath path: String) throws { capturedPath = path @@ -88,4 +98,14 @@ extension MockFileManager { return fileAttributes } + + override func moveItem(atPath srcPath: String, toPath dstPath: String) throws { + capturedOriginPath = srcPath + capturedDestinationPath = dstPath + } + + override func copyItem(atPath srcPath: String, toPath dstPath: String) throws { + capturedOriginPath = srcPath + capturedDestinationPath = dstPath + } } diff --git a/OSFilesystemLibTests/OSFLSTFileManagerTests.swift b/OSFilesystemLibTests/OSFLSTFileManagerTests.swift index cef3c66..5837222 100644 --- a/OSFilesystemLibTests/OSFLSTFileManagerTests.swift +++ b/OSFilesystemLibTests/OSFLSTFileManagerTests.swift @@ -4,8 +4,10 @@ import XCTest final class OSFLSTFileManagerTests: XCTestCase { private var sut: OSFLSTManager! +} - // MARK: - 'readFile` tests +// MARK: - 'readFile` tests +extension OSFLSTFileManagerTests { func test_readFile_withStringEncoding_returnsContentSuccessfully() throws { // Given createFileManager() @@ -31,8 +33,10 @@ final class OSFLSTFileManagerTests: XCTestCase { // Then XCTAssertEqual(fileContent, Configuration.fileContent) } +} - // MARK: - 'getFileURL' tests +// MARK: - 'getFileURL' tests +extension OSFLSTFileManagerTests { func test_getFileURL_fromDirectorySearchPath_containingSingleFile_returnsFileSuccessfully() throws { // Given let fileURL: URL = try XCTUnwrap(.init(string: "/file/directory")) @@ -115,8 +119,10 @@ final class OSFLSTFileManagerTests: XCTestCase { XCTAssertEqual($0 as? OSFLSTFileManagerError, .cantCreateURL) } } +} - // MARK: - 'deleteFile' tests +// MARK: - 'deleteFile' tests +extension OSFLSTFileManagerTests { func test_deleteFile_shouldBeSuccessful() throws { // Given let fileManager = createFileManager() @@ -153,8 +159,10 @@ final class OSFLSTFileManagerTests: XCTestCase { XCTAssertEqual($0 as? MockFileManagerError, error) } } +} - // MARK: - 'saveFile' tests +// MARK: - 'saveFile' tests +extension OSFLSTFileManagerTests { func test_saveFile_withStringEncoding_savesFileSuccessfullyAndReturnsItsURL() throws { // Given let fileManager = createFileManager() @@ -266,8 +274,10 @@ final class OSFLSTFileManagerTests: XCTestCase { XCTAssertEqual($0 as? OSFLSTFileManagerError, .missingParentFolder) } } +} - // MARK: - 'appendData' tests +// MARK: - 'appendData' tests +extension OSFLSTFileManagerTests { func test_appendData_withStringEncoding_savesFileSuccessfully() throws { // Given createFileManager() @@ -373,8 +383,10 @@ final class OSFLSTFileManagerTests: XCTestCase { XCTAssertEqual($0 as? OSFLSTFileManagerError, .cantDecodeData) } } +} - // MARK: - 'getItemAttributes' tests +// MARK: - 'getItemAttributes' tests +extension OSFLSTFileManagerTests { func test_getItemAttributes_forFile_returnsFileAttributeModelSuccessfully() throws { // Given let currentDate = Date() @@ -402,10 +414,9 @@ final class OSFLSTFileManagerTests: XCTestCase { XCTAssertEqual(fileAttributesModel.type, .file) } - // MARK: - 'getItemAttributes' tests func test_getItemAttributes_omittingValues_returnsFileAttributeModelSuccessfully() throws { // Given - let fileAttributes = Configuration.fileAttributes( + let fileAttributes = Configuration.fileAttributes( isDirectoryType: false ) let fileManager = createFileManager(fileAttributes: fileAttributes) @@ -470,6 +481,126 @@ final class OSFLSTFileManagerTests: XCTestCase { } } +// MARK: - 'renameItem' tests +extension OSFLSTFileManagerTests { + func test_renameItem_shouldBeSuccessful() throws { + // Given + let fileManager = createFileManager(fileExists: false) + let originPath = "/test/origin" + let destinationPath = "/test/destination" + + // When + try sut.renameItem(fromPath: originPath, toPath: destinationPath) + + // Then + XCTAssertEqual(fileManager.capturedOriginPath, originPath) + XCTAssertEqual(fileManager.capturedDestinationPath, destinationPath) + } + + func test_renameItem_sameOriginAndDestination_shouldDoNothing() throws { + // Given + let fileManager = createFileManager(fileExists: false) + let originPath = "/test/origin" + let destinationPath = "/test/origin" + + // When + try sut.renameItem(fromPath: originPath, toPath: destinationPath) + + // Then + XCTAssertNil(fileManager.capturedOriginPath) + XCTAssertNil(fileManager.capturedDestinationPath) + } + + func test_renameDirectory_alreadyExisting_shouldBeSuccessful() throws { + // Given + let fileManager = createFileManager() + let originPath = "/test/origin" + let destinationPath = "/test/destination" + + // When + try sut.renameItem(fromPath: originPath, toPath: destinationPath) + + // Then + XCTAssertEqual(fileManager.capturedOriginPath, originPath) + XCTAssertEqual(fileManager.capturedDestinationPath, destinationPath) + } + + func test_renameFile_alreadyExisting_shouldBeSuccessful() throws { + // Given + let fileManager = createFileManager(shouldBeDirectory: false) + let originPath = "/test/origin" + let destinationPath = "/test/destination" + + // When + try sut.renameItem(fromPath: originPath, toPath: destinationPath) + + // Then + XCTAssertEqual(fileManager.capturedPath, destinationPath) + XCTAssertEqual(fileManager.capturedOriginPath, originPath) + XCTAssertEqual(fileManager.capturedDestinationPath, destinationPath) + } +} + +// MARK: - 'copyItem' tests +extension OSFLSTFileManagerTests { + func test_copyItem_shouldBeSuccessful() throws { + // Given + let fileManager = createFileManager(fileExists: false) + let originPath = "/test/origin" + let destinationPath = "/test/destination" + + // When + try sut.copyItem(fromPath: originPath, toPath: destinationPath) + + // Then + XCTAssertEqual(fileManager.capturedOriginPath, originPath) + XCTAssertEqual(fileManager.capturedDestinationPath, destinationPath) + } + + func test_copyItem_sameOriginAndDestination_shouldDoNothing() throws { + // Given + let fileManager = createFileManager(fileExists: false) + let originPath = "/test/origin" + let destinationPath = "/test/origin" + + // When + try sut.copyItem(fromPath: originPath, toPath: destinationPath) + + // Then + XCTAssertNil(fileManager.capturedOriginPath) + XCTAssertNil(fileManager.capturedDestinationPath) + } + + func test_copyDirectory_alreadyExisting_shouldBeSuccessful() throws { + // Given + let fileManager = createFileManager() + let originPath = "/test/origin" + let destinationPath = "/test/destination" + + // When + try sut.copyItem(fromPath: originPath, toPath: destinationPath) + + // Then + XCTAssertEqual(fileManager.capturedOriginPath, originPath) + XCTAssertEqual(fileManager.capturedDestinationPath, destinationPath) + } + + func test_copyFile_alreadyExisting_shouldBeSuccessful() throws { + // Given + let fileManager = createFileManager(shouldBeDirectory: false) + let originPath = "/test/origin" + let destinationPath = "/test/destination" + + // When + try sut.copyItem(fromPath: originPath, toPath: destinationPath) + + // Then + XCTAssertEqual(fileManager.capturedPath, destinationPath) + XCTAssertEqual(fileManager.capturedOriginPath, originPath) + XCTAssertEqual(fileManager.capturedDestinationPath, destinationPath) + } +} + private extension OSFLSTFileManagerTests { struct Configuration { static let fileName = "file" @@ -504,9 +635,13 @@ private extension OSFLSTFileManagerTests { } } - @discardableResult func createFileManager(error: MockFileManagerError? = nil, urlsWithinDirectory: [URL] = [], fileExists: Bool = true, fileAttributes: [FileAttributeKey: Any] = [:]) -> MockFileManager { + @discardableResult func createFileManager(error: MockFileManagerError? = nil, urlsWithinDirectory: [URL] = [], fileExists: Bool = true, fileAttributes: [FileAttributeKey: Any] = [:], shouldBeDirectory: ObjCBool = true) -> MockFileManager { let fileManager = MockFileManager( - error: error, urlsWithinDirectory: urlsWithinDirectory, fileExists: fileExists, fileAttributes: fileAttributes + error: error, + urlsWithinDirectory: urlsWithinDirectory, + fileExists: fileExists, + fileAttributes: fileAttributes, + shouldBeDirectory: shouldBeDirectory ) sut = OSFLSTManager(fileManager: fileManager) From b843ad19e4a8e9bfc0fdd4d7a912e749bfb66a0e Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Thu, 16 Jan 2025 10:26:06 +0000 Subject: [PATCH 12/31] fix: Xcode 15 compatibility issue For Xcode 15, 'switch' statements may only be used as expression in return, throw, or as the source of an assignment. --- OSFilesystemLib/OSFLSTManager.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/OSFilesystemLib/OSFLSTManager.swift b/OSFilesystemLib/OSFLSTManager.swift index 1c9f299..ef24dca 100644 --- a/OSFilesystemLib/OSFLSTManager.swift +++ b/OSFilesystemLib/OSFLSTManager.swift @@ -100,11 +100,15 @@ extension OSFLSTManager: OSFLSTFileManager { return } - guard let dataToAppend = switch encodingMapper { - case .byteBuffer(let value): value - case .string(let encoding, let value): value.data(using: encoding.stringEncoding) - } else { - throw OSFLSTFileManagerError.cantDecodeData + let dataToAppend: Data + switch encodingMapper { + case .byteBuffer(let value): + dataToAppend = value + case .string(let encoding, let value): + guard let valueData = value.data(using: encoding.stringEncoding) else { + throw OSFLSTFileManagerError.cantDecodeData + } + dataToAppend = valueData } let fileHandle = FileHandle(forWritingAtPath: path) From 8d74194a19a099d07ea575b7ad3f1e1b14d85986 Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Thu, 16 Jan 2025 12:47:02 +0000 Subject: [PATCH 13/31] chore: rename FLST to FILE --- OSFilesystemLib.xcodeproj/project.pbxproj | 48 +++++++----- ...l.swift => OSFILEItemAttributeModel.swift} | 12 +-- OSFilesystemLib/OSFILEManager+Enums.swift | 44 +++++++++++ OSFilesystemLib/OSFILEManager+Errors.swift | 11 +++ OSFilesystemLib/OSFILEManager+Protocols.swift | 18 +++++ ...SFLSTManager.swift => OSFILEManager.swift} | 40 +++++----- OSFilesystemLib/OSFLSTManager+Assets.swift | 73 ------------------- ...wift => OSFILEDirectoryManagerTests.swift} | 10 +-- ...sts.swift => OSFILEFileManagerTests.swift} | 56 +++++++------- 9 files changed, 161 insertions(+), 151 deletions(-) rename OSFilesystemLib/{OSFLSTItemAttributeModel.swift => OSFILEItemAttributeModel.swift} (79%) create mode 100644 OSFilesystemLib/OSFILEManager+Enums.swift create mode 100644 OSFilesystemLib/OSFILEManager+Errors.swift create mode 100644 OSFilesystemLib/OSFILEManager+Protocols.swift rename OSFilesystemLib/{OSFLSTManager.swift => OSFILEManager.swift} (81%) delete mode 100644 OSFilesystemLib/OSFLSTManager+Assets.swift rename OSFilesystemLibTests/{OSFLSTDirectoryManagerTests.swift => OSFILEDirectoryManagerTests.swift} (95%) rename OSFilesystemLibTests/{OSFLSTFileManagerTests.swift => OSFILEFileManagerTests.swift} (94%) diff --git a/OSFilesystemLib.xcodeproj/project.pbxproj b/OSFilesystemLib.xcodeproj/project.pbxproj index a7f55a8..0e0f6a8 100644 --- a/OSFilesystemLib.xcodeproj/project.pbxproj +++ b/OSFilesystemLib.xcodeproj/project.pbxproj @@ -7,14 +7,16 @@ objects = { /* Begin PBXBuildFile section */ - 751328D52D3175170031BDD0 /* OSFLSTManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D42D3175170031BDD0 /* OSFLSTManager.swift */; }; - 751328DA2D318DBA0031BDD0 /* OSFLSTDirectoryManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D92D318DBA0031BDD0 /* OSFLSTDirectoryManagerTests.swift */; }; + 751328D52D3175170031BDD0 /* OSFILEManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D42D3175170031BDD0 /* OSFILEManager.swift */; }; + 751328DA2D318DBA0031BDD0 /* OSFILEDirectoryManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D92D318DBA0031BDD0 /* OSFILEDirectoryManagerTests.swift */; }; 751328DB2D318E770031BDD0 /* MockFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D82D3179430031BDD0 /* MockFileManager.swift */; }; 7575CF6A2BFCEE6F008F3FD0 /* OSFilesystemLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7575CF612BFCEE6F008F3FD0 /* OSFilesystemLib.framework */; }; - 75F8380B2D37E42000FCE044 /* OSFLSTItemAttributeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F8380A2D37E42000FCE044 /* OSFLSTItemAttributeModel.swift */; }; - 75FEB2B02D3546D7007C2686 /* OSFLSTManager+Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FEB2AF2D3546D7007C2686 /* OSFLSTManager+Assets.swift */; }; + 75F8380B2D37E42000FCE044 /* OSFILEItemAttributeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F8380A2D37E42000FCE044 /* OSFILEItemAttributeModel.swift */; }; + 75F84D662D39360E00892C89 /* OSFILEManager+Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F84D652D39360E00892C89 /* OSFILEManager+Errors.swift */; }; + 75F84D682D39362F00892C89 /* OSFILEManager+Enums.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F84D672D39362F00892C89 /* OSFILEManager+Enums.swift */; }; + 75FEB2B02D3546D7007C2686 /* OSFILEManager+Protocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FEB2AF2D3546D7007C2686 /* OSFILEManager+Protocols.swift */; }; 75FEB2B22D35470C007C2686 /* URL+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FEB2B12D35470C007C2686 /* URL+Extension.swift */; }; - 75FEB2B42D35479B007C2686 /* OSFLSTFileManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FEB2B32D35479B007C2686 /* OSFLSTFileManagerTests.swift */; }; + 75FEB2B42D35479B007C2686 /* OSFILEFileManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FEB2B32D35479B007C2686 /* OSFILEFileManagerTests.swift */; }; 75FEB2B72D355F21007C2686 /* file.txt in Resources */ = {isa = PBXBuildFile; fileRef = 75FEB2B62D355F21007C2686 /* file.txt */; }; /* End PBXBuildFile section */ @@ -29,15 +31,17 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 751328D42D3175170031BDD0 /* OSFLSTManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFLSTManager.swift; sourceTree = ""; }; + 751328D42D3175170031BDD0 /* OSFILEManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFILEManager.swift; sourceTree = ""; }; 751328D82D3179430031BDD0 /* MockFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFileManager.swift; sourceTree = ""; }; - 751328D92D318DBA0031BDD0 /* OSFLSTDirectoryManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFLSTDirectoryManagerTests.swift; sourceTree = ""; }; + 751328D92D318DBA0031BDD0 /* OSFILEDirectoryManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFILEDirectoryManagerTests.swift; sourceTree = ""; }; 7575CF612BFCEE6F008F3FD0 /* OSFilesystemLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OSFilesystemLib.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7575CF692BFCEE6F008F3FD0 /* OSFilesystemLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OSFilesystemLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 75F8380A2D37E42000FCE044 /* OSFLSTItemAttributeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFLSTItemAttributeModel.swift; sourceTree = ""; }; - 75FEB2AF2D3546D7007C2686 /* OSFLSTManager+Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSFLSTManager+Assets.swift"; sourceTree = ""; }; + 75F8380A2D37E42000FCE044 /* OSFILEItemAttributeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFILEItemAttributeModel.swift; sourceTree = ""; }; + 75F84D652D39360E00892C89 /* OSFILEManager+Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSFILEManager+Errors.swift"; sourceTree = ""; }; + 75F84D672D39362F00892C89 /* OSFILEManager+Enums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSFILEManager+Enums.swift"; sourceTree = ""; }; + 75FEB2AF2D3546D7007C2686 /* OSFILEManager+Protocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSFILEManager+Protocols.swift"; sourceTree = ""; }; 75FEB2B12D35470C007C2686 /* URL+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extension.swift"; sourceTree = ""; }; - 75FEB2B32D35479B007C2686 /* OSFLSTFileManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFLSTFileManagerTests.swift; sourceTree = ""; }; + 75FEB2B32D35479B007C2686 /* OSFILEFileManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFILEFileManagerTests.swift; sourceTree = ""; }; 75FEB2B62D355F21007C2686 /* file.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = file.txt; sourceTree = ""; }; /* End PBXFileReference section */ @@ -81,10 +85,12 @@ 7575CF632BFCEE6F008F3FD0 /* OSFilesystemLib */ = { isa = PBXGroup; children = ( - 751328D42D3175170031BDD0 /* OSFLSTManager.swift */, - 75FEB2AF2D3546D7007C2686 /* OSFLSTManager+Assets.swift */, + 751328D42D3175170031BDD0 /* OSFILEManager.swift */, + 75F84D672D39362F00892C89 /* OSFILEManager+Enums.swift */, + 75F84D652D39360E00892C89 /* OSFILEManager+Errors.swift */, + 75FEB2AF2D3546D7007C2686 /* OSFILEManager+Protocols.swift */, + 75F8380A2D37E42000FCE044 /* OSFILEItemAttributeModel.swift */, 75FEB2B12D35470C007C2686 /* URL+Extension.swift */, - 75F8380A2D37E42000FCE044 /* OSFLSTItemAttributeModel.swift */, ); path = OSFilesystemLib; sourceTree = ""; @@ -93,8 +99,8 @@ isa = PBXGroup; children = ( 751328D82D3179430031BDD0 /* MockFileManager.swift */, - 751328D92D318DBA0031BDD0 /* OSFLSTDirectoryManagerTests.swift */, - 75FEB2B32D35479B007C2686 /* OSFLSTFileManagerTests.swift */, + 751328D92D318DBA0031BDD0 /* OSFILEDirectoryManagerTests.swift */, + 75FEB2B32D35479B007C2686 /* OSFILEFileManagerTests.swift */, 75FEB2B62D355F21007C2686 /* file.txt */, ); path = OSFilesystemLibTests; @@ -236,9 +242,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 75F8380B2D37E42000FCE044 /* OSFLSTItemAttributeModel.swift in Sources */, - 75FEB2B02D3546D7007C2686 /* OSFLSTManager+Assets.swift in Sources */, - 751328D52D3175170031BDD0 /* OSFLSTManager.swift in Sources */, + 75F8380B2D37E42000FCE044 /* OSFILEItemAttributeModel.swift in Sources */, + 75FEB2B02D3546D7007C2686 /* OSFILEManager+Protocols.swift in Sources */, + 75F84D682D39362F00892C89 /* OSFILEManager+Enums.swift in Sources */, + 751328D52D3175170031BDD0 /* OSFILEManager.swift in Sources */, + 75F84D662D39360E00892C89 /* OSFILEManager+Errors.swift in Sources */, 75FEB2B22D35470C007C2686 /* URL+Extension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -247,9 +255,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 751328DA2D318DBA0031BDD0 /* OSFLSTDirectoryManagerTests.swift in Sources */, + 751328DA2D318DBA0031BDD0 /* OSFILEDirectoryManagerTests.swift in Sources */, 751328DB2D318E770031BDD0 /* MockFileManager.swift in Sources */, - 75FEB2B42D35479B007C2686 /* OSFLSTFileManagerTests.swift in Sources */, + 75FEB2B42D35479B007C2686 /* OSFILEFileManagerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/OSFilesystemLib/OSFLSTItemAttributeModel.swift b/OSFilesystemLib/OSFILEItemAttributeModel.swift similarity index 79% rename from OSFilesystemLib/OSFLSTItemAttributeModel.swift rename to OSFilesystemLib/OSFILEItemAttributeModel.swift index cfe22f4..8151548 100644 --- a/OSFilesystemLib/OSFLSTItemAttributeModel.swift +++ b/OSFilesystemLib/OSFILEItemAttributeModel.swift @@ -1,23 +1,23 @@ import Foundation -public enum OSFLSTItemType: Encodable { +public enum OSFILEItemType: Encodable { case directory case file - static func create(from fileAttributeType: String?) -> OSFLSTItemType { + static func create(from fileAttributeType: String?) -> OSFILEItemType { fileAttributeType == FileAttributeKey.FileTypeDirectoryValue ? .directory : .file } } -public struct OSFLSTItemAttributeModel { +public struct OSFILEItemAttributeModel { private(set) public var creationDateTimestamp: Double private(set) public var modificationDateTimestamp: Double private(set) public var size: UInt64 - private(set) public var type: OSFLSTItemType + private(set) public var type: OSFILEItemType } -public extension OSFLSTItemAttributeModel { - static func create(from attributeDictionary: [FileAttributeKey: Any]) -> OSFLSTItemAttributeModel { +public extension OSFILEItemAttributeModel { + static func create(from attributeDictionary: [FileAttributeKey: Any]) -> OSFILEItemAttributeModel { let creationDate = attributeDictionary[.creationDate] as? Date let modificationDate = attributeDictionary[.modificationDate] as? Date let size = attributeDictionary[.size] as? UInt64 ?? 0 diff --git a/OSFilesystemLib/OSFILEManager+Enums.swift b/OSFilesystemLib/OSFILEManager+Enums.swift new file mode 100644 index 0000000..7890314 --- /dev/null +++ b/OSFilesystemLib/OSFILEManager+Enums.swift @@ -0,0 +1,44 @@ +import Foundation + +public enum OSFILEEncoding { + case byteBuffer + case string(encoding: OSFILEStringEncoding) +} + +public enum OSFILEEncodingValueMapper { + case byteBuffer(value: Data) + case string(encoding: OSFILEStringEncoding, value: String) +} + +public enum OSFILEStringEncoding { + case ascii + case utf8 + case utf16 + + var stringEncoding: String.Encoding { + switch self { + case .ascii: .ascii + case .utf8: .utf8 + case .utf16: .utf16 + } + } +} + +public enum OSFILESearchPath { + case directory(searchPath: OSFILESearchPathDirectory) + case raw +} + +public enum OSFILESearchPathDirectory { + case cache + case document + case library + + var fileManagerSearchPathDirectory: FileManager.SearchPathDirectory { + switch self { + case .cache: .cachesDirectory + case .document: .documentDirectory + case .library: .libraryDirectory + } + } +} diff --git a/OSFilesystemLib/OSFILEManager+Errors.swift b/OSFilesystemLib/OSFILEManager+Errors.swift new file mode 100644 index 0000000..7e8e8d5 --- /dev/null +++ b/OSFilesystemLib/OSFILEManager+Errors.swift @@ -0,0 +1,11 @@ +enum OSFILEDirectoryManagerError: Error { + case notEmpty +} + +enum OSFILEFileManagerError: Error { + case cantCreateURL + case cantDecodeData + case directoryNotFound + case fileNotFound + case missingParentFolder +} diff --git a/OSFilesystemLib/OSFILEManager+Protocols.swift b/OSFilesystemLib/OSFILEManager+Protocols.swift new file mode 100644 index 0000000..65270ad --- /dev/null +++ b/OSFilesystemLib/OSFILEManager+Protocols.swift @@ -0,0 +1,18 @@ +import Foundation + +public protocol OSFILEDirectoryManager { + func createDirectory(atPath: String, includeIntermediateDirectories: Bool) throws + func removeDirectory(atPath: String, includeIntermediateDirectories: Bool) throws + func listDirectory(atPath: String) throws -> [URL] +} + +public protocol OSFILEFileManager { + func readFile(atPath: String, withEncoding: OSFILEEncoding) throws -> String + func getFileURL(atPath: String, withSearchPath: OSFILESearchPath) throws -> URL + func deleteFile(atPath: String) throws + func saveFile(atPath: String, withEncodingAndData: OSFILEEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL + func appendData(_ data: OSFILEEncodingValueMapper, atPath: String, includeIntermediateDirectories: Bool) throws + func getItemAttributes(atPath: String) throws -> OSFILEItemAttributeModel + func renameItem(fromPath: String, toPath: String) throws + func copyItem(fromPath: String, toPath: String) throws +} diff --git a/OSFilesystemLib/OSFLSTManager.swift b/OSFilesystemLib/OSFILEManager.swift similarity index 81% rename from OSFilesystemLib/OSFLSTManager.swift rename to OSFilesystemLib/OSFILEManager.swift index ef24dca..efde728 100644 --- a/OSFilesystemLib/OSFLSTManager.swift +++ b/OSFilesystemLib/OSFILEManager.swift @@ -1,6 +1,6 @@ import Foundation -public struct OSFLSTManager { +public struct OSFILEManager { private let fileManager: FileManager public init(fileManager: FileManager) { @@ -8,7 +8,7 @@ public struct OSFLSTManager { } } -extension OSFLSTManager: OSFLSTDirectoryManager { +extension OSFILEManager: OSFILEDirectoryManager { public func createDirectory(atPath path: String, includeIntermediateDirectories: Bool) throws { let pathURL = URL.create(with: path) try fileManager.createDirectory(at: pathURL, withIntermediateDirectories: includeIntermediateDirectories) @@ -19,7 +19,7 @@ extension OSFLSTManager: OSFLSTDirectoryManager { if !includeIntermediateDirectories { let directoryContents = try listDirectory(atPath: path) if !directoryContents.isEmpty { - throw OSFLSTDirectoryManagerError.notEmpty + throw OSFILEDirectoryManagerError.notEmpty } } @@ -32,8 +32,8 @@ extension OSFLSTManager: OSFLSTDirectoryManager { } } -extension OSFLSTManager: OSFLSTFileManager { - public func readFile(atPath path: String, withEncoding encoding: OSFLSTEncoding) throws -> String { +extension OSFILEManager: OSFILEFileManager { + public func readFile(atPath path: String, withEncoding encoding: OSFILEEncoding) throws -> String { let fileURL = URL.create(with: path) // Check if the URL requires security-scoped access @@ -55,7 +55,7 @@ extension OSFLSTManager: OSFLSTFileManager { } } - public func getFileURL(atPath path: String, withSearchPath searchPath: OSFLSTSearchPath) throws -> URL { + public func getFileURL(atPath path: String, withSearchPath searchPath: OSFILESearchPath) throws -> URL { return switch searchPath { case .directory(let directorySearchPath): try resolveDirectoryURL(for: directorySearchPath.fileManagerSearchPathDirectory, with: path) @@ -66,13 +66,13 @@ extension OSFLSTManager: OSFLSTFileManager { public func deleteFile(atPath path: String) throws { guard fileManager.fileExists(atPath: path) else { - throw OSFLSTFileManagerError.fileNotFound + throw OSFILEFileManagerError.fileNotFound } try fileManager.removeItem(atPath: path) } - @discardableResult public func saveFile(atPath path: String, withEncodingAndData encodingMapper: OSFLSTEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL { + @discardableResult public func saveFile(atPath path: String, withEncodingAndData encodingMapper: OSFILEEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL { let fileURL = URL.create(with: path) let fileDirectoryURL = fileURL.deletingLastPathComponent() @@ -80,7 +80,7 @@ extension OSFLSTManager: OSFLSTFileManager { if includeIntermediateDirectories { try createDirectory(atPath: fileDirectoryURL.urlPath, includeIntermediateDirectories: true) } else { - throw OSFLSTFileManagerError.missingParentFolder + throw OSFILEFileManagerError.missingParentFolder } } @@ -94,7 +94,7 @@ extension OSFLSTManager: OSFLSTFileManager { return fileURL } - public func appendData(_ encodingMapper: OSFLSTEncodingValueMapper, atPath path: String, includeIntermediateDirectories: Bool) throws { + public func appendData(_ encodingMapper: OSFILEEncodingValueMapper, atPath path: String, includeIntermediateDirectories: Bool) throws { guard fileManager.fileExists(atPath: path) else { try saveFile(atPath: path, withEncodingAndData: encodingMapper, includeIntermediateDirectories: includeIntermediateDirectories) return @@ -106,7 +106,7 @@ extension OSFLSTManager: OSFLSTFileManager { dataToAppend = value case .string(let encoding, let value): guard let valueData = value.data(using: encoding.stringEncoding) else { - throw OSFLSTFileManagerError.cantDecodeData + throw OSFILEFileManagerError.cantDecodeData } dataToAppend = valueData } @@ -117,7 +117,7 @@ extension OSFLSTManager: OSFLSTFileManager { try fileHandle?.close() } - public func getItemAttributes(atPath path: String) throws -> OSFLSTItemAttributeModel { + public func getItemAttributes(atPath path: String) throws -> OSFILEItemAttributeModel { let attributesDictionary = try fileManager.attributesOfItem(atPath: path) return .create(from: attributesDictionary) } @@ -133,31 +133,33 @@ extension OSFLSTManager: OSFLSTFileManager { try fileManager.copyItem(atPath: origin, toPath: destination) } } +} - private func readFileAsBase64EncodedString(from fileURL: URL) throws -> String { +private extension OSFILEManager { + func readFileAsBase64EncodedString(from fileURL: URL) throws -> String { try Data(contentsOf: fileURL).base64EncodedString() } - private func readFileAsString(from fileURL: URL, using stringEncoding: String.Encoding) throws -> String { + func readFileAsString(from fileURL: URL, using stringEncoding: String.Encoding) throws -> String { try String(contentsOf: fileURL, encoding: stringEncoding) } - private func resolveDirectoryURL(for searchPath: FileManager.SearchPathDirectory, with path: String) throws -> URL { + func resolveDirectoryURL(for searchPath: FileManager.SearchPathDirectory, with path: String) throws -> URL { guard let directoryURL = fileManager.urls(for: searchPath, in: .userDomainMask).first else { - throw OSFLSTFileManagerError.directoryNotFound + throw OSFILEFileManagerError.directoryNotFound } return path.isEmpty ? directoryURL : directoryURL.appendingPathComponent(path) } - private func resolveRawURL(from path: String) throws -> URL { + func resolveRawURL(from path: String) throws -> URL { guard let rawURL = URL(string: path) else { - throw OSFLSTFileManagerError.cantCreateURL + throw OSFILEFileManagerError.cantCreateURL } return rawURL } - private func copy(fromPath origin: String, toPath destination: String, performOperation: () throws -> Void) throws { + func copy(fromPath origin: String, toPath destination: String, performOperation: () throws -> Void) throws { guard origin != destination else { return } diff --git a/OSFilesystemLib/OSFLSTManager+Assets.swift b/OSFilesystemLib/OSFLSTManager+Assets.swift deleted file mode 100644 index 0773eaf..0000000 --- a/OSFilesystemLib/OSFLSTManager+Assets.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Foundation - -public protocol OSFLSTDirectoryManager { - func createDirectory(atPath: String, includeIntermediateDirectories: Bool) throws - func removeDirectory(atPath: String, includeIntermediateDirectories: Bool) throws - func listDirectory(atPath: String) throws -> [URL] -} - -enum OSFLSTDirectoryManagerError: Error { - case notEmpty -} - -public protocol OSFLSTFileManager { - func readFile(atPath: String, withEncoding: OSFLSTEncoding) throws -> String - func getFileURL(atPath: String, withSearchPath: OSFLSTSearchPath) throws -> URL - func deleteFile(atPath: String) throws - func saveFile(atPath: String, withEncodingAndData: OSFLSTEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL - func appendData(_ data: OSFLSTEncodingValueMapper, atPath: String, includeIntermediateDirectories: Bool) throws - func getItemAttributes(atPath: String) throws -> OSFLSTItemAttributeModel - func renameItem(fromPath: String, toPath: String) throws - func copyItem(fromPath: String, toPath: String) throws -} - -enum OSFLSTFileManagerError: Error { - case cantCreateURL - case cantDecodeData - case directoryNotFound - case fileNotFound - case missingParentFolder -} - -public enum OSFLSTEncoding { - case byteBuffer - case string(encoding: OSFLSTStringEncoding) -} - -public enum OSFLSTEncodingValueMapper { - case byteBuffer(value: Data) - case string(encoding: OSFLSTStringEncoding, value: String) -} - -public enum OSFLSTStringEncoding { - case ascii - case utf8 - case utf16 - - var stringEncoding: String.Encoding { - switch self { - case .ascii: .ascii - case .utf8: .utf8 - case .utf16: .utf16 - } - } -} - -public enum OSFLSTSearchPath { - case directory(searchPath: OSFLSTSearchPathDirectory) - case raw -} - -public enum OSFLSTSearchPathDirectory { - case cache - case document - case library - - var fileManagerSearchPathDirectory: FileManager.SearchPathDirectory { - switch self { - case .cache: .cachesDirectory - case .document: .documentDirectory - case .library: .libraryDirectory - } - } -} diff --git a/OSFilesystemLibTests/OSFLSTDirectoryManagerTests.swift b/OSFilesystemLibTests/OSFILEDirectoryManagerTests.swift similarity index 95% rename from OSFilesystemLibTests/OSFLSTDirectoryManagerTests.swift rename to OSFilesystemLibTests/OSFILEDirectoryManagerTests.swift index 1e61cb0..7213db0 100644 --- a/OSFilesystemLibTests/OSFLSTDirectoryManagerTests.swift +++ b/OSFilesystemLibTests/OSFILEDirectoryManagerTests.swift @@ -2,8 +2,8 @@ import XCTest @testable import OSFilesystemLib -final class OSFLSTDirectoryManagerTests: XCTestCase { - private var sut: OSFLSTManager! +final class OSFILEDirectoryManagerTests: XCTestCase { + private var sut: OSFILEManager! // MARK: - 'createDirectory' tests func test_createDirectory_shouldBeSuccessful() throws { @@ -88,7 +88,7 @@ final class OSFLSTDirectoryManagerTests: XCTestCase { try sut.removeDirectory(atPath: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) ) { // Then - XCTAssertEqual($0 as? OSFLSTDirectoryManagerError, .notEmpty) + XCTAssertEqual($0 as? OSFILEDirectoryManagerError, .notEmpty) } } @@ -136,10 +136,10 @@ final class OSFLSTDirectoryManagerTests: XCTestCase { } } -private extension OSFLSTDirectoryManagerTests { +private extension OSFILEDirectoryManagerTests { @discardableResult func createFileManager(with error: MockFileManagerError? = nil, shouldDirectoryHaveContent: Bool = false) -> MockFileManager { let fileManager = MockFileManager(error: error, shouldDirectoryHaveContent: shouldDirectoryHaveContent) - sut = OSFLSTManager(fileManager: fileManager) + sut = OSFILEManager(fileManager: fileManager) return fileManager } diff --git a/OSFilesystemLibTests/OSFLSTFileManagerTests.swift b/OSFilesystemLibTests/OSFILEFileManagerTests.swift similarity index 94% rename from OSFilesystemLibTests/OSFLSTFileManagerTests.swift rename to OSFilesystemLibTests/OSFILEFileManagerTests.swift index 5837222..312a9c8 100644 --- a/OSFilesystemLibTests/OSFLSTFileManagerTests.swift +++ b/OSFilesystemLibTests/OSFILEFileManagerTests.swift @@ -2,12 +2,12 @@ import XCTest @testable import OSFilesystemLib -final class OSFLSTFileManagerTests: XCTestCase { - private var sut: OSFLSTManager! +final class OSFILEFileManagerTests: XCTestCase { + private var sut: OSFILEManager! } // MARK: - 'readFile` tests -extension OSFLSTFileManagerTests { +extension OSFILEFileManagerTests { func test_readFile_withStringEncoding_returnsContentSuccessfully() throws { // Given createFileManager() @@ -36,13 +36,13 @@ extension OSFLSTFileManagerTests { } // MARK: - 'getFileURL' tests -extension OSFLSTFileManagerTests { +extension OSFILEFileManagerTests { func test_getFileURL_fromDirectorySearchPath_containingSingleFile_returnsFileSuccessfully() throws { // Given let fileURL: URL = try XCTUnwrap(.init(string: "/file/directory")) let fileManager = createFileManager(urlsWithinDirectory: [fileURL]) let filePath = "/test/directory" - let searchPathDirectory = OSFLSTSearchPathDirectory.cache + let searchPathDirectory = OSFILESearchPathDirectory.cache // When let returnedURL = try sut.getFileURL(atPath: filePath, withSearchPath: .directory(searchPath: searchPathDirectory)) @@ -58,7 +58,7 @@ extension OSFLSTFileManagerTests { let ignoredFileURL: URL = try XCTUnwrap(.init(string: "another_file/directory")) let fileManager = createFileManager(urlsWithinDirectory: [fileURL, ignoredFileURL]) let filePath = "/test/directory" - let searchPathDirectory = OSFLSTSearchPathDirectory.cache + let searchPathDirectory = OSFILESearchPathDirectory.cache // When let returnedURL = try sut.getFileURL(atPath: filePath, withSearchPath: .directory(searchPath: searchPathDirectory)) @@ -72,12 +72,12 @@ extension OSFLSTFileManagerTests { // Given createFileManager() let filePath = "/test/directory" - let searchPathDirectory = OSFLSTSearchPathDirectory.cache + let searchPathDirectory = OSFILESearchPathDirectory.cache // When XCTAssertThrowsError(try sut.getFileURL(atPath: filePath, withSearchPath: .directory(searchPath: searchPathDirectory))) { // Then - XCTAssertEqual($0 as? OSFLSTFileManagerError, .directoryNotFound) + XCTAssertEqual($0 as? OSFILEFileManagerError, .directoryNotFound) } } @@ -86,7 +86,7 @@ extension OSFLSTFileManagerTests { let fileURL: URL = try XCTUnwrap(.init(string: "/file/directory")) let fileManager = createFileManager(urlsWithinDirectory: [fileURL]) let emptyFilePath = "" - let searchPathDirectory = OSFLSTSearchPathDirectory.cache + let searchPathDirectory = OSFILESearchPathDirectory.cache // When let returnedURL = try sut.getFileURL(atPath: emptyFilePath, withSearchPath: .directory(searchPath: searchPathDirectory)) @@ -116,13 +116,13 @@ extension OSFLSTFileManagerTests { // When XCTAssertThrowsError(try sut.getFileURL(atPath: emptyFilePath, withSearchPath: .raw)) { // Then - XCTAssertEqual($0 as? OSFLSTFileManagerError, .cantCreateURL) + XCTAssertEqual($0 as? OSFILEFileManagerError, .cantCreateURL) } } } // MARK: - 'deleteFile' tests -extension OSFLSTFileManagerTests { +extension OSFILEFileManagerTests { func test_deleteFile_shouldBeSuccessful() throws { // Given let fileManager = createFileManager() @@ -143,7 +143,7 @@ extension OSFLSTFileManagerTests { // When XCTAssertThrowsError(try sut.deleteFile(atPath: filePath)) { // Then - XCTAssertEqual($0 as? OSFLSTFileManagerError, .fileNotFound) + XCTAssertEqual($0 as? OSFILEFileManagerError, .fileNotFound) } } @@ -162,14 +162,14 @@ extension OSFLSTFileManagerTests { } // MARK: - 'saveFile' tests -extension OSFLSTFileManagerTests { +extension OSFILEFileManagerTests { func test_saveFile_withStringEncoding_savesFileSuccessfullyAndReturnsItsURL() throws { // Given let fileManager = createFileManager() let fileURL = try XCTUnwrap(fetchConfigurationFile()) .deletingLastPathComponent() .appending(path: "\(Configuration.newFileName).\(Configuration.fileExtension)") - let stringEncoding = OSFLSTStringEncoding.ascii + let stringEncoding = OSFILEStringEncoding.ascii let contentToSave = Configuration.stringEncodedFileContent let shouldIncludeIntermediateDirectories = false @@ -228,7 +228,7 @@ extension OSFLSTFileManagerTests { .deletingLastPathComponent() let fileURL = parentFolderURL .appending(path: "\(Configuration.newFileName).\(Configuration.fileExtension)") - let stringEncoding = OSFLSTStringEncoding.ascii + let stringEncoding = OSFILEStringEncoding.ascii let contentToSave = Configuration.stringEncodedFileContent let shouldIncludeIntermediateDirectories = true @@ -260,7 +260,7 @@ extension OSFLSTFileManagerTests { .deletingLastPathComponent() let fileURL = parentFolderURL .appending(path: "\(Configuration.newFileName).\(Configuration.fileExtension)") - let stringEncoding = OSFLSTStringEncoding.ascii + let stringEncoding = OSFILEStringEncoding.ascii let contentToSave = Configuration.stringEncodedFileContent let shouldIncludeIntermediateDirectories = false @@ -271,18 +271,18 @@ extension OSFLSTFileManagerTests { includeIntermediateDirectories: shouldIncludeIntermediateDirectories) ) { // Then - XCTAssertEqual($0 as? OSFLSTFileManagerError, .missingParentFolder) + XCTAssertEqual($0 as? OSFILEFileManagerError, .missingParentFolder) } } } // MARK: - 'appendData' tests -extension OSFLSTFileManagerTests { +extension OSFILEFileManagerTests { func test_appendData_withStringEncoding_savesFileSuccessfully() throws { // Given createFileManager() let fileURL = try XCTUnwrap(fetchConfigurationFile()) - let stringEncoding = OSFLSTStringEncoding.ascii + let stringEncoding = OSFILEStringEncoding.ascii let contentToAdd = Configuration.fileExtendedContent // When @@ -341,7 +341,7 @@ extension OSFLSTFileManagerTests { .deletingLastPathComponent() let fileURL = parentFolderURL .appending(path: "\(Configuration.newFileName).\(Configuration.fileExtension)") - let stringEncoding = OSFLSTStringEncoding.ascii + let stringEncoding = OSFILEStringEncoding.ascii let contentToAdd = Configuration.stringEncodedFileContent let shouldIncludeIntermediateDirectories = true @@ -370,7 +370,7 @@ extension OSFLSTFileManagerTests { // Given createFileManager() let fileURL = try XCTUnwrap(fetchConfigurationFile()) - let stringEncoding = OSFLSTStringEncoding.ascii + let stringEncoding = OSFILEStringEncoding.ascii let contentToAdd = Configuration.emojiContent // ASCII can't represent emoji so the conversion will fail. // When @@ -380,13 +380,13 @@ extension OSFLSTFileManagerTests { includeIntermediateDirectories: false) ) { // Then - XCTAssertEqual($0 as? OSFLSTFileManagerError, .cantDecodeData) + XCTAssertEqual($0 as? OSFILEFileManagerError, .cantDecodeData) } } } // MARK: - 'getItemAttributes' tests -extension OSFLSTFileManagerTests { +extension OSFILEFileManagerTests { func test_getItemAttributes_forFile_returnsFileAttributeModelSuccessfully() throws { // Given let currentDate = Date() @@ -482,7 +482,7 @@ extension OSFLSTFileManagerTests { } // MARK: - 'renameItem' tests -extension OSFLSTFileManagerTests { +extension OSFILEFileManagerTests { func test_renameItem_shouldBeSuccessful() throws { // Given let fileManager = createFileManager(fileExists: false) @@ -542,7 +542,7 @@ extension OSFLSTFileManagerTests { } // MARK: - 'copyItem' tests -extension OSFLSTFileManagerTests { +extension OSFILEFileManagerTests { func test_copyItem_shouldBeSuccessful() throws { // Given let fileManager = createFileManager(fileExists: false) @@ -601,7 +601,7 @@ extension OSFLSTFileManagerTests { } } -private extension OSFLSTFileManagerTests { +private extension OSFILEFileManagerTests { struct Configuration { static let fileName = "file" static let newFileName = "new_file" @@ -643,12 +643,12 @@ private extension OSFLSTFileManagerTests { fileAttributes: fileAttributes, shouldBeDirectory: shouldBeDirectory ) - sut = OSFLSTManager(fileManager: fileManager) + sut = OSFILEManager(fileManager: fileManager) return fileManager } - func fetchContent(forFile file: (name: String, extension: String), withEncoding encoding: OSFLSTEncoding) throws -> String { + func fetchContent(forFile file: (name: String, extension: String), withEncoding encoding: OSFILEEncoding) throws -> String { let fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: file.name, withExtension: file.extension)) let fileURLContent = try sut.readFile(atPath: fileURL.path(), withEncoding: encoding) From db3207c817d85040ba7bd94b51982677d2b70d77 Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Thu, 16 Jan 2025 12:48:35 +0000 Subject: [PATCH 14/31] chore: add build folder to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 52fe2f7..5268abf 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,5 @@ fastlane/report.xml fastlane/Preview.html fastlane/screenshots/**/*.png fastlane/test_output + +build/ \ No newline at end of file From 3e1cab0ff02a55b5f718693bf5de11ec94f633f9 Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Fri, 17 Jan 2025 14:54:03 +0000 Subject: [PATCH 15/31] chore: use URL instead of Path This makes the whole library simpler to use. Adapt tests accordingly. Remove redundant code. Add two missing tests, one for rename and another for copy, where the FileManager fails its operations. --- OSFilesystemLib/OSFILEManager+Protocols.swift | 18 +- OSFilesystemLib/OSFILEManager.swift | 63 ++++--- OSFilesystemLib/URL+Extension.swift | 20 --- OSFilesystemLibTests/MockFileManager.swift | 46 ++--- .../OSFILEDirectoryManagerTests.swift | 38 ++--- .../OSFILEFileManagerTests.swift | 158 +++++++++++------- 6 files changed, 180 insertions(+), 163 deletions(-) diff --git a/OSFilesystemLib/OSFILEManager+Protocols.swift b/OSFilesystemLib/OSFILEManager+Protocols.swift index 65270ad..14fa159 100644 --- a/OSFilesystemLib/OSFILEManager+Protocols.swift +++ b/OSFilesystemLib/OSFILEManager+Protocols.swift @@ -1,18 +1,18 @@ import Foundation public protocol OSFILEDirectoryManager { - func createDirectory(atPath: String, includeIntermediateDirectories: Bool) throws - func removeDirectory(atPath: String, includeIntermediateDirectories: Bool) throws - func listDirectory(atPath: String) throws -> [URL] + func createDirectory(atURL: URL, includeIntermediateDirectories: Bool) throws + func removeDirectory(atURL: URL, includeIntermediateDirectories: Bool) throws + func listDirectory(atURL: URL) throws -> [URL] } public protocol OSFILEFileManager { - func readFile(atPath: String, withEncoding: OSFILEEncoding) throws -> String + func readFile(atURL: URL, withEncoding: OSFILEEncoding) throws -> String func getFileURL(atPath: String, withSearchPath: OSFILESearchPath) throws -> URL - func deleteFile(atPath: String) throws - func saveFile(atPath: String, withEncodingAndData: OSFILEEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL - func appendData(_ data: OSFILEEncodingValueMapper, atPath: String, includeIntermediateDirectories: Bool) throws + func deleteFile(atURL: URL) throws + func saveFile(atURL: URL, withEncodingAndData: OSFILEEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL + func appendData(_ data: OSFILEEncodingValueMapper, atURL: URL, includeIntermediateDirectories: Bool) throws func getItemAttributes(atPath: String) throws -> OSFILEItemAttributeModel - func renameItem(fromPath: String, toPath: String) throws - func copyItem(fromPath: String, toPath: String) throws + func renameItem(fromURL: URL, toURL: URL) throws + func copyItem(fromURL: URL, toURL: URL) throws } diff --git a/OSFilesystemLib/OSFILEManager.swift b/OSFilesystemLib/OSFILEManager.swift index efde728..f3f0ffd 100644 --- a/OSFilesystemLib/OSFILEManager.swift +++ b/OSFilesystemLib/OSFILEManager.swift @@ -3,21 +3,19 @@ import Foundation public struct OSFILEManager { private let fileManager: FileManager - public init(fileManager: FileManager) { + public init(fileManager: FileManager = .default) { self.fileManager = fileManager } } extension OSFILEManager: OSFILEDirectoryManager { - public func createDirectory(atPath path: String, includeIntermediateDirectories: Bool) throws { - let pathURL = URL.create(with: path) + public func createDirectory(atURL pathURL: URL, includeIntermediateDirectories: Bool) throws { try fileManager.createDirectory(at: pathURL, withIntermediateDirectories: includeIntermediateDirectories) } - public func removeDirectory(atPath path: String, includeIntermediateDirectories: Bool) throws { - let pathURL = URL.create(with: path) + public func removeDirectory(atURL pathURL: URL, includeIntermediateDirectories: Bool) throws { if !includeIntermediateDirectories { - let directoryContents = try listDirectory(atPath: path) + let directoryContents = try listDirectory(atURL: pathURL) if !directoryContents.isEmpty { throw OSFILEDirectoryManagerError.notEmpty } @@ -26,16 +24,13 @@ extension OSFILEManager: OSFILEDirectoryManager { try fileManager.removeItem(at: pathURL) } - public func listDirectory(atPath path: String) throws -> [URL] { - let pathURL = URL.create(with: path) + public func listDirectory(atURL pathURL: URL) throws -> [URL] { return try fileManager.contentsOfDirectory(at: pathURL, includingPropertiesForKeys: nil) } } extension OSFILEManager: OSFILEFileManager { - public func readFile(atPath path: String, withEncoding encoding: OSFILEEncoding) throws -> String { - let fileURL = URL.create(with: path) - + public func readFile(atURL fileURL: URL, withEncoding encoding: OSFILEEncoding) throws -> String { // Check if the URL requires security-scoped access let requiresSecurityScope = fileURL.startAccessingSecurityScopedResource() @@ -64,21 +59,21 @@ extension OSFILEManager: OSFILEFileManager { } } - public func deleteFile(atPath path: String) throws { - guard fileManager.fileExists(atPath: path) else { + public func deleteFile(atURL url: URL) throws { + guard fileManager.fileExists(atPath: url.urlPath) else { throw OSFILEFileManagerError.fileNotFound } - try fileManager.removeItem(atPath: path) + try fileManager.removeItem(at: url) } - @discardableResult public func saveFile(atPath path: String, withEncodingAndData encodingMapper: OSFILEEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL { - let fileURL = URL.create(with: path) + @discardableResult + public func saveFile(atURL fileURL: URL, withEncodingAndData encodingMapper: OSFILEEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL { let fileDirectoryURL = fileURL.deletingLastPathComponent() if !fileManager.fileExists(atPath: fileDirectoryURL.urlPath) { if includeIntermediateDirectories { - try createDirectory(atPath: fileDirectoryURL.urlPath, includeIntermediateDirectories: true) + try createDirectory(atURL: fileDirectoryURL, includeIntermediateDirectories: true) } else { throw OSFILEFileManagerError.missingParentFolder } @@ -94,9 +89,9 @@ extension OSFILEManager: OSFILEFileManager { return fileURL } - public func appendData(_ encodingMapper: OSFILEEncodingValueMapper, atPath path: String, includeIntermediateDirectories: Bool) throws { - guard fileManager.fileExists(atPath: path) else { - try saveFile(atPath: path, withEncodingAndData: encodingMapper, includeIntermediateDirectories: includeIntermediateDirectories) + public func appendData(_ encodingMapper: OSFILEEncodingValueMapper, atURL url: URL, includeIntermediateDirectories: Bool) throws { + guard fileManager.fileExists(atPath: url.urlPath) else { + try saveFile(atURL: url, withEncodingAndData: encodingMapper, includeIntermediateDirectories: includeIntermediateDirectories) return } @@ -111,10 +106,10 @@ extension OSFILEManager: OSFILEFileManager { dataToAppend = valueData } - let fileHandle = FileHandle(forWritingAtPath: path) - try fileHandle?.seekToEnd() - try fileHandle?.write(contentsOf: dataToAppend) - try fileHandle?.close() + let fileHandle = try FileHandle(forWritingTo: url) + try fileHandle.seekToEnd() + try fileHandle.write(contentsOf: dataToAppend) + try fileHandle.close() } public func getItemAttributes(atPath path: String) throws -> OSFILEItemAttributeModel { @@ -122,15 +117,15 @@ extension OSFILEManager: OSFILEFileManager { return .create(from: attributesDictionary) } - public func renameItem(fromPath origin: String, toPath destination: String) throws { - try copy(fromPath: origin, toPath: destination) { - try fileManager.moveItem(atPath: origin, toPath: destination) + public func renameItem(fromURL originURL: URL, toURL destinationURL: URL) throws { + try copy(fromURL: originURL, toURL: destinationURL) { + try fileManager.moveItem(at: originURL, to: destinationURL) } } - public func copyItem(fromPath origin: String, toPath destination: String) throws { - try copy(fromPath: origin, toPath: destination) { - try fileManager.copyItem(atPath: origin, toPath: destination) + public func copyItem(fromURL originURL: URL, toURL destinationURL: URL) throws { + try copy(fromURL: originURL, toURL: destinationURL) { + try fileManager.copyItem(at: originURL, to: destinationURL) } } } @@ -159,15 +154,15 @@ private extension OSFILEManager { return rawURL } - func copy(fromPath origin: String, toPath destination: String, performOperation: () throws -> Void) throws { - guard origin != destination else { + func copy(fromURL originURL: URL, toURL destinationURL: URL, performOperation: () throws -> Void) throws { + guard originURL != destinationURL else { return } var isDirectory: ObjCBool = false - if fileManager.fileExists(atPath: destination, isDirectory: &isDirectory) { + if fileManager.fileExists(atPath: destinationURL.urlPath, isDirectory: &isDirectory) { if !isDirectory.boolValue { - try deleteFile(atPath: destination) + try deleteFile(atURL: destinationURL) } } diff --git a/OSFilesystemLib/URL+Extension.swift b/OSFilesystemLib/URL+Extension.swift index e1a7381..e4f3218 100644 --- a/OSFilesystemLib/URL+Extension.swift +++ b/OSFilesystemLib/URL+Extension.swift @@ -8,24 +8,4 @@ extension URL { path } } - - static func create(with path: String) -> URL { - let url: URL - - if #available(iOS 16.0, *) { - url = .init(filePath: path) - } else { - url = .init(fileURLWithPath: path) - } - - return url - } - - func urlWithAppendingPath(_ path: String) -> URL { - if #available(iOS 16.0, *) { - appending(path: path) - } else { - appendingPathComponent(path) - } - } } diff --git a/OSFilesystemLibTests/MockFileManager.swift b/OSFilesystemLibTests/MockFileManager.swift index 4b1c1a7..40492e8 100644 --- a/OSFilesystemLibTests/MockFileManager.swift +++ b/OSFilesystemLibTests/MockFileManager.swift @@ -8,9 +8,9 @@ class MockFileManager: FileManager { var fileAttributes: [FileAttributeKey: Any] var shouldBeDirectory: ObjCBool - private(set) var capturedPath: String? - private(set) var capturedOriginPath: String? - private(set) var capturedDestinationPath: String? + private(set) var capturedPath: URL? + private(set) var capturedOriginPath: URL? + private(set) var capturedDestinationPath: URL? private(set) var capturedIntermediateDirectories: Bool = false private(set) var capturedSearchPathDirectory: FileManager.SearchPathDirectory? @@ -30,11 +30,13 @@ enum MockFileManagerError: Error { case deleteDirectoryError case deleteFileError case itemAttributesError + case moveFileError + case copyFileError } extension MockFileManager { override func createDirectory(at url: URL, withIntermediateDirectories createIntermediates: Bool, attributes: [FileAttributeKey: Any]? = nil) throws { - capturedPath = url.relativePath + capturedPath = url capturedIntermediateDirectories = createIntermediates if let error, error == .createDirectoryError { @@ -43,7 +45,7 @@ extension MockFileManager { } override func contentsOfDirectory(at url: URL, includingPropertiesForKeys keys: [URLResourceKey]?, options mask: FileManager.DirectoryEnumerationOptions = []) throws -> [URL] { - capturedPath = url.relativePath + capturedPath = url var urls = [URL]() if shouldDirectoryHaveContent { @@ -58,9 +60,9 @@ extension MockFileManager { } override func removeItem(at url: URL) throws { - capturedPath = url.relativePath + capturedPath = url - if let error, error == .deleteDirectoryError { + if let error, [MockFileManagerError.deleteDirectoryError, .deleteFileError].contains(error) { throw error } } @@ -81,16 +83,8 @@ extension MockFileManager { return fileExists } - override func removeItem(atPath path: String) throws { - capturedPath = path - - if let error, error == .deleteFileError { - throw error - } - } - override func attributesOfItem(atPath path: String) throws -> [FileAttributeKey: Any] { - capturedPath = path + capturedPath = URL(filePath: path) if let error, error == .itemAttributesError { throw error @@ -99,13 +93,21 @@ extension MockFileManager { return fileAttributes } - override func moveItem(atPath srcPath: String, toPath dstPath: String) throws { - capturedOriginPath = srcPath - capturedDestinationPath = dstPath + override func moveItem(at srcURL: URL, to dstURL: URL) throws { + capturedOriginPath = srcURL + capturedDestinationPath = dstURL + + if let error, error == .moveFileError { + throw error + } } - override func copyItem(atPath srcPath: String, toPath dstPath: String) throws { - capturedOriginPath = srcPath - capturedDestinationPath = dstPath + override func copyItem(at srcURL: URL, to dstURL: URL) throws { + capturedOriginPath = srcURL + capturedDestinationPath = dstURL + + if let error, error == .copyFileError { + throw error + } } } diff --git a/OSFilesystemLibTests/OSFILEDirectoryManagerTests.swift b/OSFilesystemLibTests/OSFILEDirectoryManagerTests.swift index 7213db0..0049641 100644 --- a/OSFilesystemLibTests/OSFILEDirectoryManagerTests.swift +++ b/OSFilesystemLibTests/OSFILEDirectoryManagerTests.swift @@ -9,11 +9,11 @@ final class OSFILEDirectoryManagerTests: XCTestCase { func test_createDirectory_shouldBeSuccessful() throws { // Given let fileManager = createFileManager() - let testDirectory = "/test/directory" + let testDirectory = URL(filePath: "/test/directory") let shouldIncludeIntermediateDirectories = false // When - try sut.createDirectory(atPath: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) + try sut.createDirectory(atURL: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) // Then XCTAssertEqual(fileManager.capturedPath, testDirectory) @@ -24,12 +24,12 @@ final class OSFILEDirectoryManagerTests: XCTestCase { // Given let error = MockFileManagerError.createDirectoryError createFileManager(with: error) - let testDirectory = "/test/directory" + let testDirectory = URL(filePath: "/test/directory") let shouldIncludeIntermediateDirectories = false // When XCTAssertThrowsError( - try sut.createDirectory(atPath: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) + try sut.createDirectory(atURL: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) ) { // Then XCTAssertEqual($0 as? MockFileManagerError, error) @@ -40,12 +40,12 @@ final class OSFILEDirectoryManagerTests: XCTestCase { func test_removeDirectory_butFails_shouldReturnAnError() { let error = MockFileManagerError.deleteDirectoryError createFileManager(with: error) - let testDirectory = "/test/directory" + let testDirectory = URL(filePath: "/test/directory") let shouldIncludeIntermediateDirectories = true // When XCTAssertThrowsError( - try sut.removeDirectory(atPath: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) + try sut.removeDirectory(atURL: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) ) { // Then XCTAssertEqual($0 as? MockFileManagerError, error) @@ -55,11 +55,11 @@ final class OSFILEDirectoryManagerTests: XCTestCase { func test_removeDirectory_includingIntermediateDirectories_shouldBeSuccessful() throws { // Given let fileManager = createFileManager() - let testDirectory = "/test/directory" + let testDirectory = URL(filePath: "/test/directory") let shouldIncludeIntermediateDirectories = true // When - try sut.removeDirectory(atPath: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) + try sut.removeDirectory(atURL: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) // Then XCTAssertEqual(fileManager.capturedPath, testDirectory) @@ -68,11 +68,11 @@ final class OSFILEDirectoryManagerTests: XCTestCase { func test_removeDirectory_excludingIntermediateDirectories_directoryDoesntHaveContent_shouldBeSuccessful() throws { // Given let fileManager = createFileManager() - let testDirectory = "/test/directory" + let testDirectory = URL(filePath: "/test/directory") let shouldIncludeIntermediateDirectories = false // When - try sut.removeDirectory(atPath: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) + try sut.removeDirectory(atURL: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) // Then XCTAssertEqual(fileManager.capturedPath, testDirectory) @@ -80,12 +80,12 @@ final class OSFILEDirectoryManagerTests: XCTestCase { func test_removeDirectory_excludingIntermediateDirectories_directoryHasContent_shouldReturnAnError() { createFileManager(shouldDirectoryHaveContent: true) - let testDirectory = "/test/directory" + let testDirectory = URL(filePath: "/test/directory") let shouldIncludeIntermediateDirectories = false // When XCTAssertThrowsError( - try sut.removeDirectory(atPath: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) + try sut.removeDirectory(atURL: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) ) { // Then XCTAssertEqual($0 as? OSFILEDirectoryManagerError, .notEmpty) @@ -96,10 +96,10 @@ final class OSFILEDirectoryManagerTests: XCTestCase { func test_listDirectory_withNoContent_shouldReturnEmptyArray() throws { // Given let fileManager = createFileManager() - let testDirectory = "/test/directory" + let testDirectory = URL(filePath: "/test/directory") // When - let directoryContent = try sut.listDirectory(atPath: testDirectory) + let directoryContent = try sut.listDirectory(atURL: testDirectory) // Then XCTAssertEqual(fileManager.capturedPath, testDirectory) @@ -110,25 +110,25 @@ final class OSFILEDirectoryManagerTests: XCTestCase { func test_listDirectory_withContent_shouldReturnEmptyArray() throws { // Given let fileManager = createFileManager(shouldDirectoryHaveContent: true) - let testDirectory = "/test/directory" + let testDirectory = URL(filePath: "/test/directory") // When - let directoryContent = try sut.listDirectory(atPath: testDirectory) + let directoryContent = try sut.listDirectory(atURL: testDirectory) // Then XCTAssertEqual(fileManager.capturedPath, testDirectory) - XCTAssertEqual(directoryContent.map { $0.relativePath }, [testDirectory]) + XCTAssertEqual(directoryContent, [testDirectory]) } func test_listDirectory_butFails_shouldReturnAnError() { // Given let error = MockFileManagerError.readDirectoryError createFileManager(with: error) - let testDirectory = "/test/directory" + let testDirectory = URL(filePath: "/test/directory") // When XCTAssertThrowsError( - try sut.listDirectory(atPath: testDirectory) + try sut.listDirectory(atURL: testDirectory) ) { // Then XCTAssertEqual($0 as? MockFileManagerError, error) diff --git a/OSFilesystemLibTests/OSFILEFileManagerTests.swift b/OSFilesystemLibTests/OSFILEFileManagerTests.swift index 312a9c8..9991ae5 100644 --- a/OSFilesystemLibTests/OSFILEFileManagerTests.swift +++ b/OSFilesystemLibTests/OSFILEFileManagerTests.swift @@ -49,7 +49,7 @@ extension OSFILEFileManagerTests { // Then XCTAssertEqual(fileManager.capturedSearchPathDirectory, searchPathDirectory.fileManagerSearchPathDirectory) - XCTAssertEqual(fileURL.urlWithAppendingPath(filePath), returnedURL) + XCTAssertEqual(fileURL.appending(path: filePath), returnedURL) } func test_getFileURL_fromDirectorySearchPath_containingMultipleFiles_returnsFirstFileSuccessfully() throws { @@ -65,7 +65,7 @@ extension OSFILEFileManagerTests { // Then XCTAssertEqual(fileManager.capturedSearchPathDirectory, searchPathDirectory.fileManagerSearchPathDirectory) - XCTAssertEqual(fileURL.urlWithAppendingPath(filePath), returnedURL) + XCTAssertEqual(fileURL.appending(path: filePath), returnedURL) } func test_getFileURL_fromDirectorySearchPath_containingNoFiles_returnsError() { @@ -126,10 +126,10 @@ extension OSFILEFileManagerTests { func test_deleteFile_shouldBeSuccessful() throws { // Given let fileManager = createFileManager() - let filePath = "/test/directory" + let filePath = URL(filePath: "/test/directory") // When - try sut.deleteFile(atPath: filePath) + try sut.deleteFile(atURL: filePath) // Then XCTAssertEqual(fileManager.capturedPath, filePath) @@ -138,10 +138,10 @@ extension OSFILEFileManagerTests { func test_deleteFile_thatDoesntExist_shouldReturnError() { // Given createFileManager(fileExists: false) - let filePath = "/test/directory" + let filePath = URL(filePath: "/test/directory") // When - XCTAssertThrowsError(try sut.deleteFile(atPath: filePath)) { + XCTAssertThrowsError(try sut.deleteFile(atURL: filePath)) { // Then XCTAssertEqual($0 as? OSFILEFileManagerError, .fileNotFound) } @@ -151,10 +151,10 @@ extension OSFILEFileManagerTests { // Given let error = MockFileManagerError.deleteFileError createFileManager(error: error) - let filePath = "/test/directory" + let filePath = URL(filePath: "/test/directory") // When - XCTAssertThrowsError(try sut.deleteFile(atPath: filePath)) { + XCTAssertThrowsError(try sut.deleteFile(atURL: filePath)) { // Then XCTAssertEqual($0 as? MockFileManagerError, error) } @@ -175,7 +175,7 @@ extension OSFILEFileManagerTests { // When let savedFileURL = try sut.saveFile( - atPath: fileURL.path(), + atURL: fileURL, withEncodingAndData: .string(encoding: stringEncoding, value: contentToSave), includeIntermediateDirectories: shouldIncludeIntermediateDirectories ) @@ -189,7 +189,7 @@ extension OSFILEFileManagerTests { ) XCTAssertEqual(savedFileContent, contentToSave) - try sut.deleteFile(atPath: fileURL.absoluteString) // keep things clean by deleting created file + try sut.deleteFile(atURL: fileURL) // keep things clean by deleting created file } func test_saveFile_withByteBufferEncoding_savesFileSuccessfullyAndReturnsItsURL() throws { @@ -204,7 +204,7 @@ extension OSFILEFileManagerTests { // When let savedFileURL = try sut.saveFile( - atPath: fileURL.path(), + atURL: fileURL, withEncodingAndData: .byteBuffer(value: contentToSaveData), includeIntermediateDirectories: shouldIncludeIntermediateDirectories ) @@ -218,7 +218,7 @@ extension OSFILEFileManagerTests { ) XCTAssertEqual(savedFileContent, contentToSave) - try sut.deleteFile(atPath: fileURL.absoluteString) // keep things clean by deleting created file + try sut.deleteFile(atURL: fileURL) // keep things clean by deleting created file } func test_saveFile_parentFolderMissing_shouldCreateIt_savesFileSuccessfullyAndReturnsItsURL() throws { @@ -234,14 +234,14 @@ extension OSFILEFileManagerTests { // When let savedFileURL = try sut.saveFile( - atPath: fileURL.path(), + atURL: fileURL, withEncodingAndData: .string(encoding: stringEncoding, value: contentToSave), includeIntermediateDirectories: shouldIncludeIntermediateDirectories ) // Then XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories) - XCTAssertEqual(fileManager.capturedPath, parentFolderURL.relativePath) + XCTAssertEqual(fileManager.capturedPath, parentFolderURL) XCTAssertEqual(savedFileURL, fileURL) let savedFileContent = try fetchContent( @@ -250,7 +250,7 @@ extension OSFILEFileManagerTests { XCTAssertEqual(savedFileContent, contentToSave) fileManager.fileExists = true - try sut.deleteFile(atPath: fileURL.absoluteString) // keep things clean by deleting created file + try sut.deleteFile(atURL: fileURL) // keep things clean by deleting created file } func test_saveFile_parentFolderMissing_shouldntCreateIt_returnsError() throws { @@ -266,7 +266,7 @@ extension OSFILEFileManagerTests { // When XCTAssertThrowsError(try sut.saveFile( - atPath: fileURL.path(), + atURL: fileURL, withEncodingAndData: .string(encoding: stringEncoding, value: contentToSave), includeIntermediateDirectories: shouldIncludeIntermediateDirectories) ) { @@ -288,7 +288,7 @@ extension OSFILEFileManagerTests { // When try sut.appendData( .string(encoding: stringEncoding, value: contentToAdd), - atPath: fileURL.path(), + atURL: fileURL, includeIntermediateDirectories: false ) @@ -300,7 +300,7 @@ extension OSFILEFileManagerTests { XCTAssertEqual(savedFileContent, Configuration.fileContent + contentToAdd) try sut.saveFile( // keep things clean by resetting file - atPath: fileURL.path(), + atURL: fileURL, withEncodingAndData: .string(encoding: stringEncoding, value: Configuration.fileContent), includeIntermediateDirectories: false ) @@ -316,7 +316,7 @@ extension OSFILEFileManagerTests { // When try sut.appendData( .byteBuffer(value: contentToAddData), - atPath: fileURL.path(), + atURL: fileURL, includeIntermediateDirectories: false ) @@ -328,7 +328,7 @@ extension OSFILEFileManagerTests { XCTAssertEqual(savedFileContent, Configuration.fileContent + contentToAdd) try sut.saveFile( // keep things clean by resetting file - atPath: fileURL.path(), + atURL: fileURL, withEncodingAndData: .string(encoding: .ascii, value: Configuration.fileContent), includeIntermediateDirectories: false ) @@ -348,11 +348,11 @@ extension OSFILEFileManagerTests { // When try sut.appendData( .string(encoding: stringEncoding, value: contentToAdd), - atPath: fileURL.path(), + atURL: fileURL, includeIntermediateDirectories: shouldIncludeIntermediateDirectories ) - XCTAssertEqual(fileManager.capturedPath, parentFolderURL.relativePath) + XCTAssertEqual(fileManager.capturedPath, parentFolderURL) XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories) // Then @@ -363,7 +363,7 @@ extension OSFILEFileManagerTests { XCTAssertEqual(savedFileContent, contentToAdd) fileManager.fileExists = true - try sut.deleteFile(atPath: fileURL.absoluteString) // keep things clean by deleting created file + try sut.deleteFile(atURL: fileURL) // keep things clean by deleting created file } func test_appendData_withStringEncoding_textCantBeDecoded_returnsError() throws { @@ -376,7 +376,7 @@ extension OSFILEFileManagerTests { // When XCTAssertThrowsError(try sut.appendData( .string(encoding: stringEncoding, value: contentToAdd), - atPath: fileURL.path(), + atURL: fileURL, includeIntermediateDirectories: false) ) { // Then @@ -397,10 +397,10 @@ extension OSFILEFileManagerTests { consideringDate: currentDate, andDifference: (createHourDifference, modificationHourDifference), size: fileSize, isDirectoryType: false ) let fileManager = createFileManager(fileAttributes: fileAttributes) - let testDirectory = "/test/directory" + let testDirectory = URL(filePath: "/test/directory") // When - let fileAttributesModel = try sut.getItemAttributes(atPath: testDirectory) + let fileAttributesModel = try sut.getItemAttributes(atPath: testDirectory.path()) // Then XCTAssertEqual(fileManager.capturedPath, testDirectory) @@ -420,10 +420,10 @@ extension OSFILEFileManagerTests { isDirectoryType: false ) let fileManager = createFileManager(fileAttributes: fileAttributes) - let testDirectory = "/test/directory" + let testDirectory = URL(filePath: "/test/directory") // When - let fileAttributesModel = try sut.getItemAttributes(atPath: testDirectory) + let fileAttributesModel = try sut.getItemAttributes(atPath: testDirectory.path()) // Then XCTAssertEqual(fileManager.capturedPath, testDirectory) @@ -443,10 +443,10 @@ extension OSFILEFileManagerTests { consideringDate: currentDate, andDifference: (createHourDifference, modificationHourDifference), size: fileSize, isDirectoryType: true ) let fileManager = createFileManager(fileAttributes: fileAttributes) - let testDirectory = "/test/directory" + let testDirectory = URL(filePath: "/test/directory") // When - let fileAttributesModel = try sut.getItemAttributes(atPath: testDirectory) + let fileAttributesModel = try sut.getItemAttributes(atPath: testDirectory.path()) // Then XCTAssertEqual(fileManager.capturedPath, testDirectory) @@ -471,10 +471,10 @@ extension OSFILEFileManagerTests { consideringDate: currentDate, andDifference: (createHourDifference, modificationHourDifference), size: fileSize, isDirectoryType: false ) createFileManager(error: error, fileAttributes: fileAttributes) - let testDirectory = "/test/directory" + let testDirectory = URL(filePath: "/test/directory") // When - XCTAssertThrowsError(try sut.getItemAttributes(atPath: testDirectory)) { + XCTAssertThrowsError(try sut.getItemAttributes(atPath: testDirectory.path())) { // Then XCTAssertEqual($0 as? MockFileManagerError, error) } @@ -486,11 +486,11 @@ extension OSFILEFileManagerTests { func test_renameItem_shouldBeSuccessful() throws { // Given let fileManager = createFileManager(fileExists: false) - let originPath = "/test/origin" - let destinationPath = "/test/destination" + let originPath = URL(filePath: "/test/origin") + let destinationPath = URL(filePath: "/test/destination") // When - try sut.renameItem(fromPath: originPath, toPath: destinationPath) + try sut.renameItem(fromURL: originPath, toURL: destinationPath) // Then XCTAssertEqual(fileManager.capturedOriginPath, originPath) @@ -500,11 +500,11 @@ extension OSFILEFileManagerTests { func test_renameItem_sameOriginAndDestination_shouldDoNothing() throws { // Given let fileManager = createFileManager(fileExists: false) - let originPath = "/test/origin" - let destinationPath = "/test/origin" + let originPath = URL(filePath: "/test/origin") + let destinationPath = URL(filePath: "/test/origin") // When - try sut.renameItem(fromPath: originPath, toPath: destinationPath) + try sut.renameItem(fromURL: originPath, toURL: destinationPath) // Then XCTAssertNil(fileManager.capturedOriginPath) @@ -514,11 +514,11 @@ extension OSFILEFileManagerTests { func test_renameDirectory_alreadyExisting_shouldBeSuccessful() throws { // Given let fileManager = createFileManager() - let originPath = "/test/origin" - let destinationPath = "/test/destination" + let originPath = URL(filePath: "/test/origin") + let destinationPath = URL(filePath: "/test/destination") // When - try sut.renameItem(fromPath: originPath, toPath: destinationPath) + try sut.renameItem(fromURL: originPath, toURL: destinationPath) // Then XCTAssertEqual(fileManager.capturedOriginPath, originPath) @@ -528,17 +528,31 @@ extension OSFILEFileManagerTests { func test_renameFile_alreadyExisting_shouldBeSuccessful() throws { // Given let fileManager = createFileManager(shouldBeDirectory: false) - let originPath = "/test/origin" - let destinationPath = "/test/destination" + let originPath = URL(filePath: "/test/origin") + let destinationPath = URL(filePath: "/test/destination") // When - try sut.renameItem(fromPath: originPath, toPath: destinationPath) + try sut.renameItem(fromURL: originPath, toURL: destinationPath) // Then XCTAssertEqual(fileManager.capturedPath, destinationPath) XCTAssertEqual(fileManager.capturedOriginPath, originPath) XCTAssertEqual(fileManager.capturedDestinationPath, destinationPath) } + + func test_renameFile_copyFails_returnsError() throws { + // Given + let error = MockFileManagerError.moveFileError + createFileManager(error: error) + let originPath = URL(filePath: "/test/origin") + let destinationPath = URL(filePath: "/test/destination") + + // When + XCTAssertThrowsError(try sut.renameItem(fromURL: originPath, toURL: destinationPath)) { + // Then + XCTAssertEqual($0 as? MockFileManagerError, error) + } + } } // MARK: - 'copyItem' tests @@ -546,11 +560,11 @@ extension OSFILEFileManagerTests { func test_copyItem_shouldBeSuccessful() throws { // Given let fileManager = createFileManager(fileExists: false) - let originPath = "/test/origin" - let destinationPath = "/test/destination" + let originPath = URL(filePath: "/test/origin") + let destinationPath = URL(filePath: "/test/destination") // When - try sut.copyItem(fromPath: originPath, toPath: destinationPath) + try sut.copyItem(fromURL: originPath, toURL: destinationPath) // Then XCTAssertEqual(fileManager.capturedOriginPath, originPath) @@ -560,11 +574,11 @@ extension OSFILEFileManagerTests { func test_copyItem_sameOriginAndDestination_shouldDoNothing() throws { // Given let fileManager = createFileManager(fileExists: false) - let originPath = "/test/origin" - let destinationPath = "/test/origin" + let originPath = URL(filePath: "/test/origin") + let destinationPath = URL(filePath: "/test/origin") // When - try sut.copyItem(fromPath: originPath, toPath: destinationPath) + try sut.copyItem(fromURL: originPath, toURL: destinationPath) // Then XCTAssertNil(fileManager.capturedOriginPath) @@ -574,11 +588,11 @@ extension OSFILEFileManagerTests { func test_copyDirectory_alreadyExisting_shouldBeSuccessful() throws { // Given let fileManager = createFileManager() - let originPath = "/test/origin" - let destinationPath = "/test/destination" + let originPath = URL(filePath: "/test/origin") + let destinationPath = URL(filePath: "/test/destination") // When - try sut.copyItem(fromPath: originPath, toPath: destinationPath) + try sut.copyItem(fromURL: originPath, toURL: destinationPath) // Then XCTAssertEqual(fileManager.capturedOriginPath, originPath) @@ -588,17 +602,31 @@ extension OSFILEFileManagerTests { func test_copyFile_alreadyExisting_shouldBeSuccessful() throws { // Given let fileManager = createFileManager(shouldBeDirectory: false) - let originPath = "/test/origin" - let destinationPath = "/test/destination" + let originPath = URL(filePath: "/test/origin") + let destinationPath = URL(filePath: "/test/destination") // When - try sut.copyItem(fromPath: originPath, toPath: destinationPath) + try sut.copyItem(fromURL: originPath, toURL: destinationPath) // Then XCTAssertEqual(fileManager.capturedPath, destinationPath) XCTAssertEqual(fileManager.capturedOriginPath, originPath) XCTAssertEqual(fileManager.capturedDestinationPath, destinationPath) } + + func test_copyFile_copyFails_returnsError() throws { + // Given + let error = MockFileManagerError.copyFileError + createFileManager(error: error) + let originPath = URL(filePath: "/test/origin") + let destinationPath = URL(filePath: "/test/destination") + + // When + XCTAssertThrowsError(try sut.copyItem(fromURL: originPath, toURL: destinationPath)) { + // Then + XCTAssertEqual($0 as? MockFileManagerError, error) + } + } } private extension OSFILEFileManagerTests { @@ -612,7 +640,12 @@ private extension OSFILEFileManagerTests { static let fileExtendedContent = " How are you?" static let emojiContent = "🙃" - static func fileAttributes(consideringDate date: Date? = nil, andDifference dateDifference: (creation: Int, modification: Int)? = nil, size: UInt64? = nil, isDirectoryType: Bool) -> [FileAttributeKey: Any] { + static func fileAttributes( + consideringDate date: Date? = nil, + andDifference dateDifference: (creation: Int, modification: Int)? = nil, + size: UInt64? = nil, + isDirectoryType: Bool + ) -> [FileAttributeKey: Any] { var result: [FileAttributeKey: Any] = [.type: isDirectoryType ? FileAttributeKey.FileTypeDirectoryValue : Configuration.fileName] if let date { @@ -635,7 +668,14 @@ private extension OSFILEFileManagerTests { } } - @discardableResult func createFileManager(error: MockFileManagerError? = nil, urlsWithinDirectory: [URL] = [], fileExists: Bool = true, fileAttributes: [FileAttributeKey: Any] = [:], shouldBeDirectory: ObjCBool = true) -> MockFileManager { + @discardableResult + func createFileManager( + error: MockFileManagerError? = nil, + urlsWithinDirectory: [URL] = [], + fileExists: Bool = true, + fileAttributes: [FileAttributeKey: Any] = [:], + shouldBeDirectory: ObjCBool = true + ) -> MockFileManager { let fileManager = MockFileManager( error: error, urlsWithinDirectory: urlsWithinDirectory, @@ -650,7 +690,7 @@ private extension OSFILEFileManagerTests { func fetchContent(forFile file: (name: String, extension: String), withEncoding encoding: OSFILEEncoding) throws -> String { let fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: file.name, withExtension: file.extension)) - let fileURLContent = try sut.readFile(atPath: fileURL.path(), withEncoding: encoding) + let fileURLContent = try sut.readFile(atURL: fileURL, withEncoding: encoding) var fileURLUnicodeScalars: String.UnicodeScalarView if case .byteBuffer = encoding { From 33876cea5dc8cd710ca54de61719fd3dfaa87b8d Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Mon, 20 Jan 2025 15:38:48 +0000 Subject: [PATCH 16/31] feat: use directory types instead of search paths and add 2 new values These new values accommodate values required by the Cordova plugin. By having this, the name "SearchPath" doesn't make sense anymore, which justifies its update to DirectoryType. Add tests for these new types. --- OSFilesystemLib/OSFILEManager+Enums.swift | 14 +-- OSFilesystemLib/OSFILEManager.swift | 44 +++++++-- OSFilesystemLib/URL+Extension.swift | 8 ++ OSFilesystemLibTests/MockFileManager.swift | 8 +- .../OSFILEFileManagerTests.swift | 89 ++++++++++++++++--- 5 files changed, 134 insertions(+), 29 deletions(-) diff --git a/OSFilesystemLib/OSFILEManager+Enums.swift b/OSFilesystemLib/OSFILEManager+Enums.swift index 7890314..6a79a11 100644 --- a/OSFilesystemLib/OSFILEManager+Enums.swift +++ b/OSFilesystemLib/OSFILEManager+Enums.swift @@ -25,20 +25,14 @@ public enum OSFILEStringEncoding { } public enum OSFILESearchPath { - case directory(searchPath: OSFILESearchPathDirectory) + case directory(type: OSFILEDirectoryType) case raw } -public enum OSFILESearchPathDirectory { +public enum OSFILEDirectoryType { case cache case document case library - - var fileManagerSearchPathDirectory: FileManager.SearchPathDirectory { - switch self { - case .cache: .cachesDirectory - case .document: .documentDirectory - case .library: .libraryDirectory - } - } + case notSyncedLibrary + case temporary } diff --git a/OSFilesystemLib/OSFILEManager.swift b/OSFilesystemLib/OSFILEManager.swift index f3f0ffd..451cb32 100644 --- a/OSFilesystemLib/OSFILEManager.swift +++ b/OSFilesystemLib/OSFILEManager.swift @@ -51,9 +51,9 @@ extension OSFILEManager: OSFILEFileManager { } public func getFileURL(atPath path: String, withSearchPath searchPath: OSFILESearchPath) throws -> URL { - return switch searchPath { - case .directory(let directorySearchPath): - try resolveDirectoryURL(for: directorySearchPath.fileManagerSearchPathDirectory, with: path) + switch searchPath { + case .directory(let type): + try resolveDirectoryURL(forType: type, with: path) case .raw: try resolveRawURL(from: path) } @@ -139,8 +139,8 @@ private extension OSFILEManager { try String(contentsOf: fileURL, encoding: stringEncoding) } - func resolveDirectoryURL(for searchPath: FileManager.SearchPathDirectory, with path: String) throws -> URL { - guard let directoryURL = fileManager.urls(for: searchPath, in: .userDomainMask).first else { + func resolveDirectoryURL(forType directoryType: OSFILEDirectoryType, with path: String) throws -> URL { + guard let directoryURL = directoryType.fetchURL(using: fileManager) else { throw OSFILEFileManagerError.directoryNotFound } @@ -169,3 +169,37 @@ private extension OSFILEManager { try performOperation() } } + +private extension OSFILEDirectoryType { + struct Keys { + static let noCloudPath = "NoCloud" + } + + func fetchURL(using fileManager: FileManager) -> URL? { + switch self { + case .cache: + fetchURL(using: fileManager, forSearchPath: .cachesDirectory) + case .document: + fetchURL(using: fileManager, forSearchPath: .documentDirectory) + case .library: + fetchURL(using: fileManager, forSearchPath: .libraryDirectory) + case .notSyncedLibrary: + fetchNotSyncedLibrary(using: fileManager) + case .temporary: + fileManager.temporaryDirectory + } + } + + private func fetchURL(using fileManager: FileManager, forSearchPath searchPath: FileManager.SearchPathDirectory) -> URL? { + fileManager.urls(for: searchPath, in: .userDomainMask).first + } + + private func fetchNotSyncedLibrary(using fileManager: FileManager) -> URL? { + var url = fileManager.urls(for: .libraryDirectory, in: .userDomainMask).first?.urlWithAppendingPath(Keys.noCloudPath) + var resourceValues = URLResourceValues() + resourceValues.isExcludedFromBackup = true + try? url?.setResourceValues(resourceValues) + + return url + } +} diff --git a/OSFilesystemLib/URL+Extension.swift b/OSFilesystemLib/URL+Extension.swift index e4f3218..f66cc1d 100644 --- a/OSFilesystemLib/URL+Extension.swift +++ b/OSFilesystemLib/URL+Extension.swift @@ -8,4 +8,12 @@ extension URL { path } } + + func urlWithAppendingPath(_ path: String) -> URL { + if #available(iOS 16.0, *) { + appending(path: path) + } else { + appendingPathComponent(path) + } + } } diff --git a/OSFilesystemLibTests/MockFileManager.swift b/OSFilesystemLibTests/MockFileManager.swift index 40492e8..9fb73cb 100644 --- a/OSFilesystemLibTests/MockFileManager.swift +++ b/OSFilesystemLibTests/MockFileManager.swift @@ -7,6 +7,7 @@ class MockFileManager: FileManager { var fileExists: Bool var fileAttributes: [FileAttributeKey: Any] var shouldBeDirectory: ObjCBool + var mockTemporaryDirectory: URL? private(set) var capturedPath: URL? private(set) var capturedOriginPath: URL? @@ -14,13 +15,14 @@ class MockFileManager: FileManager { private(set) var capturedIntermediateDirectories: Bool = false private(set) var capturedSearchPathDirectory: FileManager.SearchPathDirectory? - init(error: MockFileManagerError? = nil, shouldDirectoryHaveContent: Bool = false, urlsWithinDirectory: [URL] = [], fileExists: Bool = true, fileAttributes: [FileAttributeKey: Any] = [:], shouldBeDirectory: ObjCBool = true) { + init(error: MockFileManagerError? = nil, shouldDirectoryHaveContent: Bool = false, urlsWithinDirectory: [URL] = [], fileExists: Bool = true, fileAttributes: [FileAttributeKey: Any] = [:], shouldBeDirectory: ObjCBool = true, mockTemporaryDirectory: URL? = nil) { self.error = error self.shouldDirectoryHaveContent = shouldDirectoryHaveContent self.urlsWithinDirectory = urlsWithinDirectory self.fileExists = fileExists self.fileAttributes = fileAttributes self.shouldBeDirectory = shouldBeDirectory + self.mockTemporaryDirectory = mockTemporaryDirectory } } @@ -110,4 +112,8 @@ extension MockFileManager { throw error } } + + override var temporaryDirectory: URL { + mockTemporaryDirectory ?? .init(filePath: "") + } } diff --git a/OSFilesystemLibTests/OSFILEFileManagerTests.swift b/OSFilesystemLibTests/OSFILEFileManagerTests.swift index 9991ae5..2d652da 100644 --- a/OSFilesystemLibTests/OSFILEFileManagerTests.swift +++ b/OSFilesystemLibTests/OSFILEFileManagerTests.swift @@ -42,13 +42,13 @@ extension OSFILEFileManagerTests { let fileURL: URL = try XCTUnwrap(.init(string: "/file/directory")) let fileManager = createFileManager(urlsWithinDirectory: [fileURL]) let filePath = "/test/directory" - let searchPathDirectory = OSFILESearchPathDirectory.cache + let directoryType = OSFILEDirectoryType.cache // When - let returnedURL = try sut.getFileURL(atPath: filePath, withSearchPath: .directory(searchPath: searchPathDirectory)) + let returnedURL = try sut.getFileURL(atPath: filePath, withSearchPath: .directory(type: directoryType)) // Then - XCTAssertEqual(fileManager.capturedSearchPathDirectory, searchPathDirectory.fileManagerSearchPathDirectory) + XCTAssertEqual(fileManager.capturedSearchPathDirectory, .cachesDirectory) XCTAssertEqual(fileURL.appending(path: filePath), returnedURL) } @@ -58,24 +58,85 @@ extension OSFILEFileManagerTests { let ignoredFileURL: URL = try XCTUnwrap(.init(string: "another_file/directory")) let fileManager = createFileManager(urlsWithinDirectory: [fileURL, ignoredFileURL]) let filePath = "/test/directory" - let searchPathDirectory = OSFILESearchPathDirectory.cache + let directoryType = OSFILEDirectoryType.cache // When - let returnedURL = try sut.getFileURL(atPath: filePath, withSearchPath: .directory(searchPath: searchPathDirectory)) + let returnedURL = try sut.getFileURL(atPath: filePath, withSearchPath: .directory(type: directoryType)) // Then - XCTAssertEqual(fileManager.capturedSearchPathDirectory, searchPathDirectory.fileManagerSearchPathDirectory) + XCTAssertEqual(fileManager.capturedSearchPathDirectory, .cachesDirectory) XCTAssertEqual(fileURL.appending(path: filePath), returnedURL) } + func test_getFileURL_fromDocumentDirectorySearchPath_returnsFileSuccessfully() throws { + // Given + let fileURL: URL = try XCTUnwrap(.init(string: "/file/directory")) + let fileManager = createFileManager(urlsWithinDirectory: [fileURL]) + let filePath = "/test/directory" + let directoryType = OSFILEDirectoryType.document + + // When + let returnedURL = try sut.getFileURL(atPath: filePath, withSearchPath: .directory(type: directoryType)) + + // Then + XCTAssertEqual(fileManager.capturedSearchPathDirectory, .documentDirectory) + XCTAssertEqual(fileURL.appending(path: filePath), returnedURL) + } + + func test_getFileURL_fromLibraryDirectorySearchPath_returnsFileSuccessfully() throws { + // Given + let fileURL: URL = try XCTUnwrap(.init(string: "/file/directory")) + let fileManager = createFileManager(urlsWithinDirectory: [fileURL]) + let filePath = "/test/directory" + let directoryType = OSFILEDirectoryType.library + + // When + let returnedURL = try sut.getFileURL(atPath: filePath, withSearchPath: .directory(type: directoryType)) + + // Then + XCTAssertEqual(fileManager.capturedSearchPathDirectory, .libraryDirectory) + XCTAssertEqual(fileURL.appending(path: filePath), returnedURL) + } + + func test_getFileURL_fromNotSyncedLibraryDirectorySearchPath_returnsFileSuccessfully() throws { + // Given + let fileURL: URL = try XCTUnwrap(.init(string: "/file/directory")) + let fileManager = createFileManager(urlsWithinDirectory: [fileURL]) + let filePath = "/test/directory" + let directoryType = OSFILEDirectoryType.notSyncedLibrary + + // When + let returnedURL = try sut.getFileURL(atPath: filePath, withSearchPath: .directory(type: directoryType)) + + // Then + XCTAssertEqual(fileManager.capturedSearchPathDirectory, .libraryDirectory) + XCTAssertEqual(fileURL.appending(path: "NoCloud").appending(path: filePath), returnedURL) + } + + func test_getFileURL_fromTemporaryDirectorySearchPath_returnsFileSuccessfully() throws { + // Given + let parentFolderURL: URL = try XCTUnwrap(.init(string: "/file")) + let fileURL: URL = parentFolderURL.appending(path: "/directory") + let fileManager = createFileManager(urlsWithinDirectory: [fileURL], mockTemporaryDirectory: parentFolderURL) + let filePath = "/test/directory" + let directoryType = OSFILEDirectoryType.temporary + + // When + let returnedURL = try sut.getFileURL(atPath: filePath, withSearchPath: .directory(type: directoryType)) + + // Then + XCTAssertEqual(fileManager.temporaryDirectory, parentFolderURL) + XCTAssertEqual(parentFolderURL.appending(path: filePath), returnedURL) + } + func test_getFileURL_fromDirectorySearchPath_containingNoFiles_returnsError() { // Given createFileManager() let filePath = "/test/directory" - let searchPathDirectory = OSFILESearchPathDirectory.cache + let directoryType = OSFILEDirectoryType.cache // When - XCTAssertThrowsError(try sut.getFileURL(atPath: filePath, withSearchPath: .directory(searchPath: searchPathDirectory))) { + XCTAssertThrowsError(try sut.getFileURL(atPath: filePath, withSearchPath: .directory(type: directoryType))) { // Then XCTAssertEqual($0 as? OSFILEFileManagerError, .directoryNotFound) } @@ -86,13 +147,13 @@ extension OSFILEFileManagerTests { let fileURL: URL = try XCTUnwrap(.init(string: "/file/directory")) let fileManager = createFileManager(urlsWithinDirectory: [fileURL]) let emptyFilePath = "" - let searchPathDirectory = OSFILESearchPathDirectory.cache + let directoryType = OSFILEDirectoryType.cache // When - let returnedURL = try sut.getFileURL(atPath: emptyFilePath, withSearchPath: .directory(searchPath: searchPathDirectory)) + let returnedURL = try sut.getFileURL(atPath: emptyFilePath, withSearchPath: .directory(type: directoryType)) // Then - XCTAssertEqual(fileManager.capturedSearchPathDirectory, searchPathDirectory.fileManagerSearchPathDirectory) + XCTAssertEqual(fileManager.capturedSearchPathDirectory, .cachesDirectory) XCTAssertEqual(fileURL, returnedURL) } @@ -674,14 +735,16 @@ private extension OSFILEFileManagerTests { urlsWithinDirectory: [URL] = [], fileExists: Bool = true, fileAttributes: [FileAttributeKey: Any] = [:], - shouldBeDirectory: ObjCBool = true + shouldBeDirectory: ObjCBool = true, + mockTemporaryDirectory: URL? = nil ) -> MockFileManager { let fileManager = MockFileManager( error: error, urlsWithinDirectory: urlsWithinDirectory, fileExists: fileExists, fileAttributes: fileAttributes, - shouldBeDirectory: shouldBeDirectory + shouldBeDirectory: shouldBeDirectory, + mockTemporaryDirectory: mockTemporaryDirectory ) sut = OSFILEManager(fileManager: fileManager) From 2187e2c9d43dc325788fed48c87bb4e1555e56ce Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Wed, 22 Jan 2025 12:10:29 +0000 Subject: [PATCH 17/31] chore: remove encoding from path Apply some additional refactoring. --- OSFilesystemLib/OSFILEManager.swift | 4 ++-- OSFilesystemLib/URL+Extension.swift | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/OSFilesystemLib/OSFILEManager.swift b/OSFilesystemLib/OSFILEManager.swift index 451cb32..60d2790 100644 --- a/OSFilesystemLib/OSFILEManager.swift +++ b/OSFilesystemLib/OSFILEManager.swift @@ -25,7 +25,7 @@ extension OSFILEManager: OSFILEDirectoryManager { } public func listDirectory(atURL pathURL: URL) throws -> [URL] { - return try fileManager.contentsOfDirectory(at: pathURL, includingPropertiesForKeys: nil) + try fileManager.contentsOfDirectory(at: pathURL, includingPropertiesForKeys: nil) } } @@ -144,7 +144,7 @@ private extension OSFILEManager { throw OSFILEFileManagerError.directoryNotFound } - return path.isEmpty ? directoryURL : directoryURL.appendingPathComponent(path) + return path.isEmpty ? directoryURL : directoryURL.urlWithAppendingPath(path) } func resolveRawURL(from path: String) throws -> URL { diff --git a/OSFilesystemLib/URL+Extension.swift b/OSFilesystemLib/URL+Extension.swift index f66cc1d..8f07475 100644 --- a/OSFilesystemLib/URL+Extension.swift +++ b/OSFilesystemLib/URL+Extension.swift @@ -1,11 +1,11 @@ import Foundation extension URL { - var urlPath: String { + public var urlPath: String { if #available(iOS 16.0, *) { - path() + path(percentEncoded: false) } else { - path + path.removingPercentEncoding ?? path } } From 26b101ff18fdc33b039be90888bab5ab81aca78d Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Tue, 28 Jan 2025 13:31:47 +0000 Subject: [PATCH 18/31] feat: add read file in chunks rename the old readFile method to readEntireFile for easier separation of concerns. --- OSFilesystemLib.xcodeproj/project.pbxproj | 8 ++ OSFilesystemLib/OSFILEChunkPublisher.swift | 78 ++++++++++++ OSFilesystemLib/OSFILEManager+Errors.swift | 4 + OSFilesystemLib/OSFILEManager+Protocols.swift | 3 +- OSFilesystemLib/OSFILEManager.swift | 40 ++++--- .../OSFILEFileManagerTests.swift | 112 ++++++++++++++++-- OSFilesystemLibTests/file_emojiContent.txt | 1 + 7 files changed, 218 insertions(+), 28 deletions(-) create mode 100644 OSFilesystemLib/OSFILEChunkPublisher.swift create mode 100644 OSFilesystemLibTests/file_emojiContent.txt diff --git a/OSFilesystemLib.xcodeproj/project.pbxproj b/OSFilesystemLib.xcodeproj/project.pbxproj index 0e0f6a8..22f2a92 100644 --- a/OSFilesystemLib.xcodeproj/project.pbxproj +++ b/OSFilesystemLib.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ 751328DA2D318DBA0031BDD0 /* OSFILEDirectoryManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D92D318DBA0031BDD0 /* OSFILEDirectoryManagerTests.swift */; }; 751328DB2D318E770031BDD0 /* MockFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D82D3179430031BDD0 /* MockFileManager.swift */; }; 7575CF6A2BFCEE6F008F3FD0 /* OSFilesystemLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7575CF612BFCEE6F008F3FD0 /* OSFilesystemLib.framework */; }; + 75DA44542D48E435006DF7DE /* OSFILEChunkPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75DA44532D48E435006DF7DE /* OSFILEChunkPublisher.swift */; }; + 75DA445A2D490051006DF7DE /* file_emojiContent.txt in Resources */ = {isa = PBXBuildFile; fileRef = 75DA44592D490051006DF7DE /* file_emojiContent.txt */; }; 75F8380B2D37E42000FCE044 /* OSFILEItemAttributeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F8380A2D37E42000FCE044 /* OSFILEItemAttributeModel.swift */; }; 75F84D662D39360E00892C89 /* OSFILEManager+Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F84D652D39360E00892C89 /* OSFILEManager+Errors.swift */; }; 75F84D682D39362F00892C89 /* OSFILEManager+Enums.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F84D672D39362F00892C89 /* OSFILEManager+Enums.swift */; }; @@ -36,6 +38,8 @@ 751328D92D318DBA0031BDD0 /* OSFILEDirectoryManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFILEDirectoryManagerTests.swift; sourceTree = ""; }; 7575CF612BFCEE6F008F3FD0 /* OSFilesystemLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OSFilesystemLib.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7575CF692BFCEE6F008F3FD0 /* OSFilesystemLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OSFilesystemLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 75DA44532D48E435006DF7DE /* OSFILEChunkPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFILEChunkPublisher.swift; sourceTree = ""; }; + 75DA44592D490051006DF7DE /* file_emojiContent.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = file_emojiContent.txt; sourceTree = ""; }; 75F8380A2D37E42000FCE044 /* OSFILEItemAttributeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFILEItemAttributeModel.swift; sourceTree = ""; }; 75F84D652D39360E00892C89 /* OSFILEManager+Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSFILEManager+Errors.swift"; sourceTree = ""; }; 75F84D672D39362F00892C89 /* OSFILEManager+Enums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSFILEManager+Enums.swift"; sourceTree = ""; }; @@ -89,6 +93,7 @@ 75F84D672D39362F00892C89 /* OSFILEManager+Enums.swift */, 75F84D652D39360E00892C89 /* OSFILEManager+Errors.swift */, 75FEB2AF2D3546D7007C2686 /* OSFILEManager+Protocols.swift */, + 75DA44532D48E435006DF7DE /* OSFILEChunkPublisher.swift */, 75F8380A2D37E42000FCE044 /* OSFILEItemAttributeModel.swift */, 75FEB2B12D35470C007C2686 /* URL+Extension.swift */, ); @@ -98,6 +103,7 @@ 7575CF6D2BFCEE6F008F3FD0 /* OSFilesystemLibTests */ = { isa = PBXGroup; children = ( + 75DA44592D490051006DF7DE /* file_emojiContent.txt */, 751328D82D3179430031BDD0 /* MockFileManager.swift */, 751328D92D318DBA0031BDD0 /* OSFILEDirectoryManagerTests.swift */, 75FEB2B32D35479B007C2686 /* OSFILEFileManagerTests.swift */, @@ -210,6 +216,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 75DA445A2D490051006DF7DE /* file_emojiContent.txt in Resources */, 75FEB2B72D355F21007C2686 /* file.txt in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -243,6 +250,7 @@ buildActionMask = 2147483647; files = ( 75F8380B2D37E42000FCE044 /* OSFILEItemAttributeModel.swift in Sources */, + 75DA44542D48E435006DF7DE /* OSFILEChunkPublisher.swift in Sources */, 75FEB2B02D3546D7007C2686 /* OSFILEManager+Protocols.swift in Sources */, 75F84D682D39362F00892C89 /* OSFILEManager+Enums.swift in Sources */, 751328D52D3175170031BDD0 /* OSFILEManager.swift in Sources */, diff --git a/OSFilesystemLib/OSFILEChunkPublisher.swift b/OSFilesystemLib/OSFILEChunkPublisher.swift new file mode 100644 index 0000000..f4a9f33 --- /dev/null +++ b/OSFilesystemLib/OSFILEChunkPublisher.swift @@ -0,0 +1,78 @@ +import Combine +import Foundation + +public class OSFILEChunkPublisher: Publisher { + public typealias Output = String + public typealias Failure = Error + + private let url: URL + private let chunkSize: Int + private let encoding: OSFILEEncoding + + init(_ url: URL, _ chunkSize: Int, _ encoding: OSFILEEncoding) { + self.url = url + self.chunkSize = chunkSize + self.encoding = encoding + } + + public func receive(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input { + let subscription = OSFILEChunkSubscription(url, chunkSize, encoding, subscriber) + subscriber.receive(subscription: subscription) + } +} + +private class OSFILEChunkSubscription: Subscription where S.Input == String, S.Failure == Error { + private let fileHandle: FileHandle? + private let chunkSize: Int + private let encoding: OSFILEEncoding + private let subscriber: S + private var isCompleted = false + + init(_ url: URL, _ chunkSize: Int, _ encoding: OSFILEEncoding, _ subscriber: S) { + self.fileHandle = try? FileHandle(forReadingFrom: url) + self.chunkSize = chunkSize + self.encoding = encoding + self.subscriber = subscriber + } + + func request(_ demand: Subscribers.Demand) { + guard let fileHandle = fileHandle, !isCompleted else { return } + + while demand > .none { + do { + if let chunk = try fileHandle.read(upToCount: chunkSize), !chunk.isEmpty { + let chunkToEmit: String + switch encoding { + case .byteBuffer: chunkToEmit = chunk.base64EncodedString() + case .string(let encoding): + guard let chunkText = String(data: chunk, encoding: encoding.stringEncoding) else { + throw OSFILEChunkPublisherError.cantEncodeData + } + chunkToEmit = chunkText + } + + _ = subscriber.receive(chunkToEmit) + } else { + complete(withValue: .finished) + break + } + } catch { + complete(withValue: .failure(error)) + break + } + } + } + + func cancel() { + fileHandle?.closeFile() + } + + deinit { + fileHandle?.closeFile() + } + + private func complete(withValue value: Subscribers.Completion) { + isCompleted = true + subscriber.receive(completion: value) + } +} diff --git a/OSFilesystemLib/OSFILEManager+Errors.swift b/OSFilesystemLib/OSFILEManager+Errors.swift index 7e8e8d5..5f9d897 100644 --- a/OSFilesystemLib/OSFILEManager+Errors.swift +++ b/OSFilesystemLib/OSFILEManager+Errors.swift @@ -9,3 +9,7 @@ enum OSFILEFileManagerError: Error { case fileNotFound case missingParentFolder } + +enum OSFILEChunkPublisherError: Error { + case cantEncodeData +} diff --git a/OSFilesystemLib/OSFILEManager+Protocols.swift b/OSFilesystemLib/OSFILEManager+Protocols.swift index 14fa159..b1593e1 100644 --- a/OSFilesystemLib/OSFILEManager+Protocols.swift +++ b/OSFilesystemLib/OSFILEManager+Protocols.swift @@ -7,7 +7,8 @@ public protocol OSFILEDirectoryManager { } public protocol OSFILEFileManager { - func readFile(atURL: URL, withEncoding: OSFILEEncoding) throws -> String + func readEntireFile(atURL: URL, withEncoding: OSFILEEncoding) throws -> String + func readFileInChunks(atURL: URL, withEncoding: OSFILEEncoding, andChunkSize: Int) throws -> OSFILEChunkPublisher func getFileURL(atPath: String, withSearchPath: OSFILESearchPath) throws -> URL func deleteFile(atURL: URL) throws func saveFile(atURL: URL, withEncodingAndData: OSFILEEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL diff --git a/OSFilesystemLib/OSFILEManager.swift b/OSFilesystemLib/OSFILEManager.swift index 60d2790..7eaa04c 100644 --- a/OSFilesystemLib/OSFILEManager.swift +++ b/OSFilesystemLib/OSFILEManager.swift @@ -30,23 +30,20 @@ extension OSFILEManager: OSFILEDirectoryManager { } extension OSFILEManager: OSFILEFileManager { - public func readFile(atURL fileURL: URL, withEncoding encoding: OSFILEEncoding) throws -> String { - // Check if the URL requires security-scoped access - let requiresSecurityScope = fileURL.startAccessingSecurityScopedResource() - - // Use defer to ensure we stop accessing the security-scoped resource - // only if we started accessing it - defer { - if requiresSecurityScope { - fileURL.stopAccessingSecurityScopedResource() + public func readEntireFile(atURL fileURL: URL, withEncoding encoding: OSFILEEncoding) throws -> String { + try withSecurityScopedAccess(to: fileURL) { + switch encoding { + case .byteBuffer: + try readFileAsBase64EncodedString(from: fileURL) + case .string(let stringEncoding): + try readFileAsString(from: fileURL, using: stringEncoding.stringEncoding) } } + } - return switch encoding { - case .byteBuffer: - try readFileAsBase64EncodedString(from: fileURL) - case .string(let stringEncoding): - try readFileAsString(from: fileURL, using: stringEncoding.stringEncoding) + public func readFileInChunks(atURL fileURL: URL, withEncoding encoding: OSFILEEncoding, andChunkSize chunkSize: Int) throws -> OSFILEChunkPublisher { + try withSecurityScopedAccess(to: fileURL) { + .init(fileURL, chunkSize, encoding) } } @@ -131,6 +128,21 @@ extension OSFILEManager: OSFILEFileManager { } private extension OSFILEManager { + func withSecurityScopedAccess(to fileURL: URL, perform operation: () throws -> T) throws -> T { + // Check if the URL requires security-scoped access + let requiresSecurityScope = fileURL.startAccessingSecurityScopedResource() + + // Use defer to ensure we stop accessing the security-scoped resource + // only if we started accessing it + defer { + if requiresSecurityScope { + fileURL.stopAccessingSecurityScopedResource() + } + } + + return try operation() + } + func readFileAsBase64EncodedString(from fileURL: URL) throws -> String { try Data(contentsOf: fileURL).base64EncodedString() } diff --git a/OSFilesystemLibTests/OSFILEFileManagerTests.swift b/OSFilesystemLibTests/OSFILEFileManagerTests.swift index 2d652da..628b6ff 100644 --- a/OSFilesystemLibTests/OSFILEFileManagerTests.swift +++ b/OSFilesystemLibTests/OSFILEFileManagerTests.swift @@ -1,19 +1,59 @@ +import Combine import XCTest @testable import OSFilesystemLib final class OSFILEFileManagerTests: XCTestCase { private var sut: OSFILEManager! + private var cancellables: Set! + + override func setUp() { + cancellables = .init() + } + + override func tearDown() { + cancellables = nil + sut = nil + } +} + +// MARK: - 'readEntireFile` tests +extension OSFILEFileManagerTests { + func test_readEntireFile_withStringEncoding_returnsContentSuccessfully() throws { + // Given + createFileManager() + + // When + let fileContent = try fetchEntireContent( + forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .string(encoding: .utf8) + ) + + // Then + XCTAssertEqual(fileContent, Configuration.fileContent) + } + + func test_readEntireFile_withByteBufferEncoding_returnsContentSuccessfully() throws { + // Given + createFileManager() + + // When + let fileContent = try fetchEntireContent( + forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .byteBuffer + ) + + // Then + XCTAssertEqual(fileContent, Configuration.fileContent) + } } -// MARK: - 'readFile` tests +// MARK: - 'readFileInChunks' tests extension OSFILEFileManagerTests { - func test_readFile_withStringEncoding_returnsContentSuccessfully() throws { + func test_readFileInChunks_withStringEncoding_returnsContentSuccessfully() throws { // Given createFileManager() // When - let fileContent = try fetchContent( + let fileContent = try fetchChunkedContent( forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .string(encoding: .utf8) ) @@ -21,18 +61,31 @@ extension OSFILEFileManagerTests { XCTAssertEqual(fileContent, Configuration.fileContent) } - func test_readFile_withByteBufferEncoding_returnsContentSuccessfully() throws { + func test_readFileInChunks_withByteBufferEncoding_returnsContentSuccessfully() throws { // Given createFileManager() // When - let fileContent = try fetchContent( + let fileContent = try fetchChunkedContent( forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .byteBuffer ) // Then XCTAssertEqual(fileContent, Configuration.fileContent) } + + func test_readFileInChunks_withInvalidContent_returnsError() { + // Given + createFileManager() + + // When + XCTAssertThrowsError(try fetchChunkedContent( + forFile: (Configuration.fileWithEmojiName, Configuration.fileExtension), withEncoding: .string(encoding: .ascii) + )) { + // Then + XCTAssertEqual($0 as? OSFILEChunkPublisherError, .cantEncodeData) + } + } } // MARK: - 'getFileURL' tests @@ -245,7 +298,7 @@ extension OSFILEFileManagerTests { XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories) XCTAssertEqual(savedFileURL, fileURL) - let savedFileContent = try fetchContent( + let savedFileContent = try fetchEntireContent( forFile: (Configuration.newFileName, Configuration.fileExtension), withEncoding: .string(encoding: stringEncoding) ) XCTAssertEqual(savedFileContent, contentToSave) @@ -274,7 +327,7 @@ extension OSFILEFileManagerTests { XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories) XCTAssertEqual(savedFileURL, fileURL) - let savedFileContent = try fetchContent( + let savedFileContent = try fetchEntireContent( forFile: (Configuration.newFileName, Configuration.fileExtension), withEncoding: .byteBuffer ) XCTAssertEqual(savedFileContent, contentToSave) @@ -305,7 +358,7 @@ extension OSFILEFileManagerTests { XCTAssertEqual(fileManager.capturedPath, parentFolderURL) XCTAssertEqual(savedFileURL, fileURL) - let savedFileContent = try fetchContent( + let savedFileContent = try fetchEntireContent( forFile: (Configuration.newFileName, Configuration.fileExtension), withEncoding: .string(encoding: stringEncoding) ) XCTAssertEqual(savedFileContent, contentToSave) @@ -354,7 +407,7 @@ extension OSFILEFileManagerTests { ) // Then - let savedFileContent = try fetchContent( + let savedFileContent = try fetchEntireContent( forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .string(encoding: stringEncoding) ) @@ -382,7 +435,7 @@ extension OSFILEFileManagerTests { ) // Then - let savedFileContent = try fetchContent( + let savedFileContent = try fetchEntireContent( forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .byteBuffer ) @@ -417,7 +470,7 @@ extension OSFILEFileManagerTests { XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories) // Then - let savedFileContent = try fetchContent( + let savedFileContent = try fetchEntireContent( forFile: (Configuration.newFileName, Configuration.fileExtension), withEncoding: .string(encoding: stringEncoding) ) @@ -693,6 +746,7 @@ extension OSFILEFileManagerTests { private extension OSFILEFileManagerTests { struct Configuration { static let fileName = "file" + static let fileWithEmojiName = "file_emojiContent" static let newFileName = "new_file" static let fileExtension = "txt" static let fileContent = "Hello, world!" @@ -751,9 +805,41 @@ private extension OSFILEFileManagerTests { return fileManager } - func fetchContent(forFile file: (name: String, extension: String), withEncoding encoding: OSFILEEncoding) throws -> String { + func fetchEntireContent(forFile file: (name: String, extension: String), withEncoding encoding: OSFILEEncoding) throws -> String { let fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: file.name, withExtension: file.extension)) - let fileURLContent = try sut.readFile(atURL: fileURL, withEncoding: encoding) + return try treatContent(withEncoding: encoding) { + try sut.readEntireFile(atURL: fileURL, withEncoding: encoding) + } + } + + func fetchChunkedContent(forFile file: (name: String, extension: String), withEncoding encoding: OSFILEEncoding) throws -> String { + let fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: file.name, withExtension: file.extension)) + return try treatContent(withEncoding: encoding) { + var result = String() + var error: Error? + let expectation = XCTestExpectation(description: "Wait for chunks to be processed") + + try sut.readFileInChunks(atURL: fileURL, withEncoding: encoding, andChunkSize: 3) // 3 bytes + .sink(receiveCompletion: { completion in + if case .failure(let failure) = completion { + error = failure + } + expectation.fulfill() + }, receiveValue: { + result.append($0) + }) + .store(in: &cancellables) + + // Wait for all chunks to be processed + wait(for: [expectation], timeout: 1.0) + + if let error { throw error } + return result + } + } + + func treatContent(withEncoding encoding: OSFILEEncoding, afterReading readOperation: () throws -> String) throws -> String { + let fileURLContent = try readOperation() var fileURLUnicodeScalars: String.UnicodeScalarView if case .byteBuffer = encoding { diff --git a/OSFilesystemLibTests/file_emojiContent.txt b/OSFilesystemLibTests/file_emojiContent.txt new file mode 100644 index 0000000..600106b --- /dev/null +++ b/OSFilesystemLibTests/file_emojiContent.txt @@ -0,0 +1 @@ +🙃 From 52345b6d6f644da767130415ce931ff59a89495a Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Wed, 29 Jan 2025 13:04:33 +0000 Subject: [PATCH 19/31] chore: adapt chunk size to encoding type Byte buffers, since they're Base64 data, need to be encoding in multiple of 3 sizes. --- OSFilesystemLib/OSFILEChunkPublisher.swift | 10 ++++++++-- OSFilesystemLib/OSFILEManager+Enums.swift | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/OSFilesystemLib/OSFILEChunkPublisher.swift b/OSFilesystemLib/OSFILEChunkPublisher.swift index f4a9f33..142e1b2 100644 --- a/OSFilesystemLib/OSFILEChunkPublisher.swift +++ b/OSFilesystemLib/OSFILEChunkPublisher.swift @@ -30,7 +30,7 @@ private class OSFILEChunkSubscription: Subscription where S.Input init(_ url: URL, _ chunkSize: Int, _ encoding: OSFILEEncoding, _ subscriber: S) { self.fileHandle = try? FileHandle(forReadingFrom: url) - self.chunkSize = chunkSize + self.chunkSize = Self.chunkSizeToUse(basedOn: chunkSize, and: encoding) self.encoding = encoding self.subscriber = subscriber } @@ -70,8 +70,14 @@ private class OSFILEChunkSubscription: Subscription where S.Input deinit { fileHandle?.closeFile() } +} + +private extension OSFILEChunkSubscription { + static func chunkSizeToUse(basedOn chunkSize: Int, and encoding: OSFILEEncoding) -> Int { + encoding == .byteBuffer ? chunkSize - chunkSize % 3 + 3 : chunkSize + } - private func complete(withValue value: Subscribers.Completion) { + func complete(withValue value: Subscribers.Completion) { isCompleted = true subscriber.receive(completion: value) } diff --git a/OSFilesystemLib/OSFILEManager+Enums.swift b/OSFilesystemLib/OSFILEManager+Enums.swift index 6a79a11..2d536ce 100644 --- a/OSFilesystemLib/OSFILEManager+Enums.swift +++ b/OSFilesystemLib/OSFILEManager+Enums.swift @@ -1,6 +1,6 @@ import Foundation -public enum OSFILEEncoding { +public enum OSFILEEncoding: Equatable { case byteBuffer case string(encoding: OSFILEStringEncoding) } From a9738308e188d76a99ad4c913ee2f9e9ddcf7c0d Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Thu, 30 Jan 2025 13:27:39 +0000 Subject: [PATCH 20/31] chore: add error when not able to handle file This is added to the OSFILEChunkSubscription flow. Add unit test to validate scenario. --- OSFilesystemLib/OSFILEChunkPublisher.swift | 4 +++- OSFilesystemLib/OSFILEManager+Errors.swift | 1 + .../OSFILEFileManagerTests.swift | 20 +++++++++++++++++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/OSFilesystemLib/OSFILEChunkPublisher.swift b/OSFilesystemLib/OSFILEChunkPublisher.swift index 142e1b2..2bcfb83 100644 --- a/OSFilesystemLib/OSFILEChunkPublisher.swift +++ b/OSFilesystemLib/OSFILEChunkPublisher.swift @@ -36,7 +36,9 @@ private class OSFILEChunkSubscription: Subscription where S.Input } func request(_ demand: Subscribers.Demand) { - guard let fileHandle = fileHandle, !isCompleted else { return } + guard let fileHandle = fileHandle, !isCompleted else { + return subscriber.receive(completion: .failure(OSFILEChunkPublisherError.notAbleToReadFile)) + } while demand > .none { do { diff --git a/OSFilesystemLib/OSFILEManager+Errors.swift b/OSFilesystemLib/OSFILEManager+Errors.swift index 5f9d897..cb4ca50 100644 --- a/OSFilesystemLib/OSFILEManager+Errors.swift +++ b/OSFilesystemLib/OSFILEManager+Errors.swift @@ -12,4 +12,5 @@ enum OSFILEFileManagerError: Error { enum OSFILEChunkPublisherError: Error { case cantEncodeData + case notAbleToReadFile } diff --git a/OSFilesystemLibTests/OSFILEFileManagerTests.swift b/OSFilesystemLibTests/OSFILEFileManagerTests.swift index 628b6ff..e0bb1d8 100644 --- a/OSFilesystemLibTests/OSFILEFileManagerTests.swift +++ b/OSFilesystemLibTests/OSFILEFileManagerTests.swift @@ -74,6 +74,19 @@ extension OSFILEFileManagerTests { XCTAssertEqual(fileContent, Configuration.fileContent) } + func test_readFileInChunks_notAbleToReadFile_returnsError() { + // Given + createFileManager() + + // When + XCTAssertThrowsError(try fetchChunkedContent( + forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .string(encoding: .utf8), forceURLError: true + )) { + // Then + XCTAssertEqual($0 as? OSFILEChunkPublisherError, .notAbleToReadFile) + } + } + func test_readFileInChunks_withInvalidContent_returnsError() { // Given createFileManager() @@ -812,13 +825,16 @@ private extension OSFILEFileManagerTests { } } - func fetchChunkedContent(forFile file: (name: String, extension: String), withEncoding encoding: OSFILEEncoding) throws -> String { - let fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: file.name, withExtension: file.extension)) + func fetchChunkedContent(forFile file: (name: String, extension: String), withEncoding encoding: OSFILEEncoding, forceURLError: Bool = false) throws -> String { + var fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: file.name, withExtension: file.extension)) return try treatContent(withEncoding: encoding) { var result = String() var error: Error? let expectation = XCTestExpectation(description: "Wait for chunks to be processed") + if forceURLError { + fileURL.deleteLastPathComponent() + } try sut.readFileInChunks(atURL: fileURL, withEncoding: encoding, andChunkSize: 3) // 3 bytes .sink(receiveCompletion: { completion in if case .failure(let failure) = completion { From 6b4f37b985f3a11d5fcfb6d27abb2768a420d24d Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Thu, 30 Jan 2025 15:32:12 +0000 Subject: [PATCH 21/31] chore: change naming Adopt the ION name instead of OS. Update Gemfile bundles. --- Gemfile.lock | 48 ++++--- ...temLib.podspec => IONFilesystemLib.podspec | 14 +- .../project.pbxproj | 128 +++++++++--------- .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/swiftpm/Package.resolved | 0 .../xcschemes/IONFilesystemLib.xcscheme | 18 +-- .../IONFILEChunkPublisher.swift | 22 +-- .../IONFILEItemAttributeModel.swift | 12 +- .../IONFILEManager+Enums.swift | 16 +-- .../IONFILEManager+Errors.swift | 6 +- .../IONFILEManager+Protocols.swift | 19 +++ .../IONFILEManager.swift | 36 ++--- .../URL+Extension.swift | 0 .../IONFILEDirectoryManagerTests.swift | 12 +- .../IONFILEFileManagerTests.swift | 76 +++++------ .../MockFileManager.swift | 0 .../file.txt | 0 .../file_emojiContent.txt | 0 OSFilesystemLib/OSFILEManager+Protocols.swift | 19 --- fastlane/Fastfile | 8 +- scripts/build_framework.sh | 4 +- scripts/bump_versions.rb | 10 +- 23 files changed, 229 insertions(+), 219 deletions(-) rename OSFilesystemLib.podspec => IONFilesystemLib.podspec (53%) rename {OSFilesystemLib.xcodeproj => IONFilesystemLib.xcodeproj}/project.pbxproj (74%) rename {OSFilesystemLib.xcodeproj => IONFilesystemLib.xcodeproj}/project.xcworkspace/contents.xcworkspacedata (100%) rename {OSFilesystemLib.xcodeproj => IONFilesystemLib.xcodeproj}/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {OSFilesystemLib.xcodeproj => IONFilesystemLib.xcodeproj}/project.xcworkspace/xcshareddata/swiftpm/Package.resolved (100%) rename OSFilesystemLib.xcodeproj/xcshareddata/xcschemes/OSFilesystemLib.xcscheme => IONFilesystemLib.xcodeproj/xcshareddata/xcschemes/IONFilesystemLib.xcscheme (81%) rename OSFilesystemLib/OSFILEChunkPublisher.swift => IONFilesystemLib/IONFILEChunkPublisher.swift (74%) rename OSFilesystemLib/OSFILEItemAttributeModel.swift => IONFilesystemLib/IONFILEItemAttributeModel.swift (79%) rename OSFilesystemLib/OSFILEManager+Enums.swift => IONFilesystemLib/IONFILEManager+Enums.swift (53%) rename OSFilesystemLib/OSFILEManager+Errors.swift => IONFilesystemLib/IONFILEManager+Errors.swift (62%) create mode 100644 IONFilesystemLib/IONFILEManager+Protocols.swift rename OSFilesystemLib/OSFILEManager.swift => IONFilesystemLib/IONFILEManager.swift (86%) rename {OSFilesystemLib => IONFilesystemLib}/URL+Extension.swift (100%) rename OSFilesystemLibTests/OSFILEDirectoryManagerTests.swift => IONFilesystemLibTests/IONFILEDirectoryManagerTests.swift (94%) rename OSFilesystemLibTests/OSFILEFileManagerTests.swift => IONFilesystemLibTests/IONFILEFileManagerTests.swift (93%) rename {OSFilesystemLibTests => IONFilesystemLibTests}/MockFileManager.swift (100%) rename {OSFilesystemLibTests => IONFilesystemLibTests}/file.txt (100%) rename {OSFilesystemLibTests => IONFilesystemLibTests}/file_emojiContent.txt (100%) delete mode 100644 OSFilesystemLib/OSFILEManager+Protocols.swift diff --git a/Gemfile.lock b/Gemfile.lock index a8e6b78..cb1258d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -22,20 +22,20 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.1034.0) - aws-sdk-core (3.214.1) + aws-partitions (1.1043.0) + aws-sdk-core (3.217.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.96.0) - aws-sdk-core (~> 3, >= 3.210.0) + aws-sdk-kms (1.97.0) + aws-sdk-core (~> 3, >= 3.216.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.177.0) - aws-sdk-core (~> 3, >= 3.210.0) + aws-sdk-s3 (1.179.0) + aws-sdk-core (~> 3, >= 3.216.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.10.1) + aws-sigv4 (1.11.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) @@ -47,10 +47,10 @@ GEM colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) - concurrent-ruby (1.3.4) + concurrent-ruby (1.3.5) connection_pool (2.5.0) declarative (0.0.20) - digest-crc (0.6.5) + digest-crc (0.7.0) rake (>= 12.0.0, < 14.0.0) domain_name (0.6.20240107) dotenv (2.8.1) @@ -171,7 +171,7 @@ GEM http-cookie (1.0.8) domain_name (~> 0.5) httpclient (2.8.3) - i18n (1.14.6) + i18n (1.14.7) concurrent-ruby (~> 1.0) jmespath (1.6.2) json (2.9.1) @@ -180,25 +180,27 @@ GEM logger (1.6.5) mini_magick (4.13.2) mini_mime (1.1.5) - mini_portile2 (2.8.8) minitest (5.25.4) multi_json (1.15.0) multipart-post (2.4.1) nanaimo (0.4.0) naturally (2.2.1) nkf (0.2.0) - nokogiri (1.18.1) - mini_portile2 (~> 2.8.2) + nokogiri (1.18.2-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.1-aarch64-linux-gnu) + nokogiri (1.18.2-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.18.1-arm-linux-gnu) + nokogiri (1.18.2-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.18.1-arm64-darwin) + nokogiri (1.18.2-arm-linux-musl) racc (~> 1.4) - nokogiri (1.18.1-x86_64-darwin) + nokogiri (1.18.2-arm64-darwin) racc (~> 1.4) - nokogiri (1.18.1-x86_64-linux-gnu) + nokogiri (1.18.2-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.18.2-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.2-x86_64-linux-musl) racc (~> 1.4) optparse (0.6.0) os (1.1.4) @@ -258,12 +260,14 @@ GEM xcpretty (~> 0.2, >= 0.0.7) PLATFORMS - aarch64-linux - arm-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl arm64-darwin - x86-linux x86_64-darwin - x86_64-linux + x86_64-linux-gnu + x86_64-linux-musl DEPENDENCIES fastlane diff --git a/OSFilesystemLib.podspec b/IONFilesystemLib.podspec similarity index 53% rename from OSFilesystemLib.podspec rename to IONFilesystemLib.podspec index bf27c46..de4ac84 100644 --- a/OSFilesystemLib.podspec +++ b/IONFilesystemLib.podspec @@ -1,21 +1,21 @@ Pod::Spec.new do |spec| - spec.name = 'OSFilesystemLib' + spec.name = 'IONFilesystemLib' spec.version = '0.0.1' - spec.summary = 'The `OSFilesystemLib` is a template library.' + spec.summary = 'The `IONFilesystemLib` is a template library.' spec.description = <<-DESC - The `OSFilesystemLib` is a template library. + The `IONFilesystemLib` is a template library. - The `OSFilesystemLib` structure provides the main feature of the Library: + The `IONFilesystemLib` structure provides the main feature of the Library: - ping: A simple echo function that returns the input string. DESC - spec.homepage = 'https://github.com/ionic-team/OSFilesystemLib-iOS' + spec.homepage = 'https://github.com/ionic-team/IONFilesystemLib-iOS' spec.license = { :type => 'MIT', :file => 'LICENSE' } spec.author = { 'OutSystems Mobile Ecosystem' => 'rd.mobileecosystem.team@outsystems.com' } - spec.source = { :http => "https://github.com/ionic-team/OSFilesystemLib-iOS/releases/download/#{spec.version}/OSFilesystemLib.zip", :type => "zip" } - spec.vendored_frameworks = "OSFilesystemLib.xcframework" + spec.source = { :http => "https://github.com/ionic-team/IONFilesystemLib-iOS/releases/download/#{spec.version}/IONFilesystemLib.zip", :type => "zip" } + spec.vendored_frameworks = "IONFilesystemLib.xcframework" spec.ios.deployment_target = '14.0' spec.swift_versions = ['5.0', '5.1', '5.2', '5.3', '5.4', '5.5', '5.6', '5.7', '5.8', '5.9'] diff --git a/OSFilesystemLib.xcodeproj/project.pbxproj b/IONFilesystemLib.xcodeproj/project.pbxproj similarity index 74% rename from OSFilesystemLib.xcodeproj/project.pbxproj rename to IONFilesystemLib.xcodeproj/project.pbxproj index 22f2a92..3162958 100644 --- a/OSFilesystemLib.xcodeproj/project.pbxproj +++ b/IONFilesystemLib.xcodeproj/project.pbxproj @@ -7,18 +7,18 @@ objects = { /* Begin PBXBuildFile section */ - 751328D52D3175170031BDD0 /* OSFILEManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D42D3175170031BDD0 /* OSFILEManager.swift */; }; - 751328DA2D318DBA0031BDD0 /* OSFILEDirectoryManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D92D318DBA0031BDD0 /* OSFILEDirectoryManagerTests.swift */; }; + 751328D52D3175170031BDD0 /* IONFILEManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D42D3175170031BDD0 /* IONFILEManager.swift */; }; + 751328DA2D318DBA0031BDD0 /* IONFILEDirectoryManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D92D318DBA0031BDD0 /* IONFILEDirectoryManagerTests.swift */; }; 751328DB2D318E770031BDD0 /* MockFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D82D3179430031BDD0 /* MockFileManager.swift */; }; - 7575CF6A2BFCEE6F008F3FD0 /* OSFilesystemLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7575CF612BFCEE6F008F3FD0 /* OSFilesystemLib.framework */; }; - 75DA44542D48E435006DF7DE /* OSFILEChunkPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75DA44532D48E435006DF7DE /* OSFILEChunkPublisher.swift */; }; + 7575CF6A2BFCEE6F008F3FD0 /* IONFilesystemLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7575CF612BFCEE6F008F3FD0 /* IONFilesystemLib.framework */; }; + 75DA44542D48E435006DF7DE /* IONFILEChunkPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75DA44532D48E435006DF7DE /* IONFILEChunkPublisher.swift */; }; 75DA445A2D490051006DF7DE /* file_emojiContent.txt in Resources */ = {isa = PBXBuildFile; fileRef = 75DA44592D490051006DF7DE /* file_emojiContent.txt */; }; - 75F8380B2D37E42000FCE044 /* OSFILEItemAttributeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F8380A2D37E42000FCE044 /* OSFILEItemAttributeModel.swift */; }; - 75F84D662D39360E00892C89 /* OSFILEManager+Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F84D652D39360E00892C89 /* OSFILEManager+Errors.swift */; }; - 75F84D682D39362F00892C89 /* OSFILEManager+Enums.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F84D672D39362F00892C89 /* OSFILEManager+Enums.swift */; }; - 75FEB2B02D3546D7007C2686 /* OSFILEManager+Protocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FEB2AF2D3546D7007C2686 /* OSFILEManager+Protocols.swift */; }; + 75F8380B2D37E42000FCE044 /* IONFILEItemAttributeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F8380A2D37E42000FCE044 /* IONFILEItemAttributeModel.swift */; }; + 75F84D662D39360E00892C89 /* IONFILEManager+Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F84D652D39360E00892C89 /* IONFILEManager+Errors.swift */; }; + 75F84D682D39362F00892C89 /* IONFILEManager+Enums.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F84D672D39362F00892C89 /* IONFILEManager+Enums.swift */; }; + 75FEB2B02D3546D7007C2686 /* IONFILEManager+Protocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FEB2AF2D3546D7007C2686 /* IONFILEManager+Protocols.swift */; }; 75FEB2B22D35470C007C2686 /* URL+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FEB2B12D35470C007C2686 /* URL+Extension.swift */; }; - 75FEB2B42D35479B007C2686 /* OSFILEFileManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FEB2B32D35479B007C2686 /* OSFILEFileManagerTests.swift */; }; + 75FEB2B42D35479B007C2686 /* IONFILEFileManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FEB2B32D35479B007C2686 /* IONFILEFileManagerTests.swift */; }; 75FEB2B72D355F21007C2686 /* file.txt in Resources */ = {isa = PBXBuildFile; fileRef = 75FEB2B62D355F21007C2686 /* file.txt */; }; /* End PBXBuildFile section */ @@ -33,19 +33,19 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 751328D42D3175170031BDD0 /* OSFILEManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFILEManager.swift; sourceTree = ""; }; + 751328D42D3175170031BDD0 /* IONFILEManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IONFILEManager.swift; sourceTree = ""; }; 751328D82D3179430031BDD0 /* MockFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFileManager.swift; sourceTree = ""; }; - 751328D92D318DBA0031BDD0 /* OSFILEDirectoryManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFILEDirectoryManagerTests.swift; sourceTree = ""; }; - 7575CF612BFCEE6F008F3FD0 /* OSFilesystemLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OSFilesystemLib.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 7575CF692BFCEE6F008F3FD0 /* OSFilesystemLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OSFilesystemLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 75DA44532D48E435006DF7DE /* OSFILEChunkPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFILEChunkPublisher.swift; sourceTree = ""; }; + 751328D92D318DBA0031BDD0 /* IONFILEDirectoryManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IONFILEDirectoryManagerTests.swift; sourceTree = ""; }; + 7575CF612BFCEE6F008F3FD0 /* IONFilesystemLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = IONFilesystemLib.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7575CF692BFCEE6F008F3FD0 /* IONFilesystemLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IONFilesystemLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 75DA44532D48E435006DF7DE /* IONFILEChunkPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IONFILEChunkPublisher.swift; sourceTree = ""; }; 75DA44592D490051006DF7DE /* file_emojiContent.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = file_emojiContent.txt; sourceTree = ""; }; - 75F8380A2D37E42000FCE044 /* OSFILEItemAttributeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFILEItemAttributeModel.swift; sourceTree = ""; }; - 75F84D652D39360E00892C89 /* OSFILEManager+Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSFILEManager+Errors.swift"; sourceTree = ""; }; - 75F84D672D39362F00892C89 /* OSFILEManager+Enums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSFILEManager+Enums.swift"; sourceTree = ""; }; - 75FEB2AF2D3546D7007C2686 /* OSFILEManager+Protocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSFILEManager+Protocols.swift"; sourceTree = ""; }; + 75F8380A2D37E42000FCE044 /* IONFILEItemAttributeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IONFILEItemAttributeModel.swift; sourceTree = ""; }; + 75F84D652D39360E00892C89 /* IONFILEManager+Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IONFILEManager+Errors.swift"; sourceTree = ""; }; + 75F84D672D39362F00892C89 /* IONFILEManager+Enums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IONFILEManager+Enums.swift"; sourceTree = ""; }; + 75FEB2AF2D3546D7007C2686 /* IONFILEManager+Protocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IONFILEManager+Protocols.swift"; sourceTree = ""; }; 75FEB2B12D35470C007C2686 /* URL+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extension.swift"; sourceTree = ""; }; - 75FEB2B32D35479B007C2686 /* OSFILEFileManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFILEFileManagerTests.swift; sourceTree = ""; }; + 75FEB2B32D35479B007C2686 /* IONFILEFileManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IONFILEFileManagerTests.swift; sourceTree = ""; }; 75FEB2B62D355F21007C2686 /* file.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = file.txt; sourceTree = ""; }; /* End PBXFileReference section */ @@ -61,7 +61,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 7575CF6A2BFCEE6F008F3FD0 /* OSFilesystemLib.framework in Frameworks */, + 7575CF6A2BFCEE6F008F3FD0 /* IONFilesystemLib.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -71,8 +71,8 @@ 7575CF572BFCEE6F008F3FD0 = { isa = PBXGroup; children = ( - 7575CF632BFCEE6F008F3FD0 /* OSFilesystemLib */, - 7575CF6D2BFCEE6F008F3FD0 /* OSFilesystemLibTests */, + 7575CF632BFCEE6F008F3FD0 /* IONFilesystemLib */, + 7575CF6D2BFCEE6F008F3FD0 /* IONFilesystemLibTests */, 7575CF622BFCEE6F008F3FD0 /* Products */, ); sourceTree = ""; @@ -80,36 +80,36 @@ 7575CF622BFCEE6F008F3FD0 /* Products */ = { isa = PBXGroup; children = ( - 7575CF612BFCEE6F008F3FD0 /* OSFilesystemLib.framework */, - 7575CF692BFCEE6F008F3FD0 /* OSFilesystemLibTests.xctest */, + 7575CF612BFCEE6F008F3FD0 /* IONFilesystemLib.framework */, + 7575CF692BFCEE6F008F3FD0 /* IONFilesystemLibTests.xctest */, ); name = Products; sourceTree = ""; }; - 7575CF632BFCEE6F008F3FD0 /* OSFilesystemLib */ = { + 7575CF632BFCEE6F008F3FD0 /* IONFilesystemLib */ = { isa = PBXGroup; children = ( - 751328D42D3175170031BDD0 /* OSFILEManager.swift */, - 75F84D672D39362F00892C89 /* OSFILEManager+Enums.swift */, - 75F84D652D39360E00892C89 /* OSFILEManager+Errors.swift */, - 75FEB2AF2D3546D7007C2686 /* OSFILEManager+Protocols.swift */, - 75DA44532D48E435006DF7DE /* OSFILEChunkPublisher.swift */, - 75F8380A2D37E42000FCE044 /* OSFILEItemAttributeModel.swift */, + 751328D42D3175170031BDD0 /* IONFILEManager.swift */, + 75F84D672D39362F00892C89 /* IONFILEManager+Enums.swift */, + 75F84D652D39360E00892C89 /* IONFILEManager+Errors.swift */, + 75FEB2AF2D3546D7007C2686 /* IONFILEManager+Protocols.swift */, + 75DA44532D48E435006DF7DE /* IONFILEChunkPublisher.swift */, + 75F8380A2D37E42000FCE044 /* IONFILEItemAttributeModel.swift */, 75FEB2B12D35470C007C2686 /* URL+Extension.swift */, ); - path = OSFilesystemLib; + path = IONFilesystemLib; sourceTree = ""; }; - 7575CF6D2BFCEE6F008F3FD0 /* OSFilesystemLibTests */ = { + 7575CF6D2BFCEE6F008F3FD0 /* IONFilesystemLibTests */ = { isa = PBXGroup; children = ( - 75DA44592D490051006DF7DE /* file_emojiContent.txt */, 751328D82D3179430031BDD0 /* MockFileManager.swift */, - 751328D92D318DBA0031BDD0 /* OSFILEDirectoryManagerTests.swift */, - 75FEB2B32D35479B007C2686 /* OSFILEFileManagerTests.swift */, + 751328D92D318DBA0031BDD0 /* IONFILEDirectoryManagerTests.swift */, + 75FEB2B32D35479B007C2686 /* IONFILEFileManagerTests.swift */, 75FEB2B62D355F21007C2686 /* file.txt */, + 75DA44592D490051006DF7DE /* file_emojiContent.txt */, ); - path = OSFilesystemLibTests; + path = IONFilesystemLibTests; sourceTree = ""; }; /* End PBXGroup section */ @@ -125,9 +125,9 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ - 7575CF602BFCEE6F008F3FD0 /* OSFilesystemLib */ = { + 7575CF602BFCEE6F008F3FD0 /* IONFilesystemLib */ = { isa = PBXNativeTarget; - buildConfigurationList = 7575CF732BFCEE6F008F3FD0 /* Build configuration list for PBXNativeTarget "OSFilesystemLib" */; + buildConfigurationList = 7575CF732BFCEE6F008F3FD0 /* Build configuration list for PBXNativeTarget "IONFilesystemLib" */; buildPhases = ( 7575CF5C2BFCEE6F008F3FD0 /* Headers */, 7575CF5D2BFCEE6F008F3FD0 /* Sources */, @@ -139,14 +139,14 @@ ); dependencies = ( ); - name = OSFilesystemLib; + name = IONFilesystemLib; productName = OSInAppBrowserLib; - productReference = 7575CF612BFCEE6F008F3FD0 /* OSFilesystemLib.framework */; + productReference = 7575CF612BFCEE6F008F3FD0 /* IONFilesystemLib.framework */; productType = "com.apple.product-type.framework"; }; - 7575CF682BFCEE6F008F3FD0 /* OSFilesystemLibTests */ = { + 7575CF682BFCEE6F008F3FD0 /* IONFilesystemLibTests */ = { isa = PBXNativeTarget; - buildConfigurationList = 7575CF762BFCEE6F008F3FD0 /* Build configuration list for PBXNativeTarget "OSFilesystemLibTests" */; + buildConfigurationList = 7575CF762BFCEE6F008F3FD0 /* Build configuration list for PBXNativeTarget "IONFilesystemLibTests" */; buildPhases = ( 7575CF652BFCEE6F008F3FD0 /* Sources */, 7575CF662BFCEE6F008F3FD0 /* Frameworks */, @@ -157,9 +157,9 @@ dependencies = ( 7575CF6C2BFCEE6F008F3FD0 /* PBXTargetDependency */, ); - name = OSFilesystemLibTests; + name = IONFilesystemLibTests; productName = OSInAppBrowserLibTests; - productReference = 7575CF692BFCEE6F008F3FD0 /* OSFilesystemLibTests.xctest */; + productReference = 7575CF692BFCEE6F008F3FD0 /* IONFilesystemLibTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ @@ -182,7 +182,7 @@ }; }; }; - buildConfigurationList = 7575CF5B2BFCEE6F008F3FD0 /* Build configuration list for PBXProject "OSFilesystemLib" */; + buildConfigurationList = 7575CF5B2BFCEE6F008F3FD0 /* Build configuration list for PBXProject "IONFilesystemLib" */; compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; @@ -198,8 +198,8 @@ projectDirPath = ""; projectRoot = ""; targets = ( - 7575CF602BFCEE6F008F3FD0 /* OSFilesystemLib */, - 7575CF682BFCEE6F008F3FD0 /* OSFilesystemLibTests */, + 7575CF602BFCEE6F008F3FD0 /* IONFilesystemLib */, + 7575CF682BFCEE6F008F3FD0 /* IONFilesystemLibTests */, ); }; /* End PBXProject section */ @@ -249,12 +249,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 75F8380B2D37E42000FCE044 /* OSFILEItemAttributeModel.swift in Sources */, - 75DA44542D48E435006DF7DE /* OSFILEChunkPublisher.swift in Sources */, - 75FEB2B02D3546D7007C2686 /* OSFILEManager+Protocols.swift in Sources */, - 75F84D682D39362F00892C89 /* OSFILEManager+Enums.swift in Sources */, - 751328D52D3175170031BDD0 /* OSFILEManager.swift in Sources */, - 75F84D662D39360E00892C89 /* OSFILEManager+Errors.swift in Sources */, + 75F8380B2D37E42000FCE044 /* IONFILEItemAttributeModel.swift in Sources */, + 75DA44542D48E435006DF7DE /* IONFILEChunkPublisher.swift in Sources */, + 75FEB2B02D3546D7007C2686 /* IONFILEManager+Protocols.swift in Sources */, + 75F84D682D39362F00892C89 /* IONFILEManager+Enums.swift in Sources */, + 751328D52D3175170031BDD0 /* IONFILEManager.swift in Sources */, + 75F84D662D39360E00892C89 /* IONFILEManager+Errors.swift in Sources */, 75FEB2B22D35470C007C2686 /* URL+Extension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -263,9 +263,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 751328DA2D318DBA0031BDD0 /* OSFILEDirectoryManagerTests.swift in Sources */, + 751328DA2D318DBA0031BDD0 /* IONFILEDirectoryManagerTests.swift in Sources */, 751328DB2D318E770031BDD0 /* MockFileManager.swift in Sources */, - 75FEB2B42D35479B007C2686 /* OSFILEFileManagerTests.swift in Sources */, + 75FEB2B42D35479B007C2686 /* IONFILEFileManagerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -274,7 +274,7 @@ /* Begin PBXTargetDependency section */ 7575CF6C2BFCEE6F008F3FD0 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 7575CF602BFCEE6F008F3FD0 /* OSFilesystemLib */; + target = 7575CF602BFCEE6F008F3FD0 /* IONFilesystemLib */; targetProxy = 7575CF6B2BFCEE6F008F3FD0 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ @@ -434,7 +434,7 @@ MARKETING_VERSION = 1.0.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; - PRODUCT_BUNDLE_IDENTIFIER = com.outsystems.rd.inappbrowser.OSInAppBrowserLib; + PRODUCT_BUNDLE_IDENTIFIER = io.ionic.libs.filesystem.FilesystemLib; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; @@ -469,7 +469,7 @@ MARKETING_VERSION = 1.0.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; - PRODUCT_BUNDLE_IDENTIFIER = com.outsystems.rd.inappbrowser.OSInAppBrowserLib; + PRODUCT_BUNDLE_IDENTIFIER = io.ionic.libs.filesystem.FilesystemLib; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; @@ -487,7 +487,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.outsystems.rd.inappbrowser.OSInAppBrowserLibTests; + PRODUCT_BUNDLE_IDENTIFIER = io.ionic.libs.filesystem.FilesystemLibTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -508,7 +508,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.outsystems.rd.inappbrowser.OSInAppBrowserLibTests; + PRODUCT_BUNDLE_IDENTIFIER = io.ionic.libs.filesystem.FilesystemLibTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -522,7 +522,7 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 7575CF5B2BFCEE6F008F3FD0 /* Build configuration list for PBXProject "OSFilesystemLib" */ = { + 7575CF5B2BFCEE6F008F3FD0 /* Build configuration list for PBXProject "IONFilesystemLib" */ = { isa = XCConfigurationList; buildConfigurations = ( 7575CF712BFCEE6F008F3FD0 /* Debug */, @@ -531,7 +531,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 7575CF732BFCEE6F008F3FD0 /* Build configuration list for PBXNativeTarget "OSFilesystemLib" */ = { + 7575CF732BFCEE6F008F3FD0 /* Build configuration list for PBXNativeTarget "IONFilesystemLib" */ = { isa = XCConfigurationList; buildConfigurations = ( 7575CF742BFCEE6F008F3FD0 /* Debug */, @@ -540,7 +540,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 7575CF762BFCEE6F008F3FD0 /* Build configuration list for PBXNativeTarget "OSFilesystemLibTests" */ = { + 7575CF762BFCEE6F008F3FD0 /* Build configuration list for PBXNativeTarget "IONFilesystemLibTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 7575CF772BFCEE6F008F3FD0 /* Debug */, diff --git a/OSFilesystemLib.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/IONFilesystemLib.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from OSFilesystemLib.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to IONFilesystemLib.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/OSFilesystemLib.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/IONFilesystemLib.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from OSFilesystemLib.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to IONFilesystemLib.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/OSFilesystemLib.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/IONFilesystemLib.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved similarity index 100% rename from OSFilesystemLib.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved rename to IONFilesystemLib.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/OSFilesystemLib.xcodeproj/xcshareddata/xcschemes/OSFilesystemLib.xcscheme b/IONFilesystemLib.xcodeproj/xcshareddata/xcschemes/IONFilesystemLib.xcscheme similarity index 81% rename from OSFilesystemLib.xcodeproj/xcshareddata/xcschemes/OSFilesystemLib.xcscheme rename to IONFilesystemLib.xcodeproj/xcshareddata/xcschemes/IONFilesystemLib.xcscheme index 6c2da31..96eef5a 100644 --- a/OSFilesystemLib.xcodeproj/xcshareddata/xcschemes/OSFilesystemLib.xcscheme +++ b/IONFilesystemLib.xcodeproj/xcshareddata/xcschemes/IONFilesystemLib.xcscheme @@ -15,9 +15,9 @@ + BuildableName = "IONFilesystemLib.framework" + BlueprintName = "IONFilesystemLib" + ReferencedContainer = "container:IONFilesystemLib.xcodeproj"> @@ -35,9 +35,9 @@ + BuildableName = "IONFilesystemLibTests.xctest" + BlueprintName = "IONFilesystemLibTests" + ReferencedContainer = "container:IONFilesystemLib.xcodeproj"> @@ -63,9 +63,9 @@ + BuildableName = "IONFilesystemLib.framework" + BlueprintName = "IONFilesystemLib" + ReferencedContainer = "container:IONFilesystemLib.xcodeproj"> diff --git a/OSFilesystemLib/OSFILEChunkPublisher.swift b/IONFilesystemLib/IONFILEChunkPublisher.swift similarity index 74% rename from OSFilesystemLib/OSFILEChunkPublisher.swift rename to IONFilesystemLib/IONFILEChunkPublisher.swift index 2bcfb83..a1a035f 100644 --- a/OSFilesystemLib/OSFILEChunkPublisher.swift +++ b/IONFilesystemLib/IONFILEChunkPublisher.swift @@ -1,34 +1,34 @@ import Combine import Foundation -public class OSFILEChunkPublisher: Publisher { +public class IONFILEChunkPublisher: Publisher { public typealias Output = String public typealias Failure = Error private let url: URL private let chunkSize: Int - private let encoding: OSFILEEncoding + private let encoding: IONFILEEncoding - init(_ url: URL, _ chunkSize: Int, _ encoding: OSFILEEncoding) { + init(_ url: URL, _ chunkSize: Int, _ encoding: IONFILEEncoding) { self.url = url self.chunkSize = chunkSize self.encoding = encoding } public func receive(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input { - let subscription = OSFILEChunkSubscription(url, chunkSize, encoding, subscriber) + let subscription = IONFILEChunkSubscription(url, chunkSize, encoding, subscriber) subscriber.receive(subscription: subscription) } } -private class OSFILEChunkSubscription: Subscription where S.Input == String, S.Failure == Error { +private class IONFILEChunkSubscription: Subscription where S.Input == String, S.Failure == Error { private let fileHandle: FileHandle? private let chunkSize: Int - private let encoding: OSFILEEncoding + private let encoding: IONFILEEncoding private let subscriber: S private var isCompleted = false - init(_ url: URL, _ chunkSize: Int, _ encoding: OSFILEEncoding, _ subscriber: S) { + init(_ url: URL, _ chunkSize: Int, _ encoding: IONFILEEncoding, _ subscriber: S) { self.fileHandle = try? FileHandle(forReadingFrom: url) self.chunkSize = Self.chunkSizeToUse(basedOn: chunkSize, and: encoding) self.encoding = encoding @@ -37,7 +37,7 @@ private class OSFILEChunkSubscription: Subscription where S.Input func request(_ demand: Subscribers.Demand) { guard let fileHandle = fileHandle, !isCompleted else { - return subscriber.receive(completion: .failure(OSFILEChunkPublisherError.notAbleToReadFile)) + return subscriber.receive(completion: .failure(IONFILEChunkPublisherError.notAbleToReadFile)) } while demand > .none { @@ -48,7 +48,7 @@ private class OSFILEChunkSubscription: Subscription where S.Input case .byteBuffer: chunkToEmit = chunk.base64EncodedString() case .string(let encoding): guard let chunkText = String(data: chunk, encoding: encoding.stringEncoding) else { - throw OSFILEChunkPublisherError.cantEncodeData + throw IONFILEChunkPublisherError.cantEncodeData } chunkToEmit = chunkText } @@ -74,8 +74,8 @@ private class OSFILEChunkSubscription: Subscription where S.Input } } -private extension OSFILEChunkSubscription { - static func chunkSizeToUse(basedOn chunkSize: Int, and encoding: OSFILEEncoding) -> Int { +private extension IONFILEChunkSubscription { + static func chunkSizeToUse(basedOn chunkSize: Int, and encoding: IONFILEEncoding) -> Int { encoding == .byteBuffer ? chunkSize - chunkSize % 3 + 3 : chunkSize } diff --git a/OSFilesystemLib/OSFILEItemAttributeModel.swift b/IONFilesystemLib/IONFILEItemAttributeModel.swift similarity index 79% rename from OSFilesystemLib/OSFILEItemAttributeModel.swift rename to IONFilesystemLib/IONFILEItemAttributeModel.swift index 8151548..f2a75c9 100644 --- a/OSFilesystemLib/OSFILEItemAttributeModel.swift +++ b/IONFilesystemLib/IONFILEItemAttributeModel.swift @@ -1,23 +1,23 @@ import Foundation -public enum OSFILEItemType: Encodable { +public enum IONFILEItemType: Encodable { case directory case file - static func create(from fileAttributeType: String?) -> OSFILEItemType { + static func create(from fileAttributeType: String?) -> Self { fileAttributeType == FileAttributeKey.FileTypeDirectoryValue ? .directory : .file } } -public struct OSFILEItemAttributeModel { +public struct IONFILEItemAttributeModel { private(set) public var creationDateTimestamp: Double private(set) public var modificationDateTimestamp: Double private(set) public var size: UInt64 - private(set) public var type: OSFILEItemType + private(set) public var type: IONFILEItemType } -public extension OSFILEItemAttributeModel { - static func create(from attributeDictionary: [FileAttributeKey: Any]) -> OSFILEItemAttributeModel { +public extension IONFILEItemAttributeModel { + static func create(from attributeDictionary: [FileAttributeKey: Any]) -> IONFILEItemAttributeModel { let creationDate = attributeDictionary[.creationDate] as? Date let modificationDate = attributeDictionary[.modificationDate] as? Date let size = attributeDictionary[.size] as? UInt64 ?? 0 diff --git a/OSFilesystemLib/OSFILEManager+Enums.swift b/IONFilesystemLib/IONFILEManager+Enums.swift similarity index 53% rename from OSFilesystemLib/OSFILEManager+Enums.swift rename to IONFilesystemLib/IONFILEManager+Enums.swift index 2d536ce..654e520 100644 --- a/OSFilesystemLib/OSFILEManager+Enums.swift +++ b/IONFilesystemLib/IONFILEManager+Enums.swift @@ -1,16 +1,16 @@ import Foundation -public enum OSFILEEncoding: Equatable { +public enum IONFILEEncoding: Equatable { case byteBuffer - case string(encoding: OSFILEStringEncoding) + case string(encoding: IONFILEStringEncoding) } -public enum OSFILEEncodingValueMapper { +public enum IONFILEEncodingValueMapper { case byteBuffer(value: Data) - case string(encoding: OSFILEStringEncoding, value: String) + case string(encoding: IONFILEStringEncoding, value: String) } -public enum OSFILEStringEncoding { +public enum IONFILEStringEncoding { case ascii case utf8 case utf16 @@ -24,12 +24,12 @@ public enum OSFILEStringEncoding { } } -public enum OSFILESearchPath { - case directory(type: OSFILEDirectoryType) +public enum IONFILESearchPath { + case directory(type: IONFILEDirectoryType) case raw } -public enum OSFILEDirectoryType { +public enum IONFILEDirectoryType { case cache case document case library diff --git a/OSFilesystemLib/OSFILEManager+Errors.swift b/IONFilesystemLib/IONFILEManager+Errors.swift similarity index 62% rename from OSFilesystemLib/OSFILEManager+Errors.swift rename to IONFilesystemLib/IONFILEManager+Errors.swift index cb4ca50..6e8bef5 100644 --- a/OSFilesystemLib/OSFILEManager+Errors.swift +++ b/IONFilesystemLib/IONFILEManager+Errors.swift @@ -1,8 +1,8 @@ -enum OSFILEDirectoryManagerError: Error { +enum IONFILEDirectoryManagerError: Error { case notEmpty } -enum OSFILEFileManagerError: Error { +enum IONFILEFileManagerError: Error { case cantCreateURL case cantDecodeData case directoryNotFound @@ -10,7 +10,7 @@ enum OSFILEFileManagerError: Error { case missingParentFolder } -enum OSFILEChunkPublisherError: Error { +enum IONFILEChunkPublisherError: Error { case cantEncodeData case notAbleToReadFile } diff --git a/IONFilesystemLib/IONFILEManager+Protocols.swift b/IONFilesystemLib/IONFILEManager+Protocols.swift new file mode 100644 index 0000000..489ef52 --- /dev/null +++ b/IONFilesystemLib/IONFILEManager+Protocols.swift @@ -0,0 +1,19 @@ +import Foundation + +public protocol IONFILEDirectoryManager { + func createDirectory(atURL: URL, includeIntermediateDirectories: Bool) throws + func removeDirectory(atURL: URL, includeIntermediateDirectories: Bool) throws + func listDirectory(atURL: URL) throws -> [URL] +} + +public protocol IONFILEFileManager { + func readEntireFile(atURL: URL, withEncoding: IONFILEEncoding) throws -> String + func readFileInChunks(atURL: URL, withEncoding: IONFILEEncoding, andChunkSize: Int) throws -> IONFILEChunkPublisher + func getFileURL(atPath: String, withSearchPath: IONFILESearchPath) throws -> URL + func deleteFile(atURL: URL) throws + func saveFile(atURL: URL, withEncodingAndData: IONFILEEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL + func appendData(_ data: IONFILEEncodingValueMapper, atURL: URL, includeIntermediateDirectories: Bool) throws + func getItemAttributes(atPath: String) throws -> IONFILEItemAttributeModel + func renameItem(fromURL: URL, toURL: URL) throws + func copyItem(fromURL: URL, toURL: URL) throws +} diff --git a/OSFilesystemLib/OSFILEManager.swift b/IONFilesystemLib/IONFILEManager.swift similarity index 86% rename from OSFilesystemLib/OSFILEManager.swift rename to IONFilesystemLib/IONFILEManager.swift index 7eaa04c..3570ce9 100644 --- a/OSFilesystemLib/OSFILEManager.swift +++ b/IONFilesystemLib/IONFILEManager.swift @@ -1,6 +1,6 @@ import Foundation -public struct OSFILEManager { +public struct IONFILEManager { private let fileManager: FileManager public init(fileManager: FileManager = .default) { @@ -8,7 +8,7 @@ public struct OSFILEManager { } } -extension OSFILEManager: OSFILEDirectoryManager { +extension IONFILEManager: IONFILEDirectoryManager { public func createDirectory(atURL pathURL: URL, includeIntermediateDirectories: Bool) throws { try fileManager.createDirectory(at: pathURL, withIntermediateDirectories: includeIntermediateDirectories) } @@ -17,7 +17,7 @@ extension OSFILEManager: OSFILEDirectoryManager { if !includeIntermediateDirectories { let directoryContents = try listDirectory(atURL: pathURL) if !directoryContents.isEmpty { - throw OSFILEDirectoryManagerError.notEmpty + throw IONFILEDirectoryManagerError.notEmpty } } @@ -29,8 +29,8 @@ extension OSFILEManager: OSFILEDirectoryManager { } } -extension OSFILEManager: OSFILEFileManager { - public func readEntireFile(atURL fileURL: URL, withEncoding encoding: OSFILEEncoding) throws -> String { +extension IONFILEManager: IONFILEFileManager { + public func readEntireFile(atURL fileURL: URL, withEncoding encoding: IONFILEEncoding) throws -> String { try withSecurityScopedAccess(to: fileURL) { switch encoding { case .byteBuffer: @@ -41,13 +41,13 @@ extension OSFILEManager: OSFILEFileManager { } } - public func readFileInChunks(atURL fileURL: URL, withEncoding encoding: OSFILEEncoding, andChunkSize chunkSize: Int) throws -> OSFILEChunkPublisher { + public func readFileInChunks(atURL fileURL: URL, withEncoding encoding: IONFILEEncoding, andChunkSize chunkSize: Int) throws -> IONFILEChunkPublisher { try withSecurityScopedAccess(to: fileURL) { .init(fileURL, chunkSize, encoding) } } - public func getFileURL(atPath path: String, withSearchPath searchPath: OSFILESearchPath) throws -> URL { + public func getFileURL(atPath path: String, withSearchPath searchPath: IONFILESearchPath) throws -> URL { switch searchPath { case .directory(let type): try resolveDirectoryURL(forType: type, with: path) @@ -58,21 +58,21 @@ extension OSFILEManager: OSFILEFileManager { public func deleteFile(atURL url: URL) throws { guard fileManager.fileExists(atPath: url.urlPath) else { - throw OSFILEFileManagerError.fileNotFound + throw IONFILEFileManagerError.fileNotFound } try fileManager.removeItem(at: url) } @discardableResult - public func saveFile(atURL fileURL: URL, withEncodingAndData encodingMapper: OSFILEEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL { + public func saveFile(atURL fileURL: URL, withEncodingAndData encodingMapper: IONFILEEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL { let fileDirectoryURL = fileURL.deletingLastPathComponent() if !fileManager.fileExists(atPath: fileDirectoryURL.urlPath) { if includeIntermediateDirectories { try createDirectory(atURL: fileDirectoryURL, includeIntermediateDirectories: true) } else { - throw OSFILEFileManagerError.missingParentFolder + throw IONFILEFileManagerError.missingParentFolder } } @@ -86,7 +86,7 @@ extension OSFILEManager: OSFILEFileManager { return fileURL } - public func appendData(_ encodingMapper: OSFILEEncodingValueMapper, atURL url: URL, includeIntermediateDirectories: Bool) throws { + public func appendData(_ encodingMapper: IONFILEEncodingValueMapper, atURL url: URL, includeIntermediateDirectories: Bool) throws { guard fileManager.fileExists(atPath: url.urlPath) else { try saveFile(atURL: url, withEncodingAndData: encodingMapper, includeIntermediateDirectories: includeIntermediateDirectories) return @@ -98,7 +98,7 @@ extension OSFILEManager: OSFILEFileManager { dataToAppend = value case .string(let encoding, let value): guard let valueData = value.data(using: encoding.stringEncoding) else { - throw OSFILEFileManagerError.cantDecodeData + throw IONFILEFileManagerError.cantDecodeData } dataToAppend = valueData } @@ -109,7 +109,7 @@ extension OSFILEManager: OSFILEFileManager { try fileHandle.close() } - public func getItemAttributes(atPath path: String) throws -> OSFILEItemAttributeModel { + public func getItemAttributes(atPath path: String) throws -> IONFILEItemAttributeModel { let attributesDictionary = try fileManager.attributesOfItem(atPath: path) return .create(from: attributesDictionary) } @@ -127,7 +127,7 @@ extension OSFILEManager: OSFILEFileManager { } } -private extension OSFILEManager { +private extension IONFILEManager { func withSecurityScopedAccess(to fileURL: URL, perform operation: () throws -> T) throws -> T { // Check if the URL requires security-scoped access let requiresSecurityScope = fileURL.startAccessingSecurityScopedResource() @@ -151,9 +151,9 @@ private extension OSFILEManager { try String(contentsOf: fileURL, encoding: stringEncoding) } - func resolveDirectoryURL(forType directoryType: OSFILEDirectoryType, with path: String) throws -> URL { + func resolveDirectoryURL(forType directoryType: IONFILEDirectoryType, with path: String) throws -> URL { guard let directoryURL = directoryType.fetchURL(using: fileManager) else { - throw OSFILEFileManagerError.directoryNotFound + throw IONFILEFileManagerError.directoryNotFound } return path.isEmpty ? directoryURL : directoryURL.urlWithAppendingPath(path) @@ -161,7 +161,7 @@ private extension OSFILEManager { func resolveRawURL(from path: String) throws -> URL { guard let rawURL = URL(string: path) else { - throw OSFILEFileManagerError.cantCreateURL + throw IONFILEFileManagerError.cantCreateURL } return rawURL } @@ -182,7 +182,7 @@ private extension OSFILEManager { } } -private extension OSFILEDirectoryType { +private extension IONFILEDirectoryType { struct Keys { static let noCloudPath = "NoCloud" } diff --git a/OSFilesystemLib/URL+Extension.swift b/IONFilesystemLib/URL+Extension.swift similarity index 100% rename from OSFilesystemLib/URL+Extension.swift rename to IONFilesystemLib/URL+Extension.swift diff --git a/OSFilesystemLibTests/OSFILEDirectoryManagerTests.swift b/IONFilesystemLibTests/IONFILEDirectoryManagerTests.swift similarity index 94% rename from OSFilesystemLibTests/OSFILEDirectoryManagerTests.swift rename to IONFilesystemLibTests/IONFILEDirectoryManagerTests.swift index 0049641..c023594 100644 --- a/OSFilesystemLibTests/OSFILEDirectoryManagerTests.swift +++ b/IONFilesystemLibTests/IONFILEDirectoryManagerTests.swift @@ -1,9 +1,9 @@ import XCTest -@testable import OSFilesystemLib +@testable import IONFilesystemLib -final class OSFILEDirectoryManagerTests: XCTestCase { - private var sut: OSFILEManager! +final class IONFILEDirectoryManagerTests: XCTestCase { + private var sut: IONFILEManager! // MARK: - 'createDirectory' tests func test_createDirectory_shouldBeSuccessful() throws { @@ -88,7 +88,7 @@ final class OSFILEDirectoryManagerTests: XCTestCase { try sut.removeDirectory(atURL: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories) ) { // Then - XCTAssertEqual($0 as? OSFILEDirectoryManagerError, .notEmpty) + XCTAssertEqual($0 as? IONFILEDirectoryManagerError, .notEmpty) } } @@ -136,10 +136,10 @@ final class OSFILEDirectoryManagerTests: XCTestCase { } } -private extension OSFILEDirectoryManagerTests { +private extension IONFILEDirectoryManagerTests { @discardableResult func createFileManager(with error: MockFileManagerError? = nil, shouldDirectoryHaveContent: Bool = false) -> MockFileManager { let fileManager = MockFileManager(error: error, shouldDirectoryHaveContent: shouldDirectoryHaveContent) - sut = OSFILEManager(fileManager: fileManager) + sut = IONFILEManager(fileManager: fileManager) return fileManager } diff --git a/OSFilesystemLibTests/OSFILEFileManagerTests.swift b/IONFilesystemLibTests/IONFILEFileManagerTests.swift similarity index 93% rename from OSFilesystemLibTests/OSFILEFileManagerTests.swift rename to IONFilesystemLibTests/IONFILEFileManagerTests.swift index e0bb1d8..922508d 100644 --- a/OSFilesystemLibTests/OSFILEFileManagerTests.swift +++ b/IONFilesystemLibTests/IONFILEFileManagerTests.swift @@ -1,10 +1,10 @@ import Combine import XCTest -@testable import OSFilesystemLib +@testable import IONFilesystemLib -final class OSFILEFileManagerTests: XCTestCase { - private var sut: OSFILEManager! +final class IONFILEFileManagerTests: XCTestCase { + private var sut: IONFILEManager! private var cancellables: Set! override func setUp() { @@ -18,7 +18,7 @@ final class OSFILEFileManagerTests: XCTestCase { } // MARK: - 'readEntireFile` tests -extension OSFILEFileManagerTests { +extension IONFILEFileManagerTests { func test_readEntireFile_withStringEncoding_returnsContentSuccessfully() throws { // Given createFileManager() @@ -47,7 +47,7 @@ extension OSFILEFileManagerTests { } // MARK: - 'readFileInChunks' tests -extension OSFILEFileManagerTests { +extension IONFILEFileManagerTests { func test_readFileInChunks_withStringEncoding_returnsContentSuccessfully() throws { // Given createFileManager() @@ -83,7 +83,7 @@ extension OSFILEFileManagerTests { forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .string(encoding: .utf8), forceURLError: true )) { // Then - XCTAssertEqual($0 as? OSFILEChunkPublisherError, .notAbleToReadFile) + XCTAssertEqual($0 as? IONFILEChunkPublisherError, .notAbleToReadFile) } } @@ -96,19 +96,19 @@ extension OSFILEFileManagerTests { forFile: (Configuration.fileWithEmojiName, Configuration.fileExtension), withEncoding: .string(encoding: .ascii) )) { // Then - XCTAssertEqual($0 as? OSFILEChunkPublisherError, .cantEncodeData) + XCTAssertEqual($0 as? IONFILEChunkPublisherError, .cantEncodeData) } } } // MARK: - 'getFileURL' tests -extension OSFILEFileManagerTests { +extension IONFILEFileManagerTests { func test_getFileURL_fromDirectorySearchPath_containingSingleFile_returnsFileSuccessfully() throws { // Given let fileURL: URL = try XCTUnwrap(.init(string: "/file/directory")) let fileManager = createFileManager(urlsWithinDirectory: [fileURL]) let filePath = "/test/directory" - let directoryType = OSFILEDirectoryType.cache + let directoryType = IONFILEDirectoryType.cache // When let returnedURL = try sut.getFileURL(atPath: filePath, withSearchPath: .directory(type: directoryType)) @@ -124,7 +124,7 @@ extension OSFILEFileManagerTests { let ignoredFileURL: URL = try XCTUnwrap(.init(string: "another_file/directory")) let fileManager = createFileManager(urlsWithinDirectory: [fileURL, ignoredFileURL]) let filePath = "/test/directory" - let directoryType = OSFILEDirectoryType.cache + let directoryType = IONFILEDirectoryType.cache // When let returnedURL = try sut.getFileURL(atPath: filePath, withSearchPath: .directory(type: directoryType)) @@ -139,7 +139,7 @@ extension OSFILEFileManagerTests { let fileURL: URL = try XCTUnwrap(.init(string: "/file/directory")) let fileManager = createFileManager(urlsWithinDirectory: [fileURL]) let filePath = "/test/directory" - let directoryType = OSFILEDirectoryType.document + let directoryType = IONFILEDirectoryType.document // When let returnedURL = try sut.getFileURL(atPath: filePath, withSearchPath: .directory(type: directoryType)) @@ -154,7 +154,7 @@ extension OSFILEFileManagerTests { let fileURL: URL = try XCTUnwrap(.init(string: "/file/directory")) let fileManager = createFileManager(urlsWithinDirectory: [fileURL]) let filePath = "/test/directory" - let directoryType = OSFILEDirectoryType.library + let directoryType = IONFILEDirectoryType.library // When let returnedURL = try sut.getFileURL(atPath: filePath, withSearchPath: .directory(type: directoryType)) @@ -169,7 +169,7 @@ extension OSFILEFileManagerTests { let fileURL: URL = try XCTUnwrap(.init(string: "/file/directory")) let fileManager = createFileManager(urlsWithinDirectory: [fileURL]) let filePath = "/test/directory" - let directoryType = OSFILEDirectoryType.notSyncedLibrary + let directoryType = IONFILEDirectoryType.notSyncedLibrary // When let returnedURL = try sut.getFileURL(atPath: filePath, withSearchPath: .directory(type: directoryType)) @@ -185,7 +185,7 @@ extension OSFILEFileManagerTests { let fileURL: URL = parentFolderURL.appending(path: "/directory") let fileManager = createFileManager(urlsWithinDirectory: [fileURL], mockTemporaryDirectory: parentFolderURL) let filePath = "/test/directory" - let directoryType = OSFILEDirectoryType.temporary + let directoryType = IONFILEDirectoryType.temporary // When let returnedURL = try sut.getFileURL(atPath: filePath, withSearchPath: .directory(type: directoryType)) @@ -199,12 +199,12 @@ extension OSFILEFileManagerTests { // Given createFileManager() let filePath = "/test/directory" - let directoryType = OSFILEDirectoryType.cache + let directoryType = IONFILEDirectoryType.cache // When XCTAssertThrowsError(try sut.getFileURL(atPath: filePath, withSearchPath: .directory(type: directoryType))) { // Then - XCTAssertEqual($0 as? OSFILEFileManagerError, .directoryNotFound) + XCTAssertEqual($0 as? IONFILEFileManagerError, .directoryNotFound) } } @@ -213,7 +213,7 @@ extension OSFILEFileManagerTests { let fileURL: URL = try XCTUnwrap(.init(string: "/file/directory")) let fileManager = createFileManager(urlsWithinDirectory: [fileURL]) let emptyFilePath = "" - let directoryType = OSFILEDirectoryType.cache + let directoryType = IONFILEDirectoryType.cache // When let returnedURL = try sut.getFileURL(atPath: emptyFilePath, withSearchPath: .directory(type: directoryType)) @@ -243,13 +243,13 @@ extension OSFILEFileManagerTests { // When XCTAssertThrowsError(try sut.getFileURL(atPath: emptyFilePath, withSearchPath: .raw)) { // Then - XCTAssertEqual($0 as? OSFILEFileManagerError, .cantCreateURL) + XCTAssertEqual($0 as? IONFILEFileManagerError, .cantCreateURL) } } } // MARK: - 'deleteFile' tests -extension OSFILEFileManagerTests { +extension IONFILEFileManagerTests { func test_deleteFile_shouldBeSuccessful() throws { // Given let fileManager = createFileManager() @@ -270,7 +270,7 @@ extension OSFILEFileManagerTests { // When XCTAssertThrowsError(try sut.deleteFile(atURL: filePath)) { // Then - XCTAssertEqual($0 as? OSFILEFileManagerError, .fileNotFound) + XCTAssertEqual($0 as? IONFILEFileManagerError, .fileNotFound) } } @@ -289,14 +289,14 @@ extension OSFILEFileManagerTests { } // MARK: - 'saveFile' tests -extension OSFILEFileManagerTests { +extension IONFILEFileManagerTests { func test_saveFile_withStringEncoding_savesFileSuccessfullyAndReturnsItsURL() throws { // Given let fileManager = createFileManager() let fileURL = try XCTUnwrap(fetchConfigurationFile()) .deletingLastPathComponent() .appending(path: "\(Configuration.newFileName).\(Configuration.fileExtension)") - let stringEncoding = OSFILEStringEncoding.ascii + let stringEncoding = IONFILEStringEncoding.ascii let contentToSave = Configuration.stringEncodedFileContent let shouldIncludeIntermediateDirectories = false @@ -355,7 +355,7 @@ extension OSFILEFileManagerTests { .deletingLastPathComponent() let fileURL = parentFolderURL .appending(path: "\(Configuration.newFileName).\(Configuration.fileExtension)") - let stringEncoding = OSFILEStringEncoding.ascii + let stringEncoding = IONFILEStringEncoding.ascii let contentToSave = Configuration.stringEncodedFileContent let shouldIncludeIntermediateDirectories = true @@ -387,7 +387,7 @@ extension OSFILEFileManagerTests { .deletingLastPathComponent() let fileURL = parentFolderURL .appending(path: "\(Configuration.newFileName).\(Configuration.fileExtension)") - let stringEncoding = OSFILEStringEncoding.ascii + let stringEncoding = IONFILEStringEncoding.ascii let contentToSave = Configuration.stringEncodedFileContent let shouldIncludeIntermediateDirectories = false @@ -398,18 +398,18 @@ extension OSFILEFileManagerTests { includeIntermediateDirectories: shouldIncludeIntermediateDirectories) ) { // Then - XCTAssertEqual($0 as? OSFILEFileManagerError, .missingParentFolder) + XCTAssertEqual($0 as? IONFILEFileManagerError, .missingParentFolder) } } } // MARK: - 'appendData' tests -extension OSFILEFileManagerTests { +extension IONFILEFileManagerTests { func test_appendData_withStringEncoding_savesFileSuccessfully() throws { // Given createFileManager() let fileURL = try XCTUnwrap(fetchConfigurationFile()) - let stringEncoding = OSFILEStringEncoding.ascii + let stringEncoding = IONFILEStringEncoding.ascii let contentToAdd = Configuration.fileExtendedContent // When @@ -468,7 +468,7 @@ extension OSFILEFileManagerTests { .deletingLastPathComponent() let fileURL = parentFolderURL .appending(path: "\(Configuration.newFileName).\(Configuration.fileExtension)") - let stringEncoding = OSFILEStringEncoding.ascii + let stringEncoding = IONFILEStringEncoding.ascii let contentToAdd = Configuration.stringEncodedFileContent let shouldIncludeIntermediateDirectories = true @@ -497,7 +497,7 @@ extension OSFILEFileManagerTests { // Given createFileManager() let fileURL = try XCTUnwrap(fetchConfigurationFile()) - let stringEncoding = OSFILEStringEncoding.ascii + let stringEncoding = IONFILEStringEncoding.ascii let contentToAdd = Configuration.emojiContent // ASCII can't represent emoji so the conversion will fail. // When @@ -507,13 +507,13 @@ extension OSFILEFileManagerTests { includeIntermediateDirectories: false) ) { // Then - XCTAssertEqual($0 as? OSFILEFileManagerError, .cantDecodeData) + XCTAssertEqual($0 as? IONFILEFileManagerError, .cantDecodeData) } } } // MARK: - 'getItemAttributes' tests -extension OSFILEFileManagerTests { +extension IONFILEFileManagerTests { func test_getItemAttributes_forFile_returnsFileAttributeModelSuccessfully() throws { // Given let currentDate = Date() @@ -609,7 +609,7 @@ extension OSFILEFileManagerTests { } // MARK: - 'renameItem' tests -extension OSFILEFileManagerTests { +extension IONFILEFileManagerTests { func test_renameItem_shouldBeSuccessful() throws { // Given let fileManager = createFileManager(fileExists: false) @@ -683,7 +683,7 @@ extension OSFILEFileManagerTests { } // MARK: - 'copyItem' tests -extension OSFILEFileManagerTests { +extension IONFILEFileManagerTests { func test_copyItem_shouldBeSuccessful() throws { // Given let fileManager = createFileManager(fileExists: false) @@ -756,7 +756,7 @@ extension OSFILEFileManagerTests { } } -private extension OSFILEFileManagerTests { +private extension IONFILEFileManagerTests { struct Configuration { static let fileName = "file" static let fileWithEmojiName = "file_emojiContent" @@ -813,19 +813,19 @@ private extension OSFILEFileManagerTests { shouldBeDirectory: shouldBeDirectory, mockTemporaryDirectory: mockTemporaryDirectory ) - sut = OSFILEManager(fileManager: fileManager) + sut = IONFILEManager(fileManager: fileManager) return fileManager } - func fetchEntireContent(forFile file: (name: String, extension: String), withEncoding encoding: OSFILEEncoding) throws -> String { + func fetchEntireContent(forFile file: (name: String, extension: String), withEncoding encoding: IONFILEEncoding) throws -> String { let fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: file.name, withExtension: file.extension)) return try treatContent(withEncoding: encoding) { try sut.readEntireFile(atURL: fileURL, withEncoding: encoding) } } - func fetchChunkedContent(forFile file: (name: String, extension: String), withEncoding encoding: OSFILEEncoding, forceURLError: Bool = false) throws -> String { + func fetchChunkedContent(forFile file: (name: String, extension: String), withEncoding encoding: IONFILEEncoding, forceURLError: Bool = false) throws -> String { var fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: file.name, withExtension: file.extension)) return try treatContent(withEncoding: encoding) { var result = String() @@ -854,7 +854,7 @@ private extension OSFILEFileManagerTests { } } - func treatContent(withEncoding encoding: OSFILEEncoding, afterReading readOperation: () throws -> String) throws -> String { + func treatContent(withEncoding encoding: IONFILEEncoding, afterReading readOperation: () throws -> String) throws -> String { let fileURLContent = try readOperation() var fileURLUnicodeScalars: String.UnicodeScalarView diff --git a/OSFilesystemLibTests/MockFileManager.swift b/IONFilesystemLibTests/MockFileManager.swift similarity index 100% rename from OSFilesystemLibTests/MockFileManager.swift rename to IONFilesystemLibTests/MockFileManager.swift diff --git a/OSFilesystemLibTests/file.txt b/IONFilesystemLibTests/file.txt similarity index 100% rename from OSFilesystemLibTests/file.txt rename to IONFilesystemLibTests/file.txt diff --git a/OSFilesystemLibTests/file_emojiContent.txt b/IONFilesystemLibTests/file_emojiContent.txt similarity index 100% rename from OSFilesystemLibTests/file_emojiContent.txt rename to IONFilesystemLibTests/file_emojiContent.txt diff --git a/OSFilesystemLib/OSFILEManager+Protocols.swift b/OSFilesystemLib/OSFILEManager+Protocols.swift deleted file mode 100644 index b1593e1..0000000 --- a/OSFilesystemLib/OSFILEManager+Protocols.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation - -public protocol OSFILEDirectoryManager { - func createDirectory(atURL: URL, includeIntermediateDirectories: Bool) throws - func removeDirectory(atURL: URL, includeIntermediateDirectories: Bool) throws - func listDirectory(atURL: URL) throws -> [URL] -} - -public protocol OSFILEFileManager { - func readEntireFile(atURL: URL, withEncoding: OSFILEEncoding) throws -> String - func readFileInChunks(atURL: URL, withEncoding: OSFILEEncoding, andChunkSize: Int) throws -> OSFILEChunkPublisher - func getFileURL(atPath: String, withSearchPath: OSFILESearchPath) throws -> URL - func deleteFile(atURL: URL) throws - func saveFile(atURL: URL, withEncodingAndData: OSFILEEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL - func appendData(_ data: OSFILEEncodingValueMapper, atURL: URL, includeIntermediateDirectories: Bool) throws - func getItemAttributes(atPath: String) throws -> OSFILEItemAttributeModel - func renameItem(fromURL: URL, toURL: URL) throws - func copyItem(fromURL: URL, toURL: URL) throws -} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 1a24ea7..56f42d4 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -18,14 +18,14 @@ default_platform(:ios) platform :ios do desc "Lane to run the unit tests" lane :unit_tests do - run_tests(scheme: "OSFilesystemLib") + run_tests(scheme: "IONFilesystemLib") end desc "Code coverage" lane :coverage do slather( - scheme: "OSFilesystemLib", - proj: "OSFilesystemLib.xcodeproj", + scheme: "IONFilesystemLib", + proj: "IONFilesystemLib.xcodeproj", output_directory: "sonar-reports", sonarqube_xml: "true" ) @@ -33,7 +33,7 @@ platform :ios do lane :lint do swiftlint( - output_file: "sonar-reports/OSFilesystemLib-swiftlint.txt", + output_file: "sonar-reports/IONFilesystemLib-swiftlint.txt", ignore_exit_status: true ) end diff --git a/scripts/build_framework.sh b/scripts/build_framework.sh index c072d75..2114de8 100755 --- a/scripts/build_framework.sh +++ b/scripts/build_framework.sh @@ -1,6 +1,6 @@ BUILD_FOLDER="build" -BUILD_SCHEME="OSFilesystemLib" -FRAMEWORK_NAME="OSFilesystemLib" +BUILD_SCHEME="IONFilesystemLib" +FRAMEWORK_NAME="IONFilesystemLib" SIMULATOR_ARCHIVE_PATH="${BUILD_FOLDER}/iphonesimulator.xcarchive" IOS_DEVICE_ARCHIVE_PATH="${BUILD_FOLDER}/iphoneos.xcarchive" diff --git a/scripts/bump_versions.rb b/scripts/bump_versions.rb index 80821e8..de5216a 100644 --- a/scripts/bump_versions.rb +++ b/scripts/bump_versions.rb @@ -6,7 +6,7 @@ level = ARGV[0] # Define the path to your .podspec file -podspec_path = "./OSFilesystemLib.podspec" +podspec_path = "./IONFilesystemLib.podspec" # Read the .podspec file podspec_content = File.read(podspec_path) @@ -48,7 +48,7 @@ File.write(podspec_path, new_podspec_content) # Set the application name -LIBRARY_NAME = "OSFilesystemLib" +LIBRARY_NAME = "IONFilesystemLib" # Set the Xcode project file path project_file = "#{LIBRARY_NAME}.xcodeproj/project.pbxproj" @@ -69,4 +69,10 @@ # Write the updated content back to the project file File.open(project_file, "w") { |file| file.puts updated_content } +readme_path = "./README.md" +readme_content = File.read(readme_path) +new_readme_content = readme_content.gsub(/(pod 'IONFilesystemLib', '~> )\d+\.\d+\.\d+/, "\\1#{new_version_number}\\2") + .gsub(/(# Use the latest )\d+\.\d+/, "\\1#{[major, minor].join('.')}\\2") +File.write(readme_path, new_readme_content) + puts "Version updated to #{new_version_number} (Build Number ##{new_build_number})" \ No newline at end of file From 4c4d1f8fdedf23f92e5376ea460863dc4611eedc Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Thu, 30 Jan 2025 15:32:27 +0000 Subject: [PATCH 22/31] chore: add GitHub Actions workflows --- .github/workflows/continuous_integration.yml | 34 ++++++++ .github/workflows/prepare_release.yml | 81 ++++++++++++++++++++ .github/workflows/release_and_publish.yml | 67 ++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 .github/workflows/continuous_integration.yml create mode 100644 .github/workflows/prepare_release.yml create mode 100644 .github/workflows/release_and_publish.yml diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml new file mode 100644 index 0000000..3e935af --- /dev/null +++ b/.github/workflows/continuous_integration.yml @@ -0,0 +1,34 @@ +name: Continuous Integration + +on: + push: + branches: + - main, development + pull_request: + types: [opened, synchronize, reopened] + +jobs: + sonarcloud: + name: Unit-Tests + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Link SwiftLint or install it + run: brew link --overwrite swiftlint || brew install swiftlint + + - name: Set up XCode + run: sudo xcode-select --switch /Applications/Xcode_15.0.app + + - name: Bundle Install + run: bundle install + + - name: Unit tests + run: bundle exec fastlane unit_tests + + - name: Code Coverage + run: bundle exec fastlane coverage + + - name: Lint + run: bundle exec fastlane lint \ No newline at end of file diff --git a/.github/workflows/prepare_release.yml b/.github/workflows/prepare_release.yml new file mode 100644 index 0000000..6d3256a --- /dev/null +++ b/.github/workflows/prepare_release.yml @@ -0,0 +1,81 @@ +name: Prepare Release + +on: + workflow_dispatch: + inputs: + versionBumpLevel: + description: 'Version bump level (patch, minor, major)' + required: true + type: choice + default: 'patch' + options: + - patch + - minor + - major + +jobs: + build-and-release: + if: github.ref == 'refs/heads/main' + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Link SwiftLint or install it + run: brew link --overwrite swiftlint || brew install swiftlint + + - name: Set up XCode + run: sudo xcode-select --switch /Applications/Xcode_15.0.app + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + + - name: Bump version + run: ruby ./scripts/bump_versions.rb ${{ github.event.inputs.versionBumpLevel }} + + - name: Build XCFramework + run: ./scripts/build_framework.sh + + - name: Get new version + id: version + run: echo "VERSION=$(ruby -e 'puts File.read("./IONFilesystemLib.podspec").match(/spec.version.*=.*''(\d+\.\d+\.\d+)''/)[1]')" >> $GITHUB_ENV + + - name: Create new branch + run: | + git switch --create "prepare-new-release-${{ env.VERSION }}" + + - name: Move zip file to root and push changes + run: | + if [ -f IONFilesystemLib.zip ]; then + rm IONFilesystemLib.zip + else + echo "File does not exist." + fi + mv build/IONFilesystemLib.zip . + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + git add . + git commit -m "chore: Bump version to ${{ env.VERSION }}" + git push origin HEAD:prepare-new-release-${{ env.VERSION }} + + - name: Create pull request + id: create_pr + run: | + gh pr create -B main -H prepare-new-release-${{ env.VERSION }} --title 'Prepare `main` to Release `${{ env.VERSION }}`' --body 'Bumps version to `${{ env.VERSION }}`.
Creates an updated and ready-to-be-released `IONFilesystemLib.zip`.' + PR_NUMBER=$(gh pr view --json number --jq '.number') + echo "PR_NUMBER=${PR_NUMBER}" >> $GITHUB_ENV + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Add label to the pull request + run: | + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/${{ github.repository }}/issues/${{ env.PR_NUMBER }}/labels \ + -f "labels[]=release" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release_and_publish.yml b/.github/workflows/release_and_publish.yml new file mode 100644 index 0000000..6032bd8 --- /dev/null +++ b/.github/workflows/release_and_publish.yml @@ -0,0 +1,67 @@ +name: Release and Publish + +on: + pull_request: + types: [closed] + branches: + - 'main' + +jobs: + post-merge: + if: contains(github.event.pull_request.labels.*.name, 'release') && github.event.pull_request.merged == true + runs-on: macos-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Cocoapods + run: gem install cocoapods + + - name: Get new version + id: version + run: echo "VERSION=$(ruby -e 'puts File.read("./IONFilesystemLib.podspec").match(/spec.version.*=.*''(\d+\.\d+\.\d+)''/)[1]')" >> $GITHUB_ENV + + - name: Extract release notes + run: sh scripts/extract_release_notes.sh "${{ env.VERSION }}" >> release_notes.md + + - name: Create Tag + id: create_tag + run: | + # Define the tag name and message + TAG_NAME="${{ env.VERSION }}" + TAG_MESSAGE="Tag for version ${{ env.VERSION }}" + + # Create the tag + git tag -a "$TAG_NAME" -m "$TAG_MESSAGE" + git push origin "$TAG_NAME" + + echo "Tag created: $TAG_NAME" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create Release + run: | + # Extract the tag name + TAG_NAME="${{ env.VERSION }}" + RELEASE_NOTES="$(cat release_notes.md)" + + # Create the release using GitHub CLI + gh release create "$TAG_NAME" \ + --title "$TAG_NAME" \ + --notes "$RELEASE_NOTES" \ + "IONFilesystemLib.zip" + + echo "Release created for tag: $TAG_NAME" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Deploy to Cocoapods + run: pod trunk push ./IONFilesystemLib.podspec --allow-warnings + env: + COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} + + - name: Delete Release Branch + run: git push origin --delete prepare-new-release-${{ env.VERSION }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From f8aa0ae429bd98524e0cb02acbe7fde39d0899b4 Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Thu, 30 Jan 2025 16:01:26 +0000 Subject: [PATCH 23/31] chore: add SwiftPackageManager file --- IONFilesystemLib.xcodeproj/project.pbxproj | 2 ++ Package.swift | 24 ++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 Package.swift diff --git a/IONFilesystemLib.xcodeproj/project.pbxproj b/IONFilesystemLib.xcodeproj/project.pbxproj index 3162958..ff060e1 100644 --- a/IONFilesystemLib.xcodeproj/project.pbxproj +++ b/IONFilesystemLib.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 751328D42D3175170031BDD0 /* IONFILEManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IONFILEManager.swift; sourceTree = ""; }; 751328D82D3179430031BDD0 /* MockFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFileManager.swift; sourceTree = ""; }; 751328D92D318DBA0031BDD0 /* IONFILEDirectoryManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IONFILEDirectoryManagerTests.swift; sourceTree = ""; }; + 754D90BC2D4BD81000AD7FD4 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 7575CF612BFCEE6F008F3FD0 /* IONFilesystemLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = IONFilesystemLib.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7575CF692BFCEE6F008F3FD0 /* IONFilesystemLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IONFilesystemLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 75DA44532D48E435006DF7DE /* IONFILEChunkPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IONFILEChunkPublisher.swift; sourceTree = ""; }; @@ -71,6 +72,7 @@ 7575CF572BFCEE6F008F3FD0 = { isa = PBXGroup; children = ( + 754D90BC2D4BD81000AD7FD4 /* Package.swift */, 7575CF632BFCEE6F008F3FD0 /* IONFilesystemLib */, 7575CF6D2BFCEE6F008F3FD0 /* IONFilesystemLibTests */, 7575CF622BFCEE6F008F3FD0 /* Products */, diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..5d5f783 --- /dev/null +++ b/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "IONFilesystemLib", + platforms: [.iOS(.v14)], + products: [ + .library( + name: "IONFilesystemLib", + targets: ["IONFilesystemLib"] + ) + ], + targets: [ + .target( + name: "IONFilesystemLib", + path: "IONFilesystemLib" + ), + .testTarget( + name: "IONFilesystemLibTests", + dependencies: ["IONFilesystemLib"], + path: "IONFilesystemLibTests" + ) + ] +) From 1d8421bc6f7c28da193761391f9357ccdf0de793 Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Thu, 30 Jan 2025 16:04:24 +0000 Subject: [PATCH 24/31] chore: set initial project and marketing versions --- IONFilesystemLib.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/IONFilesystemLib.xcodeproj/project.pbxproj b/IONFilesystemLib.xcodeproj/project.pbxproj index ff060e1..ce20d7c 100644 --- a/IONFilesystemLib.xcodeproj/project.pbxproj +++ b/IONFilesystemLib.xcodeproj/project.pbxproj @@ -418,7 +418,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -433,7 +433,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 0.0.1; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; PRODUCT_BUNDLE_IDENTIFIER = io.ionic.libs.filesystem.FilesystemLib; @@ -453,7 +453,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -468,7 +468,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 0.0.1; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; PRODUCT_BUNDLE_IDENTIFIER = io.ionic.libs.filesystem.FilesystemLib; @@ -485,10 +485,10 @@ buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.0; - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 0.0.1; PRODUCT_BUNDLE_IDENTIFIER = io.ionic.libs.filesystem.FilesystemLibTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -506,10 +506,10 @@ buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.0; - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 0.0.1; PRODUCT_BUNDLE_IDENTIFIER = io.ionic.libs.filesystem.FilesystemLibTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; From 604841201290853fb0791f327f8102d712761a91 Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Thu, 30 Jan 2025 16:26:05 +0000 Subject: [PATCH 25/31] fix: failing workflow unit tests Removed a readFileInChunks test as it proved unreliable for versions below iOS 18. --- IONFilesystemLib.xcodeproj/project.pbxproj | 4 -- .../IONFILEFileManagerTests.swift | 44 +++++-------------- IONFilesystemLibTests/file_emojiContent.txt | 1 - 3 files changed, 10 insertions(+), 39 deletions(-) delete mode 100644 IONFilesystemLibTests/file_emojiContent.txt diff --git a/IONFilesystemLib.xcodeproj/project.pbxproj b/IONFilesystemLib.xcodeproj/project.pbxproj index ce20d7c..792d4ce 100644 --- a/IONFilesystemLib.xcodeproj/project.pbxproj +++ b/IONFilesystemLib.xcodeproj/project.pbxproj @@ -12,7 +12,6 @@ 751328DB2D318E770031BDD0 /* MockFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 751328D82D3179430031BDD0 /* MockFileManager.swift */; }; 7575CF6A2BFCEE6F008F3FD0 /* IONFilesystemLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7575CF612BFCEE6F008F3FD0 /* IONFilesystemLib.framework */; }; 75DA44542D48E435006DF7DE /* IONFILEChunkPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75DA44532D48E435006DF7DE /* IONFILEChunkPublisher.swift */; }; - 75DA445A2D490051006DF7DE /* file_emojiContent.txt in Resources */ = {isa = PBXBuildFile; fileRef = 75DA44592D490051006DF7DE /* file_emojiContent.txt */; }; 75F8380B2D37E42000FCE044 /* IONFILEItemAttributeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F8380A2D37E42000FCE044 /* IONFILEItemAttributeModel.swift */; }; 75F84D662D39360E00892C89 /* IONFILEManager+Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F84D652D39360E00892C89 /* IONFILEManager+Errors.swift */; }; 75F84D682D39362F00892C89 /* IONFILEManager+Enums.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F84D672D39362F00892C89 /* IONFILEManager+Enums.swift */; }; @@ -40,7 +39,6 @@ 7575CF612BFCEE6F008F3FD0 /* IONFilesystemLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = IONFilesystemLib.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7575CF692BFCEE6F008F3FD0 /* IONFilesystemLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IONFilesystemLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 75DA44532D48E435006DF7DE /* IONFILEChunkPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IONFILEChunkPublisher.swift; sourceTree = ""; }; - 75DA44592D490051006DF7DE /* file_emojiContent.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = file_emojiContent.txt; sourceTree = ""; }; 75F8380A2D37E42000FCE044 /* IONFILEItemAttributeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IONFILEItemAttributeModel.swift; sourceTree = ""; }; 75F84D652D39360E00892C89 /* IONFILEManager+Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IONFILEManager+Errors.swift"; sourceTree = ""; }; 75F84D672D39362F00892C89 /* IONFILEManager+Enums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IONFILEManager+Enums.swift"; sourceTree = ""; }; @@ -109,7 +107,6 @@ 751328D92D318DBA0031BDD0 /* IONFILEDirectoryManagerTests.swift */, 75FEB2B32D35479B007C2686 /* IONFILEFileManagerTests.swift */, 75FEB2B62D355F21007C2686 /* file.txt */, - 75DA44592D490051006DF7DE /* file_emojiContent.txt */, ); path = IONFilesystemLibTests; sourceTree = ""; @@ -218,7 +215,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 75DA445A2D490051006DF7DE /* file_emojiContent.txt in Resources */, 75FEB2B72D355F21007C2686 /* file.txt in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/IONFilesystemLibTests/IONFILEFileManagerTests.swift b/IONFilesystemLibTests/IONFILEFileManagerTests.swift index 922508d..863b57a 100644 --- a/IONFilesystemLibTests/IONFILEFileManagerTests.swift +++ b/IONFilesystemLibTests/IONFILEFileManagerTests.swift @@ -86,19 +86,6 @@ extension IONFILEFileManagerTests { XCTAssertEqual($0 as? IONFILEChunkPublisherError, .notAbleToReadFile) } } - - func test_readFileInChunks_withInvalidContent_returnsError() { - // Given - createFileManager() - - // When - XCTAssertThrowsError(try fetchChunkedContent( - forFile: (Configuration.fileWithEmojiName, Configuration.fileExtension), withEncoding: .string(encoding: .ascii) - )) { - // Then - XCTAssertEqual($0 as? IONFILEChunkPublisherError, .cantEncodeData) - } - } } // MARK: - 'getFileURL' tests @@ -311,9 +298,7 @@ extension IONFILEFileManagerTests { XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories) XCTAssertEqual(savedFileURL, fileURL) - let savedFileContent = try fetchEntireContent( - forFile: (Configuration.newFileName, Configuration.fileExtension), withEncoding: .string(encoding: stringEncoding) - ) + let savedFileContent = try fetchEntireContent(forURL: fileURL, withEncoding: .string(encoding: stringEncoding)) XCTAssertEqual(savedFileContent, contentToSave) try sut.deleteFile(atURL: fileURL) // keep things clean by deleting created file @@ -340,9 +325,7 @@ extension IONFILEFileManagerTests { XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories) XCTAssertEqual(savedFileURL, fileURL) - let savedFileContent = try fetchEntireContent( - forFile: (Configuration.newFileName, Configuration.fileExtension), withEncoding: .byteBuffer - ) + let savedFileContent = try fetchEntireContent(forURL: fileURL, withEncoding: .byteBuffer) XCTAssertEqual(savedFileContent, contentToSave) try sut.deleteFile(atURL: fileURL) // keep things clean by deleting created file @@ -371,9 +354,7 @@ extension IONFILEFileManagerTests { XCTAssertEqual(fileManager.capturedPath, parentFolderURL) XCTAssertEqual(savedFileURL, fileURL) - let savedFileContent = try fetchEntireContent( - forFile: (Configuration.newFileName, Configuration.fileExtension), withEncoding: .string(encoding: stringEncoding) - ) + let savedFileContent = try fetchEntireContent(forURL: fileURL, withEncoding: .string(encoding: stringEncoding)) XCTAssertEqual(savedFileContent, contentToSave) fileManager.fileExists = true @@ -420,10 +401,7 @@ extension IONFILEFileManagerTests { ) // Then - let savedFileContent = try fetchEntireContent( - forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .string(encoding: stringEncoding) - ) - + let savedFileContent = try fetchEntireContent(forURL: fileURL, withEncoding: .string(encoding: stringEncoding)) XCTAssertEqual(savedFileContent, Configuration.fileContent + contentToAdd) try sut.saveFile( // keep things clean by resetting file @@ -448,10 +426,7 @@ extension IONFILEFileManagerTests { ) // Then - let savedFileContent = try fetchEntireContent( - forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .byteBuffer - ) - + let savedFileContent = try fetchEntireContent(forURL: fileURL, withEncoding: .byteBuffer) XCTAssertEqual(savedFileContent, Configuration.fileContent + contentToAdd) try sut.saveFile( // keep things clean by resetting file @@ -483,9 +458,7 @@ extension IONFILEFileManagerTests { XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories) // Then - let savedFileContent = try fetchEntireContent( - forFile: (Configuration.newFileName, Configuration.fileExtension), withEncoding: .string(encoding: stringEncoding) - ) + let savedFileContent = try fetchEntireContent(forURL: fileURL, withEncoding: .string(encoding: stringEncoding)) XCTAssertEqual(savedFileContent, contentToAdd) @@ -759,7 +732,6 @@ extension IONFILEFileManagerTests { private extension IONFILEFileManagerTests { struct Configuration { static let fileName = "file" - static let fileWithEmojiName = "file_emojiContent" static let newFileName = "new_file" static let fileExtension = "txt" static let fileContent = "Hello, world!" @@ -820,6 +792,10 @@ private extension IONFILEFileManagerTests { func fetchEntireContent(forFile file: (name: String, extension: String), withEncoding encoding: IONFILEEncoding) throws -> String { let fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: file.name, withExtension: file.extension)) + return try fetchEntireContent(forURL: fileURL, withEncoding: encoding) + } + + func fetchEntireContent(forURL fileURL: URL, withEncoding encoding: IONFILEEncoding) throws -> String { return try treatContent(withEncoding: encoding) { try sut.readEntireFile(atURL: fileURL, withEncoding: encoding) } diff --git a/IONFilesystemLibTests/file_emojiContent.txt b/IONFilesystemLibTests/file_emojiContent.txt deleted file mode 100644 index 600106b..0000000 --- a/IONFilesystemLibTests/file_emojiContent.txt +++ /dev/null @@ -1 +0,0 @@ -🙃 From b12b0025b0e023aa42b1a3a990fed8778076020f Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Fri, 31 Jan 2025 09:32:11 +0000 Subject: [PATCH 26/31] chore: add CHANGELOG entries --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ae9537..af95e3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,3 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Features +- Add read operations, namely `readEntireFile(atURL:withEncoding:)`, `readFileInChunks(atURL:withEncoding:andChunkSize:)`, `listDirectory(atURL:)`, `getItemAttributes(atPath:)` and `getFileURL(atPath: withSearchPath:)`. +- Add write operations, namely `saveFile(atURL:withEncodingAndData:includeIntermediateDirectories:)` and `appendData(_:atURL:includeIntermediateDirectories:)`. +- Add directory operations, namely `createDirectory(atURL:includeIntermediateDirectories:)` and `removeDirectory(atURL:includeIntermediateDirectories:)`. +- Add file management operations, namely `deleteFile(atURL:)`, `renameItem(fromURL:toURL:)` and `copyItem(fromURL:toURL:)`. + +### Chores +- Add dependency management contract file for CocoaPods and Swift Package Manager. +- Add GitHub Actions workflows. +- Create Repository \ No newline at end of file From e4ee1b3cffdbd7182121378fe16a66d589bee5a3 Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Fri, 31 Jan 2025 15:00:34 +0000 Subject: [PATCH 27/31] fix: add possibility to return byteBuffer on both read file methods This aligns the read methods with the approach already used for write. --- IONFilesystemLib/IONFILEChunkPublisher.swift | 10 +++++----- IONFilesystemLib/IONFILEManager+Protocols.swift | 2 +- IONFilesystemLib/IONFILEManager.swift | 15 ++++++++++----- .../IONFILEFileManagerTests.swift | 17 ++++++++++++----- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/IONFilesystemLib/IONFILEChunkPublisher.swift b/IONFilesystemLib/IONFILEChunkPublisher.swift index a1a035f..5489ab9 100644 --- a/IONFilesystemLib/IONFILEChunkPublisher.swift +++ b/IONFilesystemLib/IONFILEChunkPublisher.swift @@ -2,7 +2,7 @@ import Combine import Foundation public class IONFILEChunkPublisher: Publisher { - public typealias Output = String + public typealias Output = IONFILEEncodingValueMapper public typealias Failure = Error private let url: URL @@ -21,7 +21,7 @@ public class IONFILEChunkPublisher: Publisher { } } -private class IONFILEChunkSubscription: Subscription where S.Input == String, S.Failure == Error { +private class IONFILEChunkSubscription: Subscription where S.Input == IONFILEEncodingValueMapper, S.Failure == Error { private let fileHandle: FileHandle? private let chunkSize: Int private let encoding: IONFILEEncoding @@ -43,14 +43,14 @@ private class IONFILEChunkSubscription: Subscription where S.Inpu while demand > .none { do { if let chunk = try fileHandle.read(upToCount: chunkSize), !chunk.isEmpty { - let chunkToEmit: String + let chunkToEmit: IONFILEEncodingValueMapper switch encoding { - case .byteBuffer: chunkToEmit = chunk.base64EncodedString() + case .byteBuffer: chunkToEmit = .byteBuffer(value: chunk) case .string(let encoding): guard let chunkText = String(data: chunk, encoding: encoding.stringEncoding) else { throw IONFILEChunkPublisherError.cantEncodeData } - chunkToEmit = chunkText + chunkToEmit = .string(encoding: encoding, value: chunkText) } _ = subscriber.receive(chunkToEmit) diff --git a/IONFilesystemLib/IONFILEManager+Protocols.swift b/IONFilesystemLib/IONFILEManager+Protocols.swift index 489ef52..63e148a 100644 --- a/IONFilesystemLib/IONFILEManager+Protocols.swift +++ b/IONFilesystemLib/IONFILEManager+Protocols.swift @@ -7,7 +7,7 @@ public protocol IONFILEDirectoryManager { } public protocol IONFILEFileManager { - func readEntireFile(atURL: URL, withEncoding: IONFILEEncoding) throws -> String + func readEntireFile(atURL: URL, withEncoding: IONFILEEncoding) throws -> IONFILEEncodingValueMapper func readFileInChunks(atURL: URL, withEncoding: IONFILEEncoding, andChunkSize: Int) throws -> IONFILEChunkPublisher func getFileURL(atPath: String, withSearchPath: IONFILESearchPath) throws -> URL func deleteFile(atURL: URL) throws diff --git a/IONFilesystemLib/IONFILEManager.swift b/IONFilesystemLib/IONFILEManager.swift index 3570ce9..76861ba 100644 --- a/IONFilesystemLib/IONFILEManager.swift +++ b/IONFilesystemLib/IONFILEManager.swift @@ -30,14 +30,19 @@ extension IONFILEManager: IONFILEDirectoryManager { } extension IONFILEManager: IONFILEFileManager { - public func readEntireFile(atURL fileURL: URL, withEncoding encoding: IONFILEEncoding) throws -> String { + public func readEntireFile(atURL fileURL: URL, withEncoding encoding: IONFILEEncoding) throws -> IONFILEEncodingValueMapper { try withSecurityScopedAccess(to: fileURL) { + let result: IONFILEEncodingValueMapper switch encoding { case .byteBuffer: - try readFileAsBase64EncodedString(from: fileURL) + let fileData = try readFileAsByteBuffer(from: fileURL) + result = .byteBuffer(value: fileData) case .string(let stringEncoding): - try readFileAsString(from: fileURL, using: stringEncoding.stringEncoding) + let fileData = try readFileAsString(from: fileURL, using: stringEncoding.stringEncoding) + result = .string(encoding: stringEncoding, value: fileData) } + + return result } } @@ -143,8 +148,8 @@ private extension IONFILEManager { return try operation() } - func readFileAsBase64EncodedString(from fileURL: URL) throws -> String { - try Data(contentsOf: fileURL).base64EncodedString() + func readFileAsByteBuffer(from fileURL: URL) throws -> Data { + try Data(contentsOf: fileURL) } func readFileAsString(from fileURL: URL, using stringEncoding: String.Encoding) throws -> String { diff --git a/IONFilesystemLibTests/IONFILEFileManagerTests.swift b/IONFilesystemLibTests/IONFILEFileManagerTests.swift index 863b57a..7336972 100644 --- a/IONFilesystemLibTests/IONFILEFileManagerTests.swift +++ b/IONFilesystemLibTests/IONFILEFileManagerTests.swift @@ -797,14 +797,17 @@ private extension IONFILEFileManagerTests { func fetchEntireContent(forURL fileURL: URL, withEncoding encoding: IONFILEEncoding) throws -> String { return try treatContent(withEncoding: encoding) { - try sut.readEntireFile(atURL: fileURL, withEncoding: encoding) + switch try sut.readEntireFile(atURL: fileURL, withEncoding: encoding) { + case .byteBuffer(let fileData): fileData.base64EncodedString() + case .string(_, let fileData): fileData + } } } func fetchChunkedContent(forFile file: (name: String, extension: String), withEncoding encoding: IONFILEEncoding, forceURLError: Bool = false) throws -> String { var fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: file.name, withExtension: file.extension)) return try treatContent(withEncoding: encoding) { - var result = String() + var result = [String]() var error: Error? let expectation = XCTestExpectation(description: "Wait for chunks to be processed") @@ -817,8 +820,12 @@ private extension IONFILEFileManagerTests { error = failure } expectation.fulfill() - }, receiveValue: { - result.append($0) + }, receiveValue: { value in + let chunkToAdd = switch value { + case .byteBuffer(let chunkData): chunkData.base64EncodedString() + case .string(_, let chunkData): chunkData + } + result.append(chunkToAdd) }) .store(in: &cancellables) @@ -826,7 +833,7 @@ private extension IONFILEFileManagerTests { wait(for: [expectation], timeout: 1.0) if let error { throw error } - return result + return result.joined() } } From 0cf72025c1ffc5cdd12657915f05297af6677a8d Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Fri, 31 Jan 2025 17:28:52 +0000 Subject: [PATCH 28/31] chore: address PR comments --- IONFilesystemLib/IONFILEChunkPublisher.swift | 10 ++-------- .../IONFILEDirectoryManagerTests.swift | 2 +- IONFilesystemLibTests/MockFileManager.swift | 13 +++++++------ 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/IONFilesystemLib/IONFILEChunkPublisher.swift b/IONFilesystemLib/IONFILEChunkPublisher.swift index 5489ab9..e493fea 100644 --- a/IONFilesystemLib/IONFILEChunkPublisher.swift +++ b/IONFilesystemLib/IONFILEChunkPublisher.swift @@ -30,7 +30,7 @@ private class IONFILEChunkSubscription: Subscription where S.Inpu init(_ url: URL, _ chunkSize: Int, _ encoding: IONFILEEncoding, _ subscriber: S) { self.fileHandle = try? FileHandle(forReadingFrom: url) - self.chunkSize = Self.chunkSizeToUse(basedOn: chunkSize, and: encoding) + self.chunkSize = chunkSize self.encoding = encoding self.subscriber = subscriber } @@ -72,14 +72,8 @@ private class IONFILEChunkSubscription: Subscription where S.Inpu deinit { fileHandle?.closeFile() } -} - -private extension IONFILEChunkSubscription { - static func chunkSizeToUse(basedOn chunkSize: Int, and encoding: IONFILEEncoding) -> Int { - encoding == .byteBuffer ? chunkSize - chunkSize % 3 + 3 : chunkSize - } - func complete(withValue value: Subscribers.Completion) { + private func complete(withValue value: Subscribers.Completion) { isCompleted = true subscriber.receive(completion: value) } diff --git a/IONFilesystemLibTests/IONFILEDirectoryManagerTests.swift b/IONFilesystemLibTests/IONFILEDirectoryManagerTests.swift index c023594..6f4c8cb 100644 --- a/IONFilesystemLibTests/IONFILEDirectoryManagerTests.swift +++ b/IONFilesystemLibTests/IONFILEDirectoryManagerTests.swift @@ -107,7 +107,7 @@ final class IONFILEDirectoryManagerTests: XCTestCase { } // MARK: - 'listDirectory' tests - func test_listDirectory_withContent_shouldReturnEmptyArray() throws { + func test_listDirectory_withContent_shouldReturnNotEmptyArray() throws { // Given let fileManager = createFileManager(shouldDirectoryHaveContent: true) let testDirectory = URL(filePath: "/test/directory") diff --git a/IONFilesystemLibTests/MockFileManager.swift b/IONFilesystemLibTests/MockFileManager.swift index 9fb73cb..21a9df7 100644 --- a/IONFilesystemLibTests/MockFileManager.swift +++ b/IONFilesystemLibTests/MockFileManager.swift @@ -1,13 +1,14 @@ import Foundation class MockFileManager: FileManager { - var error: MockFileManagerError? - var shouldDirectoryHaveContent: Bool - var urlsWithinDirectory: [URL] + private let error: MockFileManagerError? + private let shouldDirectoryHaveContent: Bool + private let urlsWithinDirectory: [URL] + private let fileAttributes: [FileAttributeKey: Any] + private let shouldBeDirectory: ObjCBool + private let mockTemporaryDirectory: URL? + var fileExists: Bool - var fileAttributes: [FileAttributeKey: Any] - var shouldBeDirectory: ObjCBool - var mockTemporaryDirectory: URL? private(set) var capturedPath: URL? private(set) var capturedOriginPath: URL? From 9b5630114d5401ce4b24a9998b2f6abc3b52c606 Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Mon, 3 Feb 2025 12:02:33 +0000 Subject: [PATCH 29/31] fix: address PR comments + refactors Remove the return statement from saveFile. Align getItemAttributes with the rest of the protocol's methods: to receive a URL instead of a plain-text path. Add the withSecurityScopedAccess verification to all operations. Add read file validations to validate the behaviour when the file doesn't exists. Refactor the code a bit, to remove unnecessary protocol logic. --- IONFilesystemLib/IONFILEManager+Errors.swift | 1 + .../IONFILEManager+Protocols.swift | 4 +- IONFilesystemLib/IONFILEManager.swift | 131 +++++++++-------- .../IONFILEFileManagerTests.swift | 137 ++++++++++-------- 4 files changed, 152 insertions(+), 121 deletions(-) diff --git a/IONFilesystemLib/IONFILEManager+Errors.swift b/IONFilesystemLib/IONFILEManager+Errors.swift index 6e8bef5..0759fb7 100644 --- a/IONFilesystemLib/IONFILEManager+Errors.swift +++ b/IONFilesystemLib/IONFILEManager+Errors.swift @@ -8,6 +8,7 @@ enum IONFILEFileManagerError: Error { case directoryNotFound case fileNotFound case missingParentFolder + case sameOriginAndDestinationURLs } enum IONFILEChunkPublisherError: Error { diff --git a/IONFilesystemLib/IONFILEManager+Protocols.swift b/IONFilesystemLib/IONFILEManager+Protocols.swift index 63e148a..08dfdc7 100644 --- a/IONFilesystemLib/IONFILEManager+Protocols.swift +++ b/IONFilesystemLib/IONFILEManager+Protocols.swift @@ -11,9 +11,9 @@ public protocol IONFILEFileManager { func readFileInChunks(atURL: URL, withEncoding: IONFILEEncoding, andChunkSize: Int) throws -> IONFILEChunkPublisher func getFileURL(atPath: String, withSearchPath: IONFILESearchPath) throws -> URL func deleteFile(atURL: URL) throws - func saveFile(atURL: URL, withEncodingAndData: IONFILEEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL + func saveFile(atURL: URL, withEncodingAndData: IONFILEEncodingValueMapper, includeIntermediateDirectories: Bool) throws func appendData(_ data: IONFILEEncodingValueMapper, atURL: URL, includeIntermediateDirectories: Bool) throws - func getItemAttributes(atPath: String) throws -> IONFILEItemAttributeModel + func getItemAttributes(atURL: URL) throws -> IONFILEItemAttributeModel func renameItem(fromURL: URL, toURL: URL) throws func copyItem(fromURL: URL, toURL: URL) throws } diff --git a/IONFilesystemLib/IONFILEManager.swift b/IONFilesystemLib/IONFILEManager.swift index 76861ba..aa8ded3 100644 --- a/IONFilesystemLib/IONFILEManager.swift +++ b/IONFilesystemLib/IONFILEManager.swift @@ -10,22 +10,28 @@ public struct IONFILEManager { extension IONFILEManager: IONFILEDirectoryManager { public func createDirectory(atURL pathURL: URL, includeIntermediateDirectories: Bool) throws { - try fileManager.createDirectory(at: pathURL, withIntermediateDirectories: includeIntermediateDirectories) + try withSecurityScopedAccess(to: pathURL) { + try fileManager.createDirectory(at: pathURL, withIntermediateDirectories: includeIntermediateDirectories) + } } public func removeDirectory(atURL pathURL: URL, includeIntermediateDirectories: Bool) throws { - if !includeIntermediateDirectories { - let directoryContents = try listDirectory(atURL: pathURL) - if !directoryContents.isEmpty { - throw IONFILEDirectoryManagerError.notEmpty + try withSecurityScopedAccess(to: pathURL) { + if !includeIntermediateDirectories { + let directoryContents = try listDirectory(atURL: pathURL) + if !directoryContents.isEmpty { + throw IONFILEDirectoryManagerError.notEmpty + } } - } - try fileManager.removeItem(at: pathURL) + try fileManager.removeItem(at: pathURL) + } } public func listDirectory(atURL pathURL: URL) throws -> [URL] { - try fileManager.contentsOfDirectory(at: pathURL, includingPropertiesForKeys: nil) + try withSecurityScopedAccess(to: pathURL) { + try fileManager.contentsOfDirectory(at: pathURL, includingPropertiesForKeys: nil) + } } } @@ -62,72 +68,83 @@ extension IONFILEManager: IONFILEFileManager { } public func deleteFile(atURL url: URL) throws { - guard fileManager.fileExists(atPath: url.urlPath) else { - throw IONFILEFileManagerError.fileNotFound - } + try withSecurityScopedAccess(to: url) { + guard fileManager.fileExists(atPath: url.urlPath) else { + throw IONFILEFileManagerError.fileNotFound + } - try fileManager.removeItem(at: url) + try fileManager.removeItem(at: url) + } } - @discardableResult - public func saveFile(atURL fileURL: URL, withEncodingAndData encodingMapper: IONFILEEncodingValueMapper, includeIntermediateDirectories: Bool) throws -> URL { - let fileDirectoryURL = fileURL.deletingLastPathComponent() - - if !fileManager.fileExists(atPath: fileDirectoryURL.urlPath) { - if includeIntermediateDirectories { - try createDirectory(atURL: fileDirectoryURL, includeIntermediateDirectories: true) - } else { - throw IONFILEFileManagerError.missingParentFolder + public func saveFile(atURL fileURL: URL, withEncodingAndData encodingMapper: IONFILEEncodingValueMapper, includeIntermediateDirectories: Bool) throws { + try withSecurityScopedAccess(to: fileURL) { + let fileDirectoryURL = fileURL.deletingLastPathComponent() + + if !fileManager.fileExists(atPath: fileDirectoryURL.urlPath) { + if includeIntermediateDirectories { + try createDirectory(atURL: fileDirectoryURL, includeIntermediateDirectories: true) + } else { + throw IONFILEFileManagerError.missingParentFolder + } } - } - switch encodingMapper { - case .byteBuffer(let value): - try value.write(to: fileURL) - case .string(let encoding, let value): - try value.write(to: fileURL, atomically: false, encoding: encoding.stringEncoding) + switch encodingMapper { + case .byteBuffer(let value): + try value.write(to: fileURL) + case .string(let encoding, let value): + try value.write(to: fileURL, atomically: false, encoding: encoding.stringEncoding) + } } - - return fileURL } public func appendData(_ encodingMapper: IONFILEEncodingValueMapper, atURL url: URL, includeIntermediateDirectories: Bool) throws { - guard fileManager.fileExists(atPath: url.urlPath) else { - try saveFile(atURL: url, withEncodingAndData: encodingMapper, includeIntermediateDirectories: includeIntermediateDirectories) - return - } - - let dataToAppend: Data - switch encodingMapper { - case .byteBuffer(let value): - dataToAppend = value - case .string(let encoding, let value): - guard let valueData = value.data(using: encoding.stringEncoding) else { - throw IONFILEFileManagerError.cantDecodeData + try withSecurityScopedAccess(to: url) { + guard fileManager.fileExists(atPath: url.urlPath) else { + try saveFile(atURL: url, withEncodingAndData: encodingMapper, includeIntermediateDirectories: includeIntermediateDirectories) + return + } + + let dataToAppend: Data + switch encodingMapper { + case .byteBuffer(let value): + dataToAppend = value + case .string(let encoding, let value): + guard let valueData = value.data(using: encoding.stringEncoding) else { + throw IONFILEFileManagerError.cantDecodeData + } + dataToAppend = valueData } - dataToAppend = valueData - } - let fileHandle = try FileHandle(forWritingTo: url) - try fileHandle.seekToEnd() - try fileHandle.write(contentsOf: dataToAppend) - try fileHandle.close() + let fileHandle = try FileHandle(forWritingTo: url) + try fileHandle.seekToEnd() + try fileHandle.write(contentsOf: dataToAppend) + try fileHandle.close() + } } - public func getItemAttributes(atPath path: String) throws -> IONFILEItemAttributeModel { - let attributesDictionary = try fileManager.attributesOfItem(atPath: path) - return .create(from: attributesDictionary) + public func getItemAttributes(atURL url: URL) throws -> IONFILEItemAttributeModel { + try withSecurityScopedAccess(to: url) { + let attributesDictionary = try fileManager.attributesOfItem(atPath: url.urlPath) + return .create(from: attributesDictionary) + } } public func renameItem(fromURL originURL: URL, toURL destinationURL: URL) throws { - try copy(fromURL: originURL, toURL: destinationURL) { - try fileManager.moveItem(at: originURL, to: destinationURL) + try withSecurityScopedAccess(to: originURL) { + try withSecurityScopedAccess(to: destinationURL) { + try validateIfOperationIsValid(fromURL: originURL, toURL: destinationURL) + try fileManager.moveItem(at: originURL, to: destinationURL) + } } } public func copyItem(fromURL originURL: URL, toURL destinationURL: URL) throws { - try copy(fromURL: originURL, toURL: destinationURL) { - try fileManager.copyItem(at: originURL, to: destinationURL) + try withSecurityScopedAccess(to: originURL) { + try withSecurityScopedAccess(to: destinationURL) { + try validateIfOperationIsValid(fromURL: originURL, toURL: destinationURL) + try fileManager.copyItem(at: originURL, to: destinationURL) + } } } } @@ -171,9 +188,9 @@ private extension IONFILEManager { return rawURL } - func copy(fromURL originURL: URL, toURL destinationURL: URL, performOperation: () throws -> Void) throws { + func validateIfOperationIsValid(fromURL originURL: URL, toURL destinationURL: URL) throws { guard originURL != destinationURL else { - return + throw IONFILEFileManagerError.sameOriginAndDestinationURLs } var isDirectory: ObjCBool = false @@ -182,8 +199,6 @@ private extension IONFILEManager { try deleteFile(atURL: destinationURL) } } - - try performOperation() } } diff --git a/IONFilesystemLibTests/IONFILEFileManagerTests.swift b/IONFilesystemLibTests/IONFILEFileManagerTests.swift index 7336972..f0af477 100644 --- a/IONFilesystemLibTests/IONFILEFileManagerTests.swift +++ b/IONFilesystemLibTests/IONFILEFileManagerTests.swift @@ -44,6 +44,15 @@ extension IONFILEFileManagerTests { // Then XCTAssertEqual(fileContent, Configuration.fileContent) } + + func test_readEntireFile_thatDoesntExist_returnsError() throws { + // Given + createFileManager() + let fileURL: URL = try XCTUnwrap(.init(string: "/file/directory")) + + // When and Then + XCTAssertThrowsError(try fetchEntireContent(forURL: fileURL, withEncoding: .string(encoding: .utf8))) + } } // MARK: - 'readFileInChunks' tests @@ -86,6 +95,15 @@ extension IONFILEFileManagerTests { XCTAssertEqual($0 as? IONFILEChunkPublisherError, .notAbleToReadFile) } } + + func test_readFileInChunks_thatDoesntExist_returnsError() throws { + // Given + createFileManager() + let fileURL: URL = try XCTUnwrap(.init(string: "/file/directory")) + + // When and Then + XCTAssertThrowsError(try fetchChunkedContent(forURL: fileURL, withEncoding: .string(encoding: .utf8))) + } } // MARK: - 'getFileURL' tests @@ -288,7 +306,7 @@ extension IONFILEFileManagerTests { let shouldIncludeIntermediateDirectories = false // When - let savedFileURL = try sut.saveFile( + try sut.saveFile( atURL: fileURL, withEncodingAndData: .string(encoding: stringEncoding, value: contentToSave), includeIntermediateDirectories: shouldIncludeIntermediateDirectories @@ -296,7 +314,6 @@ extension IONFILEFileManagerTests { // Then XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories) - XCTAssertEqual(savedFileURL, fileURL) let savedFileContent = try fetchEntireContent(forURL: fileURL, withEncoding: .string(encoding: stringEncoding)) XCTAssertEqual(savedFileContent, contentToSave) @@ -315,7 +332,7 @@ extension IONFILEFileManagerTests { let shouldIncludeIntermediateDirectories = false // When - let savedFileURL = try sut.saveFile( + try sut.saveFile( atURL: fileURL, withEncodingAndData: .byteBuffer(value: contentToSaveData), includeIntermediateDirectories: shouldIncludeIntermediateDirectories @@ -323,7 +340,6 @@ extension IONFILEFileManagerTests { // Then XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories) - XCTAssertEqual(savedFileURL, fileURL) let savedFileContent = try fetchEntireContent(forURL: fileURL, withEncoding: .byteBuffer) XCTAssertEqual(savedFileContent, contentToSave) @@ -343,7 +359,7 @@ extension IONFILEFileManagerTests { let shouldIncludeIntermediateDirectories = true // When - let savedFileURL = try sut.saveFile( + try sut.saveFile( atURL: fileURL, withEncodingAndData: .string(encoding: stringEncoding, value: contentToSave), includeIntermediateDirectories: shouldIncludeIntermediateDirectories @@ -352,7 +368,6 @@ extension IONFILEFileManagerTests { // Then XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories) XCTAssertEqual(fileManager.capturedPath, parentFolderURL) - XCTAssertEqual(savedFileURL, fileURL) let savedFileContent = try fetchEntireContent(forURL: fileURL, withEncoding: .string(encoding: stringEncoding)) XCTAssertEqual(savedFileContent, contentToSave) @@ -500,7 +515,7 @@ extension IONFILEFileManagerTests { let testDirectory = URL(filePath: "/test/directory") // When - let fileAttributesModel = try sut.getItemAttributes(atPath: testDirectory.path()) + let fileAttributesModel = try sut.getItemAttributes(atURL: testDirectory) // Then XCTAssertEqual(fileManager.capturedPath, testDirectory) @@ -523,7 +538,7 @@ extension IONFILEFileManagerTests { let testDirectory = URL(filePath: "/test/directory") // When - let fileAttributesModel = try sut.getItemAttributes(atPath: testDirectory.path()) + let fileAttributesModel = try sut.getItemAttributes(atURL: testDirectory) // Then XCTAssertEqual(fileManager.capturedPath, testDirectory) @@ -546,7 +561,7 @@ extension IONFILEFileManagerTests { let testDirectory = URL(filePath: "/test/directory") // When - let fileAttributesModel = try sut.getItemAttributes(atPath: testDirectory.path()) + let fileAttributesModel = try sut.getItemAttributes(atURL: testDirectory) // Then XCTAssertEqual(fileManager.capturedPath, testDirectory) @@ -574,7 +589,7 @@ extension IONFILEFileManagerTests { let testDirectory = URL(filePath: "/test/directory") // When - XCTAssertThrowsError(try sut.getItemAttributes(atPath: testDirectory.path())) { + XCTAssertThrowsError(try sut.getItemAttributes(atURL: testDirectory)) { // Then XCTAssertEqual($0 as? MockFileManagerError, error) } @@ -597,18 +612,17 @@ extension IONFILEFileManagerTests { XCTAssertEqual(fileManager.capturedDestinationPath, destinationPath) } - func test_renameItem_sameOriginAndDestination_shouldDoNothing() throws { + func test_renameItem_sameOriginAndDestination_shouldReturnError() { // Given - let fileManager = createFileManager(fileExists: false) + createFileManager(fileExists: false) let originPath = URL(filePath: "/test/origin") let destinationPath = URL(filePath: "/test/origin") // When - try sut.renameItem(fromURL: originPath, toURL: destinationPath) - - // Then - XCTAssertNil(fileManager.capturedOriginPath) - XCTAssertNil(fileManager.capturedDestinationPath) + XCTAssertThrowsError(try sut.renameItem(fromURL: originPath, toURL: destinationPath)) { + // Then + XCTAssertEqual($0 as? IONFILEFileManagerError, .sameOriginAndDestinationURLs) + } } func test_renameDirectory_alreadyExisting_shouldBeSuccessful() throws { @@ -671,18 +685,17 @@ extension IONFILEFileManagerTests { XCTAssertEqual(fileManager.capturedDestinationPath, destinationPath) } - func test_copyItem_sameOriginAndDestination_shouldDoNothing() throws { + func test_copyItem_sameOriginAndDestination_shouldReturnError() { // Given - let fileManager = createFileManager(fileExists: false) + createFileManager(fileExists: false) let originPath = URL(filePath: "/test/origin") let destinationPath = URL(filePath: "/test/origin") // When - try sut.copyItem(fromURL: originPath, toURL: destinationPath) - - // Then - XCTAssertNil(fileManager.capturedOriginPath) - XCTAssertNil(fileManager.capturedDestinationPath) + XCTAssertThrowsError(try sut.copyItem(fromURL: originPath, toURL: destinationPath)) { + // Then + XCTAssertEqual($0 as? IONFILEFileManagerError, .sameOriginAndDestinationURLs) + } } func test_copyDirectory_alreadyExisting_shouldBeSuccessful() throws { @@ -795,51 +808,53 @@ private extension IONFILEFileManagerTests { return try fetchEntireContent(forURL: fileURL, withEncoding: encoding) } + @discardableResult func fetchEntireContent(forURL fileURL: URL, withEncoding encoding: IONFILEEncoding) throws -> String { - return try treatContent(withEncoding: encoding) { - switch try sut.readEntireFile(atURL: fileURL, withEncoding: encoding) { - case .byteBuffer(let fileData): fileData.base64EncodedString() - case .string(_, let fileData): fileData - } + let content = switch try sut.readEntireFile(atURL: fileURL, withEncoding: encoding) { + case .byteBuffer(let fileData): fileData.base64EncodedString() + case .string(_, let fileData): fileData } + return try treat(content: content, withEncoding: encoding) } func fetchChunkedContent(forFile file: (name: String, extension: String), withEncoding encoding: IONFILEEncoding, forceURLError: Bool = false) throws -> String { - var fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: file.name, withExtension: file.extension)) - return try treatContent(withEncoding: encoding) { - var result = [String]() - var error: Error? - let expectation = XCTestExpectation(description: "Wait for chunks to be processed") - - if forceURLError { - fileURL.deleteLastPathComponent() - } - try sut.readFileInChunks(atURL: fileURL, withEncoding: encoding, andChunkSize: 3) // 3 bytes - .sink(receiveCompletion: { completion in - if case .failure(let failure) = completion { - error = failure - } - expectation.fulfill() - }, receiveValue: { value in - let chunkToAdd = switch value { - case .byteBuffer(let chunkData): chunkData.base64EncodedString() - case .string(_, let chunkData): chunkData - } - result.append(chunkToAdd) - }) - .store(in: &cancellables) - - // Wait for all chunks to be processed - wait(for: [expectation], timeout: 1.0) - - if let error { throw error } - return result.joined() - } + let fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: file.name, withExtension: file.extension)) + return try fetchChunkedContent(forURL: fileURL, withEncoding: encoding, forceURLError: forceURLError) } - func treatContent(withEncoding encoding: IONFILEEncoding, afterReading readOperation: () throws -> String) throws -> String { - let fileURLContent = try readOperation() + @discardableResult + func fetchChunkedContent(forURL url: URL, withEncoding encoding: IONFILEEncoding, forceURLError: Bool = false) throws -> String { + var fileURL = url + var contentArray = [String]() + var error: Error? + let expectation = XCTestExpectation(description: "Wait for chunks to be processed") + + if forceURLError { + fileURL.deleteLastPathComponent() + } + try sut.readFileInChunks(atURL: fileURL, withEncoding: encoding, andChunkSize: 3) // 3 bytes + .sink(receiveCompletion: { completion in + if case .failure(let failure) = completion { + error = failure + } + expectation.fulfill() + }, receiveValue: { value in + let chunkToAdd = switch value { + case .byteBuffer(let chunkData): chunkData.base64EncodedString() + case .string(_, let chunkData): chunkData + } + contentArray.append(chunkToAdd) + }) + .store(in: &cancellables) + + // Wait for all chunks to be processed + wait(for: [expectation], timeout: 1.0) + + if let error { throw error } + return try treat(content: contentArray.joined(), withEncoding: encoding) + } + func treat(content fileURLContent: String, withEncoding encoding: IONFILEEncoding) throws -> String { var fileURLUnicodeScalars: String.UnicodeScalarView if case .byteBuffer = encoding { let fileURLData = try XCTUnwrap(Data(base64Encoded: fileURLContent)) From b527a42a6603437a5bdec8a3ad0487b17b0fee1b Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Mon, 3 Feb 2025 14:35:23 +0000 Subject: [PATCH 30/31] chore: revert code for copy and rename This introduced a change in the logic so it reverts it. --- IONFilesystemLib/IONFILEManager+Errors.swift | 1 - IONFilesystemLib/IONFILEManager.swift | 14 +++++++--- .../IONFILEFileManagerTests.swift | 27 ++++++++++--------- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/IONFilesystemLib/IONFILEManager+Errors.swift b/IONFilesystemLib/IONFILEManager+Errors.swift index 0759fb7..6e8bef5 100644 --- a/IONFilesystemLib/IONFILEManager+Errors.swift +++ b/IONFilesystemLib/IONFILEManager+Errors.swift @@ -8,7 +8,6 @@ enum IONFILEFileManagerError: Error { case directoryNotFound case fileNotFound case missingParentFolder - case sameOriginAndDestinationURLs } enum IONFILEChunkPublisherError: Error { diff --git a/IONFilesystemLib/IONFILEManager.swift b/IONFilesystemLib/IONFILEManager.swift index aa8ded3..8c852ac 100644 --- a/IONFilesystemLib/IONFILEManager.swift +++ b/IONFilesystemLib/IONFILEManager.swift @@ -133,7 +133,9 @@ extension IONFILEManager: IONFILEFileManager { public func renameItem(fromURL originURL: URL, toURL destinationURL: URL) throws { try withSecurityScopedAccess(to: originURL) { try withSecurityScopedAccess(to: destinationURL) { - try validateIfOperationIsValid(fromURL: originURL, toURL: destinationURL) + guard try shouldPerformDualPathOperation(fromURL: originURL, toURL: destinationURL) else { + return + } try fileManager.moveItem(at: originURL, to: destinationURL) } } @@ -142,7 +144,9 @@ extension IONFILEManager: IONFILEFileManager { public func copyItem(fromURL originURL: URL, toURL destinationURL: URL) throws { try withSecurityScopedAccess(to: originURL) { try withSecurityScopedAccess(to: destinationURL) { - try validateIfOperationIsValid(fromURL: originURL, toURL: destinationURL) + guard try shouldPerformDualPathOperation(fromURL: originURL, toURL: destinationURL) else { + return + } try fileManager.copyItem(at: originURL, to: destinationURL) } } @@ -188,9 +192,9 @@ private extension IONFILEManager { return rawURL } - func validateIfOperationIsValid(fromURL originURL: URL, toURL destinationURL: URL) throws { + func shouldPerformDualPathOperation(fromURL originURL: URL, toURL destinationURL: URL) throws -> Bool { guard originURL != destinationURL else { - throw IONFILEFileManagerError.sameOriginAndDestinationURLs + return false } var isDirectory: ObjCBool = false @@ -199,6 +203,8 @@ private extension IONFILEManager { try deleteFile(atURL: destinationURL) } } + + return true } } diff --git a/IONFilesystemLibTests/IONFILEFileManagerTests.swift b/IONFilesystemLibTests/IONFILEFileManagerTests.swift index f0af477..b9e5dd5 100644 --- a/IONFilesystemLibTests/IONFILEFileManagerTests.swift +++ b/IONFilesystemLibTests/IONFILEFileManagerTests.swift @@ -612,17 +612,18 @@ extension IONFILEFileManagerTests { XCTAssertEqual(fileManager.capturedDestinationPath, destinationPath) } - func test_renameItem_sameOriginAndDestination_shouldReturnError() { + func test_renameItem_sameOriginAndDestination_shouldDoNothing() throws { // Given - createFileManager(fileExists: false) + let fileManager = createFileManager(fileExists: false) let originPath = URL(filePath: "/test/origin") let destinationPath = URL(filePath: "/test/origin") // When - XCTAssertThrowsError(try sut.renameItem(fromURL: originPath, toURL: destinationPath)) { - // Then - XCTAssertEqual($0 as? IONFILEFileManagerError, .sameOriginAndDestinationURLs) - } + try sut.renameItem(fromURL: originPath, toURL: destinationPath) + + // Then + XCTAssertNil(fileManager.capturedOriginPath) + XCTAssertNil(fileManager.capturedDestinationPath) } func test_renameDirectory_alreadyExisting_shouldBeSuccessful() throws { @@ -685,17 +686,19 @@ extension IONFILEFileManagerTests { XCTAssertEqual(fileManager.capturedDestinationPath, destinationPath) } - func test_copyItem_sameOriginAndDestination_shouldReturnError() { + func test_copyItem_sameOriginAndDestination_shouldDoNothing() throws { // Given - createFileManager(fileExists: false) + let fileManager = createFileManager(fileExists: false) let originPath = URL(filePath: "/test/origin") let destinationPath = URL(filePath: "/test/origin") // When - XCTAssertThrowsError(try sut.copyItem(fromURL: originPath, toURL: destinationPath)) { - // Then - XCTAssertEqual($0 as? IONFILEFileManagerError, .sameOriginAndDestinationURLs) - } + try sut.copyItem(fromURL: originPath, toURL: destinationPath) + + // Then + XCTAssertNil(fileManager.capturedOriginPath) + XCTAssertNil(fileManager.capturedDestinationPath) + } func test_copyDirectory_alreadyExisting_shouldBeSuccessful() throws { From c964bb2cd974b459a982d2ad4eaf5d58953a2084 Mon Sep 17 00:00:00 2001 From: OS-ricardomoreirasilva Date: Tue, 4 Feb 2025 12:40:54 +0000 Subject: [PATCH 31/31] chore: add descriptive message to all errors --- IONFilesystemLib/IONFILEChunkPublisher.swift | 2 +- IONFilesystemLib/IONFILEManager+Enums.swift | 2 +- IONFilesystemLib/IONFILEManager+Errors.swift | 39 +++++++++++++++---- IONFilesystemLib/IONFILEManager.swift | 11 +++--- .../IONFILEFileManagerTests.swift | 8 ++-- 5 files changed, 43 insertions(+), 19 deletions(-) diff --git a/IONFilesystemLib/IONFILEChunkPublisher.swift b/IONFilesystemLib/IONFILEChunkPublisher.swift index e493fea..cc435bf 100644 --- a/IONFilesystemLib/IONFILEChunkPublisher.swift +++ b/IONFilesystemLib/IONFILEChunkPublisher.swift @@ -48,7 +48,7 @@ private class IONFILEChunkSubscription: Subscription where S.Inpu case .byteBuffer: chunkToEmit = .byteBuffer(value: chunk) case .string(let encoding): guard let chunkText = String(data: chunk, encoding: encoding.stringEncoding) else { - throw IONFILEChunkPublisherError.cantEncodeData + throw IONFILEChunkPublisherError.cantEncodeData(usingEncoding: encoding) } chunkToEmit = .string(encoding: encoding, value: chunkText) } diff --git a/IONFilesystemLib/IONFILEManager+Enums.swift b/IONFilesystemLib/IONFILEManager+Enums.swift index 654e520..a21992e 100644 --- a/IONFilesystemLib/IONFILEManager+Enums.swift +++ b/IONFilesystemLib/IONFILEManager+Enums.swift @@ -10,7 +10,7 @@ public enum IONFILEEncodingValueMapper { case string(encoding: IONFILEStringEncoding, value: String) } -public enum IONFILEStringEncoding { +public enum IONFILEStringEncoding: String { case ascii case utf8 case utf16 diff --git a/IONFilesystemLib/IONFILEManager+Errors.swift b/IONFilesystemLib/IONFILEManager+Errors.swift index 6e8bef5..3c72626 100644 --- a/IONFilesystemLib/IONFILEManager+Errors.swift +++ b/IONFilesystemLib/IONFILEManager+Errors.swift @@ -1,16 +1,39 @@ -enum IONFILEDirectoryManagerError: Error { +import Foundation + +enum IONFILEDirectoryManagerError: LocalizedError { case notEmpty + + var errorDescription: String? { + "Folder is not empty." + } } -enum IONFILEFileManagerError: Error { - case cantCreateURL - case cantDecodeData - case directoryNotFound - case fileNotFound +enum IONFILEFileManagerError: LocalizedError, Equatable { + case cantCreateURL(forPath: String) + case cantDecodeData(usingEncoding: IONFILEStringEncoding) + case directoryNotFound(atPath: String) + case fileNotFound(atPath: String) case missingParentFolder + + var errorDescription: String? { + switch self { + case .cantCreateURL(let path): "Can't create URL for path '\(path)'." + case .cantDecodeData(let encoding): "Can't decode data using encoding .\(encoding.rawValue)." + case .directoryNotFound(let path): "Can't find directory at path '\(path)'." + case .fileNotFound(let path): "Can't find file at path '\(path)'." + case .missingParentFolder: "Parent folder doesn't exist." + } + } } -enum IONFILEChunkPublisherError: Error { - case cantEncodeData +enum IONFILEChunkPublisherError: LocalizedError, Equatable { + case cantEncodeData(usingEncoding: IONFILEStringEncoding) case notAbleToReadFile + + var errorDescription: String? { + switch self { + case .cantEncodeData(let encoding): "Can't encode data using encoding .\(encoding.rawValue)." + case .notAbleToReadFile: "Can't read file." + } + } } diff --git a/IONFilesystemLib/IONFILEManager.swift b/IONFilesystemLib/IONFILEManager.swift index 8c852ac..be8061d 100644 --- a/IONFilesystemLib/IONFILEManager.swift +++ b/IONFilesystemLib/IONFILEManager.swift @@ -69,8 +69,9 @@ extension IONFILEManager: IONFILEFileManager { public func deleteFile(atURL url: URL) throws { try withSecurityScopedAccess(to: url) { - guard fileManager.fileExists(atPath: url.urlPath) else { - throw IONFILEFileManagerError.fileNotFound + let path = url.urlPath + guard fileManager.fileExists(atPath: path) else { + throw IONFILEFileManagerError.fileNotFound(atPath: path) } try fileManager.removeItem(at: url) @@ -111,7 +112,7 @@ extension IONFILEManager: IONFILEFileManager { dataToAppend = value case .string(let encoding, let value): guard let valueData = value.data(using: encoding.stringEncoding) else { - throw IONFILEFileManagerError.cantDecodeData + throw IONFILEFileManagerError.cantDecodeData(usingEncoding: encoding) } dataToAppend = valueData } @@ -179,7 +180,7 @@ private extension IONFILEManager { func resolveDirectoryURL(forType directoryType: IONFILEDirectoryType, with path: String) throws -> URL { guard let directoryURL = directoryType.fetchURL(using: fileManager) else { - throw IONFILEFileManagerError.directoryNotFound + throw IONFILEFileManagerError.directoryNotFound(atPath: path) } return path.isEmpty ? directoryURL : directoryURL.urlWithAppendingPath(path) @@ -187,7 +188,7 @@ private extension IONFILEManager { func resolveRawURL(from path: String) throws -> URL { guard let rawURL = URL(string: path) else { - throw IONFILEFileManagerError.cantCreateURL + throw IONFILEFileManagerError.cantCreateURL(forPath: path) } return rawURL } diff --git a/IONFilesystemLibTests/IONFILEFileManagerTests.swift b/IONFilesystemLibTests/IONFILEFileManagerTests.swift index b9e5dd5..438a0da 100644 --- a/IONFilesystemLibTests/IONFILEFileManagerTests.swift +++ b/IONFilesystemLibTests/IONFILEFileManagerTests.swift @@ -209,7 +209,7 @@ extension IONFILEFileManagerTests { // When XCTAssertThrowsError(try sut.getFileURL(atPath: filePath, withSearchPath: .directory(type: directoryType))) { // Then - XCTAssertEqual($0 as? IONFILEFileManagerError, .directoryNotFound) + XCTAssertEqual($0 as? IONFILEFileManagerError, .directoryNotFound(atPath: filePath)) } } @@ -248,7 +248,7 @@ extension IONFILEFileManagerTests { // When XCTAssertThrowsError(try sut.getFileURL(atPath: emptyFilePath, withSearchPath: .raw)) { // Then - XCTAssertEqual($0 as? IONFILEFileManagerError, .cantCreateURL) + XCTAssertEqual($0 as? IONFILEFileManagerError, .cantCreateURL(forPath: emptyFilePath)) } } } @@ -275,7 +275,7 @@ extension IONFILEFileManagerTests { // When XCTAssertThrowsError(try sut.deleteFile(atURL: filePath)) { // Then - XCTAssertEqual($0 as? IONFILEFileManagerError, .fileNotFound) + XCTAssertEqual($0 as? IONFILEFileManagerError, .fileNotFound(atPath: filePath.urlPath)) } } @@ -495,7 +495,7 @@ extension IONFILEFileManagerTests { includeIntermediateDirectories: false) ) { // Then - XCTAssertEqual($0 as? IONFILEFileManagerError, .cantDecodeData) + XCTAssertEqual($0 as? IONFILEFileManagerError, .cantDecodeData(usingEncoding: stringEncoding)) } } }