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
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
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/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
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 64%
rename from OSFilesystemLib.xcodeproj/project.pbxproj
rename to IONFilesystemLib.xcodeproj/project.pbxproj
index 5b259b9..792d4ce 100644
--- a/OSFilesystemLib.xcodeproj/project.pbxproj
+++ b/IONFilesystemLib.xcodeproj/project.pbxproj
@@ -7,9 +7,18 @@
objects = {
/* Begin PBXBuildFile section */
- 756346652C00F21000685AA3 /* OSFilesystemLib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 756346612C00F21000685AA3 /* OSFilesystemLib.swift */; };
- 7575CF6A2BFCEE6F008F3FD0 /* OSFilesystemLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7575CF612BFCEE6F008F3FD0 /* OSFilesystemLib.framework */; };
- 7575CF802BFCEEEA008F3FD0 /* OSFilesystemLib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7575CF7D2BFCEEEA008F3FD0 /* OSFilesystemLib.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 /* IONFilesystemLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7575CF612BFCEE6F008F3FD0 /* IONFilesystemLib.framework */; };
+ 75DA44542D48E435006DF7DE /* IONFILEChunkPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75DA44532D48E435006DF7DE /* IONFILEChunkPublisher.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 /* IONFILEFileManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FEB2B32D35479B007C2686 /* IONFILEFileManagerTests.swift */; };
+ 75FEB2B72D355F21007C2686 /* file.txt in Resources */ = {isa = PBXBuildFile; fileRef = 75FEB2B62D355F21007C2686 /* file.txt */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -23,10 +32,20 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
- 756346612C00F21000685AA3 /* OSFilesystemLib.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSFilesystemLib.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 = ""; };
+ 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 = ""; };
+ 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 /* 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 */
/* Begin PBXFrameworksBuildPhase section */
@@ -41,7 +60,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 7575CF6A2BFCEE6F008F3FD0 /* OSFilesystemLib.framework in Frameworks */,
+ 7575CF6A2BFCEE6F008F3FD0 /* IONFilesystemLib.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -51,8 +70,9 @@
7575CF572BFCEE6F008F3FD0 = {
isa = PBXGroup;
children = (
- 7575CF632BFCEE6F008F3FD0 /* OSFilesystemLib */,
- 7575CF6D2BFCEE6F008F3FD0 /* OSFilesystemLibTests */,
+ 754D90BC2D4BD81000AD7FD4 /* Package.swift */,
+ 7575CF632BFCEE6F008F3FD0 /* IONFilesystemLib */,
+ 7575CF6D2BFCEE6F008F3FD0 /* IONFilesystemLibTests */,
7575CF622BFCEE6F008F3FD0 /* Products */,
);
sourceTree = "";
@@ -60,26 +80,35 @@
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 = (
- 756346612C00F21000685AA3 /* OSFilesystemLib.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 = (
- 7575CF7D2BFCEEEA008F3FD0 /* OSFilesystemLib.swift */,
+ 751328D82D3179430031BDD0 /* MockFileManager.swift */,
+ 751328D92D318DBA0031BDD0 /* IONFILEDirectoryManagerTests.swift */,
+ 75FEB2B32D35479B007C2686 /* IONFILEFileManagerTests.swift */,
+ 75FEB2B62D355F21007C2686 /* file.txt */,
);
- path = OSFilesystemLibTests;
+ path = IONFilesystemLibTests;
sourceTree = "";
};
/* End PBXGroup section */
@@ -95,9 +124,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 */,
@@ -109,14 +138,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 */,
@@ -127,9 +156,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 */
@@ -140,19 +169,19 @@
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;
};
};
};
- buildConfigurationList = 7575CF5B2BFCEE6F008F3FD0 /* Build configuration list for PBXProject "OSFilesystemLib" */;
+ buildConfigurationList = 7575CF5B2BFCEE6F008F3FD0 /* Build configuration list for PBXProject "IONFilesystemLib" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
@@ -168,8 +197,8 @@
projectDirPath = "";
projectRoot = "";
targets = (
- 7575CF602BFCEE6F008F3FD0 /* OSFilesystemLib */,
- 7575CF682BFCEE6F008F3FD0 /* OSFilesystemLibTests */,
+ 7575CF602BFCEE6F008F3FD0 /* IONFilesystemLib */,
+ 7575CF682BFCEE6F008F3FD0 /* IONFilesystemLibTests */,
);
};
/* End PBXProject section */
@@ -186,6 +215,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 75FEB2B72D355F21007C2686 /* file.txt in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -217,7 +247,13 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 756346652C00F21000685AA3 /* OSFilesystemLib.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;
};
@@ -225,7 +261,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 7575CF802BFCEEEA008F3FD0 /* OSFilesystemLib.swift in Sources */,
+ 751328DA2D318DBA0031BDD0 /* IONFILEDirectoryManagerTests.swift in Sources */,
+ 751328DB2D318E770031BDD0 /* MockFileManager.swift in Sources */,
+ 75FEB2B42D35479B007C2686 /* IONFILEFileManagerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -234,7 +272,7 @@
/* Begin PBXTargetDependency section */
7575CF6C2BFCEE6F008F3FD0 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
- target = 7575CF602BFCEE6F008F3FD0 /* OSFilesystemLib */;
+ target = 7575CF602BFCEE6F008F3FD0 /* IONFilesystemLib */;
targetProxy = 7575CF6B2BFCEE6F008F3FD0 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
@@ -374,8 +412,9 @@
buildSettings = {
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
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";
@@ -384,16 +423,16 @@
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",
"@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 = 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;
@@ -408,8 +447,9 @@
buildSettings = {
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
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";
@@ -418,16 +458,16 @@
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",
"@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 = 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;
@@ -439,14 +479,13 @@
7575CF772BFCEE6F008F3FD0 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
- ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
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;
- PRODUCT_BUNDLE_IDENTIFIER = com.outsystems.rd.inappbrowser.OSInAppBrowserLibTests;
+ MARKETING_VERSION = 0.0.1;
+ PRODUCT_BUNDLE_IDENTIFIER = io.ionic.libs.filesystem.FilesystemLibTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
@@ -461,14 +500,13 @@
7575CF782BFCEE6F008F3FD0 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
- ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
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;
- PRODUCT_BUNDLE_IDENTIFIER = com.outsystems.rd.inappbrowser.OSInAppBrowserLibTests;
+ MARKETING_VERSION = 0.0.1;
+ PRODUCT_BUNDLE_IDENTIFIER = io.ionic.libs.filesystem.FilesystemLibTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
@@ -482,7 +520,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 */,
@@ -491,7 +529,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
- 7575CF732BFCEE6F008F3FD0 /* Build configuration list for PBXNativeTarget "OSFilesystemLib" */ = {
+ 7575CF732BFCEE6F008F3FD0 /* Build configuration list for PBXNativeTarget "IONFilesystemLib" */ = {
isa = XCConfigurationList;
buildConfigurations = (
7575CF742BFCEE6F008F3FD0 /* Debug */,
@@ -500,7 +538,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/IONFilesystemLib/IONFILEChunkPublisher.swift b/IONFilesystemLib/IONFILEChunkPublisher.swift
new file mode 100644
index 0000000..cc435bf
--- /dev/null
+++ b/IONFilesystemLib/IONFILEChunkPublisher.swift
@@ -0,0 +1,80 @@
+import Combine
+import Foundation
+
+public class IONFILEChunkPublisher: Publisher {
+ public typealias Output = IONFILEEncodingValueMapper
+ public typealias Failure = Error
+
+ private let url: URL
+ private let chunkSize: Int
+ private let encoding: IONFILEEncoding
+
+ 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 = IONFILEChunkSubscription(url, chunkSize, encoding, subscriber)
+ subscriber.receive(subscription: subscription)
+ }
+}
+
+private class IONFILEChunkSubscription: Subscription where S.Input == IONFILEEncodingValueMapper, S.Failure == Error {
+ private let fileHandle: FileHandle?
+ private let chunkSize: Int
+ private let encoding: IONFILEEncoding
+ private let subscriber: S
+ private var isCompleted = false
+
+ init(_ url: URL, _ chunkSize: Int, _ encoding: IONFILEEncoding, _ 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 subscriber.receive(completion: .failure(IONFILEChunkPublisherError.notAbleToReadFile))
+ }
+
+ while demand > .none {
+ do {
+ if let chunk = try fileHandle.read(upToCount: chunkSize), !chunk.isEmpty {
+ let chunkToEmit: IONFILEEncodingValueMapper
+ switch encoding {
+ case .byteBuffer: chunkToEmit = .byteBuffer(value: chunk)
+ case .string(let encoding):
+ guard let chunkText = String(data: chunk, encoding: encoding.stringEncoding) else {
+ throw IONFILEChunkPublisherError.cantEncodeData(usingEncoding: encoding)
+ }
+ chunkToEmit = .string(encoding: encoding, value: 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/IONFilesystemLib/IONFILEItemAttributeModel.swift b/IONFilesystemLib/IONFILEItemAttributeModel.swift
new file mode 100644
index 0000000..f2a75c9
--- /dev/null
+++ b/IONFilesystemLib/IONFILEItemAttributeModel.swift
@@ -0,0 +1,43 @@
+import Foundation
+
+public enum IONFILEItemType: Encodable {
+ case directory
+ case file
+
+ static func create(from fileAttributeType: String?) -> Self {
+ fileAttributeType == FileAttributeKey.FileTypeDirectoryValue ? .directory : .file
+ }
+}
+
+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: IONFILEItemType
+}
+
+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
+ 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/IONFilesystemLib/IONFILEManager+Enums.swift b/IONFilesystemLib/IONFILEManager+Enums.swift
new file mode 100644
index 0000000..a21992e
--- /dev/null
+++ b/IONFilesystemLib/IONFILEManager+Enums.swift
@@ -0,0 +1,38 @@
+import Foundation
+
+public enum IONFILEEncoding: Equatable {
+ case byteBuffer
+ case string(encoding: IONFILEStringEncoding)
+}
+
+public enum IONFILEEncodingValueMapper {
+ case byteBuffer(value: Data)
+ case string(encoding: IONFILEStringEncoding, value: String)
+}
+
+public enum IONFILEStringEncoding: String {
+ case ascii
+ case utf8
+ case utf16
+
+ var stringEncoding: String.Encoding {
+ switch self {
+ case .ascii: .ascii
+ case .utf8: .utf8
+ case .utf16: .utf16
+ }
+ }
+}
+
+public enum IONFILESearchPath {
+ case directory(type: IONFILEDirectoryType)
+ case raw
+}
+
+public enum IONFILEDirectoryType {
+ case cache
+ case document
+ case library
+ case notSyncedLibrary
+ case temporary
+}
diff --git a/IONFilesystemLib/IONFILEManager+Errors.swift b/IONFilesystemLib/IONFILEManager+Errors.swift
new file mode 100644
index 0000000..3c72626
--- /dev/null
+++ b/IONFilesystemLib/IONFILEManager+Errors.swift
@@ -0,0 +1,39 @@
+import Foundation
+
+enum IONFILEDirectoryManagerError: LocalizedError {
+ case notEmpty
+
+ var errorDescription: String? {
+ "Folder is not empty."
+ }
+}
+
+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: 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+Protocols.swift b/IONFilesystemLib/IONFILEManager+Protocols.swift
new file mode 100644
index 0000000..08dfdc7
--- /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 -> IONFILEEncodingValueMapper
+ 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
+ func appendData(_ data: IONFILEEncodingValueMapper, atURL: URL, includeIntermediateDirectories: Bool) throws
+ 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
new file mode 100644
index 0000000..be8061d
--- /dev/null
+++ b/IONFilesystemLib/IONFILEManager.swift
@@ -0,0 +1,244 @@
+import Foundation
+
+public struct IONFILEManager {
+ private let fileManager: FileManager
+
+ public init(fileManager: FileManager = .default) {
+ self.fileManager = fileManager
+ }
+}
+
+extension IONFILEManager: IONFILEDirectoryManager {
+ public func createDirectory(atURL pathURL: URL, includeIntermediateDirectories: Bool) throws {
+ try withSecurityScopedAccess(to: pathURL) {
+ try fileManager.createDirectory(at: pathURL, withIntermediateDirectories: includeIntermediateDirectories)
+ }
+ }
+
+ public func removeDirectory(atURL pathURL: URL, includeIntermediateDirectories: Bool) throws {
+ try withSecurityScopedAccess(to: pathURL) {
+ if !includeIntermediateDirectories {
+ let directoryContents = try listDirectory(atURL: pathURL)
+ if !directoryContents.isEmpty {
+ throw IONFILEDirectoryManagerError.notEmpty
+ }
+ }
+
+ try fileManager.removeItem(at: pathURL)
+ }
+ }
+
+ public func listDirectory(atURL pathURL: URL) throws -> [URL] {
+ try withSecurityScopedAccess(to: pathURL) {
+ try fileManager.contentsOfDirectory(at: pathURL, includingPropertiesForKeys: nil)
+ }
+ }
+}
+
+extension IONFILEManager: IONFILEFileManager {
+ public func readEntireFile(atURL fileURL: URL, withEncoding encoding: IONFILEEncoding) throws -> IONFILEEncodingValueMapper {
+ try withSecurityScopedAccess(to: fileURL) {
+ let result: IONFILEEncodingValueMapper
+ switch encoding {
+ case .byteBuffer:
+ let fileData = try readFileAsByteBuffer(from: fileURL)
+ result = .byteBuffer(value: fileData)
+ case .string(let stringEncoding):
+ let fileData = try readFileAsString(from: fileURL, using: stringEncoding.stringEncoding)
+ result = .string(encoding: stringEncoding, value: fileData)
+ }
+
+ return result
+ }
+ }
+
+ 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: IONFILESearchPath) throws -> URL {
+ switch searchPath {
+ case .directory(let type):
+ try resolveDirectoryURL(forType: type, with: path)
+ case .raw:
+ try resolveRawURL(from: path)
+ }
+ }
+
+ public func deleteFile(atURL url: URL) throws {
+ try withSecurityScopedAccess(to: url) {
+ let path = url.urlPath
+ guard fileManager.fileExists(atPath: path) else {
+ throw IONFILEFileManagerError.fileNotFound(atPath: path)
+ }
+
+ try fileManager.removeItem(at: url)
+ }
+ }
+
+ 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)
+ }
+ }
+ }
+
+ public func appendData(_ encodingMapper: IONFILEEncodingValueMapper, atURL url: URL, includeIntermediateDirectories: Bool) throws {
+ 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(usingEncoding: encoding)
+ }
+ dataToAppend = valueData
+ }
+
+ let fileHandle = try FileHandle(forWritingTo: url)
+ try fileHandle.seekToEnd()
+ try fileHandle.write(contentsOf: dataToAppend)
+ try fileHandle.close()
+ }
+ }
+
+ 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 withSecurityScopedAccess(to: originURL) {
+ try withSecurityScopedAccess(to: destinationURL) {
+ guard try shouldPerformDualPathOperation(fromURL: originURL, toURL: destinationURL) else {
+ return
+ }
+ try fileManager.moveItem(at: originURL, to: destinationURL)
+ }
+ }
+ }
+
+ public func copyItem(fromURL originURL: URL, toURL destinationURL: URL) throws {
+ try withSecurityScopedAccess(to: originURL) {
+ try withSecurityScopedAccess(to: destinationURL) {
+ guard try shouldPerformDualPathOperation(fromURL: originURL, toURL: destinationURL) else {
+ return
+ }
+ try fileManager.copyItem(at: originURL, to: destinationURL)
+ }
+ }
+ }
+}
+
+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()
+
+ // 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 readFileAsByteBuffer(from fileURL: URL) throws -> Data {
+ try Data(contentsOf: fileURL)
+ }
+
+ func readFileAsString(from fileURL: URL, using stringEncoding: String.Encoding) throws -> String {
+ try String(contentsOf: fileURL, encoding: stringEncoding)
+ }
+
+ func resolveDirectoryURL(forType directoryType: IONFILEDirectoryType, with path: String) throws -> URL {
+ guard let directoryURL = directoryType.fetchURL(using: fileManager) else {
+ throw IONFILEFileManagerError.directoryNotFound(atPath: path)
+ }
+
+ return path.isEmpty ? directoryURL : directoryURL.urlWithAppendingPath(path)
+ }
+
+ func resolveRawURL(from path: String) throws -> URL {
+ guard let rawURL = URL(string: path) else {
+ throw IONFILEFileManagerError.cantCreateURL(forPath: path)
+ }
+ return rawURL
+ }
+
+ func shouldPerformDualPathOperation(fromURL originURL: URL, toURL destinationURL: URL) throws -> Bool {
+ guard originURL != destinationURL else {
+ return false
+ }
+
+ var isDirectory: ObjCBool = false
+ if fileManager.fileExists(atPath: destinationURL.urlPath, isDirectory: &isDirectory) {
+ if !isDirectory.boolValue {
+ try deleteFile(atURL: destinationURL)
+ }
+ }
+
+ return true
+ }
+}
+
+private extension IONFILEDirectoryType {
+ 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/IONFilesystemLib/URL+Extension.swift b/IONFilesystemLib/URL+Extension.swift
new file mode 100644
index 0000000..8f07475
--- /dev/null
+++ b/IONFilesystemLib/URL+Extension.swift
@@ -0,0 +1,19 @@
+import Foundation
+
+extension URL {
+ public var urlPath: String {
+ if #available(iOS 16.0, *) {
+ path(percentEncoded: false)
+ } else {
+ path.removingPercentEncoding ?? path
+ }
+ }
+
+ func urlWithAppendingPath(_ path: String) -> URL {
+ if #available(iOS 16.0, *) {
+ appending(path: path)
+ } else {
+ appendingPathComponent(path)
+ }
+ }
+}
diff --git a/IONFilesystemLibTests/IONFILEDirectoryManagerTests.swift b/IONFilesystemLibTests/IONFILEDirectoryManagerTests.swift
new file mode 100644
index 0000000..6f4c8cb
--- /dev/null
+++ b/IONFilesystemLibTests/IONFILEDirectoryManagerTests.swift
@@ -0,0 +1,146 @@
+import XCTest
+
+@testable import IONFilesystemLib
+
+final class IONFILEDirectoryManagerTests: XCTestCase {
+ private var sut: IONFILEManager!
+
+ // MARK: - 'createDirectory' tests
+ func test_createDirectory_shouldBeSuccessful() throws {
+ // Given
+ let fileManager = createFileManager()
+ let testDirectory = URL(filePath: "/test/directory")
+ let shouldIncludeIntermediateDirectories = false
+
+ // When
+ try sut.createDirectory(atURL: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories)
+
+ // Then
+ XCTAssertEqual(fileManager.capturedPath, testDirectory)
+ XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories)
+ }
+
+ func test_createDirectory_butFails_shouldReturnAnError() {
+ // Given
+ let error = MockFileManagerError.createDirectoryError
+ createFileManager(with: error)
+ let testDirectory = URL(filePath: "/test/directory")
+ let shouldIncludeIntermediateDirectories = false
+
+ // When
+ XCTAssertThrowsError(
+ try sut.createDirectory(atURL: 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 = URL(filePath: "/test/directory")
+ let shouldIncludeIntermediateDirectories = true
+
+ // When
+ XCTAssertThrowsError(
+ try sut.removeDirectory(atURL: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories)
+ ) {
+ // Then
+ XCTAssertEqual($0 as? MockFileManagerError, error)
+ }
+ }
+
+ func test_removeDirectory_includingIntermediateDirectories_shouldBeSuccessful() throws {
+ // Given
+ let fileManager = createFileManager()
+ let testDirectory = URL(filePath: "/test/directory")
+ let shouldIncludeIntermediateDirectories = true
+
+ // When
+ try sut.removeDirectory(atURL: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories)
+
+ // Then
+ XCTAssertEqual(fileManager.capturedPath, testDirectory)
+ }
+
+ func test_removeDirectory_excludingIntermediateDirectories_directoryDoesntHaveContent_shouldBeSuccessful() throws {
+ // Given
+ let fileManager = createFileManager()
+ let testDirectory = URL(filePath: "/test/directory")
+ let shouldIncludeIntermediateDirectories = false
+
+ // When
+ try sut.removeDirectory(atURL: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories)
+
+ // Then
+ XCTAssertEqual(fileManager.capturedPath, testDirectory)
+ }
+
+ func test_removeDirectory_excludingIntermediateDirectories_directoryHasContent_shouldReturnAnError() {
+ createFileManager(shouldDirectoryHaveContent: true)
+ let testDirectory = URL(filePath: "/test/directory")
+ let shouldIncludeIntermediateDirectories = false
+
+ // When
+ XCTAssertThrowsError(
+ try sut.removeDirectory(atURL: testDirectory, includeIntermediateDirectories: shouldIncludeIntermediateDirectories)
+ ) {
+ // Then
+ XCTAssertEqual($0 as? IONFILEDirectoryManagerError, .notEmpty)
+ }
+ }
+
+ // MARK: - 'listDirectory' tests
+ func test_listDirectory_withNoContent_shouldReturnEmptyArray() throws {
+ // Given
+ let fileManager = createFileManager()
+ let testDirectory = URL(filePath: "/test/directory")
+
+ // When
+ let directoryContent = try sut.listDirectory(atURL: testDirectory)
+
+ // Then
+ XCTAssertEqual(fileManager.capturedPath, testDirectory)
+ XCTAssertTrue(directoryContent.isEmpty)
+ }
+
+ // MARK: - 'listDirectory' tests
+ func test_listDirectory_withContent_shouldReturnNotEmptyArray() throws {
+ // Given
+ let fileManager = createFileManager(shouldDirectoryHaveContent: true)
+ let testDirectory = URL(filePath: "/test/directory")
+
+ // When
+ let directoryContent = try sut.listDirectory(atURL: testDirectory)
+
+ // Then
+ XCTAssertEqual(fileManager.capturedPath, testDirectory)
+ XCTAssertEqual(directoryContent, [testDirectory])
+ }
+
+ func test_listDirectory_butFails_shouldReturnAnError() {
+ // Given
+ let error = MockFileManagerError.readDirectoryError
+ createFileManager(with: error)
+ let testDirectory = URL(filePath: "/test/directory")
+
+ // When
+ XCTAssertThrowsError(
+ try sut.listDirectory(atURL: testDirectory)
+ ) {
+ // Then
+ XCTAssertEqual($0 as? MockFileManagerError, error)
+ }
+ }
+}
+
+private extension IONFILEDirectoryManagerTests {
+ @discardableResult func createFileManager(with error: MockFileManagerError? = nil, shouldDirectoryHaveContent: Bool = false) -> MockFileManager {
+ let fileManager = MockFileManager(error: error, shouldDirectoryHaveContent: shouldDirectoryHaveContent)
+ sut = IONFILEManager(fileManager: fileManager)
+
+ return fileManager
+ }
+}
diff --git a/IONFilesystemLibTests/IONFILEFileManagerTests.swift b/IONFilesystemLibTests/IONFILEFileManagerTests.swift
new file mode 100644
index 0000000..438a0da
--- /dev/null
+++ b/IONFilesystemLibTests/IONFILEFileManagerTests.swift
@@ -0,0 +1,881 @@
+import Combine
+import XCTest
+
+@testable import IONFilesystemLib
+
+final class IONFILEFileManagerTests: XCTestCase {
+ private var sut: IONFILEManager!
+ private var cancellables: Set!
+
+ override func setUp() {
+ cancellables = .init()
+ }
+
+ override func tearDown() {
+ cancellables = nil
+ sut = nil
+ }
+}
+
+// MARK: - 'readEntireFile` tests
+extension IONFILEFileManagerTests {
+ 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)
+ }
+
+ 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
+extension IONFILEFileManagerTests {
+ func test_readFileInChunks_withStringEncoding_returnsContentSuccessfully() throws {
+ // Given
+ createFileManager()
+
+ // When
+ let fileContent = try fetchChunkedContent(
+ forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .string(encoding: .utf8)
+ )
+
+ // Then
+ XCTAssertEqual(fileContent, Configuration.fileContent)
+ }
+
+ func test_readFileInChunks_withByteBufferEncoding_returnsContentSuccessfully() throws {
+ // Given
+ createFileManager()
+
+ // When
+ let fileContent = try fetchChunkedContent(
+ forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .byteBuffer
+ )
+
+ // Then
+ 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? 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
+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 = IONFILEDirectoryType.cache
+
+ // When
+ let returnedURL = try sut.getFileURL(atPath: filePath, withSearchPath: .directory(type: directoryType))
+
+ // Then
+ XCTAssertEqual(fileManager.capturedSearchPathDirectory, .cachesDirectory)
+ XCTAssertEqual(fileURL.appending(path: 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 directoryType = IONFILEDirectoryType.cache
+
+ // When
+ let returnedURL = try sut.getFileURL(atPath: filePath, withSearchPath: .directory(type: directoryType))
+
+ // Then
+ 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 = IONFILEDirectoryType.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 = IONFILEDirectoryType.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 = IONFILEDirectoryType.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 = IONFILEDirectoryType.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 directoryType = IONFILEDirectoryType.cache
+
+ // When
+ XCTAssertThrowsError(try sut.getFileURL(atPath: filePath, withSearchPath: .directory(type: directoryType))) {
+ // Then
+ XCTAssertEqual($0 as? IONFILEFileManagerError, .directoryNotFound(atPath: filePath))
+ }
+ }
+
+ 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 directoryType = IONFILEDirectoryType.cache
+
+ // When
+ let returnedURL = try sut.getFileURL(atPath: emptyFilePath, withSearchPath: .directory(type: directoryType))
+
+ // Then
+ XCTAssertEqual(fileManager.capturedSearchPathDirectory, .cachesDirectory)
+ 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? IONFILEFileManagerError, .cantCreateURL(forPath: emptyFilePath))
+ }
+ }
+}
+
+// MARK: - 'deleteFile' tests
+extension IONFILEFileManagerTests {
+ func test_deleteFile_shouldBeSuccessful() throws {
+ // Given
+ let fileManager = createFileManager()
+ let filePath = URL(filePath: "/test/directory")
+
+ // When
+ try sut.deleteFile(atURL: filePath)
+
+ // Then
+ XCTAssertEqual(fileManager.capturedPath, filePath)
+ }
+
+ func test_deleteFile_thatDoesntExist_shouldReturnError() {
+ // Given
+ createFileManager(fileExists: false)
+ let filePath = URL(filePath: "/test/directory")
+
+ // When
+ XCTAssertThrowsError(try sut.deleteFile(atURL: filePath)) {
+ // Then
+ XCTAssertEqual($0 as? IONFILEFileManagerError, .fileNotFound(atPath: filePath.urlPath))
+ }
+ }
+
+ func test_deleteFile_thatFailsWhileDeleting_shouldReturnError() {
+ // Given
+ let error = MockFileManagerError.deleteFileError
+ createFileManager(error: error)
+ let filePath = URL(filePath: "/test/directory")
+
+ // When
+ XCTAssertThrowsError(try sut.deleteFile(atURL: filePath)) {
+ // Then
+ XCTAssertEqual($0 as? MockFileManagerError, error)
+ }
+ }
+}
+
+// MARK: - 'saveFile' tests
+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 = IONFILEStringEncoding.ascii
+ let contentToSave = Configuration.stringEncodedFileContent
+ let shouldIncludeIntermediateDirectories = false
+
+ // When
+ try sut.saveFile(
+ atURL: fileURL,
+ withEncodingAndData: .string(encoding: stringEncoding, value: contentToSave),
+ includeIntermediateDirectories: shouldIncludeIntermediateDirectories
+ )
+
+ // Then
+ XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories)
+
+ 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
+ }
+
+ 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
+ try sut.saveFile(
+ atURL: fileURL,
+ withEncodingAndData: .byteBuffer(value: contentToSaveData),
+ includeIntermediateDirectories: shouldIncludeIntermediateDirectories
+ )
+
+ // Then
+ XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories)
+
+ let savedFileContent = try fetchEntireContent(forURL: fileURL, withEncoding: .byteBuffer)
+ XCTAssertEqual(savedFileContent, contentToSave)
+
+ try sut.deleteFile(atURL: fileURL) // 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 = IONFILEStringEncoding.ascii
+ let contentToSave = Configuration.stringEncodedFileContent
+ let shouldIncludeIntermediateDirectories = true
+
+ // When
+ try sut.saveFile(
+ atURL: fileURL,
+ withEncodingAndData: .string(encoding: stringEncoding, value: contentToSave),
+ includeIntermediateDirectories: shouldIncludeIntermediateDirectories
+ )
+
+ // Then
+ XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories)
+ XCTAssertEqual(fileManager.capturedPath, parentFolderURL)
+
+ let savedFileContent = try fetchEntireContent(forURL: fileURL, withEncoding: .string(encoding: stringEncoding))
+ XCTAssertEqual(savedFileContent, contentToSave)
+
+ fileManager.fileExists = true
+ try sut.deleteFile(atURL: fileURL) // keep things clean by deleting created file
+ }
+
+ 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 = IONFILEStringEncoding.ascii
+ let contentToSave = Configuration.stringEncodedFileContent
+ let shouldIncludeIntermediateDirectories = false
+
+ // When
+ XCTAssertThrowsError(try sut.saveFile(
+ atURL: fileURL,
+ withEncodingAndData: .string(encoding: stringEncoding, value: contentToSave),
+ includeIntermediateDirectories: shouldIncludeIntermediateDirectories)
+ ) {
+ // Then
+ XCTAssertEqual($0 as? IONFILEFileManagerError, .missingParentFolder)
+ }
+ }
+}
+
+// MARK: - 'appendData' tests
+extension IONFILEFileManagerTests {
+ func test_appendData_withStringEncoding_savesFileSuccessfully() throws {
+ // Given
+ createFileManager()
+ let fileURL = try XCTUnwrap(fetchConfigurationFile())
+ let stringEncoding = IONFILEStringEncoding.ascii
+ let contentToAdd = Configuration.fileExtendedContent
+
+ // When
+ try sut.appendData(
+ .string(encoding: stringEncoding, value: contentToAdd),
+ atURL: fileURL,
+ includeIntermediateDirectories: false
+ )
+
+ // Then
+ let savedFileContent = try fetchEntireContent(forURL: fileURL, withEncoding: .string(encoding: stringEncoding))
+ XCTAssertEqual(savedFileContent, Configuration.fileContent + contentToAdd)
+
+ try sut.saveFile( // keep things clean by resetting file
+ atURL: fileURL,
+ 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),
+ atURL: fileURL,
+ includeIntermediateDirectories: false
+ )
+
+ // Then
+ let savedFileContent = try fetchEntireContent(forURL: fileURL, withEncoding: .byteBuffer)
+ XCTAssertEqual(savedFileContent, Configuration.fileContent + contentToAdd)
+
+ try sut.saveFile( // keep things clean by resetting file
+ atURL: fileURL,
+ 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 = IONFILEStringEncoding.ascii
+ let contentToAdd = Configuration.stringEncodedFileContent
+ let shouldIncludeIntermediateDirectories = true
+
+ // When
+ try sut.appendData(
+ .string(encoding: stringEncoding, value: contentToAdd),
+ atURL: fileURL,
+ includeIntermediateDirectories: shouldIncludeIntermediateDirectories
+ )
+
+ XCTAssertEqual(fileManager.capturedPath, parentFolderURL)
+ XCTAssertEqual(fileManager.capturedIntermediateDirectories, shouldIncludeIntermediateDirectories)
+
+ // Then
+ let savedFileContent = try fetchEntireContent(forURL: fileURL, withEncoding: .string(encoding: stringEncoding))
+
+ XCTAssertEqual(savedFileContent, contentToAdd)
+
+ fileManager.fileExists = true
+ try sut.deleteFile(atURL: fileURL) // keep things clean by deleting created file
+ }
+
+ func test_appendData_withStringEncoding_textCantBeDecoded_returnsError() throws {
+ // Given
+ createFileManager()
+ let fileURL = try XCTUnwrap(fetchConfigurationFile())
+ let stringEncoding = IONFILEStringEncoding.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),
+ atURL: fileURL,
+ includeIntermediateDirectories: false)
+ ) {
+ // Then
+ XCTAssertEqual($0 as? IONFILEFileManagerError, .cantDecodeData(usingEncoding: stringEncoding))
+ }
+ }
+}
+
+// MARK: - 'getItemAttributes' tests
+extension IONFILEFileManagerTests {
+ 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 = URL(filePath: "/test/directory")
+
+ // When
+ let fileAttributesModel = try sut.getItemAttributes(atURL: 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)
+ }
+
+ func test_getItemAttributes_omittingValues_returnsFileAttributeModelSuccessfully() throws {
+ // Given
+ let fileAttributes = Configuration.fileAttributes(
+ isDirectoryType: false
+ )
+ let fileManager = createFileManager(fileAttributes: fileAttributes)
+ let testDirectory = URL(filePath: "/test/directory")
+
+ // When
+ let fileAttributesModel = try sut.getItemAttributes(atURL: 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 = URL(filePath: "/test/directory")
+
+ // When
+ let fileAttributesModel = try sut.getItemAttributes(atURL: 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 = URL(filePath: "/test/directory")
+
+ // When
+ XCTAssertThrowsError(try sut.getItemAttributes(atURL: testDirectory)) {
+ // Then
+ XCTAssertEqual($0 as? MockFileManagerError, error)
+ }
+ }
+}
+
+// MARK: - 'renameItem' tests
+extension IONFILEFileManagerTests {
+ func test_renameItem_shouldBeSuccessful() throws {
+ // Given
+ let fileManager = createFileManager(fileExists: false)
+ let originPath = URL(filePath: "/test/origin")
+ let destinationPath = URL(filePath: "/test/destination")
+
+ // When
+ try sut.renameItem(fromURL: originPath, toURL: destinationPath)
+
+ // Then
+ XCTAssertEqual(fileManager.capturedOriginPath, originPath)
+ XCTAssertEqual(fileManager.capturedDestinationPath, destinationPath)
+ }
+
+ func test_renameItem_sameOriginAndDestination_shouldDoNothing() throws {
+ // Given
+ let fileManager = 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)
+ }
+
+ func test_renameDirectory_alreadyExisting_shouldBeSuccessful() throws {
+ // Given
+ let fileManager = createFileManager()
+ let originPath = URL(filePath: "/test/origin")
+ let destinationPath = URL(filePath: "/test/destination")
+
+ // When
+ try sut.renameItem(fromURL: originPath, toURL: destinationPath)
+
+ // Then
+ XCTAssertEqual(fileManager.capturedOriginPath, originPath)
+ XCTAssertEqual(fileManager.capturedDestinationPath, destinationPath)
+ }
+
+ func test_renameFile_alreadyExisting_shouldBeSuccessful() throws {
+ // Given
+ let fileManager = createFileManager(shouldBeDirectory: false)
+ let originPath = URL(filePath: "/test/origin")
+ let destinationPath = URL(filePath: "/test/destination")
+
+ // When
+ 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
+extension IONFILEFileManagerTests {
+ func test_copyItem_shouldBeSuccessful() throws {
+ // Given
+ let fileManager = createFileManager(fileExists: false)
+ let originPath = URL(filePath: "/test/origin")
+ let destinationPath = URL(filePath: "/test/destination")
+
+ // When
+ try sut.copyItem(fromURL: originPath, toURL: destinationPath)
+
+ // Then
+ XCTAssertEqual(fileManager.capturedOriginPath, originPath)
+ XCTAssertEqual(fileManager.capturedDestinationPath, destinationPath)
+ }
+
+ func test_copyItem_sameOriginAndDestination_shouldDoNothing() throws {
+ // Given
+ let fileManager = 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)
+
+ }
+
+ func test_copyDirectory_alreadyExisting_shouldBeSuccessful() throws {
+ // Given
+ let fileManager = createFileManager()
+ let originPath = URL(filePath: "/test/origin")
+ let destinationPath = URL(filePath: "/test/destination")
+
+ // When
+ try sut.copyItem(fromURL: originPath, toURL: destinationPath)
+
+ // Then
+ XCTAssertEqual(fileManager.capturedOriginPath, originPath)
+ XCTAssertEqual(fileManager.capturedDestinationPath, destinationPath)
+ }
+
+ func test_copyFile_alreadyExisting_shouldBeSuccessful() throws {
+ // Given
+ let fileManager = createFileManager(shouldBeDirectory: false)
+ let originPath = URL(filePath: "/test/origin")
+ let destinationPath = URL(filePath: "/test/destination")
+
+ // When
+ 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 IONFILEFileManagerTests {
+ 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!"
+ 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,
+ fileAttributes: [FileAttributeKey: Any] = [:],
+ shouldBeDirectory: ObjCBool = true,
+ mockTemporaryDirectory: URL? = nil
+ ) -> MockFileManager {
+ let fileManager = MockFileManager(
+ error: error,
+ urlsWithinDirectory: urlsWithinDirectory,
+ fileExists: fileExists,
+ fileAttributes: fileAttributes,
+ shouldBeDirectory: shouldBeDirectory,
+ mockTemporaryDirectory: mockTemporaryDirectory
+ )
+ sut = IONFILEManager(fileManager: fileManager)
+
+ return fileManager
+ }
+
+ 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)
+ }
+
+ @discardableResult
+ func fetchEntireContent(forURL fileURL: URL, withEncoding encoding: IONFILEEncoding) throws -> String {
+ 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 {
+ 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)
+ }
+
+ @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))
+ 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)
+ }
+
+ 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
+ }
+}
diff --git a/IONFilesystemLibTests/MockFileManager.swift b/IONFilesystemLibTests/MockFileManager.swift
new file mode 100644
index 0000000..21a9df7
--- /dev/null
+++ b/IONFilesystemLibTests/MockFileManager.swift
@@ -0,0 +1,120 @@
+import Foundation
+
+class MockFileManager: FileManager {
+ 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
+
+ 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?
+
+ 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
+ }
+}
+
+enum MockFileManagerError: Error {
+ case createDirectoryError
+ case readDirectoryError
+ 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
+ capturedIntermediateDirectories = createIntermediates
+
+ if let error, error == .createDirectoryError {
+ throw error
+ }
+ }
+
+ override func contentsOfDirectory(at url: URL, includingPropertiesForKeys keys: [URLResourceKey]?, options mask: FileManager.DirectoryEnumerationOptions = []) throws -> [URL] {
+ capturedPath = 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
+
+ if let error, [MockFileManagerError.deleteDirectoryError, .deleteFileError].contains(error) {
+ throw error
+ }
+ }
+
+ override func urls(for directory: FileManager.SearchPathDirectory, in domainMask: FileManager.SearchPathDomainMask) -> [URL] {
+ capturedSearchPathDirectory = directory
+
+ return urlsWithinDirectory
+ }
+
+ override func fileExists(atPath path: String) -> Bool {
+ fileExists
+ }
+
+ override func fileExists(atPath path: String, isDirectory: UnsafeMutablePointer?) -> Bool {
+ isDirectory?.pointee = shouldBeDirectory
+
+ return fileExists
+ }
+
+ override func attributesOfItem(atPath path: String) throws -> [FileAttributeKey: Any] {
+ capturedPath = URL(filePath: path)
+
+ if let error, error == .itemAttributesError {
+ throw error
+ }
+
+ return fileAttributes
+ }
+
+ override func moveItem(at srcURL: URL, to dstURL: URL) throws {
+ capturedOriginPath = srcURL
+ capturedDestinationPath = dstURL
+
+ if let error, error == .moveFileError {
+ throw error
+ }
+ }
+
+ override func copyItem(at srcURL: URL, to dstURL: URL) throws {
+ capturedOriginPath = srcURL
+ capturedDestinationPath = dstURL
+
+ if let error, error == .copyFileError {
+ throw error
+ }
+ }
+
+ override var temporaryDirectory: URL {
+ mockTemporaryDirectory ?? .init(filePath: "")
+ }
+}
diff --git a/IONFilesystemLibTests/file.txt b/IONFilesystemLibTests/file.txt
new file mode 100644
index 0000000..af5626b
--- /dev/null
+++ b/IONFilesystemLibTests/file.txt
@@ -0,0 +1 @@
+Hello, world!
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/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() }
-}
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"
+ )
+ ]
+)
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