374 lines
11 KiB
Swift
374 lines
11 KiB
Swift
// Copyright 2024-present 650 Industries. All rights reserved.
|
|
|
|
import ExpoModulesCore
|
|
|
|
@available(iOS 14, tvOS 14, *)
|
|
public final class FileSystemModule: Module {
|
|
#if os(iOS)
|
|
private lazy var filePickingHandler = FilePickingHandler(module: self)
|
|
#endif
|
|
|
|
var documentDirectory: URL? {
|
|
return appContext?.config.documentDirectory
|
|
}
|
|
|
|
var cacheDirectory: URL? {
|
|
return appContext?.config.cacheDirectory
|
|
}
|
|
|
|
var totalDiskSpace: Int64? {
|
|
guard let path = documentDirectory?.path,
|
|
let attributes = try? FileManager.default.attributesOfFileSystem(forPath: path) else {
|
|
return nil
|
|
}
|
|
return attributes[.systemFreeSize] as? Int64
|
|
}
|
|
|
|
var availableDiskSpace: Int64? {
|
|
guard let path = documentDirectory?.path,
|
|
let attributes = try? FileManager.default.attributesOfFileSystem(forPath: path) else {
|
|
return nil
|
|
}
|
|
return attributes[.systemFreeSize] as? Int64
|
|
}
|
|
|
|
public func definition() -> ModuleDefinition {
|
|
Name("FileSystem")
|
|
|
|
Constant("documentDirectory") {
|
|
return documentDirectory?.absoluteString
|
|
}
|
|
|
|
Constant("cacheDirectory") {
|
|
return cacheDirectory?.absoluteString
|
|
}
|
|
|
|
Constant("bundleDirectory") {
|
|
return Bundle.main.bundlePath
|
|
}
|
|
|
|
Constant("appleSharedContainers") {
|
|
return getAppleSharedContainers()
|
|
}
|
|
|
|
Property("totalDiskSpace") {
|
|
return totalDiskSpace
|
|
}
|
|
|
|
Property("availableDiskSpace") {
|
|
return availableDiskSpace
|
|
}
|
|
|
|
// swiftlint:disable:next closure_body_length
|
|
AsyncFunction("downloadFileAsync") { (url: URL, to: FileSystemPath, options: DownloadOptions?, promise: Promise) in
|
|
try to.validatePermission(.write)
|
|
|
|
var request = URLRequest(url: url)
|
|
|
|
if let headers = options?.headers {
|
|
headers.forEach { key, value in
|
|
request.addValue(value, forHTTPHeaderField: key)
|
|
}
|
|
}
|
|
|
|
let downloadTask = URLSession.shared.downloadTask(with: request) { urlOrNil, responseOrNil, errorOrNil in
|
|
guard errorOrNil == nil else {
|
|
return promise.reject(UnableToDownloadException(errorOrNil?.localizedDescription ?? "unspecified error"))
|
|
}
|
|
guard let httpResponse = responseOrNil as? HTTPURLResponse else {
|
|
return promise.reject(UnableToDownloadException("no response"))
|
|
}
|
|
guard httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 else {
|
|
return promise.reject(UnableToDownloadException("response has status \(httpResponse.statusCode)"))
|
|
}
|
|
guard let fileURL = urlOrNil else {
|
|
return promise.reject(UnableToDownloadException("no file url"))
|
|
}
|
|
|
|
do {
|
|
let destination: URL
|
|
if let to = to as? FileSystemDirectory {
|
|
let filename = httpResponse.suggestedFilename ?? url.lastPathComponent
|
|
destination = to.url.appendingPathComponent(filename)
|
|
} else {
|
|
destination = to.url
|
|
}
|
|
if FileManager.default.fileExists(atPath: destination.path) {
|
|
if options?.idempotent == true {
|
|
try FileManager.default.removeItem(at: destination)
|
|
} else {
|
|
throw DestinationAlreadyExistsException()
|
|
}
|
|
}
|
|
try FileManager.default.moveItem(at: fileURL, to: destination)
|
|
// TODO: Remove .url.absoluteString once returning shared objects works
|
|
promise.resolve(destination.absoluteString)
|
|
} catch {
|
|
promise.reject(error)
|
|
}
|
|
}
|
|
downloadTask.resume()
|
|
}
|
|
|
|
AsyncFunction("pickDirectoryAsync") { (initialUri: URL?, promise: Promise) in
|
|
#if os(iOS)
|
|
filePickingHandler.presentDocumentPicker(
|
|
picker: createDirectoryPicker(initialUri: initialUri),
|
|
isDirectory: true,
|
|
initialUri: initialUri,
|
|
mimeType: nil,
|
|
promise: promise
|
|
)
|
|
#else
|
|
promise.reject(FeatureNotAvailableOnPlatformException())
|
|
#endif
|
|
}.runOnQueue(.main)
|
|
|
|
AsyncFunction("pickFileAsync") { (initialUri: URL?, mimeType: String?, promise: Promise) in
|
|
#if os(iOS)
|
|
filePickingHandler.presentDocumentPicker(
|
|
picker: createFilePicker(initialUri: initialUri, mimeType: mimeType),
|
|
isDirectory: false,
|
|
initialUri: initialUri,
|
|
mimeType: mimeType,
|
|
promise: promise
|
|
)
|
|
#else
|
|
promise.reject(FeatureNotAvailableOnPlatformException())
|
|
#endif
|
|
}.runOnQueue(.main)
|
|
|
|
Function("info") { (url: URL) in
|
|
let output = PathInfo()
|
|
output.exists = false
|
|
output.isDirectory = nil
|
|
|
|
guard let fileSystemManager = appContext?.fileSystem else {
|
|
return output
|
|
}
|
|
|
|
if fileSystemManager.getPathPermissions(url.path).contains(.read) {
|
|
var isDirectory: ObjCBool = false
|
|
if FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) {
|
|
output.exists = true
|
|
output.isDirectory = isDirectory.boolValue
|
|
return output
|
|
}
|
|
}
|
|
return output
|
|
}
|
|
|
|
// swiftlint:disable:next closure_body_length
|
|
Class(FileSystemFile.self) {
|
|
Constructor { (url: URL) in
|
|
return FileSystemFile(url: url.standardizedFileURL)
|
|
}
|
|
|
|
// we can't throw in a constructor, so this is a workaround
|
|
Function("validatePath") { file in
|
|
try file.validatePath()
|
|
}
|
|
|
|
// maybe asString, readAsString, readAsText, readText, ect.
|
|
AsyncFunction("text") { file in
|
|
return try file.text()
|
|
}
|
|
|
|
Function("textSync") { file in
|
|
return try file.text()
|
|
}
|
|
|
|
AsyncFunction("base64") { file in
|
|
return try file.base64()
|
|
}
|
|
|
|
Function("base64Sync") { file in
|
|
return try file.base64()
|
|
}
|
|
|
|
AsyncFunction("bytes") { file in
|
|
return try file.bytes()
|
|
}
|
|
|
|
Function("bytesSync") { file in
|
|
return try file.bytes()
|
|
}
|
|
|
|
Function("open") { file in
|
|
return try FileSystemFileHandle(file: file)
|
|
}
|
|
|
|
Function("info") { (file: FileSystemFile, options: InfoOptions?) in
|
|
return try file.info(options: options ?? InfoOptions())
|
|
}
|
|
|
|
Function("write") { (file: FileSystemFile, content: Either<String, TypedArray>, options: WriteOptions?) in
|
|
let append = options?.append ?? false
|
|
if let content: String = content.get() {
|
|
if options?.encoding == WriteEncoding.base64 {
|
|
guard let data = Data(base64Encoded: content, options: .ignoreUnknownCharacters) else {
|
|
throw UnableToWriteBase64DataException(file.url.absoluteString)
|
|
}
|
|
try file.write(data, append: append)
|
|
} else {
|
|
try file.write(content, append: append)
|
|
}
|
|
}
|
|
if let content: TypedArray = content.get() {
|
|
try file.write(content, append: append)
|
|
}
|
|
}
|
|
|
|
Property("size") { file in
|
|
try? file.size
|
|
}
|
|
|
|
Property("md5") { file in
|
|
try? file.md5
|
|
}
|
|
|
|
Property("modificationTime") { file in
|
|
try? file.modificationTime
|
|
}
|
|
|
|
Property("creationTime") { file in
|
|
try? file.creationTime
|
|
}
|
|
|
|
Property("type") { file in
|
|
file.type
|
|
}
|
|
|
|
Function("delete") { file in
|
|
try file.delete()
|
|
}
|
|
|
|
Property("exists") { file in
|
|
return file.exists
|
|
}
|
|
|
|
Function("create") { (file, options: CreateOptions?) in
|
|
try file.create(options ?? CreateOptions())
|
|
}
|
|
|
|
Function("copy") { (file, to: FileSystemPath) in
|
|
try file.copy(to: to)
|
|
}
|
|
|
|
Function("move") { (file, to: FileSystemPath) in
|
|
try file.move(to: to)
|
|
}
|
|
|
|
Function("rename") { (file, newName: String) in
|
|
try file.rename(newName)
|
|
}
|
|
|
|
Property("uri") { file in
|
|
return file.url.absoluteString
|
|
}
|
|
}
|
|
|
|
Class(FileSystemFileHandle.self) {
|
|
Function("readBytes") { (fileHandle, bytes: Int) in
|
|
try fileHandle.read(bytes)
|
|
}
|
|
|
|
Function("writeBytes") { (fileHandle, bytes: Data) in
|
|
try fileHandle.write(bytes)
|
|
}
|
|
|
|
Function("close") { fileHandle in
|
|
try fileHandle.close()
|
|
}
|
|
|
|
Property("offset") { fileHandle in
|
|
fileHandle.offset
|
|
}.set { (fileHandle, volume: UInt64) in
|
|
fileHandle.offset = volume
|
|
}
|
|
|
|
Property("size") { fileHandle in
|
|
fileHandle.size
|
|
}
|
|
}
|
|
|
|
// swiftlint:disable:next closure_body_length
|
|
Class(FileSystemDirectory.self) {
|
|
Constructor { (url: URL) in
|
|
return FileSystemDirectory(url: url.standardizedFileURL)
|
|
}
|
|
|
|
Function("info") { directory in
|
|
try directory.info()
|
|
}
|
|
|
|
// we can't throw in a constructor, so this is a workaround
|
|
Function("validatePath") { directory in
|
|
try directory.validatePath()
|
|
}
|
|
|
|
Function("delete") { directory in
|
|
try directory.delete()
|
|
}
|
|
|
|
Property("exists") { directory in
|
|
return directory.exists
|
|
}
|
|
|
|
Function("create") { (directory, options: CreateOptions?) in
|
|
try directory.create(options ?? CreateOptions())
|
|
}
|
|
|
|
Function("copy") { (directory, to: FileSystemPath) in
|
|
try directory.copy(to: to)
|
|
}
|
|
|
|
Function("move") { (directory, to: FileSystemPath) in
|
|
try directory.move(to: to)
|
|
}
|
|
|
|
Function("rename") { (directory, newName: String) in
|
|
try directory.rename(newName)
|
|
}
|
|
|
|
// this function is internal and will be removed in the future (when returning arrays of shared objects is supported)
|
|
Function("listAsRecords") { directory in
|
|
try directory.listAsRecords()
|
|
}
|
|
|
|
Function("createFile") { (directory, name: String, content: String?) in
|
|
let file = FileSystemFile(url: directory.url.appendingPathComponent(name))
|
|
try file.create(CreateOptions())
|
|
return file
|
|
}
|
|
|
|
Function("createDirectory") { (directory, name: String) in
|
|
let newDirectory = FileSystemDirectory(url: directory.url.appendingPathComponent(name))
|
|
try newDirectory.create(CreateOptions())
|
|
return newDirectory
|
|
}
|
|
|
|
Property("uri") { directory in
|
|
return directory.url.absoluteString
|
|
}
|
|
|
|
Property("size") { directory in
|
|
return try? directory.size
|
|
}
|
|
}
|
|
}
|
|
|
|
private func getAppleSharedContainers() -> [String: String] {
|
|
guard let appContext else {
|
|
return [:]
|
|
}
|
|
var result: [String: String] = [:]
|
|
for appGroup in appContext.appCodeSignEntitlements.appGroups ?? [] {
|
|
if let directory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) {
|
|
result[appGroup] = directory.standardizedFileURL.path
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
}
|