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 definitionnull24 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 } 264