- App.tsx: full navigation (Auth stack + Main tabs with 5 screens) - Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen - HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors - MapScreen: interactive map with layer toggles (6 layers) - MarketplaceScreen: categories (6), products (5), search - ChatScreen: AI chat with quick prompts (4), bot responses - ProfileScreen: user info, stats, menu (9 items), logout - AlertsScreen: alert list with severity, acknowledge - SensorsScreen: sensor list with type filters (6 types), search - ZonesScreen: zone cards with stats - SettingsScreen: language picker (FR/EN/ES/DE), privacy, about - Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n - Hooks: useSensors, useAlerts, useNotifications, useLocation - Components: Card, Button, LoadingSpinner, ErrorBoundary, Header - Services: iotService, notificationService (with axios API client) - Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN) - Theme: colors.ts with full design system (Blue Ocean palette) - Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
279 lines
11 KiB
Swift
279 lines
11 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 FileSystemModule: Module {
|
|
private lazy var sessionTaskDispatcher = EXSessionTaskDispatcher(sessionHandler: ExpoAppDelegate.getSubscriberOfType(FileSystemBackgroundSessionHandler.self))
|
|
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
|
|
}
|
|
|
|
// swiftlint:disable:next cyclomatic_complexity
|
|
public func definition() -> ModuleDefinition {
|
|
Name("ExponentFileSystem")
|
|
|
|
Constants {
|
|
return [
|
|
"documentDirectory": documentDirectory?.absoluteString,
|
|
"cacheDirectory": cacheDirectory?.absoluteString,
|
|
"bundleDirectory": Bundle.main.bundlePath
|
|
]
|
|
}
|
|
|
|
Events(EVENT_DOWNLOAD_PROGRESS, EVENT_UPLOAD_PROGRESS)
|
|
|
|
AsyncFunction("getInfoAsync") { (url: URL, options: InfoOptions, promise: Promise) in
|
|
switch url.scheme {
|
|
case "file":
|
|
EXFileSystemLocalFileHandler.getInfoForFile(url, withOptions: options.toDictionary(), resolver: promise.resolver, rejecter: promise.legacyRejecter)
|
|
case "assets-library", "ph":
|
|
EXFileSystemAssetLibraryHandler.getInfoForFile(url, withOptions: options.toDictionary(), 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)
|
|
|
|
if options.encoding == .base64 {
|
|
try writeFileAsBase64(path: url.path, string: string)
|
|
return
|
|
}
|
|
do {
|
|
try string.write(toFile: url.path, atomically: true, encoding: options.encoding.toStringEncoding() ?? .utf8)
|
|
} 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: DownloadOptions, 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: DownloadOptions, resumeDataString: String?, promise: Promise) in
|
|
try ensureFileDirectoryExists(localUrl)
|
|
try ensurePathPermission(appContext, path: localUrl.path, flag: .write)
|
|
|
|
let session = options.sessionType == .background ? backgroundSession : foregroundSession
|
|
let resumeData = resumeDataString != nil ? Data(base64Encoded: resumeDataString ?? "") : nil
|
|
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") { () -> Int in
|
|
// Uses required reason API based on the following reason: E174.1 85F4.1
|
|
let resourceValues = try getResourceValues(from: documentDirectory, forKeys: [.volumeAvailableCapacityKey])
|
|
|
|
guard let availableCapacity = resourceValues?.volumeAvailableCapacity else {
|
|
throw CannotDetermineDiskCapacity()
|
|
}
|
|
return availableCapacity
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|