@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.
Files changed (111) hide show
  1. package/.eslintrc.js +2 -0
  2. package/CHANGELOG.md +317 -0
  3. package/CMakeLists.txt +52 -0
  4. package/LICENSE +7 -0
  5. package/README.kepler.md +260 -0
  6. package/README.md +46 -0
  7. package/THIRD_PARTY_NOTICES.txt +24 -0
  8. package/android/build.gradle +118 -0
  9. package/android/src/main/AndroidManifest.xml +17 -0
  10. package/android/src/main/java/expo/modules/filesystem/AppDirectoriesModule.kt +26 -0
  11. package/android/src/main/java/expo/modules/filesystem/CountingRequestBody.kt +52 -0
  12. package/android/src/main/java/expo/modules/filesystem/FilePermissionModule.kt +48 -0
  13. package/android/src/main/java/expo/modules/filesystem/FileSystemExceptions.kt +48 -0
  14. package/android/src/main/java/expo/modules/filesystem/FileSystemFileProvider.kt +5 -0
  15. package/android/src/main/java/expo/modules/filesystem/FileSystemModule.kt +1110 -0
  16. package/android/src/main/java/expo/modules/filesystem/FileSystemPackage.kt +10 -0
  17. package/android/src/main/java/expo/modules/filesystem/FileSystemRecords.kt +92 -0
  18. package/android/src/main/res/xml/file_system_provider_paths.xml +5 -0
  19. package/app.plugin.js +1 -0
  20. package/build/ExponentFileSystem.d.ts +4 -0
  21. package/build/ExponentFileSystem.d.ts.map +1 -0
  22. package/build/ExponentFileSystem.js +5 -0
  23. package/build/ExponentFileSystem.js.map +1 -0
  24. package/build/ExponentFileSystem.kepler.d.ts +4 -0
  25. package/build/ExponentFileSystem.kepler.d.ts.map +1 -0
  26. package/build/ExponentFileSystem.kepler.js +112 -0
  27. package/build/ExponentFileSystem.kepler.js.map +1 -0
  28. package/build/ExponentFileSystem.web.d.ts +3 -0
  29. package/build/ExponentFileSystem.web.d.ts.map +1 -0
  30. package/build/ExponentFileSystem.web.js +3 -0
  31. package/build/ExponentFileSystem.web.js.map +1 -0
  32. package/build/ExponentFileSystemShim.d.ts +4 -0
  33. package/build/ExponentFileSystemShim.d.ts.map +1 -0
  34. package/build/ExponentFileSystemShim.js +21 -0
  35. package/build/ExponentFileSystemShim.js.map +1 -0
  36. package/build/FileSystem.d.ts +361 -0
  37. package/build/FileSystem.d.ts.map +1 -0
  38. package/build/FileSystem.js +616 -0
  39. package/build/FileSystem.js.map +1 -0
  40. package/build/FileSystem.types.d.ts +304 -0
  41. package/build/FileSystem.types.d.ts.map +1 -0
  42. package/build/FileSystem.types.js +47 -0
  43. package/build/FileSystem.types.js.map +1 -0
  44. package/build/index.d.ts +3 -0
  45. package/build/index.d.ts.map +1 -0
  46. package/build/index.js +3 -0
  47. package/build/index.js.map +1 -0
  48. package/build/types.d.ts +35 -0
  49. package/build/types.d.ts.map +1 -0
  50. package/build/types.js +2 -0
  51. package/build/types.js.map +1 -0
  52. package/expo-module.config.json +11 -0
  53. package/ios/EXFilePermissionModule.h +16 -0
  54. package/ios/EXFilePermissionModule.m +68 -0
  55. package/ios/EXFileSystem.h +25 -0
  56. package/ios/EXFileSystem.m +1084 -0
  57. package/ios/EXFileSystemAssetLibraryHandler.h +6 -0
  58. package/ios/EXFileSystemAssetLibraryHandler.m +142 -0
  59. package/ios/EXFileSystemHandler.h +17 -0
  60. package/ios/EXFileSystemLocalFileHandler.h +7 -0
  61. package/ios/EXFileSystemLocalFileHandler.m +79 -0
  62. package/ios/EXSessionTasks/EXSessionCancelableUploadTaskDelegate.h +16 -0
  63. package/ios/EXSessionTasks/EXSessionCancelableUploadTaskDelegate.m +55 -0
  64. package/ios/EXSessionTasks/EXSessionDownloadTaskDelegate.h +13 -0
  65. package/ios/EXSessionTasks/EXSessionDownloadTaskDelegate.m +64 -0
  66. package/ios/EXSessionTasks/EXSessionHandler.h +19 -0
  67. package/ios/EXSessionTasks/EXSessionHandler.m +49 -0
  68. package/ios/EXSessionTasks/EXSessionResumableDownloadTaskDelegate.h +18 -0
  69. package/ios/EXSessionTasks/EXSessionResumableDownloadTaskDelegate.m +66 -0
  70. package/ios/EXSessionTasks/EXSessionTaskDelegate.h +32 -0
  71. package/ios/EXSessionTasks/EXSessionTaskDelegate.m +58 -0
  72. package/ios/EXSessionTasks/EXSessionTaskDispatcher.h +19 -0
  73. package/ios/EXSessionTasks/EXSessionTaskDispatcher.m +96 -0
  74. package/ios/EXSessionTasks/EXSessionUploadTaskDelegate.h +8 -0
  75. package/ios/EXSessionTasks/EXSessionUploadTaskDelegate.m +52 -0
  76. package/ios/EXSessionTasks/EXTaskHandlersManager.h +21 -0
  77. package/ios/EXSessionTasks/EXTaskHandlersManager.m +56 -0
  78. package/ios/Encoding.swift +88 -0
  79. package/ios/ExpoFileSystem.h +1 -0
  80. package/ios/ExpoFileSystem.podspec +32 -0
  81. package/ios/FileSystemBackgroundSessionHandler.swift +25 -0
  82. package/ios/FileSystemExceptions.swift +69 -0
  83. package/ios/FileSystemHelpers.swift +66 -0
  84. package/ios/FileSystemModule.swift +263 -0
  85. package/ios/FileSystemRecords.swift +74 -0
  86. package/ios/NSData+EXFileSystem.h +9 -0
  87. package/ios/NSData+EXFileSystem.m +19 -0
  88. package/ios/NetworkingHelpers.swift +84 -0
  89. package/ios/Tests/EXFileSystemSpec.swift +55 -0
  90. package/jest.config.js +3 -0
  91. package/kepler/ExponentFileSystem.cpp +498 -0
  92. package/kepler/ExponentFileSystem.h +60 -0
  93. package/kepler/KeplerInit.cpp +28 -0
  94. package/kepler/NetworkingRequestHandler.cpp +47 -0
  95. package/kepler/test/FileSystem.cpp +425 -0
  96. package/package.json +1 -0
  97. package/plugin/build/withFileSystem.d.ts +3 -0
  98. package/plugin/build/withFileSystem.js +12 -0
  99. package/plugin/src/withFileSystem.ts +13 -0
  100. package/plugin/tsconfig.json +9 -0
  101. package/plugin/tsconfig.tsbuildinfo +1 -0
  102. package/react-native.config.js +21 -0
  103. package/src/ExponentFileSystem.kepler.ts +167 -0
  104. package/src/ExponentFileSystem.ts +7 -0
  105. package/src/ExponentFileSystem.web.ts +2 -0
  106. package/src/ExponentFileSystemShim.ts +23 -0
  107. package/src/FileSystem.ts +756 -0
  108. package/src/FileSystem.types.ts +339 -0
  109. package/src/index.ts +2 -0
  110. package/src/types.ts +34 -0
  111. 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,9 @@
1
+ // Copyright 2015-present 650 Industries. All rights reserved.
2
+
3
+ #import <Foundation/Foundation.h>
4
+
5
+ @interface NSData (EXFileSystem)
6
+
7
+ - (NSString *)md5String;
8
+
9
+ @end
@@ -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
@@ -0,0 +1,3 @@
1
+ const skipPackageTestsConfig = require('expo-module-scripts/skip-package-tests.jest.config');
2
+
3
+ module.exports = skipPackageTestsConfig;