Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion ios/Modules/Helper/StorageHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}
}
}
219 changes: 79 additions & 140 deletions ios/Modules/NativeFsModule/NativeFsModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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] {
Expand All @@ -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)
Expand All @@ -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))
}
Expand All @@ -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)
Expand All @@ -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))
Expand All @@ -297,46 +235,36 @@ 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],
filepath: String,
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)
}
}

Expand All @@ -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
}
}
}

6 changes: 3 additions & 3 deletions ios/Modules/NativeOtaModule/OtaHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}