@hot-updater/react-native 0.16.7-0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/HotUpdater.podspec +7 -11
- package/android/{generated/java/com/hotupdater → app/build/generated/source/codegen/java/com/facebook/fbreact/specs}/NativeHotUpdaterSpec.java +3 -2
- package/android/app/build/generated/source/codegen/jni/HotUpdater-generated.cpp +68 -0
- package/android/app/build/generated/source/codegen/jni/HotUpdater.h +31 -0
- package/android/{generated → app/build/generated/source/codegen}/jni/HotUpdaterSpec-generated.cpp +2 -2
- package/android/{generated/jni/react/renderer/components/HotUpdaterSpec/HotUpdaterSpecJSI-generated.cpp → app/build/generated/source/codegen/jni/react/renderer/components/HotUpdater/HotUpdaterJSI-generated.cpp} +3 -4
- package/{ios/generated/HotUpdaterSpecJSI.h → android/app/build/generated/source/codegen/jni/react/renderer/components/HotUpdater/HotUpdaterJSI.h} +53 -6
- package/{ios/generated → android/app/build/generated/source/codegen/jni/react/renderer/components/HotUpdaterSpec}/HotUpdaterSpecJSI-generated.cpp +2 -3
- package/android/{generated → app/build/generated/source/codegen}/jni/react/renderer/components/HotUpdaterSpec/HotUpdaterSpecJSI.h +59 -8
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +200 -0
- package/android/src/main/java/com/hotupdater/FileManagerService.kt +104 -0
- package/android/src/main/java/com/hotupdater/HotUpdater.kt +62 -305
- package/android/src/main/java/com/hotupdater/HotUpdaterFactory.kt +49 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +176 -0
- package/android/src/main/java/com/hotupdater/HttpDownloadService.kt +98 -0
- package/android/src/main/java/com/hotupdater/VersionedPreferencesService.kt +69 -0
- package/android/src/main/java/com/hotupdater/ZipFileUnzipService.kt +52 -0
- package/android/src/newarch/HotUpdaterModule.kt +31 -34
- package/android/src/newarch/ReactIntegrationManager.kt +17 -3
- package/android/src/oldarch/HotUpdaterModule.kt +32 -34
- package/android/src/oldarch/HotUpdaterSpec.kt +2 -9
- package/android/src/oldarch/ReactIntegrationManager.kt +0 -2
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +593 -0
- package/ios/HotUpdater/Internal/FileManagerService.swift +97 -0
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +8 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +241 -0
- package/ios/HotUpdater/Internal/HotUpdaterFactory.swift +24 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +143 -0
- package/ios/HotUpdater/Internal/NotificationExtension.swift +6 -0
- package/ios/HotUpdater/Internal/SSZipArchiveUnzipService.swift +25 -0
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +101 -0
- package/ios/HotUpdater/Internal/VersionedPreferencesService.swift +82 -0
- package/ios/HotUpdater/Package.resolved +15 -0
- package/ios/HotUpdater/Public/HotUpdater.h +29 -0
- package/lib/commonjs/checkForUpdate.js +70 -0
- package/lib/commonjs/checkForUpdate.js.map +1 -0
- package/lib/commonjs/error.js +14 -0
- package/lib/commonjs/error.js.map +1 -0
- package/lib/commonjs/fetchUpdateInfo.js +74 -0
- package/lib/commonjs/fetchUpdateInfo.js.map +1 -0
- package/lib/commonjs/hooks/useEventCallback.js +17 -0
- package/lib/commonjs/hooks/useEventCallback.js.map +1 -0
- package/lib/commonjs/index.js +234 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/native.js +132 -0
- package/lib/commonjs/native.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/runUpdateProcess.js +69 -0
- package/lib/commonjs/runUpdateProcess.js.map +1 -0
- package/lib/commonjs/specs/NativeHotUpdater.js +9 -0
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -0
- package/lib/commonjs/store.js +48 -0
- package/lib/commonjs/store.js.map +1 -0
- package/lib/commonjs/wrap.js +98 -0
- package/lib/commonjs/wrap.js.map +1 -0
- package/lib/module/checkForUpdate.js +64 -0
- package/lib/module/checkForUpdate.js.map +1 -0
- package/lib/module/error.js +9 -0
- package/lib/module/error.js.map +1 -0
- package/lib/module/fetchUpdateInfo.js +69 -0
- package/lib/module/fetchUpdateInfo.js.map +1 -0
- package/lib/module/hooks/useEventCallback.js +13 -0
- package/lib/module/hooks/useEventCallback.js.map +1 -0
- package/lib/module/index.js +211 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/native.js +119 -0
- package/lib/module/native.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/runUpdateProcess.js +64 -0
- package/lib/module/runUpdateProcess.js.map +1 -0
- package/lib/module/specs/NativeHotUpdater.js +5 -0
- package/lib/module/specs/NativeHotUpdater.js.map +1 -0
- package/lib/module/store.js +42 -0
- package/lib/module/store.js.map +1 -0
- package/lib/module/wrap.js +94 -0
- package/lib/module/wrap.js.map +1 -0
- package/lib/typescript/commonjs/checkForUpdate.d.ts +22 -0
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -0
- package/{dist → lib/typescript/commonjs}/error.d.ts +1 -0
- package/lib/typescript/commonjs/error.d.ts.map +1 -0
- package/lib/typescript/commonjs/fetchUpdateInfo.d.ts +4 -0
- package/lib/typescript/commonjs/fetchUpdateInfo.d.ts.map +1 -0
- package/{dist → lib/typescript/commonjs}/hooks/useEventCallback.d.ts +1 -0
- package/lib/typescript/commonjs/hooks/useEventCallback.d.ts.map +1 -0
- package/{dist → lib/typescript/commonjs}/index.d.ts +38 -12
- package/lib/typescript/commonjs/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/native.d.ts +64 -0
- package/lib/typescript/commonjs/native.d.ts.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/{dist → lib/typescript/commonjs}/runUpdateProcess.d.ts +1 -0
- package/lib/typescript/commonjs/runUpdateProcess.d.ts.map +1 -0
- package/{dist → lib/typescript/commonjs}/specs/NativeHotUpdater.d.ts +8 -9
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -0
- package/{dist → lib/typescript/commonjs}/store.d.ts +1 -0
- package/lib/typescript/commonjs/store.d.ts.map +1 -0
- package/{dist → lib/typescript/commonjs}/wrap.d.ts +3 -2
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -0
- package/lib/typescript/module/checkForUpdate.d.ts +22 -0
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -0
- package/lib/typescript/module/error.d.ts +4 -0
- package/lib/typescript/module/error.d.ts.map +1 -0
- package/lib/typescript/module/fetchUpdateInfo.d.ts +4 -0
- package/lib/typescript/module/fetchUpdateInfo.d.ts.map +1 -0
- package/lib/typescript/module/hooks/useEventCallback.d.ts +5 -0
- package/lib/typescript/module/hooks/useEventCallback.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +202 -0
- package/lib/typescript/module/index.d.ts.map +1 -0
- package/lib/typescript/module/native.d.ts +64 -0
- package/lib/typescript/module/native.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/runUpdateProcess.d.ts +49 -0
- package/lib/typescript/module/runUpdateProcess.d.ts.map +1 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +19 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -0
- package/lib/typescript/module/store.d.ts +11 -0
- package/lib/typescript/module/store.d.ts.map +1 -0
- package/lib/typescript/module/wrap.d.ts +51 -0
- package/lib/typescript/module/wrap.d.ts.map +1 -0
- package/package.json +59 -30
- package/src/checkForUpdate.ts +59 -9
- package/src/fetchUpdateInfo.ts +40 -12
- package/src/index.ts +37 -11
- package/src/native.ts +87 -41
- package/src/runUpdateProcess.ts +2 -2
- package/src/specs/NativeHotUpdater.ts +8 -10
- package/src/wrap.tsx +9 -13
- package/android/src/main/java/com/hotupdater/HotUpdaterPrefs.kt +0 -42
- package/dist/checkForUpdate.d.ts +0 -12
- package/dist/fetchUpdateInfo.d.ts +0 -3
- package/dist/index.js +0 -341
- package/dist/index.mjs +0 -301
- package/dist/native.d.ts +0 -41
- package/ios/HotUpdater/HotUpdater.h +0 -15
- package/ios/HotUpdater/HotUpdater.mm +0 -468
- package/ios/HotUpdater/HotUpdater.modulemap +0 -6
- package/ios/HotUpdater/HotUpdaterPrefs.h +0 -9
- package/ios/HotUpdater/HotUpdaterPrefs.mm +0 -45
- package/ios/generated/HotUpdaterSpec/HotUpdaterSpec-generated.mm +0 -81
- package/ios/generated/HotUpdaterSpec/HotUpdaterSpec.h +0 -112
- package/react-native.config.js +0 -12
- package/src/global.d.ts +0 -3
- /package/android/{generated → app/build/generated/source/codegen}/jni/CMakeLists.txt +0 -0
- /package/android/{generated → app/build/generated/source/codegen}/jni/HotUpdaterSpec.h +0 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
public enum BundleStorageError: Error {
|
|
4
|
+
case bundleNotFound
|
|
5
|
+
case directoryCreationFailed
|
|
6
|
+
case downloadFailed(Error)
|
|
7
|
+
case extractionFailed(Error)
|
|
8
|
+
case invalidBundle
|
|
9
|
+
case moveOperationFailed(Error)
|
|
10
|
+
case copyOperationFailed(Error)
|
|
11
|
+
case fileSystemError(Error)
|
|
12
|
+
case unknown(Error?)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Protocol for interacting with bundle storage system.
|
|
17
|
+
* `updateBundle` operates asynchronously using a completion handler.
|
|
18
|
+
* Other operations are synchronous.
|
|
19
|
+
*/
|
|
20
|
+
public protocol BundleStorageService {
|
|
21
|
+
|
|
22
|
+
// Bundle URL operations
|
|
23
|
+
func setBundleURL(localPath: String?) -> Result<Void, Error>
|
|
24
|
+
func getCachedBundleURL() -> URL?
|
|
25
|
+
func getFallbackBundleURL() -> URL? // Synchronous as it's lightweight
|
|
26
|
+
func getBundleURL() -> URL?
|
|
27
|
+
|
|
28
|
+
// Bundle update
|
|
29
|
+
func updateBundle(bundleId: String, fileUrl: URL?, completion: @escaping (Result<Bool, Error>) -> Void)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class BundleFileStorageService: BundleStorageService {
|
|
33
|
+
private let fileSystem: FileSystemService
|
|
34
|
+
private let downloadService: DownloadService
|
|
35
|
+
private let unzipService: UnzipService
|
|
36
|
+
private let preferences: PreferencesService
|
|
37
|
+
|
|
38
|
+
// Queue for potentially long-running sequences within updateBundle or for explicit background tasks.
|
|
39
|
+
private let fileOperationQueue: DispatchQueue
|
|
40
|
+
|
|
41
|
+
private var activeTasks: [URLSessionTask] = []
|
|
42
|
+
|
|
43
|
+
public init(fileSystem: FileSystemService,
|
|
44
|
+
downloadService: DownloadService,
|
|
45
|
+
unzipService: UnzipService,
|
|
46
|
+
preferences: PreferencesService) {
|
|
47
|
+
|
|
48
|
+
self.fileSystem = fileSystem
|
|
49
|
+
self.downloadService = downloadService
|
|
50
|
+
self.unzipService = unzipService
|
|
51
|
+
self.preferences = preferences
|
|
52
|
+
|
|
53
|
+
self.fileOperationQueue = DispatchQueue(label: "com.hotupdater.fileoperations",
|
|
54
|
+
qos: .utility,
|
|
55
|
+
attributes: .concurrent)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// MARK: - Directory Management
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Ensures a directory exists at the specified path. Creates it if necessary.
|
|
62
|
+
* Executes synchronously on the calling thread.
|
|
63
|
+
* @param path The path where directory should exist
|
|
64
|
+
* @return Result with the path or an error
|
|
65
|
+
*/
|
|
66
|
+
private func ensureDirectoryExists(path: String) -> Result<String, Error> {
|
|
67
|
+
if !self.fileSystem.fileExists(atPath: path) {
|
|
68
|
+
if !self.fileSystem.createDirectory(atPath: path) {
|
|
69
|
+
return .failure(BundleStorageError.directoryCreationFailed)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return .success(path)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Gets the path to the bundle store directory.
|
|
77
|
+
* Executes synchronously on the calling thread.
|
|
78
|
+
* @return Result with the directory path or error
|
|
79
|
+
*/
|
|
80
|
+
func bundleStoreDir() -> Result<String, Error> {
|
|
81
|
+
let path = (fileSystem.documentsPath() as NSString).appendingPathComponent("bundle-store")
|
|
82
|
+
return ensureDirectoryExists(path: path)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Gets the path to the temporary directory.
|
|
87
|
+
* Executes synchronously on the calling thread.
|
|
88
|
+
* @return Result with the directory path or error
|
|
89
|
+
*/
|
|
90
|
+
func tempDir() -> Result<String, Error> {
|
|
91
|
+
let path = (fileSystem.documentsPath() as NSString).appendingPathComponent("bundle-temp")
|
|
92
|
+
return ensureDirectoryExists(path: path)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Cleans up temporary files safely. Executes synchronously on the calling thread.
|
|
97
|
+
* @param paths Array of file/directory paths to clean up
|
|
98
|
+
*/
|
|
99
|
+
private func cleanupTemporaryFiles(_ paths: [String]) {
|
|
100
|
+
let workItem = DispatchWorkItem {
|
|
101
|
+
for path in paths {
|
|
102
|
+
do {
|
|
103
|
+
if self.fileSystem.fileExists(atPath: path) {
|
|
104
|
+
try self.fileSystem.removeItem(atPath: path)
|
|
105
|
+
NSLog("[BundleStorage] Cleaned up temporary file: \(path)")
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
NSLog("[BundleStorage] Failed to clean up temporary file \(path): \(error.localizedDescription)")
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
DispatchQueue.global(qos: .background).async(execute: workItem)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// MARK: - Bundle File Operations
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Finds the bundle file within a directory by checking direct paths.
|
|
119
|
+
* Executes synchronously on the calling thread.
|
|
120
|
+
* @param directoryPath Directory to search in
|
|
121
|
+
* @return Result with path to bundle file or error
|
|
122
|
+
*/
|
|
123
|
+
func findBundleFile(in directoryPath: String) -> Result<String?, Error> {
|
|
124
|
+
NSLog("[BundleStorage] Searching for bundle file in directory: \(directoryPath)")
|
|
125
|
+
|
|
126
|
+
// Check directory contents
|
|
127
|
+
do {
|
|
128
|
+
let contents = try self.fileSystem.contentsOfDirectory(atPath: directoryPath)
|
|
129
|
+
NSLog("[BundleStorage] Directory contents: \(contents)")
|
|
130
|
+
|
|
131
|
+
// Check for iOS bundle file directly
|
|
132
|
+
let iosBundlePath = (directoryPath as NSString).appendingPathComponent("index.ios.bundle")
|
|
133
|
+
if self.fileSystem.fileExists(atPath: iosBundlePath) {
|
|
134
|
+
NSLog("[BundleStorage] Found iOS bundle atPath: \(iosBundlePath)")
|
|
135
|
+
return .success(iosBundlePath)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check for main bundle file
|
|
139
|
+
let mainBundlePath = (directoryPath as NSString).appendingPathComponent("main.jsbundle")
|
|
140
|
+
if self.fileSystem.fileExists(atPath: mainBundlePath) {
|
|
141
|
+
NSLog("[BundleStorage] Found main bundle atPath: \(mainBundlePath)")
|
|
142
|
+
return .success(mainBundlePath)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Additional search: check all .bundle files
|
|
146
|
+
for file in contents {
|
|
147
|
+
if file.hasSuffix(".bundle") {
|
|
148
|
+
let bundlePath = (directoryPath as NSString).appendingPathComponent(file)
|
|
149
|
+
NSLog("[BundleStorage] Found alternative bundle atPath: \(bundlePath)")
|
|
150
|
+
return .success(bundlePath)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
NSLog("[BundleStorage] No bundle file found in directory: \(directoryPath)")
|
|
155
|
+
NSLog("[BundleStorage] Available files: \(contents)")
|
|
156
|
+
return .success(nil)
|
|
157
|
+
} catch let error {
|
|
158
|
+
NSLog("[BundleStorage] Error reading directory contents: \(error.localizedDescription)")
|
|
159
|
+
return .failure(error)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Cleans up old bundles, keeping only the current and latest bundles.
|
|
165
|
+
* Executes synchronously on the calling thread.
|
|
166
|
+
* @param currentBundleId ID of the current active bundle (optional)
|
|
167
|
+
* @return Result of operation
|
|
168
|
+
*/
|
|
169
|
+
func cleanupOldBundles(currentBundleId: String?) -> Result<Void, Error> {
|
|
170
|
+
let storeDirResult = bundleStoreDir()
|
|
171
|
+
|
|
172
|
+
guard case .success(let storeDir) = storeDirResult else {
|
|
173
|
+
return .failure(storeDirResult.failureError ?? BundleStorageError.unknown(nil))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
do {
|
|
177
|
+
var contents: [String]
|
|
178
|
+
do {
|
|
179
|
+
contents = try self.fileSystem.contentsOfDirectory(atPath: storeDir)
|
|
180
|
+
} catch let error {
|
|
181
|
+
NSLog("[BundleStorage] Failed to list contents of bundle store directory: \(storeDir)")
|
|
182
|
+
return .failure(BundleStorageError.fileSystemError(error))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if contents.isEmpty {
|
|
186
|
+
NSLog("[BundleStorage] No bundles to clean up.")
|
|
187
|
+
return .success(())
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let currentBundlePath = currentBundleId != nil ?
|
|
191
|
+
(storeDir as NSString).appendingPathComponent(currentBundleId!) : nil
|
|
192
|
+
|
|
193
|
+
var latestBundlePath: String? = nil
|
|
194
|
+
var latestModDate: Date = .distantPast
|
|
195
|
+
|
|
196
|
+
for item in contents {
|
|
197
|
+
let fullPath = (storeDir as NSString).appendingPathComponent(item)
|
|
198
|
+
|
|
199
|
+
if fullPath == currentBundlePath {
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if self.fileSystem.fileExists(atPath: fullPath) {
|
|
204
|
+
do {
|
|
205
|
+
let attributes = try self.fileSystem.attributesOfItem(atPath: fullPath)
|
|
206
|
+
if let modDate = attributes[FileAttributeKey.modificationDate] as? Date {
|
|
207
|
+
if modDate > latestModDate {
|
|
208
|
+
latestModDate = modDate
|
|
209
|
+
latestBundlePath = fullPath
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
NSLog("[BundleStorage] Warning: Could not get attributes for \(fullPath): \(error)")
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
var bundlesToKeep = Set<String>()
|
|
219
|
+
|
|
220
|
+
if let currentPath = currentBundlePath, self.fileSystem.fileExists(atPath: currentPath) {
|
|
221
|
+
bundlesToKeep.insert(currentPath)
|
|
222
|
+
NSLog("[BundleStorage] Keeping current bundle: \(currentBundleId!)")
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if let latestPath = latestBundlePath {
|
|
226
|
+
bundlesToKeep.insert(latestPath)
|
|
227
|
+
NSLog("[BundleStorage] Keeping latest bundle: \((latestPath as NSString).lastPathComponent)")
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
var removedCount = 0
|
|
231
|
+
for item in contents {
|
|
232
|
+
let fullPath = (storeDir as NSString).appendingPathComponent(item)
|
|
233
|
+
if !bundlesToKeep.contains(fullPath) {
|
|
234
|
+
do {
|
|
235
|
+
try self.fileSystem.removeItem(atPath: fullPath)
|
|
236
|
+
removedCount += 1
|
|
237
|
+
NSLog("[BundleStorage] Removed old bundle: \(item)")
|
|
238
|
+
} catch {
|
|
239
|
+
NSLog("[BundleStorage] Failed to remove old bundle at \(fullPath): \(error)")
|
|
240
|
+
// Optionally, collect errors and return a multiple error type or first error
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if removedCount == 0 {
|
|
246
|
+
NSLog("[BundleStorage] No old bundles to remove.")
|
|
247
|
+
} else {
|
|
248
|
+
NSLog("[BundleStorage] Removed \(removedCount) old bundle(s).")
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return .success(())
|
|
252
|
+
} catch let error {
|
|
253
|
+
return .failure(error)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Sets the current bundle URL in preferences.
|
|
259
|
+
* Executes synchronously on the calling thread.
|
|
260
|
+
* @param localPath Path to the bundle file (or nil to reset)
|
|
261
|
+
* @return Result of operation
|
|
262
|
+
*/
|
|
263
|
+
func setBundleURL(localPath: String?) -> Result<Void, Error> {
|
|
264
|
+
do {
|
|
265
|
+
NSLog("[BundleStorage] Setting bundle URL to: \(localPath ?? "nil")")
|
|
266
|
+
try self.preferences.setItem(localPath, forKey: "HotUpdaterBundleURL")
|
|
267
|
+
return .success(())
|
|
268
|
+
} catch let error {
|
|
269
|
+
return .failure(error)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Gets the URL to the cached bundle file if it exists.
|
|
275
|
+
*/
|
|
276
|
+
func getCachedBundleURL() -> URL? {
|
|
277
|
+
do {
|
|
278
|
+
guard let savedURLString = try self.preferences.getItem(forKey: "HotUpdaterBundleURL"),
|
|
279
|
+
let bundleURL = URL(string: savedURLString),
|
|
280
|
+
self.fileSystem.fileExists(atPath: bundleURL.path) else {
|
|
281
|
+
return nil
|
|
282
|
+
}
|
|
283
|
+
return bundleURL
|
|
284
|
+
} catch {
|
|
285
|
+
NSLog("[BundleStorage] Error getting cached bundle URL: \(error.localizedDescription)")
|
|
286
|
+
return nil
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Gets the URL to the fallback bundle included in the app.
|
|
292
|
+
* @return URL to the fallback bundle or nil if not found
|
|
293
|
+
*/
|
|
294
|
+
func getFallbackBundleURL() -> URL? {
|
|
295
|
+
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
public func getBundleURL() -> URL? {
|
|
299
|
+
return getCachedBundleURL() ?? getFallbackBundleURL()
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// MARK: - Bundle Update
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Updates the bundle from the specified URL. This operation is asynchronous.
|
|
306
|
+
* @param bundleId ID of the bundle to update
|
|
307
|
+
* @param fileUrl URL of the bundle file to download (or nil to reset)
|
|
308
|
+
* @param completion Callback with result of the operation
|
|
309
|
+
*/
|
|
310
|
+
func updateBundle(bundleId: String, fileUrl: URL?, completion: @escaping (Result<Bool, Error>) -> Void) {
|
|
311
|
+
guard let validFileUrl = fileUrl else {
|
|
312
|
+
NSLog("[BundleStorage] fileUrl is nil, resetting bundle URL.")
|
|
313
|
+
// Dispatch the sequence to the file operation queue to ensure completion is called asynchronously
|
|
314
|
+
// and to keep file operations off the calling thread if it's the main thread.
|
|
315
|
+
fileOperationQueue.async {
|
|
316
|
+
let setResult = self.setBundleURL(localPath: nil)
|
|
317
|
+
switch setResult {
|
|
318
|
+
case .success:
|
|
319
|
+
let cleanupResult = self.cleanupOldBundles(currentBundleId: nil)
|
|
320
|
+
switch cleanupResult {
|
|
321
|
+
case .success:
|
|
322
|
+
completion(.success(true))
|
|
323
|
+
case .failure(let error):
|
|
324
|
+
NSLog("[BundleStorage] Error during cleanup after reset: \(error)")
|
|
325
|
+
completion(.failure(error))
|
|
326
|
+
}
|
|
327
|
+
case .failure(let error):
|
|
328
|
+
NSLog("[BundleStorage] Error resetting bundle URL: \(error)")
|
|
329
|
+
completion(.failure(error))
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Start the bundle update process, dispatching the main logic to fileOperationQueue
|
|
336
|
+
fileOperationQueue.async {
|
|
337
|
+
let storeDirResult = self.bundleStoreDir()
|
|
338
|
+
guard case .success(let storeDir) = storeDirResult else {
|
|
339
|
+
completion(.failure(storeDirResult.failureError ?? BundleStorageError.unknown(nil)))
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let finalBundleDir = (storeDir as NSString).appendingPathComponent(bundleId)
|
|
344
|
+
|
|
345
|
+
if self.fileSystem.fileExists(atPath: finalBundleDir) {
|
|
346
|
+
let findResult = self.findBundleFile(in: finalBundleDir)
|
|
347
|
+
switch findResult {
|
|
348
|
+
case .success(let existingBundlePath):
|
|
349
|
+
if let bundlePath = existingBundlePath {
|
|
350
|
+
NSLog("[BundleStorage] Using cached bundle at path: \(bundlePath)")
|
|
351
|
+
do {
|
|
352
|
+
try self.fileSystem.setAttributes([.modificationDate: Date()], ofItemAtPath: finalBundleDir)
|
|
353
|
+
|
|
354
|
+
let setResult = self.setBundleURL(localPath: bundlePath)
|
|
355
|
+
switch setResult {
|
|
356
|
+
case .success:
|
|
357
|
+
let cleanupResult = self.cleanupOldBundles(currentBundleId: bundleId)
|
|
358
|
+
switch cleanupResult {
|
|
359
|
+
case .success:
|
|
360
|
+
completion(.success(true))
|
|
361
|
+
case .failure(let error):
|
|
362
|
+
NSLog("[BundleStorage] Warning: Cleanup failed but bundle is set: \(error)")
|
|
363
|
+
completion(.failure(error)) // Or consider .success(true) if main operation succeeded
|
|
364
|
+
}
|
|
365
|
+
case .failure(let error):
|
|
366
|
+
completion(.failure(error))
|
|
367
|
+
}
|
|
368
|
+
} catch let error {
|
|
369
|
+
completion(.failure(error))
|
|
370
|
+
}
|
|
371
|
+
return
|
|
372
|
+
} else {
|
|
373
|
+
NSLog("[BundleStorage] Cached directory exists but invalid, removing: \(finalBundleDir)")
|
|
374
|
+
do {
|
|
375
|
+
try self.fileSystem.removeItem(atPath: finalBundleDir)
|
|
376
|
+
// Continue with download process on success (must be called on a thread that can continue,
|
|
377
|
+
// but prepareAndDownloadBundle will manage its own threading for download)
|
|
378
|
+
self.prepareAndDownloadBundle(bundleId: bundleId, fileUrl: validFileUrl, finalBundleDir: finalBundleDir, completion: completion)
|
|
379
|
+
} catch let error {
|
|
380
|
+
NSLog("[BundleStorage] Failed to remove invalid bundle dir: \(error.localizedDescription)")
|
|
381
|
+
completion(.failure(BundleStorageError.fileSystemError(error)))
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
case .failure(let error):
|
|
385
|
+
completion(.failure(error))
|
|
386
|
+
}
|
|
387
|
+
} else {
|
|
388
|
+
self.prepareAndDownloadBundle(bundleId: bundleId, fileUrl: validFileUrl, finalBundleDir: finalBundleDir, completion: completion)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Prepares directories and starts the download process.
|
|
395
|
+
* This method is part of the asynchronous `updateBundle` flow.
|
|
396
|
+
* @param bundleId ID of the bundle to update
|
|
397
|
+
* @param fileUrl URL of the bundle file to download
|
|
398
|
+
* @param finalBundleDir Final directory for the bundle
|
|
399
|
+
* @param completion Callback with result of the operation
|
|
400
|
+
*/
|
|
401
|
+
private func prepareAndDownloadBundle(bundleId: String, fileUrl: URL, finalBundleDir: String, completion: @escaping (Result<Bool, Error>) -> Void) {
|
|
402
|
+
// tempDir() is now synchronous. The rest of this function manages async download.
|
|
403
|
+
let tempDirResult = tempDir()
|
|
404
|
+
guard case .success(let tempDirectory) = tempDirResult else {
|
|
405
|
+
completion(.failure(tempDirResult.failureError ?? BundleStorageError.unknown(nil)))
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// The rest of the operations (cleanup, dir creation, download) should still be on a background thread
|
|
410
|
+
// This is already within a fileOperationQueue.async block from updateBundle or needs to be if called directly.
|
|
411
|
+
// For safety, ensure this block runs on the fileOperationQueue if it's not already.
|
|
412
|
+
// However, prepareAndDownloadBundle is called from an existing fileOperationQueue.async block in updateBundle.
|
|
413
|
+
|
|
414
|
+
// Clean up any previous temp dir (sync operation)
|
|
415
|
+
try? self.fileSystem.removeItem(atPath: tempDirectory)
|
|
416
|
+
|
|
417
|
+
// Create necessary directories (sync operation)
|
|
418
|
+
if !self.fileSystem.createDirectory(atPath: tempDirectory) {
|
|
419
|
+
completion(.failure(BundleStorageError.directoryCreationFailed))
|
|
420
|
+
return
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
let tempZipFile = (tempDirectory as NSString).appendingPathComponent("bundle.zip")
|
|
424
|
+
let extractedDir = (tempDirectory as NSString).appendingPathComponent("extracted")
|
|
425
|
+
|
|
426
|
+
if !self.fileSystem.createDirectory(atPath: extractedDir) {
|
|
427
|
+
completion(.failure(BundleStorageError.directoryCreationFailed))
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
NSLog("[BundleStorage] Starting download from \(fileUrl)")
|
|
432
|
+
|
|
433
|
+
// DownloadService handles its own threading for the download task.
|
|
434
|
+
// The completion handler for downloadService.downloadFile is then dispatched to fileOperationQueue.
|
|
435
|
+
let task = self.downloadService.downloadFile(from: fileUrl, to: tempZipFile, progressHandler: { progress in
|
|
436
|
+
// Progress updates handled by notification system
|
|
437
|
+
}, completion: { [weak self] result in
|
|
438
|
+
guard let self = self else {
|
|
439
|
+
let error = NSError(domain: "HotUpdaterError", code: 998,
|
|
440
|
+
userInfo: [NSLocalizedDescriptionKey: "Self deallocated during download"])
|
|
441
|
+
completion(.failure(error))
|
|
442
|
+
return
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Dispatch the processing of the downloaded file to the file operation queue
|
|
446
|
+
let workItem = DispatchWorkItem {
|
|
447
|
+
switch result {
|
|
448
|
+
case .success(let location):
|
|
449
|
+
self.processDownloadedFile(
|
|
450
|
+
location: location,
|
|
451
|
+
tempZipFile: tempZipFile,
|
|
452
|
+
extractedDir: extractedDir,
|
|
453
|
+
finalBundleDir: finalBundleDir,
|
|
454
|
+
bundleId: bundleId,
|
|
455
|
+
tempDirectory: tempDirectory,
|
|
456
|
+
completion: completion
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
case .failure(let error):
|
|
460
|
+
NSLog("[BundleStorage] Download failed: \(error.localizedDescription)")
|
|
461
|
+
self.cleanupTemporaryFiles([tempDirectory]) // Sync cleanup
|
|
462
|
+
completion(.failure(BundleStorageError.downloadFailed(error)))
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
self.fileOperationQueue.async(execute: workItem)
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
if let task = task {
|
|
469
|
+
self.activeTasks.append(task) // Manage active tasks
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Processes a downloaded bundle file.
|
|
475
|
+
* This method is part of the asynchronous `updateBundle` flow and is expected to run on a background thread.
|
|
476
|
+
* @param location URL of the downloaded file
|
|
477
|
+
* @param tempZipFile Path to store the downloaded zip file
|
|
478
|
+
* @param extractedDir Directory to extract contents to
|
|
479
|
+
* @param finalBundleDir Final directory for the bundle
|
|
480
|
+
* @param bundleId ID of the bundle being processed
|
|
481
|
+
* @param tempDirectory Temporary directory for processing
|
|
482
|
+
* @param completion Callback with result of the operation
|
|
483
|
+
*/
|
|
484
|
+
private func processDownloadedFile(
|
|
485
|
+
location: URL,
|
|
486
|
+
tempZipFile: String,
|
|
487
|
+
extractedDir: String,
|
|
488
|
+
finalBundleDir: String,
|
|
489
|
+
bundleId: String,
|
|
490
|
+
tempDirectory: String,
|
|
491
|
+
completion: @escaping (Result<Bool, Error>) -> Void
|
|
492
|
+
) {
|
|
493
|
+
NSLog("[BundleStorage] Processing downloaded file atPath: \(location.path)")
|
|
494
|
+
|
|
495
|
+
// 1. Check if source file exists
|
|
496
|
+
guard self.fileSystem.fileExists(atPath: location.path) else {
|
|
497
|
+
NSLog("[BundleStorage] Source file does not exist atPath: \(location.path)")
|
|
498
|
+
self.cleanupTemporaryFiles([tempDirectory])
|
|
499
|
+
completion(.failure(BundleStorageError.fileSystemError(NSError(
|
|
500
|
+
domain: "HotUpdaterError",
|
|
501
|
+
code: 1,
|
|
502
|
+
userInfo: [NSLocalizedDescriptionKey: "Source file does not exist atPath: \(location.path)"]
|
|
503
|
+
))))
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// 2. Create target directory
|
|
508
|
+
do {
|
|
509
|
+
let tempZipFileURL = URL(fileURLWithPath: tempZipFile)
|
|
510
|
+
let tempZipFileDirectory = tempZipFileURL.deletingLastPathComponent()
|
|
511
|
+
|
|
512
|
+
if !self.fileSystem.fileExists(atPath: tempZipFileDirectory.path) {
|
|
513
|
+
try self.fileSystem.createDirectory(atPath: tempZipFileDirectory.path)
|
|
514
|
+
NSLog("[BundleStorage] Created directory atPath: \(tempZipFileDirectory.path)")
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
try self.fileSystem.moveItem(atPath: location.path, toPath: tempZipFile)
|
|
518
|
+
NSLog("[BundleStorage] Successfully moved file to: \(tempZipFile)")
|
|
519
|
+
|
|
520
|
+
try self.unzipService.unzip(file: tempZipFile, to: extractedDir)
|
|
521
|
+
NSLog("[BundleStorage] Successfully extracted to: \(extractedDir)")
|
|
522
|
+
|
|
523
|
+
// 6. Remove temporary zip file
|
|
524
|
+
try? self.fileSystem.removeItem(atPath: tempZipFile)
|
|
525
|
+
|
|
526
|
+
// 7. Search for bundle file
|
|
527
|
+
switch self.findBundleFile(in: extractedDir) {
|
|
528
|
+
case .success(let bundlePath):
|
|
529
|
+
if let bundlePath = bundlePath {
|
|
530
|
+
NSLog("[BundleStorage] Found bundle atPath: \(bundlePath)")
|
|
531
|
+
|
|
532
|
+
// 8. Create final bundle directory
|
|
533
|
+
if !self.fileSystem.fileExists(atPath: finalBundleDir) {
|
|
534
|
+
try self.fileSystem.createDirectory(atPath: finalBundleDir)
|
|
535
|
+
NSLog("[BundleStorage] Created final bundle directory atPath: \(finalBundleDir)")
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// 9. Move entire extracted directory to final location
|
|
539
|
+
if self.fileSystem.fileExists(atPath: finalBundleDir) {
|
|
540
|
+
try self.fileSystem.removeItem(atPath: finalBundleDir)
|
|
541
|
+
}
|
|
542
|
+
try self.fileSystem.moveItem(atPath: extractedDir, toPath: finalBundleDir)
|
|
543
|
+
NSLog("[BundleStorage] Successfully moved entire bundle directory to: \(finalBundleDir)")
|
|
544
|
+
|
|
545
|
+
let findResult = self.findBundleFile(in: finalBundleDir)
|
|
546
|
+
switch findResult {
|
|
547
|
+
case .success(let finalBundlePath):
|
|
548
|
+
if let finalBundlePath = finalBundlePath {
|
|
549
|
+
let setResult = self.setBundleURL(localPath: finalBundlePath)
|
|
550
|
+
switch setResult {
|
|
551
|
+
case .success:
|
|
552
|
+
// 10. Cleanup
|
|
553
|
+
self.cleanupTemporaryFiles([tempDirectory])
|
|
554
|
+
completion(.success(true))
|
|
555
|
+
case .failure(let error):
|
|
556
|
+
self.cleanupTemporaryFiles([tempDirectory])
|
|
557
|
+
completion(.failure(error))
|
|
558
|
+
}
|
|
559
|
+
} else {
|
|
560
|
+
NSLog("[BundleStorage] No bundle file found in final directory")
|
|
561
|
+
self.cleanupTemporaryFiles([tempDirectory])
|
|
562
|
+
completion(.failure(BundleStorageError.invalidBundle))
|
|
563
|
+
}
|
|
564
|
+
case .failure(let error):
|
|
565
|
+
NSLog("[BundleStorage] Error finding bundle file: \(error.localizedDescription)")
|
|
566
|
+
self.cleanupTemporaryFiles([tempDirectory])
|
|
567
|
+
completion(.failure(error))
|
|
568
|
+
}
|
|
569
|
+
} else {
|
|
570
|
+
NSLog("[BundleStorage] No bundle file found in extracted directory")
|
|
571
|
+
self.cleanupTemporaryFiles([tempDirectory])
|
|
572
|
+
completion(.failure(BundleStorageError.invalidBundle))
|
|
573
|
+
}
|
|
574
|
+
case .failure(let error):
|
|
575
|
+
NSLog("[BundleStorage] Error finding bundle file: \(error.localizedDescription)")
|
|
576
|
+
self.cleanupTemporaryFiles([tempDirectory])
|
|
577
|
+
completion(.failure(error))
|
|
578
|
+
}
|
|
579
|
+
} catch let error {
|
|
580
|
+
NSLog("[BundleStorage] Error processing downloaded file: \(error.localizedDescription)")
|
|
581
|
+
self.cleanupTemporaryFiles([tempDirectory])
|
|
582
|
+
completion(.failure(BundleStorageError.fileSystemError(error)))
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Helper to get the associated error from a Result, if it's a failure
|
|
588
|
+
extension Result {
|
|
589
|
+
var failureError: Failure? {
|
|
590
|
+
guard case .failure(let error) = self else { return nil }
|
|
591
|
+
return error
|
|
592
|
+
}
|
|
593
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
// MARK: - File System Service
|
|
4
|
+
|
|
5
|
+
enum FileSystemError: Error {
|
|
6
|
+
case createDirectoryFailed(String)
|
|
7
|
+
case fileOperationFailed(String, Error)
|
|
8
|
+
case fileNotFound(String)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
protocol FileSystemService {
|
|
12
|
+
func fileExists(atPath path: String) -> Bool
|
|
13
|
+
func createDirectory(atPath path: String) -> Bool
|
|
14
|
+
func removeItem(atPath path: String) throws
|
|
15
|
+
func moveItem(atPath srcPath: String, toPath dstPath: String) throws
|
|
16
|
+
func copyItem(atPath srcPath: String, toPath dstPath: String) throws
|
|
17
|
+
func contentsOfDirectory(atPath path: String) throws -> [String]
|
|
18
|
+
func setAttributes(_ attributes: [FileAttributeKey: Any], ofItemAtPath path: String) throws
|
|
19
|
+
func attributesOfItem(atPath path: String) throws -> [FileAttributeKey: Any]
|
|
20
|
+
func documentsPath() -> String
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class FileManagerService: FileSystemService {
|
|
24
|
+
private let fileManager = FileManager.default
|
|
25
|
+
|
|
26
|
+
func fileExists(atPath path: String) -> Bool {
|
|
27
|
+
return fileManager.fileExists(atPath: path)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func createDirectory(atPath path: String) -> Bool {
|
|
31
|
+
do {
|
|
32
|
+
try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
|
|
33
|
+
return true
|
|
34
|
+
} catch let error {
|
|
35
|
+
NSLog("[FileSystemService] Failed to create directory at \(path): \(error)")
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func removeItem(atPath path: String) throws {
|
|
41
|
+
do {
|
|
42
|
+
try fileManager.removeItem(atPath: path)
|
|
43
|
+
} catch let error {
|
|
44
|
+
NSLog("[FileSystemService] Failed to remove item at \(path): \(error)")
|
|
45
|
+
throw FileSystemError.fileOperationFailed(path, error)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func moveItem(atPath srcPath: String, toPath dstPath: String) throws {
|
|
50
|
+
do {
|
|
51
|
+
try fileManager.moveItem(atPath: srcPath, toPath: dstPath)
|
|
52
|
+
} catch let error {
|
|
53
|
+
NSLog("[FileSystemService] Failed to move item from \(srcPath) to \(dstPath): \(error)")
|
|
54
|
+
throw FileSystemError.fileOperationFailed(srcPath, error)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func copyItem(atPath srcPath: String, toPath dstPath: String) throws {
|
|
59
|
+
do {
|
|
60
|
+
try fileManager.copyItem(atPath: srcPath, toPath: dstPath)
|
|
61
|
+
} catch let error {
|
|
62
|
+
NSLog("[FileSystemService] Failed to copy item from \(srcPath) to \(dstPath): \(error)")
|
|
63
|
+
throw FileSystemError.fileOperationFailed(srcPath, error)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func contentsOfDirectory(atPath path: String) throws -> [String] {
|
|
68
|
+
do {
|
|
69
|
+
return try fileManager.contentsOfDirectory(atPath: path)
|
|
70
|
+
} catch let error {
|
|
71
|
+
NSLog("[FileSystemService] Failed to get directory contents at \(path): \(error)")
|
|
72
|
+
throw FileSystemError.fileOperationFailed(path, error)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func setAttributes(_ attributes: [FileAttributeKey: Any], ofItemAtPath path: String) throws {
|
|
77
|
+
do {
|
|
78
|
+
try fileManager.setAttributes(attributes, ofItemAtPath: path)
|
|
79
|
+
} catch let error {
|
|
80
|
+
NSLog("[FileSystemService] Failed to set attributes for \(path): \(error)")
|
|
81
|
+
throw FileSystemError.fileOperationFailed(path, error)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func attributesOfItem(atPath path: String) throws -> [FileAttributeKey: Any] {
|
|
86
|
+
do {
|
|
87
|
+
return try fileManager.attributesOfItem(atPath: path)
|
|
88
|
+
} catch let error {
|
|
89
|
+
NSLog("[FileSystemService] Failed to get attributes for \(path): \(error)")
|
|
90
|
+
throw FileSystemError.fileOperationFailed(path, error)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func documentsPath() -> String {
|
|
95
|
+
return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
|
|
96
|
+
}
|
|
97
|
+
}
|