317 lines
12 KiB
Swift
317 lines
12 KiB
Swift
// Copyright 2023-present 650 Industries. All rights reserved.
|
|
|
|
import ExpoModulesCore
|
|
import Photos
|
|
|
|
private let EVENT_DOWNLOAD_PROGRESS = "expo-file-system.downloadProgress"
|
|
private let EVENT_UPLOAD_PROGRESS = "expo-file-system.uploadProgress"
|
|
|
|
public final class FileSystemLegacyModule: Module {
|
|
private var sessionTaskDispatcher: EXSessionTaskDispatcher!
|
|
private lazy var taskHandlersManager = EXTaskHandlersManager()
|
|
private lazy var resourceManager = PHAssetResourceManager()
|
|
|
|
private lazy var backgroundSession = createUrlSession(type: .background, delegate: sessionTaskDispatcher)
|
|
private lazy var foregroundSession = createUrlSession(type: .foreground, delegate: sessionTaskDispatcher)
|
|
|
|
private var documentDirectory: URL? {
|
|
return appContext?.config.documentDirectory
|
|
}
|
|
|
|
private var cacheDirectory: URL? {
|
|
return appContext?.config.cacheDirectory
|
|
}
|
|
|
|
public func definition() -> ModuleDefinition {
|
|
Name("ExponentFileSystem")
|
|
|
|
Constant("documentDirectory") {
|
|
return documentDirectory?.absoluteString
|
|
}
|
|
|
|
Constant("cacheDirectory") {
|
|
return cacheDirectory?.absoluteString
|
|
}
|
|
|
|
Constant("bundleDirectory") {
|
|
return Bundle.main.bundlePath
|
|
}
|
|
|
|
Events(EVENT_DOWNLOAD_PROGRESS, EVENT_UPLOAD_PROGRESS)
|
|
|
|
OnCreate {
|
|
Task { @MainActor in
|
|
sessionTaskDispatcher = EXSessionTaskDispatcher(
|
|
sessionHandler: ExpoAppDelegateSubscriberRepository.getSubscriberOfType(FileSystemBackgroundSessionHandler.self)
|
|
)
|
|
}
|
|
}
|
|
|
|
AsyncFunction("getInfoAsync") { (url: URL, options: InfoOptions, promise: Promise) in
|
|
let optionsDict = options.toDictionary(appContext: appContext)
|
|
switch url.scheme {
|
|
case "file":
|
|
EXFileSystemLocalFileHandler.getInfoForFile(url, withOptions: optionsDict, resolver: promise.resolver, rejecter: promise.legacyRejecter)
|
|
case "assets-library", "ph":
|
|
EXFileSystemAssetLibraryHandler.getInfoForFile(url, withOptions: optionsDict, resolver: promise.resolver, rejecter: promise.legacyRejecter)
|
|
default:
|
|
throw UnsupportedSchemeException(url.scheme)
|
|
}
|
|
}
|
|
|
|
AsyncFunction("readAsStringAsync") { (url: URL, options: ReadingOptions) -> String in
|
|
try ensurePathPermission(appContext, path: url.path, flag: .read)
|
|
|
|
if options.encoding == .base64 {
|
|
return try readFileAsBase64(path: url.path, options: options)
|
|
}
|
|
do {
|
|
return try String(contentsOfFile: url.path, encoding: options.encoding.toStringEncoding() ?? .utf8)
|
|
} catch {
|
|
throw FileNotReadableException(url.path)
|
|
}
|
|
}
|
|
|
|
AsyncFunction("writeAsStringAsync") { (url: URL, string: String, options: WritingOptions) in
|
|
try ensurePathPermission(appContext, path: url.path, flag: .write)
|
|
|
|
let data: Data?
|
|
if options.encoding == .base64 {
|
|
data = Data(base64Encoded: string, options: .ignoreUnknownCharacters)
|
|
} else {
|
|
data = string.data(using: options.encoding.toStringEncoding() ?? .utf8)
|
|
}
|
|
|
|
guard let data else {
|
|
throw FileNotWritableException(url.path)
|
|
}
|
|
|
|
do {
|
|
if options.append {
|
|
if !FileManager.default.fileExists(atPath: url.path) {
|
|
try data.write(to: url, options: .atomic)
|
|
} else {
|
|
let fileHandle = try FileHandle(forWritingTo: url)
|
|
defer {
|
|
fileHandle.closeFile()
|
|
}
|
|
fileHandle.seekToEndOfFile()
|
|
fileHandle.write(data)
|
|
}
|
|
} else {
|
|
try data.write(to: url, options: .atomic)
|
|
}
|
|
} catch {
|
|
throw FileNotWritableException(url.path)
|
|
.causedBy(error)
|
|
}
|
|
}
|
|
|
|
AsyncFunction("deleteAsync") { (url: URL, options: DeletingOptions) in
|
|
guard url.isFileURL else {
|
|
throw InvalidFileUrlException(url)
|
|
}
|
|
try ensurePathPermission(appContext, path: url.appendingPathComponent("..").path, flag: .write)
|
|
try removeFile(path: url.path, idempotent: options.idempotent)
|
|
}
|
|
|
|
AsyncFunction("moveAsync") { (options: RelocatingOptions) in
|
|
let (fromUrl, toUrl) = try options.asTuple()
|
|
|
|
guard fromUrl.isFileURL else {
|
|
throw InvalidFileUrlException(fromUrl)
|
|
}
|
|
guard toUrl.isFileURL else {
|
|
throw InvalidFileUrlException(toUrl)
|
|
}
|
|
|
|
try ensurePathPermission(appContext, path: fromUrl.appendingPathComponent("..").path, flag: .write)
|
|
try ensurePathPermission(appContext, path: toUrl.path, flag: .write)
|
|
try removeFile(path: toUrl.path, idempotent: true)
|
|
try FileManager.default.moveItem(atPath: fromUrl.path, toPath: toUrl.path)
|
|
}
|
|
|
|
AsyncFunction("copyAsync") { (options: RelocatingOptions, promise: Promise) in
|
|
let (fromUrl, toUrl) = try options.asTuple()
|
|
|
|
if isPHAsset(path: fromUrl.absoluteString) {
|
|
copyPHAsset(fromUrl: fromUrl, toUrl: toUrl, with: resourceManager, promise: promise)
|
|
return
|
|
}
|
|
|
|
try ensurePathPermission(appContext, path: fromUrl.path, flag: .read)
|
|
try ensurePathPermission(appContext, path: toUrl.path, flag: .write)
|
|
|
|
if fromUrl.scheme == "file" {
|
|
EXFileSystemLocalFileHandler.copy(from: fromUrl, to: toUrl, resolver: promise.resolver, rejecter: promise.legacyRejecter)
|
|
} else if ["ph", "assets-library"].contains(fromUrl.scheme) {
|
|
EXFileSystemAssetLibraryHandler.copy(from: fromUrl, to: toUrl, resolver: promise.resolver, rejecter: promise.legacyRejecter)
|
|
} else {
|
|
throw InvalidFileUrlException(fromUrl)
|
|
}
|
|
}
|
|
|
|
AsyncFunction("makeDirectoryAsync") { (url: URL, options: MakeDirectoryOptions) in
|
|
guard url.isFileURL else {
|
|
throw InvalidFileUrlException(url)
|
|
}
|
|
|
|
try ensurePathPermission(appContext, path: url.path, flag: .write)
|
|
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: options.intermediates, attributes: nil)
|
|
}
|
|
|
|
AsyncFunction("readDirectoryAsync") { (url: URL) -> [String] in
|
|
guard url.isFileURL else {
|
|
throw InvalidFileUrlException(url)
|
|
}
|
|
try ensurePathPermission(appContext, path: url.path, flag: .read)
|
|
|
|
return try FileManager.default.contentsOfDirectory(atPath: url.path)
|
|
}
|
|
|
|
AsyncFunction("downloadAsync") { (sourceUrl: URL, localUrl: URL, options: DownloadOptionsLegacy, promise: Promise) in
|
|
try ensureFileDirectoryExists(localUrl)
|
|
try ensurePathPermission(appContext, path: localUrl.path, flag: .write)
|
|
|
|
if sourceUrl.isFileURL {
|
|
try ensurePathPermission(appContext, path: sourceUrl.path, flag: .read)
|
|
EXFileSystemLocalFileHandler.copy(from: sourceUrl, to: localUrl, resolver: promise.resolver, rejecter: promise.legacyRejecter)
|
|
return
|
|
}
|
|
let session = options.sessionType == .background ? backgroundSession : foregroundSession
|
|
let request = createUrlRequest(url: sourceUrl, headers: options.headers)
|
|
let downloadTask = session.downloadTask(with: request)
|
|
let taskDelegate = EXSessionDownloadTaskDelegate(
|
|
resolve: promise.resolver,
|
|
reject: promise.legacyRejecter,
|
|
localUrl: localUrl,
|
|
shouldCalculateMd5: options.md5
|
|
)
|
|
|
|
sessionTaskDispatcher.register(taskDelegate, for: downloadTask)
|
|
downloadTask.resume()
|
|
}
|
|
|
|
AsyncFunction("uploadAsync") { (targetUrl: URL, localUrl: URL, options: UploadOptions, promise: Promise) in
|
|
guard localUrl.isFileURL else {
|
|
throw InvalidFileUrlException(localUrl)
|
|
}
|
|
guard FileManager.default.fileExists(atPath: localUrl.path) else {
|
|
throw FileNotExistsException(localUrl.path)
|
|
}
|
|
let session = options.sessionType == .background ? backgroundSession : foregroundSession
|
|
let task = try createUploadTask(session: session, targetUrl: targetUrl, sourceUrl: localUrl, options: options)
|
|
let taskDelegate = EXSessionUploadTaskDelegate(resolve: promise.resolver, reject: promise.legacyRejecter)
|
|
|
|
sessionTaskDispatcher.register(taskDelegate, for: task)
|
|
task.resume()
|
|
}
|
|
|
|
AsyncFunction("uploadTaskStartAsync") { (targetUrl: URL, localUrl: URL, uuid: String, options: UploadOptions, promise: Promise) in
|
|
let session = options.sessionType == .background ? backgroundSession : foregroundSession
|
|
let task = try createUploadTask(session: session, targetUrl: targetUrl, sourceUrl: localUrl, options: options)
|
|
let onSend: EXUploadDelegateOnSendCallback = { [weak self] _, _, totalBytesSent, totalBytesExpectedToSend in
|
|
self?.sendEvent(EVENT_UPLOAD_PROGRESS, [
|
|
"uuid": uuid,
|
|
"data": [
|
|
"totalBytesSent": totalBytesSent,
|
|
"totalBytesExpectedToSend": totalBytesExpectedToSend
|
|
]
|
|
])
|
|
}
|
|
let taskDelegate = EXSessionCancelableUploadTaskDelegate(
|
|
resolve: promise.resolver,
|
|
reject: promise.legacyRejecter,
|
|
onSendCallback: onSend,
|
|
resumableManager: taskHandlersManager,
|
|
uuid: uuid
|
|
)
|
|
|
|
sessionTaskDispatcher.register(taskDelegate, for: task)
|
|
taskHandlersManager.register(task, uuid: uuid)
|
|
task.resume()
|
|
}
|
|
|
|
// swiftlint:disable:next line_length closure_body_length
|
|
AsyncFunction("downloadResumableStartAsync") { (sourceUrl: URL, localUrl: URL, uuid: String, options: DownloadOptionsLegacy, resumeDataString: String?, promise: Promise) in
|
|
try ensureFileDirectoryExists(localUrl)
|
|
try ensurePathPermission(appContext, path: localUrl.path, flag: .write)
|
|
|
|
let session = options.sessionType == .background ? backgroundSession : foregroundSession
|
|
let onWrite: EXDownloadDelegateOnWriteCallback = { [weak self] _, _, totalBytesWritten, totalBytesExpectedToWrite in
|
|
self?.sendEvent(EVENT_DOWNLOAD_PROGRESS, [
|
|
"uuid": uuid,
|
|
"data": [
|
|
"totalBytesWritten": totalBytesWritten,
|
|
"totalBytesExpectedToWrite": totalBytesExpectedToWrite
|
|
]
|
|
])
|
|
}
|
|
let task: URLSessionDownloadTask
|
|
|
|
if let resumeDataString, let resumeData = Data(base64Encoded: resumeDataString) {
|
|
task = session.downloadTask(withResumeData: resumeData)
|
|
} else {
|
|
let request = createUrlRequest(url: sourceUrl, headers: options.headers)
|
|
task = session.downloadTask(with: request)
|
|
}
|
|
|
|
let taskDelegate = EXSessionResumableDownloadTaskDelegate(
|
|
resolve: promise.resolver,
|
|
reject: promise.legacyRejecter,
|
|
localUrl: localUrl,
|
|
shouldCalculateMd5: options.md5,
|
|
onWriteCallback: onWrite,
|
|
resumableManager: taskHandlersManager,
|
|
uuid: uuid
|
|
)
|
|
|
|
sessionTaskDispatcher.register(taskDelegate, for: task)
|
|
taskHandlersManager.register(task, uuid: uuid)
|
|
task.resume()
|
|
}
|
|
|
|
AsyncFunction("downloadResumablePauseAsync") { (id: String) -> [String: String?] in
|
|
guard let task = taskHandlersManager.downloadTask(forId: id) else {
|
|
throw DownloadTaskNotFoundException(id)
|
|
}
|
|
let resumeData = await task.cancelByProducingResumeData()
|
|
|
|
return [
|
|
"resumeData": resumeData?.base64EncodedString()
|
|
]
|
|
}
|
|
|
|
AsyncFunction("networkTaskCancelAsync") { (id: String) in
|
|
taskHandlersManager.task(forId: id)?.cancel()
|
|
}
|
|
|
|
AsyncFunction("getFreeDiskStorageAsync") { () -> Int64 in
|
|
// Uses required reason API based on the following reason: E174.1 85F4.1
|
|
#if !os(tvOS)
|
|
let resourceValues = try getResourceValues(from: documentDirectory, forKeys: [.volumeAvailableCapacityForImportantUsageKey])
|
|
guard let availableCapacity = resourceValues?.volumeAvailableCapacityForImportantUsage else {
|
|
throw CannotDetermineDiskCapacity()
|
|
}
|
|
return availableCapacity
|
|
#else
|
|
let resourceValues = try getResourceValues(from: cacheDirectory, forKeys: [.volumeAvailableCapacityKey])
|
|
guard let availableCapacity = resourceValues?.volumeAvailableCapacity else {
|
|
throw CannotDetermineDiskCapacity()
|
|
}
|
|
return Int64(availableCapacity)
|
|
#endif
|
|
}
|
|
|
|
AsyncFunction("getTotalDiskCapacityAsync") { () -> Int in
|
|
// Uses required reason API based on the following reason: E174.1 85F4.1
|
|
let resourceValues = try getResourceValues(from: documentDirectory, forKeys: [.volumeTotalCapacityKey])
|
|
|
|
guard let totalCapacity = resourceValues?.volumeTotalCapacity else {
|
|
throw CannotDetermineDiskCapacity()
|
|
}
|
|
return totalCapacity
|
|
}
|
|
}
|
|
}
|