@amazon-devices/expo-file-system 2.0.1758683737
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/.eslintrc.js +2 -0
- package/CHANGELOG.md +317 -0
- package/CMakeLists.txt +52 -0
- package/LICENSE +7 -0
- package/README.kepler.md +260 -0
- package/README.md +46 -0
- package/THIRD_PARTY_NOTICES.txt +24 -0
- package/android/build.gradle +118 -0
- package/android/src/main/AndroidManifest.xml +17 -0
- package/android/src/main/java/expo/modules/filesystem/AppDirectoriesModule.kt +26 -0
- package/android/src/main/java/expo/modules/filesystem/CountingRequestBody.kt +52 -0
- package/android/src/main/java/expo/modules/filesystem/FilePermissionModule.kt +48 -0
- package/android/src/main/java/expo/modules/filesystem/FileSystemExceptions.kt +48 -0
- package/android/src/main/java/expo/modules/filesystem/FileSystemFileProvider.kt +5 -0
- package/android/src/main/java/expo/modules/filesystem/FileSystemModule.kt +1110 -0
- package/android/src/main/java/expo/modules/filesystem/FileSystemPackage.kt +10 -0
- package/android/src/main/java/expo/modules/filesystem/FileSystemRecords.kt +92 -0
- package/android/src/main/res/xml/file_system_provider_paths.xml +5 -0
- package/app.plugin.js +1 -0
- package/build/ExponentFileSystem.d.ts +4 -0
- package/build/ExponentFileSystem.d.ts.map +1 -0
- package/build/ExponentFileSystem.js +5 -0
- package/build/ExponentFileSystem.js.map +1 -0
- package/build/ExponentFileSystem.kepler.d.ts +4 -0
- package/build/ExponentFileSystem.kepler.d.ts.map +1 -0
- package/build/ExponentFileSystem.kepler.js +112 -0
- package/build/ExponentFileSystem.kepler.js.map +1 -0
- package/build/ExponentFileSystem.web.d.ts +3 -0
- package/build/ExponentFileSystem.web.d.ts.map +1 -0
- package/build/ExponentFileSystem.web.js +3 -0
- package/build/ExponentFileSystem.web.js.map +1 -0
- package/build/ExponentFileSystemShim.d.ts +4 -0
- package/build/ExponentFileSystemShim.d.ts.map +1 -0
- package/build/ExponentFileSystemShim.js +21 -0
- package/build/ExponentFileSystemShim.js.map +1 -0
- package/build/FileSystem.d.ts +361 -0
- package/build/FileSystem.d.ts.map +1 -0
- package/build/FileSystem.js +616 -0
- package/build/FileSystem.js.map +1 -0
- package/build/FileSystem.types.d.ts +304 -0
- package/build/FileSystem.types.d.ts.map +1 -0
- package/build/FileSystem.types.js +47 -0
- package/build/FileSystem.types.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +3 -0
- package/build/index.js.map +1 -0
- package/build/types.d.ts +35 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/expo-module.config.json +11 -0
- package/ios/EXFilePermissionModule.h +16 -0
- package/ios/EXFilePermissionModule.m +68 -0
- package/ios/EXFileSystem.h +25 -0
- package/ios/EXFileSystem.m +1084 -0
- package/ios/EXFileSystemAssetLibraryHandler.h +6 -0
- package/ios/EXFileSystemAssetLibraryHandler.m +142 -0
- package/ios/EXFileSystemHandler.h +17 -0
- package/ios/EXFileSystemLocalFileHandler.h +7 -0
- package/ios/EXFileSystemLocalFileHandler.m +79 -0
- package/ios/EXSessionTasks/EXSessionCancelableUploadTaskDelegate.h +16 -0
- package/ios/EXSessionTasks/EXSessionCancelableUploadTaskDelegate.m +55 -0
- package/ios/EXSessionTasks/EXSessionDownloadTaskDelegate.h +13 -0
- package/ios/EXSessionTasks/EXSessionDownloadTaskDelegate.m +64 -0
- package/ios/EXSessionTasks/EXSessionHandler.h +19 -0
- package/ios/EXSessionTasks/EXSessionHandler.m +49 -0
- package/ios/EXSessionTasks/EXSessionResumableDownloadTaskDelegate.h +18 -0
- package/ios/EXSessionTasks/EXSessionResumableDownloadTaskDelegate.m +66 -0
- package/ios/EXSessionTasks/EXSessionTaskDelegate.h +32 -0
- package/ios/EXSessionTasks/EXSessionTaskDelegate.m +58 -0
- package/ios/EXSessionTasks/EXSessionTaskDispatcher.h +19 -0
- package/ios/EXSessionTasks/EXSessionTaskDispatcher.m +96 -0
- package/ios/EXSessionTasks/EXSessionUploadTaskDelegate.h +8 -0
- package/ios/EXSessionTasks/EXSessionUploadTaskDelegate.m +52 -0
- package/ios/EXSessionTasks/EXTaskHandlersManager.h +21 -0
- package/ios/EXSessionTasks/EXTaskHandlersManager.m +56 -0
- package/ios/Encoding.swift +88 -0
- package/ios/ExpoFileSystem.h +1 -0
- package/ios/ExpoFileSystem.podspec +32 -0
- package/ios/FileSystemBackgroundSessionHandler.swift +25 -0
- package/ios/FileSystemExceptions.swift +69 -0
- package/ios/FileSystemHelpers.swift +66 -0
- package/ios/FileSystemModule.swift +263 -0
- package/ios/FileSystemRecords.swift +74 -0
- package/ios/NSData+EXFileSystem.h +9 -0
- package/ios/NSData+EXFileSystem.m +19 -0
- package/ios/NetworkingHelpers.swift +84 -0
- package/ios/Tests/EXFileSystemSpec.swift +55 -0
- package/jest.config.js +3 -0
- package/kepler/ExponentFileSystem.cpp +498 -0
- package/kepler/ExponentFileSystem.h +60 -0
- package/kepler/KeplerInit.cpp +28 -0
- package/kepler/NetworkingRequestHandler.cpp +47 -0
- package/kepler/test/FileSystem.cpp +425 -0
- package/package.json +1 -0
- package/plugin/build/withFileSystem.d.ts +3 -0
- package/plugin/build/withFileSystem.js +12 -0
- package/plugin/src/withFileSystem.ts +13 -0
- package/plugin/tsconfig.json +9 -0
- package/plugin/tsconfig.tsbuildinfo +1 -0
- package/react-native.config.js +21 -0
- package/src/ExponentFileSystem.kepler.ts +167 -0
- package/src/ExponentFileSystem.ts +7 -0
- package/src/ExponentFileSystem.web.ts +2 -0
- package/src/ExponentFileSystemShim.ts +23 -0
- package/src/FileSystem.ts +756 -0
- package/src/FileSystem.types.ts +339 -0
- package/src/index.ts +2 -0
- package/src/types.ts +34 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// Copyright 2023-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesCore
|
|
4
|
+
|
|
5
|
+
private let EVENT_DOWNLOAD_PROGRESS = "expo-file-system.downloadProgress"
|
|
6
|
+
private let EVENT_UPLOAD_PROGRESS = "expo-file-system.uploadProgress"
|
|
7
|
+
|
|
8
|
+
public final class FileSystemModule: Module {
|
|
9
|
+
private lazy var sessionTaskDispatcher = EXSessionTaskDispatcher(sessionHandler: ExpoAppDelegate.getSubscriberOfType(FileSystemBackgroundSessionHandler.self))
|
|
10
|
+
private lazy var taskHandlersManager = EXTaskHandlersManager()
|
|
11
|
+
|
|
12
|
+
private lazy var backgroundSession = createUrlSession(type: .background, delegate: sessionTaskDispatcher)
|
|
13
|
+
private lazy var foregroundSession = createUrlSession(type: .foreground, delegate: sessionTaskDispatcher)
|
|
14
|
+
|
|
15
|
+
private var documentDirectory: URL? {
|
|
16
|
+
return appContext?.config.documentDirectory
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private var cacheDirectory: URL? {
|
|
20
|
+
return appContext?.config.cacheDirectory
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// swiftlint:disable:next cyclomatic_complexity
|
|
24
|
+
public func definition() -> ModuleDefinition {
|
|
25
|
+
Name("ExponentFileSystem")
|
|
26
|
+
|
|
27
|
+
Constants {
|
|
28
|
+
return [
|
|
29
|
+
"documentDirectory": documentDirectory?.absoluteString,
|
|
30
|
+
"cacheDirectory": cacheDirectory?.absoluteString,
|
|
31
|
+
"bundleDirectory": Bundle.main.bundlePath
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
Events(EVENT_DOWNLOAD_PROGRESS, EVENT_UPLOAD_PROGRESS)
|
|
36
|
+
|
|
37
|
+
AsyncFunction("getInfoAsync") { (url: URL, options: InfoOptions, promise: Promise) in
|
|
38
|
+
switch url.scheme {
|
|
39
|
+
case "file":
|
|
40
|
+
EXFileSystemLocalFileHandler.getInfoForFile(url, withOptions: options.toDictionary(), resolver: promise.resolver, rejecter: promise.legacyRejecter)
|
|
41
|
+
case "assets-library", "ph":
|
|
42
|
+
EXFileSystemAssetLibraryHandler.getInfoForFile(url, withOptions: options.toDictionary(), resolver: promise.resolver, rejecter: promise.legacyRejecter)
|
|
43
|
+
default:
|
|
44
|
+
throw UnsupportedSchemeException(url.scheme)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
AsyncFunction("readAsStringAsync") { (url: URL, options: ReadingOptions) -> String in
|
|
49
|
+
try ensurePathPermission(appContext, path: url.path, flag: .read)
|
|
50
|
+
|
|
51
|
+
if options.encoding == .base64 {
|
|
52
|
+
return try readFileAsBase64(path: url.path, options: options)
|
|
53
|
+
}
|
|
54
|
+
do {
|
|
55
|
+
return try String(contentsOfFile: url.path, encoding: options.encoding.toStringEncoding() ?? .utf8)
|
|
56
|
+
} catch {
|
|
57
|
+
throw FileNotReadableException(url.path)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
AsyncFunction("writeAsStringAsync") { (url: URL, string: String, options: WritingOptions) in
|
|
62
|
+
try ensurePathPermission(appContext, path: url.path, flag: .write)
|
|
63
|
+
|
|
64
|
+
if options.encoding == .base64 {
|
|
65
|
+
try writeFileAsBase64(path: url.path, string: string)
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
do {
|
|
69
|
+
try string.write(toFile: url.path, atomically: true, encoding: options.encoding.toStringEncoding() ?? .utf8)
|
|
70
|
+
} catch {
|
|
71
|
+
throw FileNotWritableException(url.path)
|
|
72
|
+
.causedBy(error)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
AsyncFunction("deleteAsync") { (url: URL, options: DeletingOptions) in
|
|
77
|
+
guard url.isFileURL else {
|
|
78
|
+
throw InvalidFileUrlException(url)
|
|
79
|
+
}
|
|
80
|
+
try removeFile(path: url.path, idempotent: options.idempotent)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
AsyncFunction("moveAsync") { (options: RelocatingOptions) in
|
|
84
|
+
let (fromUrl, toUrl) = try options.asTuple()
|
|
85
|
+
|
|
86
|
+
guard fromUrl.isFileURL else {
|
|
87
|
+
throw InvalidFileUrlException(fromUrl)
|
|
88
|
+
}
|
|
89
|
+
guard toUrl.isFileURL else {
|
|
90
|
+
throw InvalidFileUrlException(toUrl)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try ensurePathPermission(appContext, path: fromUrl.appendingPathComponent("..").path, flag: .write)
|
|
94
|
+
try ensurePathPermission(appContext, path: toUrl.path, flag: .write)
|
|
95
|
+
try removeFile(path: toUrl.path, idempotent: true)
|
|
96
|
+
try FileManager.default.moveItem(atPath: fromUrl.path, toPath: toUrl.path)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
AsyncFunction("copyAsync") { (options: RelocatingOptions, promise: Promise) in
|
|
100
|
+
let (fromUrl, toUrl) = try options.asTuple()
|
|
101
|
+
|
|
102
|
+
try ensurePathPermission(appContext, path: fromUrl.path, flag: .read)
|
|
103
|
+
try ensurePathPermission(appContext, path: toUrl.path, flag: .write)
|
|
104
|
+
|
|
105
|
+
if fromUrl.scheme == "file" {
|
|
106
|
+
EXFileSystemLocalFileHandler.copy(from: fromUrl, to: toUrl, resolver: promise.resolver, rejecter: promise.legacyRejecter)
|
|
107
|
+
} else if ["ph", "assets-library"].contains(fromUrl.scheme) {
|
|
108
|
+
EXFileSystemAssetLibraryHandler.copy(from: fromUrl, to: toUrl, resolver: promise.resolver, rejecter: promise.legacyRejecter)
|
|
109
|
+
} else {
|
|
110
|
+
throw InvalidFileUrlException(fromUrl)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
AsyncFunction("makeDirectoryAsync") { (url: URL, options: MakeDirectoryOptions) in
|
|
115
|
+
guard url.isFileURL else {
|
|
116
|
+
throw InvalidFileUrlException(url)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try ensurePathPermission(appContext, path: url.path, flag: .write)
|
|
120
|
+
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: options.intermediates, attributes: nil)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
AsyncFunction("readDirectoryAsync") { (url: URL) -> [String] in
|
|
124
|
+
guard url.isFileURL else {
|
|
125
|
+
throw InvalidFileUrlException(url)
|
|
126
|
+
}
|
|
127
|
+
try ensurePathPermission(appContext, path: url.path, flag: .read)
|
|
128
|
+
|
|
129
|
+
return try FileManager.default.contentsOfDirectory(atPath: url.path)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
AsyncFunction("downloadAsync") { (sourceUrl: URL, localUrl: URL, options: DownloadOptions, promise: Promise) in
|
|
133
|
+
try ensureFileDirectoryExists(localUrl)
|
|
134
|
+
try ensurePathPermission(appContext, path: localUrl.path, flag: .write)
|
|
135
|
+
|
|
136
|
+
let session = options.sessionType == .background ? backgroundSession : foregroundSession
|
|
137
|
+
let request = createUrlRequest(url: sourceUrl, headers: options.headers)
|
|
138
|
+
let downloadTask = session.downloadTask(with: request)
|
|
139
|
+
let taskDelegate = EXSessionDownloadTaskDelegate(
|
|
140
|
+
resolve: promise.resolver,
|
|
141
|
+
reject: promise.legacyRejecter,
|
|
142
|
+
localUrl: localUrl,
|
|
143
|
+
shouldCalculateMd5: options.md5
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
sessionTaskDispatcher.register(taskDelegate, for: downloadTask)
|
|
147
|
+
downloadTask.resume()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
AsyncFunction("uploadAsync") { (targetUrl: URL, localUrl: URL, options: UploadOptions, promise: Promise) in
|
|
151
|
+
guard localUrl.isFileURL else {
|
|
152
|
+
throw InvalidFileUrlException(localUrl)
|
|
153
|
+
}
|
|
154
|
+
guard FileManager.default.fileExists(atPath: localUrl.path) else {
|
|
155
|
+
throw FileNotExistsException(localUrl.path)
|
|
156
|
+
}
|
|
157
|
+
let session = options.sessionType == .background ? backgroundSession : foregroundSession
|
|
158
|
+
let task = createUploadTask(session: session, targetUrl: targetUrl, sourceUrl: localUrl, options: options)
|
|
159
|
+
let taskDelegate = EXSessionUploadTaskDelegate(resolve: promise.resolver, reject: promise.legacyRejecter)
|
|
160
|
+
|
|
161
|
+
sessionTaskDispatcher.register(taskDelegate, for: task)
|
|
162
|
+
task.resume()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
AsyncFunction("uploadTaskStartAsync") { (targetUrl: URL, localUrl: URL, uuid: String, options: UploadOptions, promise: Promise) in
|
|
166
|
+
let session = options.sessionType == .background ? backgroundSession : foregroundSession
|
|
167
|
+
let task = createUploadTask(session: session, targetUrl: targetUrl, sourceUrl: localUrl, options: options)
|
|
168
|
+
let onSend: EXUploadDelegateOnSendCallback = { [weak self] _, _, totalBytesSent, totalBytesExpectedToSend in
|
|
169
|
+
self?.sendEvent(EVENT_UPLOAD_PROGRESS, [
|
|
170
|
+
"uuid": uuid,
|
|
171
|
+
"data": [
|
|
172
|
+
"totalBytesSent": totalBytesSent,
|
|
173
|
+
"totalBytesExpectedToSend": totalBytesExpectedToSend
|
|
174
|
+
]
|
|
175
|
+
])
|
|
176
|
+
}
|
|
177
|
+
let taskDelegate = EXSessionCancelableUploadTaskDelegate(
|
|
178
|
+
resolve: promise.resolver,
|
|
179
|
+
reject: promise.legacyRejecter,
|
|
180
|
+
onSendCallback: onSend,
|
|
181
|
+
resumableManager: taskHandlersManager,
|
|
182
|
+
uuid: uuid
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
sessionTaskDispatcher.register(taskDelegate, for: task)
|
|
186
|
+
taskHandlersManager.register(task, uuid: uuid)
|
|
187
|
+
task.resume()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// swiftlint:disable:next line_length closure_body_length
|
|
191
|
+
AsyncFunction("downloadResumableStartAsync") { (sourceUrl: URL, localUrl: URL, uuid: String, options: DownloadOptions, resumeDataString: String?, promise: Promise) in
|
|
192
|
+
try ensureFileDirectoryExists(localUrl)
|
|
193
|
+
try ensurePathPermission(appContext, path: localUrl.path, flag: .write)
|
|
194
|
+
|
|
195
|
+
let session = options.sessionType == .background ? backgroundSession : foregroundSession
|
|
196
|
+
let resumeData = resumeDataString != nil ? Data(base64Encoded: resumeDataString ?? "") : nil
|
|
197
|
+
let onWrite: EXDownloadDelegateOnWriteCallback = { [weak self] _, _, totalBytesWritten, totalBytesExpectedToWrite in
|
|
198
|
+
self?.sendEvent(EVENT_DOWNLOAD_PROGRESS, [
|
|
199
|
+
"uuid": uuid,
|
|
200
|
+
"data": [
|
|
201
|
+
"totalBytesWritten": totalBytesWritten,
|
|
202
|
+
"totalBytesExpectedToWrite": totalBytesExpectedToWrite
|
|
203
|
+
]
|
|
204
|
+
])
|
|
205
|
+
}
|
|
206
|
+
let task: URLSessionDownloadTask
|
|
207
|
+
|
|
208
|
+
if let resumeDataString, let resumeData = Data(base64Encoded: resumeDataString) {
|
|
209
|
+
task = session.downloadTask(withResumeData: resumeData)
|
|
210
|
+
} else {
|
|
211
|
+
let request = createUrlRequest(url: sourceUrl, headers: options.headers)
|
|
212
|
+
task = session.downloadTask(with: request)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let taskDelegate = EXSessionResumableDownloadTaskDelegate(
|
|
216
|
+
resolve: promise.resolver,
|
|
217
|
+
reject: promise.legacyRejecter,
|
|
218
|
+
localUrl: localUrl,
|
|
219
|
+
shouldCalculateMd5: options.md5,
|
|
220
|
+
onWriteCallback: onWrite,
|
|
221
|
+
resumableManager: taskHandlersManager,
|
|
222
|
+
uuid: uuid
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
sessionTaskDispatcher.register(taskDelegate, for: task)
|
|
226
|
+
taskHandlersManager.register(task, uuid: uuid)
|
|
227
|
+
task.resume()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
AsyncFunction("downloadResumablePauseAsync") { (id: String) -> [String: String?] in
|
|
231
|
+
guard let task = taskHandlersManager.downloadTask(forId: id) else {
|
|
232
|
+
throw DownloadTaskNotFoundException(id)
|
|
233
|
+
}
|
|
234
|
+
let resumeData = await task.cancelByProducingResumeData()
|
|
235
|
+
|
|
236
|
+
return [
|
|
237
|
+
"resumeData": resumeData?.base64EncodedString()
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
AsyncFunction("networkTaskCancelAsync") { (id: String) in
|
|
242
|
+
taskHandlersManager.task(forId: id)?.cancel()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
AsyncFunction("getFreeDiskStorageAsync") { () -> Int in
|
|
246
|
+
let resourceValues = try getResourceValues(from: documentDirectory, forKeys: [.volumeAvailableCapacityKey])
|
|
247
|
+
|
|
248
|
+
guard let availableCapacity = resourceValues?.volumeAvailableCapacity else {
|
|
249
|
+
throw CannotDetermineDiskCapacity()
|
|
250
|
+
}
|
|
251
|
+
return availableCapacity
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
AsyncFunction("getTotalDiskCapacityAsync") { () -> Int in
|
|
255
|
+
let resourceValues = try getResourceValues(from: documentDirectory, forKeys: [.volumeTotalCapacityKey])
|
|
256
|
+
|
|
257
|
+
guard let totalCapacity = resourceValues?.volumeTotalCapacity else {
|
|
258
|
+
throw CannotDetermineDiskCapacity()
|
|
259
|
+
}
|
|
260
|
+
return totalCapacity
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Copyright 2023-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesCore
|
|
4
|
+
|
|
5
|
+
struct InfoOptions: Record {
|
|
6
|
+
@Field var md5: Bool = false
|
|
7
|
+
@Field var size: Bool = false
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
struct ReadingOptions: Record {
|
|
11
|
+
@Field var encoding: Encoding = .utf8
|
|
12
|
+
@Field var position: Int?
|
|
13
|
+
@Field var length: Int?
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
struct WritingOptions: Record {
|
|
17
|
+
@Field var encoding: Encoding = .utf8
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
struct DeletingOptions: Record {
|
|
21
|
+
@Field var idempotent: Bool = false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
struct RelocatingOptions: Record {
|
|
25
|
+
@Field var from: URL?
|
|
26
|
+
@Field var to: URL?
|
|
27
|
+
|
|
28
|
+
func asTuple() throws -> (URL, URL) {
|
|
29
|
+
guard let from, let to else {
|
|
30
|
+
let missingOptionName = from == nil ? "from" : "to"
|
|
31
|
+
throw Exception(name: "MissingParameterException", description: "Missing option '\(missingOptionName)'")
|
|
32
|
+
}
|
|
33
|
+
return (from, to)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
struct MakeDirectoryOptions: Record {
|
|
38
|
+
@Field var intermediates: Bool = false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
struct DownloadOptions: Record {
|
|
42
|
+
@Field var md5: Bool = false
|
|
43
|
+
@Field var cache: Bool = false
|
|
44
|
+
@Field var headers: [String: String]?
|
|
45
|
+
@Field var sessionType: SessionType = .background
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
struct UploadOptions: Record {
|
|
49
|
+
@Field var headers: [String: String]?
|
|
50
|
+
@Field var httpMethod: HttpMethod = .post
|
|
51
|
+
@Field var sessionType: SessionType = .background
|
|
52
|
+
@Field var uploadType: UploadType = .binaryContent
|
|
53
|
+
|
|
54
|
+
// Multipart
|
|
55
|
+
@Field var fieldName: String?
|
|
56
|
+
@Field var mimeType: String?
|
|
57
|
+
@Field var parameters: [String: String]?
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
enum SessionType: Int, Enumerable {
|
|
61
|
+
case background = 0
|
|
62
|
+
case foreground = 1
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
enum HttpMethod: String, Enumerable {
|
|
66
|
+
case post = "POST"
|
|
67
|
+
case put = "PUT"
|
|
68
|
+
case patch = "PATCH"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
enum UploadType: Int, Enumerable {
|
|
72
|
+
case binaryContent = 0
|
|
73
|
+
case multipart = 1
|
|
74
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Copyright 2015-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
#import <ExpoFileSystem/NSData+EXFileSystem.h>
|
|
4
|
+
#import <CommonCrypto/CommonDigest.h>
|
|
5
|
+
|
|
6
|
+
@implementation NSData (EXFileSystem)
|
|
7
|
+
|
|
8
|
+
- (NSString *)md5String
|
|
9
|
+
{
|
|
10
|
+
unsigned char digest[CC_MD5_DIGEST_LENGTH];
|
|
11
|
+
CC_MD5(self.bytes, (CC_LONG) self.length, digest);
|
|
12
|
+
NSMutableString *md5 = [NSMutableString stringWithCapacity:2 * CC_MD5_DIGEST_LENGTH];
|
|
13
|
+
for (unsigned int i = 0; i < CC_MD5_DIGEST_LENGTH; ++i) {
|
|
14
|
+
[md5 appendFormat:@"%02x", digest[i]];
|
|
15
|
+
}
|
|
16
|
+
return md5;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Copyright 2023-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import CoreServices
|
|
4
|
+
import ExpoModulesCore
|
|
5
|
+
|
|
6
|
+
func findMimeType(forAttachment attachment: URL) -> String {
|
|
7
|
+
if let identifier = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, attachment.pathExtension as CFString, nil)?.takeRetainedValue() {
|
|
8
|
+
if let type = UTTypeCopyPreferredTagWithClass(identifier, kUTTagClassMIMEType)?.takeRetainedValue() {
|
|
9
|
+
return type as String
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return "application/octet-stream"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
func createUrlSession(type: SessionType, delegate: URLSessionDelegate) -> URLSession {
|
|
16
|
+
let configuration = type == .foreground ? URLSessionConfiguration.default : URLSessionConfiguration.background(withIdentifier: UUID().uuidString)
|
|
17
|
+
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
|
|
18
|
+
configuration.urlCache = nil
|
|
19
|
+
return URLSession(configuration: configuration, delegate: delegate, delegateQueue: .main)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func createUrlRequest(url: URL, headers: [String: String]?) -> URLRequest {
|
|
23
|
+
var request = URLRequest(url: url)
|
|
24
|
+
|
|
25
|
+
if let headers {
|
|
26
|
+
for (key, value) in headers {
|
|
27
|
+
request.setValue(value, forHTTPHeaderField: key)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return request
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func createUploadTask(session: URLSession, targetUrl: URL, sourceUrl: URL, options: UploadOptions) -> URLSessionUploadTask {
|
|
34
|
+
var request = createUrlRequest(url: targetUrl, headers: options.headers)
|
|
35
|
+
|
|
36
|
+
switch options.uploadType {
|
|
37
|
+
case .binaryContent:
|
|
38
|
+
return session.uploadTask(with: request, fromFile: sourceUrl)
|
|
39
|
+
case .multipart:
|
|
40
|
+
let boundaryString = UUID().uuidString
|
|
41
|
+
let data = try? createMultipartBody(boundary: boundaryString, sourceUrl: sourceUrl, options: options)
|
|
42
|
+
|
|
43
|
+
request.setValue("multipart/form-data; boundary=\(boundaryString)", forHTTPHeaderField: "Content-Type")
|
|
44
|
+
request.httpBody = data
|
|
45
|
+
|
|
46
|
+
return session.uploadTask(withStreamedRequest: request)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func createMultipartBody(boundary: String, sourceUrl: URL, options: UploadOptions) throws -> Data {
|
|
51
|
+
let fileName = options.fieldName ?? sourceUrl.lastPathComponent
|
|
52
|
+
let fileContents = try String(contentsOf: sourceUrl)
|
|
53
|
+
let mimeType = options.mimeType ?? findMimeType(forAttachment: sourceUrl)
|
|
54
|
+
|
|
55
|
+
let body = """
|
|
56
|
+
\(headersForMultipartParams(options.parameters, boundary: boundary))
|
|
57
|
+
--\(boundary)
|
|
58
|
+
Content-Disposition: form-data; name="\(fileName)"; filename="\(sourceUrl.lastPathComponent)"
|
|
59
|
+
Content-Type: \(mimeType)
|
|
60
|
+
|
|
61
|
+
\(fileContents)
|
|
62
|
+
--\(boundary)--
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
guard let bodyData = body.data(using: .utf8) else {
|
|
66
|
+
throw HeaderEncodingFailedException(sourceUrl.absoluteString)
|
|
67
|
+
}
|
|
68
|
+
return bodyData
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func headersForMultipartParams(_ params: [String: String]?, boundary: String) -> String {
|
|
72
|
+
guard let params else {
|
|
73
|
+
return ""
|
|
74
|
+
}
|
|
75
|
+
return params.map { (key: String, value: String) in
|
|
76
|
+
"""
|
|
77
|
+
--\(boundary)
|
|
78
|
+
Content-Disposition: form-data; name="\(key)"
|
|
79
|
+
|
|
80
|
+
\(value)
|
|
81
|
+
"""
|
|
82
|
+
}
|
|
83
|
+
.joined()
|
|
84
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import ExpoModulesTestCore
|
|
2
|
+
|
|
3
|
+
@testable import ExpoModulesCore
|
|
4
|
+
@testable import ExpoFileSystem
|
|
5
|
+
|
|
6
|
+
class EXFileSystemSpec: ExpoSpec {
|
|
7
|
+
override func spec() {
|
|
8
|
+
let fileSystem = EXFileSystem()
|
|
9
|
+
|
|
10
|
+
describe("percentEncodedURLFromURIString") {
|
|
11
|
+
it("should handle encoded URIs") {
|
|
12
|
+
let encodedUriInput = "file:///var/mobile/@username/branch"
|
|
13
|
+
let encodedUriExpectedOutput = "file:///var/mobile/@username/branch"
|
|
14
|
+
let encodedUri = fileSystem.percentEncodedURL(fromURIString: encodedUriInput)
|
|
15
|
+
|
|
16
|
+
expect(encodedUri?.absoluteString) == encodedUriExpectedOutput
|
|
17
|
+
expect(encodedUri?.scheme) == "file"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
it("should handle UTF-8 characters") {
|
|
21
|
+
let utf8UriInput = "file:///var/mobile/中文"
|
|
22
|
+
let utf8UriExpectedOutput = "file:///var/mobile/%E4%B8%AD%E6%96%87"
|
|
23
|
+
let utf8Uri = fileSystem.percentEncodedURL(fromURIString:utf8UriInput)
|
|
24
|
+
|
|
25
|
+
expect(utf8Uri?.absoluteString) == utf8UriExpectedOutput
|
|
26
|
+
expect(utf8Uri?.scheme) == "file"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
it("should handle URI with percent, numbers and UTF-8 characters") {
|
|
30
|
+
let input = "file:///document/directory/%40%2F中文"
|
|
31
|
+
let expectedOutput = "file:///document/directory/%40%2F%E4%B8%AD%E6%96%87"
|
|
32
|
+
let uri = fileSystem.percentEncodedURL(fromURIString:input)
|
|
33
|
+
|
|
34
|
+
expect(uri?.absoluteString) == expectedOutput
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
it("should not decode percentages in URI") {
|
|
38
|
+
let input = "file:///document/hello%2Fworld.txt"
|
|
39
|
+
let unexpectedOutput = "file:///document/hello/world.txt"
|
|
40
|
+
let uri = fileSystem.percentEncodedURL(fromURIString:input)
|
|
41
|
+
|
|
42
|
+
// Should not create a directory named "hello"
|
|
43
|
+
expect(uri?.absoluteString) != unexpectedOutput
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
it("should handle assets-library URIs") {
|
|
47
|
+
let assetsLibraryUriInput = "assets-library://asset/asset.JPG?id=3C1D9C54-9521-488F-BB27-AA1EA0F8AF04/L0/001&ext=JPG"
|
|
48
|
+
let assetsLibraryUri = fileSystem.percentEncodedURL(fromURIString:assetsLibraryUriInput)
|
|
49
|
+
|
|
50
|
+
expect(assetsLibraryUri?.absoluteString) == assetsLibraryUriInput
|
|
51
|
+
expect(assetsLibraryUri?.scheme) == "assets-library"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
package/jest.config.js
ADDED