diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cb319f..e635ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- We fixed an issue that caused a FileNotFoundException during file deletion operations. - We updated mendix-native to support react v19 and react native v0.78.2. ## [v0.1.3] - 2025-12-05 diff --git a/README.md b/README.md index 2a8be0b..b82271a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Mendix native mobile package for React Native applications. Before you begin, ensure you have the following installed: -- **Node.js**: Version 18 (specified in `.nvmrc`) +- **Node.js**: Version 24 (specified in `.nvmrc`) - **Yarn**: Package manager (Yarn workspaces are required) - **React Native development environment** (optional, only needed if running the example app): Follow the [React Native environment setup guide](https://reactnative.dev/docs/environment-setup) - For iOS: Xcode and CocoaPods diff --git a/ios/Modules/Helper/StorageHelper.swift b/ios/Modules/Helper/StorageHelper.swift index 24208a5..53e4864 100644 --- a/ios/Modules/Helper/StorageHelper.swift +++ b/ios/Modules/Helper/StorageHelper.swift @@ -19,6 +19,10 @@ public class StorageHelper { public static func clearDataAt(url: URL, component: String) { let path = url.appendingPathComponent(component).path - _ = NativeFsModule.remove(path, error: nil) + do { + try NativeFsModule.remove(path) + } catch { + NSLog("Failed to clear data at path: \(path), error: \(error.localizedDescription)") + } } } diff --git a/ios/Modules/NativeFsModule/NativeFsModule.swift b/ios/Modules/NativeFsModule/NativeFsModule.swift index 4870876..0042045 100644 --- a/ios/Modules/NativeFsModule/NativeFsModule.swift +++ b/ios/Modules/NativeFsModule/NativeFsModule.swift @@ -24,12 +24,27 @@ public class NativeFsModule: NSObject { return "\(String(describing: NativeFsModule.self)): \(message)" } + private func getBlobManager() -> RCTBlobManager? { + guard let blobManager: RCTBlobManager = ReactAppProvider.getModule(type: RCTBlobManager.self) else { + NSLog("NativeFsModule: Failed to get RCTBlobManager") + return nil + } + return blobManager + } + private func readBlobRefAsData(_ blob: [String: Any]) -> Data? { - return RCTBlobManager().resolve(blob) + guard let data = getBlobManager()?.resolve(blob) else { + NSLog("NativeFsModule: Failed to resolve blob") + return nil + } + return data } private func readDataAsBlobRef(_ data: Data) -> [String: Any]? { - let blobId = RCTBlobManager().store(data) + guard let blobId = getBlobManager()?.store(data) else { + NSLog("NativeFsModule: Failed to store data as blob") + return nil + } return [ "blobId": blobId as Any, "offset": 0, @@ -49,87 +64,41 @@ public class NativeFsModule: NSObject { } } - static func readJson(_ filePath: String, error: NSErrorPointer) -> [String: Any]? { + static func readJson(_ filePath: String) throws -> [String: Any]? { guard let data = readData(filePath) else { return nil } - - do { - let result = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) - return result as? [String: Any] - } catch let jsonError { - error?.pointee = jsonError as NSError - return nil - } + let result = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) + return result as? [String: Any] } - static func save(_ data: Data, filepath: String, error: NSErrorPointer) -> Bool { + static func save(_ data: Data, filepath: String) throws { let directoryURL = URL(fileURLWithPath: (filepath as NSString).deletingLastPathComponent) - - do { - try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) - } catch let directoryError { - error?.pointee = directoryError as NSError - return false - } - - var options: Data.WritingOptions = .atomic - if encryptionEnabled { - options = [.atomic, .completeFileProtection] - } - - do { - try data.write(to: URL(fileURLWithPath: filepath), options: options) - return true - } catch let writeError { - error?.pointee = writeError as NSError - return false - } + try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) + let options: Data.WritingOptions = encryptionEnabled ? [.atomic, .completeFileProtection] : .atomic + try data.write(to: URL(fileURLWithPath: filepath), options: options) } - static func move(_ filepath: String, newPath: String, error: NSErrorPointer) -> Bool { + static func move(_ filepath: String, newPath: String) throws { let fileManager = FileManager.default - guard fileManager.fileExists(atPath: filepath) else { - error?.pointee = NSError(domain: NativeFsErrorDomain, code: -1, userInfo: [NSLocalizedDescriptionKey: "File does not exist"]) - return false + throw NSError(domain: NativeFsErrorDomain, code: -1, userInfo: [NSLocalizedDescriptionKey: "File does not exist"]) } - let directoryURL = URL(fileURLWithPath: (newPath as NSString).deletingLastPathComponent) - - do { - try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) - } catch let directoryError { - error?.pointee = directoryError as NSError - return false - } - - do { - try fileManager.moveItem(atPath: filepath, toPath: newPath) - return true - } catch let moveError { - error?.pointee = moveError as NSError - return false - } + try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) + try fileManager.moveItem(atPath: filepath, toPath: newPath) } - static func remove(_ filepath: String, error: NSErrorPointer) -> Bool { + static func remove(_ filepath: String) throws { let fileManager = FileManager.default - guard fileManager.fileExists(atPath: filepath) else { - return false - } - - do { - try fileManager.removeItem(atPath: filepath) - return true - } catch let removeError { - error?.pointee = removeError as NSError - return false + NSLog("Trying to delete non-existing file: \(filepath)") + return } + try fileManager.removeItem(atPath: filepath) } - static func ensureWhiteListedPath(_ paths: [String], error: NSErrorPointer) -> Bool { + static func ensureWhiteListedPath(_ paths: [String]) throws { let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first ?? "" let cachesPath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first ?? "" let tempPath = (NSTemporaryDirectory() as NSString).standardizingPath @@ -138,15 +107,13 @@ public class NativeFsModule: NSObject { if !path.hasPrefix(documentsPath) && !path.hasPrefix(cachesPath) && !path.hasPrefix(tempPath) { - error?.pointee = NSError( + throw NSError( domain: NativeFsErrorDomain, code: 999, userInfo: [NSLocalizedDescriptionKey: "The path \(path) does not point to the documents directory"] ) - return false } } - return true } static func list(_ dirPath: String) -> [String] { @@ -169,34 +136,26 @@ public class NativeFsModule: NSObject { reject: @escaping RCTPromiseRejectBlock ) { - var error: NSError? - if !NativeFsModule.ensureWhiteListedPath([filepath], error: &error) { - reject(NativeFsModule.INVALID_PATH, NativeFsModule.formatError("Path not accessible"), error) - return - } + guard isWhiteListedPath(filepath, reject: reject) else { return } guard let data = readBlobRefAsData(blob) else { reject(NativeFsModule.ERROR_READ_FAILED, NativeFsModule.formatError("Failed to read blob"), nil) return } - if !NativeFsModule.save(data, filepath: filepath, error: &error) { + do { + try NativeFsModule.save(data, filepath: filepath) + resolve(nil) + } catch { reject(NativeFsModule.ERROR_SAVE_FAILED, NativeFsModule.formatError("Save failed"), error) - return } - - resolve(nil) } public func read(_ filepath: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { - var error: NSError? - if !NativeFsModule.ensureWhiteListedPath([filepath], error: &error) { - reject(NativeFsModule.INVALID_PATH, NativeFsModule.formatError("Path not accessible"), error) - return - } + guard isWhiteListedPath(filepath, reject: reject) else { return } guard let data = NativeFsModule.readData(filepath) else { resolve(nil) @@ -216,45 +175,33 @@ public class NativeFsModule: NSObject { resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { - var error: NSError? - if !NativeFsModule.ensureWhiteListedPath([filepath, newPath], error: &error) { - reject(NativeFsModule.INVALID_PATH, NativeFsModule.formatError("Path not accessible"), error) - return - } + guard isWhiteListedPath(filepath, newPath, reject: reject) else { return } - if !NativeFsModule.move(filepath, newPath: newPath, error: &error) { + do { + try NativeFsModule.move(filepath, newPath: newPath) + resolve(nil) + } catch { reject(NativeFsModule.ERROR_MOVE_FAILED, NativeFsModule.formatError("Failed to move file"), error) - return } - - resolve(nil) } public func remove(_ filepath: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { - var error: NSError? - if !NativeFsModule.ensureWhiteListedPath([filepath], error: &error) { - reject(NativeFsModule.INVALID_PATH, NativeFsModule.formatError("Path not accessible"), error) - return - } + guard isWhiteListedPath(filepath, reject: reject) else { return } - if !NativeFsModule.remove(filepath, error: &error) { + do { + try NativeFsModule.remove(filepath) + resolve(nil) + } catch { reject(NativeFsModule.ERROR_DELETE_FAILED, NativeFsModule.formatError("Failed to delete file"), error) - return } - - resolve(nil) } public func list(_ dirPath: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { - var error: NSError? - if !NativeFsModule.ensureWhiteListedPath([dirPath], error: &error) { - reject(NativeFsModule.INVALID_PATH, NativeFsModule.formatError("Path not accessible"), error) - return - } + guard isWhiteListedPath(dirPath, reject: reject) else { return } resolve(NativeFsModule.list(dirPath)) } @@ -263,11 +210,7 @@ public class NativeFsModule: NSObject { resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { - var error: NSError? - if !NativeFsModule.ensureWhiteListedPath([filePath], error: &error) { - reject(NativeFsModule.INVALID_PATH, NativeFsModule.formatError("Path not accessible"), error) - return - } + guard isWhiteListedPath(filePath, reject: reject) else { return } guard let data = NativeFsModule.readData(filePath) else { resolve(nil) @@ -282,12 +225,7 @@ public class NativeFsModule: NSObject { public func fileExists(_ filepath: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { - - var error: NSError? - if !NativeFsModule.ensureWhiteListedPath([filepath], error: &error) { - reject(NativeFsModule.INVALID_PATH, NativeFsModule.formatError("Path not accessible"), error) - return - } + guard isWhiteListedPath(filepath, reject: reject) else { return } let exists = FileManager.default.fileExists(atPath: filepath) resolve(NSNumber(value: exists)) @@ -297,22 +235,14 @@ public class NativeFsModule: NSObject { resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { - var error: NSError? - if !NativeFsModule.ensureWhiteListedPath([filepath], error: &error) { - reject(NativeFsModule.INVALID_PATH, NativeFsModule.formatError("Path not accessible"), error) - return - } + guard isWhiteListedPath(filepath, reject: reject) else { return } - guard let data = NativeFsModule.readJson(filepath, error: &error) else { - if let error = error { - reject(NativeFsModule.ERROR_SERIALIZATION_FAILED, NativeFsModule.formatError("Failed to deserialize JSON"), error) - } else { - resolve(nil) - } - return + do { + let data = try NativeFsModule.readJson(filepath) + resolve(data) + } catch { + reject(NativeFsModule.ERROR_SERIALIZATION_FAILED, NativeFsModule.formatError("Failed to deserialize JSON"), error) } - - resolve(data) } public func writeJson(_ data: [String: Any], @@ -320,23 +250,21 @@ public class NativeFsModule: NSObject { resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { - var error: NSError? - if !NativeFsModule.ensureWhiteListedPath([filepath], error: &error) { - reject(NativeFsModule.INVALID_PATH, "Path not accessible", error) + guard isWhiteListedPath(filepath, reject: reject) else { return } + + var jsonData: Data + do { + jsonData = try JSONSerialization.data(withJSONObject: data, options: .prettyPrinted) + } catch { + reject(NativeFsModule.ERROR_SERIALIZATION_FAILED, NativeFsModule.formatError("Failed to serialize JSON"), error) return } do { - let jsonData = try JSONSerialization.data(withJSONObject: data, options: .prettyPrinted) - - if !NativeFsModule.save(jsonData, filepath: filepath, error: &error) { - reject(NativeFsModule.ERROR_SAVE_FAILED, NativeFsModule.formatError("Failed to write JSON"), error) - return - } - + try NativeFsModule.save(jsonData, filepath: filepath) resolve(nil) } catch { - reject(NativeFsModule.ERROR_SERIALIZATION_FAILED, NativeFsModule.formatError("Failed to serialize JSON"), error) + reject(NativeFsModule.ERROR_SAVE_FAILED, NativeFsModule.formatError("Failed to write JSON"), error) } } @@ -345,4 +273,15 @@ public class NativeFsModule: NSObject { "SUPPORTS_DIRECTORY_MOVE": true, "SUPPORTS_ENCRYPTION": true ] + + private func isWhiteListedPath(_ paths: String..., reject: RCTPromiseRejectBlock) -> Bool { + do { + try NativeFsModule.ensureWhiteListedPath(paths) + return true + } catch let error { + reject(NativeFsModule.INVALID_PATH, NativeFsModule.formatError("Path not accessible"), error) + return false + } + } } + diff --git a/ios/Modules/NativeOtaModule/OtaHelpers.swift b/ios/Modules/NativeOtaModule/OtaHelpers.swift index 5492fc6..782f67c 100644 --- a/ios/Modules/NativeOtaModule/OtaHelpers.swift +++ b/ios/Modules/NativeOtaModule/OtaHelpers.swift @@ -50,10 +50,10 @@ class OtaHelpers: NSObject { } static func getNativeDependencies() -> [String: Any] { - guard let path = Bundle.main.path(forResource: "native_dependencies", ofType: "json") else { + guard let path = Bundle.main.path(forResource: "native_dependencies", ofType: "json"), + let data = try? NativeFsModule.readJson(path) else { return [:] } - - return NativeFsModule.readJson(path, error: nil) ?? [:] + return data } }