From 52f6e693a0a2ba6a02671b23f6b44dee13fad5e2 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Fri, 10 Oct 2025 15:14:36 +0530 Subject: [PATCH 1/9] feat: fileObject --- package-lock.json | 11 + package.json | 4 +- src/fileSystem/NativeFileWrapper.ts | 216 ++++++++++++++ src/fileSystem/fileObject.ts | 141 +++++++++ src/plugins/nativeFile/package.json | 17 ++ src/plugins/nativeFile/plugin.xml | 15 + .../nativeFile/src/android/nativeFile.java | 281 ++++++++++++++++++ 7 files changed, 684 insertions(+), 1 deletion(-) create mode 100644 src/fileSystem/NativeFileWrapper.ts create mode 100644 src/fileSystem/fileObject.ts create mode 100644 src/plugins/nativeFile/package.json create mode 100644 src/plugins/nativeFile/plugin.xml create mode 100644 src/plugins/nativeFile/src/android/nativeFile.java diff --git a/package-lock.json b/package-lock.json index f2d00d40d..2439d8381 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,6 +72,7 @@ "cordova-plugin-websocket": "file:src/plugins/websocket", "css-loader": "^7.1.2", "mini-css-extract-plugin": "^2.9.3", + "native_file": "file:src/plugins/nativeFile", "path-browserify": "^1.0.1", "postcss-loader": "^8.1.1", "prettier": "^3.6.2", @@ -7912,6 +7913,10 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/native_file": { + "resolved": "src/plugins/nativeFile", + "link": true + }, "node_modules/negotiator": { "version": "0.6.3", "license": "MIT", @@ -11204,6 +11209,12 @@ "dev": true, "license": "ISC" }, + "src/plugins/nativeFile": { + "name": "native_file", + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, "src/plugins/proot": { "name": "com.foxdebug.acode.rk.exec.proot", "version": "1.0.0", diff --git a/package.json b/package.json index 741b987c6..0b9de5a18 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "cordova-plugin-browser": {}, "com.foxdebug.acode.rk.exec.terminal": {}, "com.foxdebug.acode.rk.exec.proot": {}, - "cordova-plugin-sftp": {} + "cordova-plugin-sftp": {}, + "com.foxdebug.acode.rk.nativeFile": {} }, "platforms": [ "android" @@ -81,6 +82,7 @@ "cordova-plugin-websocket": "file:src/plugins/websocket", "css-loader": "^7.1.2", "mini-css-extract-plugin": "^2.9.3", + "native_file": "file:src/plugins/nativeFile", "path-browserify": "^1.0.1", "postcss-loader": "^8.1.1", "prettier": "^3.6.2", diff --git a/src/fileSystem/NativeFileWrapper.ts b/src/fileSystem/NativeFileWrapper.ts new file mode 100644 index 000000000..fca2cd119 --- /dev/null +++ b/src/fileSystem/NativeFileWrapper.ts @@ -0,0 +1,216 @@ +import {FileObject} from "./fileObject"; + +declare var cordova: any; + +declare global { + interface Window { + NativeFileWrapper: typeof NativeFileWrapper; + } +} + +export class NativeFileWrapper implements FileObject { + private readonly path: string; + + constructor(absolutePath: string) { + this.path = absolutePath; + } + + private execPlugin(action: string, args: any[] = []): Promise { + return new Promise((resolve, reject) => { + cordova.exec( + (result: any) => resolve(result), + (error: any) => reject(error), + 'nativeFile', + action, + [this.path, ...args] + ); + }); + } + + async canRead(): Promise { + const result = await this.execPlugin('canRead'); + return result === 1; + } + + async canWrite(): Promise { + const result = await this.execPlugin('canWrite'); + return result === 1; + } + + async childByNameExists(name: string): Promise { + try { + const result = await this.execPlugin('childByNameExists', [name]); + return result === 1; + } catch (error) { + return null; + } + } + + async createNewFile(): Promise { + try { + const result = await this.execPlugin('createNewFile'); + return result === 1; + } catch (error) { + return false; + } + } + + async delete(): Promise { + try { + const result = await this.execPlugin('delete'); + return result === 1; + } catch (error) { + return false; + } + } + + async exists(): Promise { + try { + const result = await this.execPlugin('exists'); + return result === 1; + } catch (error) { + return false; + } + } + + async getChildByName(name: string): Promise { + try { + const childPath = await this.execPlugin('getChildByName', [name]); + if (childPath && childPath !== "") { + return new NativeFileWrapper(childPath); + } + return null; + } catch (error) { + return null; + } + } + + async getLength(): Promise { + try { + return await this.execPlugin('getLength'); + } catch (error) { + return 0; + } + } + + async getName(): Promise { + try { + return await this.execPlugin('getName'); + } catch (error) { + throw new Error(`Failed to read file name: ${error}`); + } + } + + async getParentFile(): Promise { + try { + const parentPath = await this.execPlugin('getParentFile'); + if (parentPath && parentPath !== "") { + return new NativeFileWrapper(parentPath); + } + return null; + } catch (error) { + return null; + } + } + + async isDirectory(): Promise { + try { + const result = await this.execPlugin('isDirectory'); + return result === 1; + } catch (error) { + return false; + } + } + + async isFile(): Promise { + try { + const result = await this.execPlugin('isFile'); + return result === 1; + } catch (error) { + return false; + } + } + + async isLink(): Promise { + try { + const result = await this.execPlugin('isLink'); + return result === 1; + } catch (error) { + return false; + } + } + + async isNative(): Promise { + try { + const result = await this.execPlugin('isNative'); + return result === 1; + } catch (error) { + return true; // Default to true for native implementation + } + } + + async isUnixLike(): Promise { + try { + const result = await this.execPlugin('isUnixLike'); + return result === 1; + } catch (error) { + return true; // Default to true for Android + } + } + + async listFiles(): Promise { + try { + const paths: string[] = await this.execPlugin('listFiles'); + return paths.map(path => new NativeFileWrapper(path)); + } catch (error) { + return []; + } + } + + async mkdir(): Promise { + try { + const result = await this.execPlugin('mkdir'); + return result === 1; + } catch (error) { + return false; + } + } + + async mkdirs(): Promise { + try { + const result = await this.execPlugin('mkdirs'); + return result === 1; + } catch (error) { + return false; + } + } + + async readText(encoding: string = "UTF-8"): Promise { + try { + return await this.execPlugin('readText', [encoding]); + } catch (error) { + throw new Error(`Failed to read file: ${error}`); + } + } + + async toUri(): Promise { + try { + return await this.execPlugin('toUri'); + } catch (error) { + return `file://${this.path}`; + } + } + + async writeText(text: string, encoding: string = "UTF-8"): Promise { + try { + await this.execPlugin('writeText', [text, encoding]); + } catch (error) { + throw new Error(`Failed to write file: ${error}`); + } + } + + // Utility method to get the absolute path + getPath(): string { + return this.path; + } +} \ No newline at end of file diff --git a/src/fileSystem/fileObject.ts b/src/fileSystem/fileObject.ts new file mode 100644 index 000000000..2168f0231 --- /dev/null +++ b/src/fileSystem/fileObject.ts @@ -0,0 +1,141 @@ +/** + * Represents a virtual file or directory that can be backed by any type of storage, + * such as a local filesystem, remote server, or sandboxed virtual environment. + */ + +export interface FileObject { + /** + * Lists all files and directories within this directory. + * @returns A promise resolving to an array of child `FileObject`s. + * @throws If the current object is not a directory. + */ + listFiles(): Promise; + + /** + * Checks if this object represents a directory. + * @returns A promise resolving to `true` if it's a directory, otherwise `false`. + */ + isDirectory(): Promise; + + /** + * Checks if this object represents a regular file. + * @returns A promise resolving to `true` if it's a file, otherwise `false`. + */ + isFile(): Promise; + + /** + * Checks if this object represents a symbolic link or alias. + * @returns A promise resolving to `true` if it's a link, otherwise `false`. + */ + isLink(): Promise; + + /** + * Gets the name of this file or directory (not the full path). + * @returns The name as a string. + */ + getName(): Promise + + /** + * Checks whether the file or directory is readable. + * @returns A promise resolving to `true` if it can be read, otherwise `false`. + */ + canRead(): Promise; + + /** + * Checks whether the file or directory is writable. + * @returns A promise resolving to `true` if it can be written to, otherwise `false`. + */ + canWrite(): Promise; + + /** + * Determines if this file is backed by the native filesystem and + * can be accessed using `java.io.File` or similar APIs. + * @returns A promise resolving to `true` if it’s a native file. + */ + isNative(): Promise; + + /** + * Determines whether the file path uses a Unix-style format (forward slashes, starting with `/`). + * @returns A promise resolving to `true` if the path is Unix-like. + */ + isUnixLike(): Promise; + + /** + * Gets the file size in bytes. + * @returns A promise resolving to the file’s size. + */ + getLength(): Promise; + + /** + * Checks whether this file or directory exists. + * @returns A promise resolving to `true` if it exists, otherwise `false`. + */ + exists(): Promise; + + /** + * Creates a new empty file at this path. + * @returns A promise resolving to `true` if the file was created, `false` if it already exists. + */ + createNewFile(): Promise; + + /** + * Creates this directory (non-recursively). + * Fails if the parent directory does not exist. + * @returns A promise resolving to `true` if created successfully. + */ + mkdir(): Promise; + + /** + * Creates this directory and all missing parent directories if necessary. + * @returns A promise resolving to `true` if created successfully. + */ + mkdirs(): Promise; + + /** + * Writes text content to this file. + * @param text The text to write. + * @param encoding Optional text encoding (e.g., `"utf-8"`). Defaults to UTF-8. + */ + writeText(text: string, encoding?: string): Promise; + + /** + * Reads the entire content of this file as text. + * @param encoding Optional text encoding (e.g., `"utf-8"`). Defaults to UTF-8. + * @returns A promise resolving to the file’s text content. + */ + readText(encoding?: string): Promise; + + /** + * Deletes this file or directory. + * @returns A promise resolving to `true` if deleted successfully, otherwise `false`. + */ + delete(): Promise; + + /** + * Returns a URI representation of this file (e.g., `file://`, `content://`, or custom scheme). + * @returns A promise resolving to the file’s URI string. + */ + toUri(): Promise; + + /** + * Checks whether a child with the given name exists inside this directory. + * @param name The name of the child file or directory. + * @returns A promise resolving to `true` if it exists, `false` if not, or `null` if unknown. + */ + childByNameExists(name: string): Promise; + + /** + * Gets a `FileObject` representing a child entry with the given name. + * The child may or may not exist yet. + * @param name The name of the child. + * @returns A promise resolving to a `FileObject`, or `null` if the child is impossible + */ + getChildByName(name: string): Promise; + + /** + * Returns the parent directory of this file. + * @returns A promise resolving to the parent `FileObject`, or `null` if there’s no parent. + * @throws If unable to determine the parent. + */ + getParentFile(): Promise; +} diff --git a/src/plugins/nativeFile/package.json b/src/plugins/nativeFile/package.json new file mode 100644 index 000000000..a2d9b38bc --- /dev/null +++ b/src/plugins/nativeFile/package.json @@ -0,0 +1,17 @@ +{ + "name": "native_file", + "version": "1.0.0", + "description": "Full api of java.io.File", + "cordova": { + "id": "com.foxdebug.acode.rk.nativeFile", + "platforms": [ + "android" + ] + }, + "keywords": [ + "ecosystem:cordova", + "cordova-android" + ], + "author": "@RohitKushvaha01", + "license": "MIT" +} diff --git a/src/plugins/nativeFile/plugin.xml b/src/plugins/nativeFile/plugin.xml new file mode 100644 index 000000000..be82687de --- /dev/null +++ b/src/plugins/nativeFile/plugin.xml @@ -0,0 +1,15 @@ + + + nativeFile + + + + + + + + + + + + \ No newline at end of file diff --git a/src/plugins/nativeFile/src/android/nativeFile.java b/src/plugins/nativeFile/src/android/nativeFile.java new file mode 100644 index 000000000..7f628bb9a --- /dev/null +++ b/src/plugins/nativeFile/src/android/nativeFile.java @@ -0,0 +1,281 @@ +package com.foxdebug.acode.rk.nativeFile; + +import org.apache.cordova.*; +import org.json.JSONArray; +import org.json.JSONException; + +import android.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class nativeFile extends CordovaPlugin { + + @Override + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + try { + switch (action) { + case "exists": + return handleExists(args, callbackContext); + + case "isFile": + return handleIsFile(args, callbackContext); + + case "isDirectory": + return handleIsDirectory(args, callbackContext); + + case "getLength": + return handleGetLength(args, callbackContext); + + case "getName": + return handleGetName(args, callbackContext); + + case "getParentFile": + return handleGetParentFile(args, callbackContext); + + case "listFiles": + return handleListFiles(args, callbackContext); + + case "createNewFile": + return handleCreateNewFile(args, callbackContext); + + case "mkdir": + return handleMkdir(args, callbackContext); + + case "mkdirs": + return handleMkdirs(args, callbackContext); + + case "delete": + return handleDelete(args, callbackContext); + + case "readText": + return handleReadText(args, callbackContext); + + case "writeText": + return handleWriteText(args, callbackContext); + + case "canRead": + return handleCanRead(args, callbackContext); + + case "canWrite": + return handleCanWrite(args, callbackContext); + + case "childByNameExists": + return handleChildByNameExists(args, callbackContext); + + case "getChildByName": + return handleGetChildByName(args, callbackContext); + + case "isLink": + return handleIsLink(args, callbackContext); + + case "toUri": + return handleToUri(args, callbackContext); + + default: + return false; + } + } catch (Exception e) { + callbackContext.error("Error: " + e.getMessage()); + return true; + } + } + + private boolean handleExists(JSONArray args, CallbackContext cb) throws JSONException { + File f = new File(args.getString(0)); + cb.success(f.exists() ? 1 : 0); + return true; + } + + private boolean handleIsFile(JSONArray args, CallbackContext cb) throws JSONException { + File f = new File(args.getString(0)); + cb.success(f.isFile() ? 1 : 0); + return true; + } + + private boolean handleIsDirectory(JSONArray args, CallbackContext cb) throws JSONException { + File f = new File(args.getString(0)); + cb.success(f.isDirectory() ? 1 : 0); + return true; + } + + private boolean handleGetLength(JSONArray args, CallbackContext cb) throws JSONException { + File f = new File(args.getString(0)); + cb.success((int) f.length()); + return true; + } + + private boolean handleGetName(JSONArray args, CallbackContext cb) throws JSONException { + File f = new File(args.getString(0)); + cb.success(f.getName()); + return true; + } + + private boolean handleGetParentFile(JSONArray args, CallbackContext cb) throws JSONException { + File f = new File(args.getString(0)); + File parent = f.getParentFile(); + cb.success(parent != null ? parent.getAbsolutePath() : ""); + return true; + } + + private boolean handleListFiles(JSONArray args, CallbackContext cb) throws JSONException { + File f = new File(args.getString(0)); + if (!f.isDirectory()) { + cb.error("Not a directory"); + return true; + } + File[] files = f.listFiles(); + JSONArray result = new JSONArray(); + if (files != null) { + for (File file : files) { + result.put(file.getAbsolutePath()); + } + } + cb.success(result); + return true; + } + + private boolean handleCreateNewFile(JSONArray args, CallbackContext cb) throws JSONException { + File f = new File(args.getString(0)); + try { + boolean created = f.createNewFile(); + cb.success(created ? 1 : 0); + } catch (IOException e) { + cb.error(e.getMessage()); + } + return true; + } + + private boolean handleMkdir(JSONArray args, CallbackContext cb) throws JSONException { + File f = new File(args.getString(0)); + cb.success(f.mkdir() ? 1 : 0); + return true; + } + + private boolean handleMkdirs(JSONArray args, CallbackContext cb) throws JSONException { + File f = new File(args.getString(0)); + cb.success(f.mkdirs() ? 1 : 0); + return true; + } + + private boolean handleDelete(JSONArray args, CallbackContext cb) throws JSONException { + File f = new File(args.getString(0)); + cb.success(f.delete() ? 1 : 0); + return true; + } + + private boolean handleReadText(JSONArray args, CallbackContext cb) throws JSONException { + File f = new File(args.getString(0)); + String encoding = args.length() > 1 ? args.getString(1) : "UTF-8"; + try (FileInputStream fis = new FileInputStream(f)) { + byte[] data = new byte[(int) f.length()]; + fis.read(data); + String text = new String(data, encoding); + cb.success(text); + } catch (Exception e) { + cb.error(e.getMessage()); + } + return true; + } + + private boolean handleWriteText(JSONArray args, CallbackContext cb) throws JSONException { + File f = new File(args.getString(0)); + String text = args.getString(1); + String encoding = args.length() > 2 ? args.getString(2) : "UTF-8"; + try (FileOutputStream fos = new FileOutputStream(f)) { + fos.write(text.getBytes(encoding)); + fos.flush(); + cb.success(1); + } catch (Exception e) { + cb.error(e.getMessage()); + } + return true; + } + + // New methods below + + private boolean handleCanRead(JSONArray args, CallbackContext cb) throws JSONException { + File f = new File(args.getString(0)); + cb.success(f.canRead() ? 1 : 0); + return true; + } + + private boolean handleCanWrite(JSONArray args, CallbackContext cb) throws JSONException { + File f = new File(args.getString(0)); + cb.success(f.canWrite() ? 1 : 0); + return true; + } + + private boolean handleChildByNameExists(JSONArray args, CallbackContext cb) throws JSONException { + File parent = new File(args.getString(0)); + String childName = args.getString(1); + + if (!parent.isDirectory()) { + cb.success(0); + return true; + } + + File child = new File(parent, childName); + cb.success(child.exists() ? 1 : 0); + return true; + } + + private boolean handleGetChildByName(JSONArray args, CallbackContext cb) throws JSONException { + File parent = new File(args.getString(0)); + String childName = args.getString(1); + + if (!parent.isDirectory()) { + cb.success(""); + return true; + } + + File child = new File(parent, childName); + if (child.exists()) { + cb.success(child.getAbsolutePath()); + } else { + cb.success(""); + } + return true; + } + + private boolean handleIsLink(JSONArray args, CallbackContext cb) throws JSONException { + File f = new File(args.getString(0)); + + // For API 26+, use Files.isSymbolicLink + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + try { + Path path = Paths.get(f.getAbsolutePath()); + cb.success(Files.isSymbolicLink(path) ? 1 : 0); + } catch (Exception e) { + cb.success(0); + } + } else { + // Fallback for older Android versions + try { + String canonicalPath = f.getCanonicalPath(); + String absolutePath = f.getAbsolutePath(); + boolean isLink = !canonicalPath.equals(absolutePath); + cb.success(isLink ? 1 : 0); + } catch (IOException e) { + cb.success(0); + } + } + return true; + } + + private boolean handleToUri(JSONArray args, CallbackContext cb) throws JSONException { + File f = new File(args.getString(0)); + try { + String uri = f.toURI().toString(); + cb.success(uri); + } catch (Exception e) { + cb.error(e.getMessage()); + } + return true; + } +} \ No newline at end of file From f8638d85b727123ac4d5b7ac3b62da16b7c154a5 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Sun, 19 Oct 2025 14:04:44 +0530 Subject: [PATCH 2/9] feat: documentFileWrapper --- package-lock.json | 153 ++++++++++- package.json | 8 +- src/fileSystem/DocumentFileWrapper.ts | 248 ++++++++++++++++++ src/fileSystem/NativeFileWrapper.ts | 80 ++++-- src/lib/acode.js | 2 + src/plugins/{nativeFile => file}/package.json | 4 +- src/plugins/{nativeFile => file}/plugin.xml | 12 +- .../file/src/android/documentFile.java | 212 +++++++++++++++ .../src/android/nativeFile.java | 2 +- tsconfig.json | 17 ++ webpack.config.js | 20 +- 11 files changed, 725 insertions(+), 33 deletions(-) create mode 100644 src/fileSystem/DocumentFileWrapper.ts rename src/plugins/{nativeFile => file}/package.json (77%) rename src/plugins/{nativeFile => file}/plugin.xml (56%) create mode 100644 src/plugins/file/src/android/documentFile.java rename src/plugins/{nativeFile => file}/src/android/nativeFile.java (99%) create mode 100644 tsconfig.json diff --git a/package-lock.json b/package-lock.json index 6c9fb319f..951380809 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,7 @@ "@types/url-parse": "^1.4.11", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", + "com.foxdebug.acode.rk.exec.proot": "file:src/plugins/proot", "com.foxdebug.acode.rk.exec.terminal": "file:src/plugins/terminal", "cordova-android": "^14.0.1", "cordova-clipboard": "^1.3.0", @@ -70,6 +71,7 @@ "cordova-plugin-system": "file:src/plugins/system", "cordova-plugin-websocket": "file:src/plugins/websocket", "css-loader": "^7.1.2", + "file": "file:src/plugins/file", "mini-css-extract-plugin": "^2.9.3", "native_file": "file:src/plugins/nativeFile", "path-browserify": "^1.0.1", @@ -81,6 +83,8 @@ "sass-loader": "^16.0.5", "style-loader": "^4.0.0", "terminal": "^0.1.4", + "ts-loader": "^9.5.4", + "typescript": "^5.9.3", "webpack": "^5.101.0", "webpack-cli": "^6.0.1" } @@ -4335,6 +4339,10 @@ "dev": true, "license": "MIT" }, + "node_modules/com.foxdebug.acode.rk.exec.proot": { + "resolved": "src/plugins/proot", + "link": true + }, "node_modules/com.foxdebug.acode.rk.exec.terminal": { "resolved": "src/plugins/terminal", "link": true @@ -5986,6 +5994,10 @@ "node": ">=0.8.0" } }, + "node_modules/file": { + "resolved": "src/plugins/file", + "link": true + }, "node_modules/filesize": { "version": "11.0.2", "resolved": "https://registry.npmjs.org/filesize/-/filesize-11.0.2.tgz", @@ -10354,6 +10366,126 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/ts-loader": { + "version": "9.5.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", + "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ts-loader/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ts-loader/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ts-loader/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tslib": { "version": "1.14.1", "license": "0BSD" @@ -10415,6 +10547,20 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/uc.micro": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", @@ -11192,6 +11338,11 @@ "extraneous": true, "license": "MIT" }, + "src/plugins/file": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, "src/plugins/ftp": { "name": "cordova-plugin-ftp", "version": "1.1.1", @@ -11213,7 +11364,7 @@ "src/plugins/proot": { "name": "com.foxdebug.acode.rk.exec.proot", "version": "1.0.0", - "extraneous": true, + "dev": true, "license": "MIT" }, "src/plugins/sdcard": { diff --git a/package.json b/package.json index 64e85f8e5..3966c6e44 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,8 @@ "com.foxdebug.acode.rk.exec.terminal": {}, "com.foxdebug.acode.rk.exec.proot": {}, "cordova-plugin-sftp": {}, - "com.foxdebug.acode.rk.nativeFile": {}, - "cordova-plugin-system": {} + "cordova-plugin-system": {}, + "com.foxdebug.acode.rk.file": {} }, "platforms": [ "android" @@ -64,6 +64,7 @@ "@types/url-parse": "^1.4.11", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", + "com.foxdebug.acode.rk.exec.proot": "file:src/plugins/proot", "com.foxdebug.acode.rk.exec.terminal": "file:src/plugins/terminal", "cordova-android": "^14.0.1", "cordova-clipboard": "^1.3.0", @@ -80,6 +81,7 @@ "cordova-plugin-system": "file:src/plugins/system", "cordova-plugin-websocket": "file:src/plugins/websocket", "css-loader": "^7.1.2", + "file": "file:src/plugins/file", "mini-css-extract-plugin": "^2.9.3", "native_file": "file:src/plugins/nativeFile", "path-browserify": "^1.0.1", @@ -91,6 +93,8 @@ "sass-loader": "^16.0.5", "style-loader": "^4.0.0", "terminal": "^0.1.4", + "ts-loader": "^9.5.4", + "typescript": "^5.9.3", "webpack": "^5.101.0", "webpack-cli": "^6.0.1" }, diff --git a/src/fileSystem/DocumentFileWrapper.ts b/src/fileSystem/DocumentFileWrapper.ts new file mode 100644 index 000000000..c359216e2 --- /dev/null +++ b/src/fileSystem/DocumentFileWrapper.ts @@ -0,0 +1,248 @@ +import { FileObject } from "./fileObject"; + +declare var cordova: any; + +export class DocumentFileWrapper implements FileObject { + private readonly uri: string; + + constructor(uri: string) { + this.uri = uri; + //console.log(`[DocumentFileWrapper] Created for uri: ${uri}`); + } + + private execPlugin(action: string, args: any[] = []): Promise { + //console.log(`[DocumentFileWrapper] execPlugin called: action=${action}, args=${JSON.stringify(args)}`); + return new Promise((resolve, reject) => { + cordova.exec( + (result: any) => { + //console.log(`[DocumentFileWrapper] execPlugin success: action=${action}, result=${JSON.stringify(result)}`); + resolve(result); + }, + (error: any) => { + console.error(`[DocumentFileWrapper] execPlugin error: action=${action}, error=${JSON.stringify(error)}`); + reject(error); + }, + 'nativeFile', + action, + [this.uri, ...args] + ); + }); + } + + async canRead(): Promise { + const result = await this.execPlugin('canRead'); + //console.log(`[canRead] uri=${this.uri}, result=${result}`); + return result === 1; + } + + async canWrite(): Promise { + const result = await this.execPlugin('canWrite'); + //console.log(`[canWrite] uri=${this.uri}, result=${result}`); + return result === 1; + } + + async childByNameExists(name: string): Promise { + try { + const result = await this.execPlugin('childByNameExists', [name]); + //console.log(`[childByNameExists] uri=${this.uri}, name=${name}, result=${result}`); + return result === 1; + } catch (error) { + console.error(`[childByNameExists] uri=${this.uri}, name=${name}, error=${error}`); + return null; + } + } + + async createNewFile(): Promise { + try { + const result = await this.execPlugin('createNewFile'); + //console.log(`[createNewFile] uri=${this.uri}, result=${result}`); + return result === 1; + } catch (error) { + console.error(`[createNewFile] uri=${this.uri}, error=${error}`); + return false; + } + } + + async delete(): Promise { + try { + const result = await this.execPlugin('delete'); + //console.log(`[delete] uri=${this.uri}, result=${result}`); + return result === 1; + } catch (error) { + console.error(`[delete] uri=${this.uri}, error=${error}`); + return false; + } + } + + async exists(): Promise { + try { + const result = await this.execPlugin('exists'); + //console.log(`[exists] uri=${this.uri}, result=${result}`); + return result === 1; + } catch (error) { + console.error(`[exists] uri=${this.uri}, error=${error}`); + return false; + } + } + + async getChildByName(name: string): Promise { + try { + const childPath = await this.execPlugin('getChildByName', [name]); + //console.log(`[getChildByName] uri=${this.uri}, name=${name}, childPath=${childPath}`); + if (childPath && childPath !== "") { + return new DocumentFileWrapper(childPath); + } + return null; + } catch (error) { + console.error(`[getChildByName] uri=${this.uri}, name=${name}, error=${error}`); + return null; + } + } + + async getLength(): Promise { + try { + const result = await this.execPlugin('getLength'); + //console.log(`[getLength] uri=${this.uri}, length=${result}`); + return result; + } catch (error) { + console.error(`[getLength] uri=${this.uri}, error=${error}`); + return 0; + } + } + + async getName(): Promise { + try { + const name = await this.execPlugin('getName'); + //console.log(`[getName] uri=${this.uri}, name=${name}`); + return name; + } catch (error) { + console.error(`[getName] uri=${this.uri}, error=${error}`); + throw new Error(`Failed to read file name: ${error}`); + } + } + + async getParentFile(): Promise { + try { + const parentPath = await this.execPlugin('getParentFile'); + //console.log(`[getParentFile] uri=${this.uri}, parentPath=${parentPath}`); + if (parentPath && parentPath !== "") { + return new DocumentFileWrapper(parentPath); + } + return null; + } catch (error) { + console.error(`[getParentFile] uri=${this.uri}, error=${error}`); + return null; + } + } + + async isDirectory(): Promise { + try { + const result = await this.execPlugin('isDirectory'); + //console.log(`[isDirectory] uri=${this.uri}, result=${result}`); + return result === 1; + } catch (error) { + console.error(`[isDirectory] uri=${this.uri}, error=${error}`); + return false; + } + } + + async isFile(): Promise { + try { + const result = await this.execPlugin('isFile'); + //console.log(`[isFile] uri=${this.uri}, result=${result}`); + return result === 1; + } catch (error) { + console.error(`[isFile] uri=${this.uri}, error=${error}`); + return false; + } + } + + async isLink(): Promise { + try { + const result = await this.execPlugin('isLink'); + //console.log(`[isLink] uri=${this.uri}, result=${result}`); + return result === 1; + } catch (error) { + console.error(`[isLink] uri=${this.uri}, error=${error}`); + return false; + } + } + + async isNative(): Promise { + return false; + } + + async isUnixLike(): Promise { + return false; + } + + async listFiles(): Promise { + try { + const uris: string[] = await this.execPlugin('listFiles'); + //console.log(`[listFiles] uri=${this.uri}, files=${JSON.stringify(uris)}`); + return uris.map(uri => new DocumentFileWrapper(uri)); + } catch (error) { + console.error(`[listFiles] uri=${this.uri}, error=${error}`); + return []; + } + } + + async mkdir(): Promise { + try { + const result = await this.execPlugin('mkdir'); + //console.log(`[mkdir] uri=${this.uri}, result=${result}`); + return result === 1; + } catch (error) { + console.error(`[mkdir] uri=${this.uri}, error=${error}`); + return false; + } + } + + async mkdirs(): Promise { + try { + const result = await this.execPlugin('mkdirs'); + //console.log(`[mkdirs] uri=${this.uri}, result=${result}`); + return result === 1; + } catch (error) { + console.error(`[mkdirs] uri=${this.uri}, error=${error}`); + return false; + } + } + + async readText(encoding: string = "UTF-8"): Promise { + try { + const content = await this.execPlugin('readText', [encoding]); + //console.log(`[readText] uri=${this.uri}, content length=${content?.length}`); + return content; + } catch (error) { + console.error(`[readText] uri=${this.uri}, error=${error}`); + throw new Error(`Failed to read file: ${error}`); + } + } + + async toUri(): Promise { + try { + const uri = await this.execPlugin('toUri'); + //console.log(`[toUri] uri=${this.uri}, uri=${uri}`); + return uri; + } catch (error) { + console.error(`[toUri] uri=${this.uri}, error=${error}`); + return `file://${this.uri}`; + } + } + + async writeText(text: string, encoding: string = "UTF-8"): Promise { + try { + await this.execPlugin('writeText', [text, encoding]); + //console.log(`[writeText] uri=${this.uri}, text length=${text.length}`); + } catch (error) { + console.error(`[writeText] uri=${this.uri}, error=${error}`); + throw new Error(`Failed to write file: ${error}`); + } + } + + getPath(): string { + //console.log(`[getPath] returning uri=${this.uri}`); + return this.uri; + } +} diff --git a/src/fileSystem/NativeFileWrapper.ts b/src/fileSystem/NativeFileWrapper.ts index fca2cd119..2e3218a76 100644 --- a/src/fileSystem/NativeFileWrapper.ts +++ b/src/fileSystem/NativeFileWrapper.ts @@ -1,25 +1,27 @@ -import {FileObject} from "./fileObject"; +import { FileObject } from "./fileObject"; declare var cordova: any; -declare global { - interface Window { - NativeFileWrapper: typeof NativeFileWrapper; - } -} - export class NativeFileWrapper implements FileObject { private readonly path: string; constructor(absolutePath: string) { this.path = absolutePath; + //console.log(`[NativeFileWrapper] Created for path: ${absolutePath}`); } private execPlugin(action: string, args: any[] = []): Promise { + //console.log(`[NativeFileWrapper] execPlugin called: action=${action}, args=${JSON.stringify(args)}`); return new Promise((resolve, reject) => { cordova.exec( - (result: any) => resolve(result), - (error: any) => reject(error), + (result: any) => { + //console.log(`[NativeFileWrapper] execPlugin success: action=${action}, result=${JSON.stringify(result)}`); + resolve(result); + }, + (error: any) => { + console.error(`[NativeFileWrapper] execPlugin error: action=${action}, error=${JSON.stringify(error)}`); + reject(error); + }, 'nativeFile', action, [this.path, ...args] @@ -29,19 +31,23 @@ export class NativeFileWrapper implements FileObject { async canRead(): Promise { const result = await this.execPlugin('canRead'); + //console.log(`[canRead] path=${this.path}, result=${result}`); return result === 1; } async canWrite(): Promise { const result = await this.execPlugin('canWrite'); + //console.log(`[canWrite] path=${this.path}, result=${result}`); return result === 1; } async childByNameExists(name: string): Promise { try { const result = await this.execPlugin('childByNameExists', [name]); + //console.log(`[childByNameExists] path=${this.path}, name=${name}, result=${result}`); return result === 1; } catch (error) { + console.error(`[childByNameExists] path=${this.path}, name=${name}, error=${error}`); return null; } } @@ -49,8 +55,10 @@ export class NativeFileWrapper implements FileObject { async createNewFile(): Promise { try { const result = await this.execPlugin('createNewFile'); + //console.log(`[createNewFile] path=${this.path}, result=${result}`); return result === 1; } catch (error) { + console.error(`[createNewFile] path=${this.path}, error=${error}`); return false; } } @@ -58,8 +66,10 @@ export class NativeFileWrapper implements FileObject { async delete(): Promise { try { const result = await this.execPlugin('delete'); + //console.log(`[delete] path=${this.path}, result=${result}`); return result === 1; } catch (error) { + console.error(`[delete] path=${this.path}, error=${error}`); return false; } } @@ -67,8 +77,10 @@ export class NativeFileWrapper implements FileObject { async exists(): Promise { try { const result = await this.execPlugin('exists'); + //console.log(`[exists] path=${this.path}, result=${result}`); return result === 1; } catch (error) { + console.error(`[exists] path=${this.path}, error=${error}`); return false; } } @@ -76,27 +88,35 @@ export class NativeFileWrapper implements FileObject { async getChildByName(name: string): Promise { try { const childPath = await this.execPlugin('getChildByName', [name]); + //console.log(`[getChildByName] path=${this.path}, name=${name}, childPath=${childPath}`); if (childPath && childPath !== "") { return new NativeFileWrapper(childPath); } return null; } catch (error) { + console.error(`[getChildByName] path=${this.path}, name=${name}, error=${error}`); return null; } } async getLength(): Promise { try { - return await this.execPlugin('getLength'); + const result = await this.execPlugin('getLength'); + //console.log(`[getLength] path=${this.path}, length=${result}`); + return result; } catch (error) { + console.error(`[getLength] path=${this.path}, error=${error}`); return 0; } } async getName(): Promise { try { - return await this.execPlugin('getName'); + const name = await this.execPlugin('getName'); + //console.log(`[getName] path=${this.path}, name=${name}`); + return name; } catch (error) { + console.error(`[getName] path=${this.path}, error=${error}`); throw new Error(`Failed to read file name: ${error}`); } } @@ -104,11 +124,13 @@ export class NativeFileWrapper implements FileObject { async getParentFile(): Promise { try { const parentPath = await this.execPlugin('getParentFile'); + //console.log(`[getParentFile] path=${this.path}, parentPath=${parentPath}`); if (parentPath && parentPath !== "") { return new NativeFileWrapper(parentPath); } return null; } catch (error) { + console.error(`[getParentFile] path=${this.path}, error=${error}`); return null; } } @@ -116,8 +138,10 @@ export class NativeFileWrapper implements FileObject { async isDirectory(): Promise { try { const result = await this.execPlugin('isDirectory'); + //console.log(`[isDirectory] path=${this.path}, result=${result}`); return result === 1; } catch (error) { + console.error(`[isDirectory] path=${this.path}, error=${error}`); return false; } } @@ -125,8 +149,10 @@ export class NativeFileWrapper implements FileObject { async isFile(): Promise { try { const result = await this.execPlugin('isFile'); + //console.log(`[isFile] path=${this.path}, result=${result}`); return result === 1; } catch (error) { + console.error(`[isFile] path=${this.path}, error=${error}`); return false; } } @@ -134,8 +160,10 @@ export class NativeFileWrapper implements FileObject { async isLink(): Promise { try { const result = await this.execPlugin('isLink'); + //console.log(`[isLink] path=${this.path}, result=${result}`); return result === 1; } catch (error) { + console.error(`[isLink] path=${this.path}, error=${error}`); return false; } } @@ -143,26 +171,32 @@ export class NativeFileWrapper implements FileObject { async isNative(): Promise { try { const result = await this.execPlugin('isNative'); + //console.log(`[isNative] path=${this.path}, result=${result}`); return result === 1; } catch (error) { - return true; // Default to true for native implementation + console.error(`[isNative] path=${this.path}, error=${error}`); + return true; } } async isUnixLike(): Promise { try { const result = await this.execPlugin('isUnixLike'); + //console.log(`[isUnixLike] path=${this.path}, result=${result}`); return result === 1; } catch (error) { - return true; // Default to true for Android + console.error(`[isUnixLike] path=${this.path}, error=${error}`); + return true; } } async listFiles(): Promise { try { const paths: string[] = await this.execPlugin('listFiles'); + //console.log(`[listFiles] path=${this.path}, files=${JSON.stringify(paths)}`); return paths.map(path => new NativeFileWrapper(path)); } catch (error) { + console.error(`[listFiles] path=${this.path}, error=${error}`); return []; } } @@ -170,8 +204,10 @@ export class NativeFileWrapper implements FileObject { async mkdir(): Promise { try { const result = await this.execPlugin('mkdir'); + //console.log(`[mkdir] path=${this.path}, result=${result}`); return result === 1; } catch (error) { + console.error(`[mkdir] path=${this.path}, error=${error}`); return false; } } @@ -179,24 +215,32 @@ export class NativeFileWrapper implements FileObject { async mkdirs(): Promise { try { const result = await this.execPlugin('mkdirs'); + //console.log(`[mkdirs] path=${this.path}, result=${result}`); return result === 1; } catch (error) { + console.error(`[mkdirs] path=${this.path}, error=${error}`); return false; } } async readText(encoding: string = "UTF-8"): Promise { try { - return await this.execPlugin('readText', [encoding]); + const content = await this.execPlugin('readText', [encoding]); + //console.log(`[readText] path=${this.path}, content length=${content?.length}`); + return content; } catch (error) { + console.error(`[readText] path=${this.path}, error=${error}`); throw new Error(`Failed to read file: ${error}`); } } async toUri(): Promise { try { - return await this.execPlugin('toUri'); + const uri = await this.execPlugin('toUri'); + //console.log(`[toUri] path=${this.path}, uri=${uri}`); + return uri; } catch (error) { + console.error(`[toUri] path=${this.path}, error=${error}`); return `file://${this.path}`; } } @@ -204,13 +248,15 @@ export class NativeFileWrapper implements FileObject { async writeText(text: string, encoding: string = "UTF-8"): Promise { try { await this.execPlugin('writeText', [text, encoding]); + //console.log(`[writeText] path=${this.path}, text length=${text.length}`); } catch (error) { + console.error(`[writeText] path=${this.path}, error=${error}`); throw new Error(`Failed to write file: ${error}`); } } - // Utility method to get the absolute path getPath(): string { + //console.log(`[getPath] returning path=${this.path}`); return this.path; } -} \ No newline at end of file +} diff --git a/src/lib/acode.js b/src/lib/acode.js index ea024037e..0cca3591d 100644 --- a/src/lib/acode.js +++ b/src/lib/acode.js @@ -44,6 +44,7 @@ import helpers from "utils/helpers"; import KeyboardEvent from "utils/keyboardEvent"; import Url from "utils/Url"; import constants from "./constants"; +import {NativeFileWrapper} from "../fileSystem/NativeFileWrapper"; export default class Acode { #modules = {}; @@ -119,6 +120,7 @@ export default class Acode { }, }; + this.define("nativeFile",NativeFileWrapper); this.define("Url", Url); this.define("page", Page); this.define("Color", Color); diff --git a/src/plugins/nativeFile/package.json b/src/plugins/file/package.json similarity index 77% rename from src/plugins/nativeFile/package.json rename to src/plugins/file/package.json index a2d9b38bc..1692f62b4 100644 --- a/src/plugins/nativeFile/package.json +++ b/src/plugins/file/package.json @@ -1,9 +1,9 @@ { - "name": "native_file", + "name": "file", "version": "1.0.0", "description": "Full api of java.io.File", "cordova": { - "id": "com.foxdebug.acode.rk.nativeFile", + "id": "com.foxdebug.acode.rk.file", "platforms": [ "android" ] diff --git a/src/plugins/nativeFile/plugin.xml b/src/plugins/file/plugin.xml similarity index 56% rename from src/plugins/nativeFile/plugin.xml rename to src/plugins/file/plugin.xml index be82687de..932c33acc 100644 --- a/src/plugins/nativeFile/plugin.xml +++ b/src/plugins/file/plugin.xml @@ -1,15 +1,19 @@ - - nativeFile + + file - + + + + - + + \ No newline at end of file diff --git a/src/plugins/file/src/android/documentFile.java b/src/plugins/file/src/android/documentFile.java new file mode 100644 index 000000000..8dc494f44 --- /dev/null +++ b/src/plugins/file/src/android/documentFile.java @@ -0,0 +1,212 @@ +package com.foxdebug.acode.rk.file; + +import org.apache.cordova.*; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.net.Uri; +import android.content.Context; +import android.util.Log; + +import androidx.documentfile.provider.DocumentFile; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +public class documentFile extends CordovaPlugin { + + private Context getContext() { + return cordova.getContext(); + } + + @Override + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + try { + switch (action) { + case "exists": return handleExists(args, callbackContext); + case "isFile": return handleIsFile(args, callbackContext); + case "isDirectory": return handleIsDirectory(args, callbackContext); + case "getLength": return handleGetLength(args, callbackContext); + case "getName": return handleGetName(args, callbackContext); + case "getParentFile": return handleGetParentFile(args, callbackContext); + case "listFiles": return handleListFiles(args, callbackContext); + case "createNewFile": return handleCreateNewFile(args, callbackContext); + case "mkdir": return handleMkdir(args, callbackContext); + case "delete": return handleDelete(args, callbackContext); + case "readText": return handleReadText(args, callbackContext); + case "writeText": return handleWriteText(args, callbackContext); + case "canRead": return handleCanRead(args, callbackContext); + case "canWrite": return handleCanWrite(args, callbackContext); + case "childByNameExists": return handleChildByNameExists(args, callbackContext); + case "getChildByName": return handleGetChildByName(args, callbackContext); + case "toUri": return handleToUri(args, callbackContext); + default: return false; + } + } catch (Exception e) { + callbackContext.error("Error: " + e.getMessage()); + return true; + } + } + + private DocumentFile fromUri(String uriStr) { + Uri uri = Uri.parse(uriStr); + return DocumentFile.fromTreeUri(getContext(), uri); + } + + private boolean handleExists(JSONArray args, CallbackContext cb) throws JSONException { + DocumentFile f = fromUri(args.getString(0)); + cb.success(f != null && f.exists() ? 1 : 0); + return true; + } + + private boolean handleIsFile(JSONArray args, CallbackContext cb) throws JSONException { + DocumentFile f = fromUri(args.getString(0)); + cb.success(f != null && f.isFile() ? 1 : 0); + return true; + } + + private boolean handleIsDirectory(JSONArray args, CallbackContext cb) throws JSONException { + DocumentFile f = fromUri(args.getString(0)); + cb.success(f != null && f.isDirectory() ? 1 : 0); + return true; + } + + private boolean handleGetLength(JSONArray args, CallbackContext cb) throws JSONException { + DocumentFile f = fromUri(args.getString(0)); + cb.success(f != null ? (int) f.length() : 0); + return true; + } + + private boolean handleGetName(JSONArray args, CallbackContext cb) throws JSONException { + DocumentFile f = fromUri(args.getString(0)); + cb.success(f != null ? f.getName() : ""); + return true; + } + + private boolean handleGetParentFile(JSONArray args, CallbackContext cb) throws JSONException { + DocumentFile f = fromUri(args.getString(0)); + DocumentFile parent = f != null ? f.getParentFile() : null; + cb.success(parent != null ? parent.getUri().toString() : ""); + return true; + } + + private boolean handleListFiles(JSONArray args, CallbackContext cb) throws JSONException { + DocumentFile f = fromUri(args.getString(0)); + if (f == null || !f.isDirectory()) { + cb.error("Not a directory or invalid URI"); + return true; + } + + JSONArray result = new JSONArray(); + for (DocumentFile file : f.listFiles()) { + result.put(file.getUri().toString()); + } + cb.success(result); + return true; + } + + private boolean handleCreateNewFile(JSONArray args, CallbackContext cb) throws JSONException { + DocumentFile parent = fromUri(args.getString(0)); + String mime = args.optString(1, "text/plain"); + String name = args.optString(2, "newfile.txt"); + + if (parent != null && parent.isDirectory()) { + DocumentFile newFile = parent.createFile(mime, name); + cb.success(newFile != null ? newFile.getUri().toString() : ""); + } else { + cb.error("Invalid parent directory"); + } + return true; + } + + private boolean handleMkdir(JSONArray args, CallbackContext cb) throws JSONException { + DocumentFile parent = fromUri(args.getString(0)); + String name = args.optString(1, "NewFolder"); + + if (parent != null && parent.isDirectory()) { + DocumentFile newDir = parent.createDirectory(name); + cb.success(newDir != null ? 1 : 0); + } else { + cb.error("Invalid parent directory"); + } + return true; + } + + private boolean handleDelete(JSONArray args, CallbackContext cb) throws JSONException { + DocumentFile f = fromUri(args.getString(0)); + cb.success(f != null && f.delete() ? 1 : 0); + return true; + } + + private boolean handleReadText(JSONArray args, CallbackContext cb) throws JSONException { + DocumentFile f = fromUri(args.getString(0)); + String encoding = args.length() > 1 ? args.getString(1) : "UTF-8"; + + try (InputStream in = getContext().getContentResolver().openInputStream(f.getUri())) { + byte[] data = in.readAllBytes(); + cb.success(new String(data, encoding)); + } catch (Exception e) { + cb.error(e.getMessage()); + } + return true; + } + + private boolean handleWriteText(JSONArray args, CallbackContext cb) throws JSONException { + DocumentFile f = fromUri(args.getString(0)); + String text = args.getString(1); + String encoding = args.length() > 2 ? args.getString(2) : "UTF-8"; + + try (OutputStream out = getContext().getContentResolver().openOutputStream(f.getUri(), "wt")) { + out.write(text.getBytes(encoding)); + out.flush(); + cb.success(1); + } catch (Exception e) { + cb.error(e.getMessage()); + } + return true; + } + + private boolean handleCanRead(JSONArray args, CallbackContext cb) throws JSONException { + DocumentFile f = fromUri(args.getString(0)); + cb.success(f != null && f.canRead() ? 1 : 0); + return true; + } + + private boolean handleCanWrite(JSONArray args, CallbackContext cb) throws JSONException { + DocumentFile f = fromUri(args.getString(0)); + cb.success(f != null && f.canWrite() ? 1 : 0); + return true; + } + + private boolean handleChildByNameExists(JSONArray args, CallbackContext cb) throws JSONException { + DocumentFile parent = fromUri(args.getString(0)); + String childName = args.getString(1); + if (parent == null || !parent.isDirectory()) { + cb.success(0); + return true; + } + DocumentFile child = parent.findFile(childName); + cb.success(child != null && child.exists() ? 1 : 0); + return true; + } + + private boolean handleGetChildByName(JSONArray args, CallbackContext cb) throws JSONException { + DocumentFile parent = fromUri(args.getString(0)); + String childName = args.getString(1); + if (parent == null || !parent.isDirectory()) { + cb.success(""); + return true; + } + DocumentFile child = parent.findFile(childName); + cb.success(child != null ? child.getUri().toString() : ""); + return true; + } + + private boolean handleToUri(JSONArray args, CallbackContext cb) throws JSONException { + DocumentFile f = fromUri(args.getString(0)); + cb.success(f != null ? f.getUri().toString() : ""); + return true; + } +} diff --git a/src/plugins/nativeFile/src/android/nativeFile.java b/src/plugins/file/src/android/nativeFile.java similarity index 99% rename from src/plugins/nativeFile/src/android/nativeFile.java rename to src/plugins/file/src/android/nativeFile.java index 7f628bb9a..bbe8340c7 100644 --- a/src/plugins/nativeFile/src/android/nativeFile.java +++ b/src/plugins/file/src/android/nativeFile.java @@ -1,4 +1,4 @@ -package com.foxdebug.acode.rk.nativeFile; +package com.foxdebug.acode.rk.file; import org.apache.cordova.*; import org.json.JSONArray; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..b84c64980 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "ESNext", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "allowJs": true, + "checkJs": false, + "outDir": "./dist", + "sourceMap": true, + "resolveJsonModule": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "www", "dist"] +} diff --git a/webpack.config.js b/webpack.config.js index 29c42385d..f449a2e39 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -36,6 +36,13 @@ module.exports = (env, options) => { }, ]; + rules.push({ + test: /\.ts$/, + use: 'ts-loader', + exclude: /node_modules/, + } + ) + // if (mode === 'production') { rules.push({ test: /\.m?js$/, @@ -68,13 +75,14 @@ module.exports = (env, options) => { module: { rules, }, - resolve: { - fallback: { - path: require.resolve('path-browserify'), - crypto: false, + resolve: { + extensions: ['.ts', '.js'], + fallback: { + path: require.resolve('path-browserify'), + crypto: false, + }, + modules: ["node_modules", "src"], }, - modules: ["node_modules", "src"], - }, plugins: [ new MiniCssExtractPlugin({ filename: '[name].css', From b2724507788dfbbac20835269264c38aefebc323 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Thu, 30 Oct 2025 17:22:03 +0530 Subject: [PATCH 3/9] feat: FileServer --- src/fileServer/fileServer.ts | 43 +++++ src/fileSystem/DocumentFileWrapper.ts | 248 -------------------------- src/fileSystem/NativeFileWrapper.ts | 53 ++++-- src/fileSystem/SAFDocumentFile.ts | 242 +++++++++++++++++++++++++ src/lib/Log.ts | 28 +++ 5 files changed, 355 insertions(+), 259 deletions(-) create mode 100644 src/fileServer/fileServer.ts delete mode 100644 src/fileSystem/DocumentFileWrapper.ts create mode 100644 src/fileSystem/SAFDocumentFile.ts create mode 100644 src/lib/Log.ts diff --git a/src/fileServer/fileServer.ts b/src/fileServer/fileServer.ts new file mode 100644 index 000000000..20ddabd0c --- /dev/null +++ b/src/fileServer/fileServer.ts @@ -0,0 +1,43 @@ +import {FileObject} from "../fileSystem/fileObject"; +import {Log} from "../lib/Log"; + + +class FileServer { + private readonly file: FileObject; + private readonly port: number; + private httpServer:Server | undefined; + private readonly log:Log = new Log("fileServer"); + + constructor(port:number,file:FileObject) { + this.file = file; + this.port = port; + } + + start(onSuccess: (msg: any) => void, onError: (err: any) => void,):void{ + this.httpServer = CreateServer(this.port,onSuccess,onError) + + // @ts-ignore + httpServer.setOnRequestHandler(this.handleRequest.bind(this)); + } + + private handleRequest(req: { requestId: string; path: string }): void { + this.log.d("Request received:", req); + // handle file serving logic here + this.log.d("Received request:", req.requestId); + this.log.d("Request Path", req.path); + this.sendText("This is a test",req.requestId,null) + this.log.d("Response sent") + } + + private sendText(text:string, id:string, mimeType:string | null | undefined) { + this.httpServer?.send(id, { + status: 200, + body: text, + headers: { + "Content-Type": mimeType || "text/html", + }, + },()=>{},this.log.e); + } + + +} \ No newline at end of file diff --git a/src/fileSystem/DocumentFileWrapper.ts b/src/fileSystem/DocumentFileWrapper.ts deleted file mode 100644 index c359216e2..000000000 --- a/src/fileSystem/DocumentFileWrapper.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { FileObject } from "./fileObject"; - -declare var cordova: any; - -export class DocumentFileWrapper implements FileObject { - private readonly uri: string; - - constructor(uri: string) { - this.uri = uri; - //console.log(`[DocumentFileWrapper] Created for uri: ${uri}`); - } - - private execPlugin(action: string, args: any[] = []): Promise { - //console.log(`[DocumentFileWrapper] execPlugin called: action=${action}, args=${JSON.stringify(args)}`); - return new Promise((resolve, reject) => { - cordova.exec( - (result: any) => { - //console.log(`[DocumentFileWrapper] execPlugin success: action=${action}, result=${JSON.stringify(result)}`); - resolve(result); - }, - (error: any) => { - console.error(`[DocumentFileWrapper] execPlugin error: action=${action}, error=${JSON.stringify(error)}`); - reject(error); - }, - 'nativeFile', - action, - [this.uri, ...args] - ); - }); - } - - async canRead(): Promise { - const result = await this.execPlugin('canRead'); - //console.log(`[canRead] uri=${this.uri}, result=${result}`); - return result === 1; - } - - async canWrite(): Promise { - const result = await this.execPlugin('canWrite'); - //console.log(`[canWrite] uri=${this.uri}, result=${result}`); - return result === 1; - } - - async childByNameExists(name: string): Promise { - try { - const result = await this.execPlugin('childByNameExists', [name]); - //console.log(`[childByNameExists] uri=${this.uri}, name=${name}, result=${result}`); - return result === 1; - } catch (error) { - console.error(`[childByNameExists] uri=${this.uri}, name=${name}, error=${error}`); - return null; - } - } - - async createNewFile(): Promise { - try { - const result = await this.execPlugin('createNewFile'); - //console.log(`[createNewFile] uri=${this.uri}, result=${result}`); - return result === 1; - } catch (error) { - console.error(`[createNewFile] uri=${this.uri}, error=${error}`); - return false; - } - } - - async delete(): Promise { - try { - const result = await this.execPlugin('delete'); - //console.log(`[delete] uri=${this.uri}, result=${result}`); - return result === 1; - } catch (error) { - console.error(`[delete] uri=${this.uri}, error=${error}`); - return false; - } - } - - async exists(): Promise { - try { - const result = await this.execPlugin('exists'); - //console.log(`[exists] uri=${this.uri}, result=${result}`); - return result === 1; - } catch (error) { - console.error(`[exists] uri=${this.uri}, error=${error}`); - return false; - } - } - - async getChildByName(name: string): Promise { - try { - const childPath = await this.execPlugin('getChildByName', [name]); - //console.log(`[getChildByName] uri=${this.uri}, name=${name}, childPath=${childPath}`); - if (childPath && childPath !== "") { - return new DocumentFileWrapper(childPath); - } - return null; - } catch (error) { - console.error(`[getChildByName] uri=${this.uri}, name=${name}, error=${error}`); - return null; - } - } - - async getLength(): Promise { - try { - const result = await this.execPlugin('getLength'); - //console.log(`[getLength] uri=${this.uri}, length=${result}`); - return result; - } catch (error) { - console.error(`[getLength] uri=${this.uri}, error=${error}`); - return 0; - } - } - - async getName(): Promise { - try { - const name = await this.execPlugin('getName'); - //console.log(`[getName] uri=${this.uri}, name=${name}`); - return name; - } catch (error) { - console.error(`[getName] uri=${this.uri}, error=${error}`); - throw new Error(`Failed to read file name: ${error}`); - } - } - - async getParentFile(): Promise { - try { - const parentPath = await this.execPlugin('getParentFile'); - //console.log(`[getParentFile] uri=${this.uri}, parentPath=${parentPath}`); - if (parentPath && parentPath !== "") { - return new DocumentFileWrapper(parentPath); - } - return null; - } catch (error) { - console.error(`[getParentFile] uri=${this.uri}, error=${error}`); - return null; - } - } - - async isDirectory(): Promise { - try { - const result = await this.execPlugin('isDirectory'); - //console.log(`[isDirectory] uri=${this.uri}, result=${result}`); - return result === 1; - } catch (error) { - console.error(`[isDirectory] uri=${this.uri}, error=${error}`); - return false; - } - } - - async isFile(): Promise { - try { - const result = await this.execPlugin('isFile'); - //console.log(`[isFile] uri=${this.uri}, result=${result}`); - return result === 1; - } catch (error) { - console.error(`[isFile] uri=${this.uri}, error=${error}`); - return false; - } - } - - async isLink(): Promise { - try { - const result = await this.execPlugin('isLink'); - //console.log(`[isLink] uri=${this.uri}, result=${result}`); - return result === 1; - } catch (error) { - console.error(`[isLink] uri=${this.uri}, error=${error}`); - return false; - } - } - - async isNative(): Promise { - return false; - } - - async isUnixLike(): Promise { - return false; - } - - async listFiles(): Promise { - try { - const uris: string[] = await this.execPlugin('listFiles'); - //console.log(`[listFiles] uri=${this.uri}, files=${JSON.stringify(uris)}`); - return uris.map(uri => new DocumentFileWrapper(uri)); - } catch (error) { - console.error(`[listFiles] uri=${this.uri}, error=${error}`); - return []; - } - } - - async mkdir(): Promise { - try { - const result = await this.execPlugin('mkdir'); - //console.log(`[mkdir] uri=${this.uri}, result=${result}`); - return result === 1; - } catch (error) { - console.error(`[mkdir] uri=${this.uri}, error=${error}`); - return false; - } - } - - async mkdirs(): Promise { - try { - const result = await this.execPlugin('mkdirs'); - //console.log(`[mkdirs] uri=${this.uri}, result=${result}`); - return result === 1; - } catch (error) { - console.error(`[mkdirs] uri=${this.uri}, error=${error}`); - return false; - } - } - - async readText(encoding: string = "UTF-8"): Promise { - try { - const content = await this.execPlugin('readText', [encoding]); - //console.log(`[readText] uri=${this.uri}, content length=${content?.length}`); - return content; - } catch (error) { - console.error(`[readText] uri=${this.uri}, error=${error}`); - throw new Error(`Failed to read file: ${error}`); - } - } - - async toUri(): Promise { - try { - const uri = await this.execPlugin('toUri'); - //console.log(`[toUri] uri=${this.uri}, uri=${uri}`); - return uri; - } catch (error) { - console.error(`[toUri] uri=${this.uri}, error=${error}`); - return `file://${this.uri}`; - } - } - - async writeText(text: string, encoding: string = "UTF-8"): Promise { - try { - await this.execPlugin('writeText', [text, encoding]); - //console.log(`[writeText] uri=${this.uri}, text length=${text.length}`); - } catch (error) { - console.error(`[writeText] uri=${this.uri}, error=${error}`); - throw new Error(`Failed to write file: ${error}`); - } - } - - getPath(): string { - //console.log(`[getPath] returning uri=${this.uri}`); - return this.uri; - } -} diff --git a/src/fileSystem/NativeFileWrapper.ts b/src/fileSystem/NativeFileWrapper.ts index 2e3218a76..63be7e1f2 100644 --- a/src/fileSystem/NativeFileWrapper.ts +++ b/src/fileSystem/NativeFileWrapper.ts @@ -1,15 +1,47 @@ -import { FileObject } from "./fileObject"; +import {FileObject} from "./fileObject"; declare var cordova: any; + +//alternative for internalFs.js export class NativeFileWrapper implements FileObject { - private readonly path: string; + private path: string | undefined; + + //always check if fileobject is ready before calling any class function + ready: Promise; + + - constructor(absolutePath: string) { - this.path = absolutePath; - //console.log(`[NativeFileWrapper] Created for path: ${absolutePath}`); + private removePrefix(str: string, prefix: string): string { + return str.startsWith(prefix) ? str.slice(prefix.length) : str; } + + constructor(absolutePathOrUri: string,onReady:(obj:NativeFileWrapper)=>void) { + this.ready = (async () => { + let temp = absolutePathOrUri; + + if (absolutePathOrUri.startsWith("cdvfile://")) { + temp = await new Promise((resolve, reject) => { + // @ts-ignore + window.resolveLocalFileSystemURL( + absolutePathOrUri, + (entry: any) => resolve(entry.toURL()), + reject + ); + }); + } + + this.path = this.removePrefix(temp, "file://"); + + onReady(this) + })(); + } + + + + + private execPlugin(action: string, args: any[] = []): Promise { //console.log(`[NativeFileWrapper] execPlugin called: action=${action}, args=${JSON.stringify(args)}`); return new Promise((resolve, reject) => { @@ -90,7 +122,7 @@ export class NativeFileWrapper implements FileObject { const childPath = await this.execPlugin('getChildByName', [name]); //console.log(`[getChildByName] path=${this.path}, name=${name}, childPath=${childPath}`); if (childPath && childPath !== "") { - return new NativeFileWrapper(childPath); + return new NativeFileWrapper(childPath,()=>{}); } return null; } catch (error) { @@ -126,7 +158,7 @@ export class NativeFileWrapper implements FileObject { const parentPath = await this.execPlugin('getParentFile'); //console.log(`[getParentFile] path=${this.path}, parentPath=${parentPath}`); if (parentPath && parentPath !== "") { - return new NativeFileWrapper(parentPath); + return new NativeFileWrapper(parentPath,()=>{}); } return null; } catch (error) { @@ -194,7 +226,7 @@ export class NativeFileWrapper implements FileObject { try { const paths: string[] = await this.execPlugin('listFiles'); //console.log(`[listFiles] path=${this.path}, files=${JSON.stringify(paths)}`); - return paths.map(path => new NativeFileWrapper(path)); + return paths.map(path => new NativeFileWrapper(path,()=>{})); } catch (error) { console.error(`[listFiles] path=${this.path}, error=${error}`); return []; @@ -236,9 +268,8 @@ export class NativeFileWrapper implements FileObject { async toUri(): Promise { try { - const uri = await this.execPlugin('toUri'); //console.log(`[toUri] path=${this.path}, uri=${uri}`); - return uri; + return await this.execPlugin('toUri'); } catch (error) { console.error(`[toUri] path=${this.path}, error=${error}`); return `file://${this.path}`; @@ -257,6 +288,6 @@ export class NativeFileWrapper implements FileObject { getPath(): string { //console.log(`[getPath] returning path=${this.path}`); - return this.path; + return this.path!!; } } diff --git a/src/fileSystem/SAFDocumentFile.ts b/src/fileSystem/SAFDocumentFile.ts new file mode 100644 index 000000000..afe0200fc --- /dev/null +++ b/src/fileSystem/SAFDocumentFile.ts @@ -0,0 +1,242 @@ +// @ts-ignore +import loader from "dialogs/loader"; +// @ts-ignore +import { decode, encode } from "utils/encodings"; +// @ts-ignore +import helpers from "utils/helpers"; +// @ts-ignore +import Url from "utils/Url"; + + +import { FileObject } from "./fileObject"; + +declare const sdcard: any; + +//alternative for externalFs.js +export class SAFDocumentFile implements FileObject { + constructor(private uri: string) {} + + async canRead(): Promise { + const stat = await this.stat(); + return !!stat.canRead; + } + + async canWrite(): Promise { + const stat = await this.stat(); + return !!stat.canWrite; + } + + async childByNameExists(name: string): Promise { + const children = await this.listFiles(); + return children.some(c => c.getName().then(n => n === name)); + } + + async createNewFile(): Promise { + try { + await this.writeText(""); + return true; + } catch { + return false; + } + } + + async delete(): Promise { + return new Promise((resolve, reject) => { + sdcard.delete(this.uri, () => resolve(true), reject); + }); + } + + async exists(): Promise { + try { + await this.stat(); + return true; + } catch { + return false; + } + } + + async getChildByName(name: string): Promise { + const children = await this.listFiles(); + for (const child of children) { + if (await child.getName() === name) return child; + } + return null; + } + + async getLength(): Promise { + const stat = await this.stat(); + return stat.size ?? 0; + } + + async getName(): Promise { + const parts = this.uri.split("/"); + return parts[parts.length - 1] || ""; + } + + async getParentFile(): Promise { + const parent = Url.dirname(this.uri); + if (!parent || parent === this.uri) return null; + return new SAFDocumentFile(parent); + } + + async isDirectory(): Promise { + const stat = await this.stat(); + return stat.isDirectory === true; + } + + async isFile(): Promise { + const stat = await this.stat(); + return stat.isFile === true; + } + + async isLink(): Promise { + return false; + } + + async isNative(): Promise { + return true; + } + + async isUnixLike(): Promise { + return false; + } + + async listFiles(): Promise { + const uri = await this.formatUri(this.uri); + return new Promise((resolve, reject) => { + sdcard.listDir( + uri, + (entries: any[]) => { + const files = entries.map((e) => new SAFDocumentFile(e.url || e.uri)); + resolve(files); + }, + reject, + ); + }); + } + + async mkdir(): Promise { + const parent = await this.getParentFile(); + if (!parent) return false; + return new Promise((resolve, reject) => { + // @ts-ignore + sdcard.createDir(parent.uri, this.getName(), () => resolve(true), reject); + }); + } + + async mkdirs(): Promise { + // Simplified version that only creates one level + return this.mkdir(); + } + + async readText(encoding = "utf8"): Promise { + const uri = await this.formatUri(this.uri); + return new Promise((resolve, reject) => { + sdcard.read( + uri, + async (data: ArrayBuffer) => { + const text = await decode(data, encoding); + resolve(text); + }, + reject, + ); + }); + } + + async writeText(text: string, encoding = "utf8"): Promise { + const encoded = await encode(text, encoding); + return new Promise((resolve, reject) => { + sdcard.write(this.uri, encoded, resolve, reject); + }); + } + + async toUri(): Promise { + return this.uri; + } + + // ---- Extra helpers translated from externalFs ---- + + private async formatUri(uri: string): Promise { + return new Promise((resolve, reject) => { + sdcard.formatUri(uri, resolve, reject); + }); + } + + private async stat(): Promise { + const storageList = helpers.parseJSON(localStorage.getItem("storageList")); + + if (Array.isArray(storageList)) { + const storage = storageList.find((s) => s.uri === this.uri); + if (storage) { + const stat = { + size: 0, + name: storage.name, + type: "dir", + canRead: true, + canWrite: true, + modifiedDate: new Date(), + isDirectory: true, + isFile: false, + url: this.uri, + }; + + helpers.defineDeprecatedProperty( + stat, + "uri", + function () { + // @ts-ignore + return this.url; + }, + function (value:any) { + // @ts-ignore + this.url = value; + }, + ); + + return stat; + } + } + + const uri = await this.formatUri(this.uri); + return new Promise((resolve, reject) => { + sdcard.stats( + uri, + (stats: any) => { + helpers.defineDeprecatedProperty( + stats, + "uri", + function () { + // @ts-ignore + return this.url; + }, + function (val:any) { + // @ts-ignore + this.url = val; + }, + ); + resolve(stats); + }, + reject, + ); + }); + } + + // ---- Optional static helpers for mount management ---- + + static async listStorages(): Promise { + return new Promise((resolve, reject) => { + sdcard.listStorages(resolve, reject); + }); + } + + static async getStorageAccessPermission(uuid: string, name: string): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => loader.destroy(), 100); + sdcard.getStorageAccessPermission(uuid, resolve, reject); + }); + } + + static test(url: string): boolean { + return /^content:/.test(url); + } +} diff --git a/src/lib/Log.ts b/src/lib/Log.ts new file mode 100644 index 000000000..12108c8e3 --- /dev/null +++ b/src/lib/Log.ts @@ -0,0 +1,28 @@ +/** + * Android.util.Log + */ +export class Log { + constructor(private tag: string) {} + + private format(level: string, message: any): string { + const time = new Date().toISOString(); + return `[${time}] [${this.tag}] [${level}] ${message}`; + } + + i(message: any, ...args: any[]): void { + console.log(`%c${this.format("INFO", message)}`, "color: #00aaff", ...args); + } + + w(message: any, ...args: any[]): void { + console.warn(`%c${this.format("WARN", message)}`, "color: #ffaa00", ...args); + } + + e(message: any, ...args: any[]): void { + console.error(`%c${this.format("ERROR", message)}`, "color: #ff4444", ...args); + } + + d(message: any, ...args: any[]): void { + //TODO: Only show debug messages in debug mode + console.debug(`%c${this.format("DEBUG", message)}`, "color: #999999", ...args); + } +} From 0365bef7ee04dc4e9a163578a670aaa70c271a31 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Thu, 30 Oct 2025 17:35:54 +0530 Subject: [PATCH 4/9] format --- src/fileServer/fileServer.ts | 88 +++-- src/fileSystem/NativeFileWrapper.ts | 575 ++++++++++++++-------------- src/fileSystem/SAFDocumentFile.ts | 454 +++++++++++----------- src/fileSystem/fileObject.ts | 268 ++++++------- src/lib/Log.ts | 48 ++- src/lib/acode.js | 8 +- 6 files changed, 733 insertions(+), 708 deletions(-) diff --git a/src/fileServer/fileServer.ts b/src/fileServer/fileServer.ts index 20ddabd0c..e0f2798b8 100644 --- a/src/fileServer/fileServer.ts +++ b/src/fileServer/fileServer.ts @@ -1,43 +1,49 @@ -import {FileObject} from "../fileSystem/fileObject"; -import {Log} from "../lib/Log"; - +import { FileObject } from "../fileSystem/fileObject"; +import { Log } from "../lib/Log"; class FileServer { - private readonly file: FileObject; - private readonly port: number; - private httpServer:Server | undefined; - private readonly log:Log = new Log("fileServer"); - - constructor(port:number,file:FileObject) { - this.file = file; - this.port = port; - } - - start(onSuccess: (msg: any) => void, onError: (err: any) => void,):void{ - this.httpServer = CreateServer(this.port,onSuccess,onError) - - // @ts-ignore - httpServer.setOnRequestHandler(this.handleRequest.bind(this)); - } - - private handleRequest(req: { requestId: string; path: string }): void { - this.log.d("Request received:", req); - // handle file serving logic here - this.log.d("Received request:", req.requestId); - this.log.d("Request Path", req.path); - this.sendText("This is a test",req.requestId,null) - this.log.d("Response sent") - } - - private sendText(text:string, id:string, mimeType:string | null | undefined) { - this.httpServer?.send(id, { - status: 200, - body: text, - headers: { - "Content-Type": mimeType || "text/html", - }, - },()=>{},this.log.e); - } - - -} \ No newline at end of file + private readonly file: FileObject; + private readonly port: number; + private httpServer: Server | undefined; + private readonly log: Log = new Log("fileServer"); + + constructor(port: number, file: FileObject) { + this.file = file; + this.port = port; + } + + start(onSuccess: (msg: any) => void, onError: (err: any) => void): void { + this.httpServer = CreateServer(this.port, onSuccess, onError); + + // @ts-ignore + httpServer.setOnRequestHandler(this.handleRequest.bind(this)); + } + + private handleRequest(req: { requestId: string; path: string }): void { + this.log.d("Request received:", req); + // handle file serving logic here + this.log.d("Received request:", req.requestId); + this.log.d("Request Path", req.path); + this.sendText("This is a test", req.requestId, null); + this.log.d("Response sent"); + } + + private sendText( + text: string, + id: string, + mimeType: string | null | undefined, + ) { + this.httpServer?.send( + id, + { + status: 200, + body: text, + headers: { + "Content-Type": mimeType || "text/html", + }, + }, + () => {}, + this.log.e, + ); + } +} diff --git a/src/fileSystem/NativeFileWrapper.ts b/src/fileSystem/NativeFileWrapper.ts index 63be7e1f2..db1cee2b3 100644 --- a/src/fileSystem/NativeFileWrapper.ts +++ b/src/fileSystem/NativeFileWrapper.ts @@ -1,293 +1,294 @@ -import {FileObject} from "./fileObject"; +import { FileObject } from "./fileObject"; declare var cordova: any; - //alternative for internalFs.js export class NativeFileWrapper implements FileObject { - private path: string | undefined; - - //always check if fileobject is ready before calling any class function - ready: Promise; - - - - private removePrefix(str: string, prefix: string): string { - return str.startsWith(prefix) ? str.slice(prefix.length) : str; - } - - - constructor(absolutePathOrUri: string,onReady:(obj:NativeFileWrapper)=>void) { - this.ready = (async () => { - let temp = absolutePathOrUri; - - if (absolutePathOrUri.startsWith("cdvfile://")) { - temp = await new Promise((resolve, reject) => { - // @ts-ignore - window.resolveLocalFileSystemURL( - absolutePathOrUri, - (entry: any) => resolve(entry.toURL()), - reject - ); - }); - } - - this.path = this.removePrefix(temp, "file://"); - - onReady(this) - })(); - } - - - - - - private execPlugin(action: string, args: any[] = []): Promise { - //console.log(`[NativeFileWrapper] execPlugin called: action=${action}, args=${JSON.stringify(args)}`); - return new Promise((resolve, reject) => { - cordova.exec( - (result: any) => { - //console.log(`[NativeFileWrapper] execPlugin success: action=${action}, result=${JSON.stringify(result)}`); - resolve(result); - }, - (error: any) => { - console.error(`[NativeFileWrapper] execPlugin error: action=${action}, error=${JSON.stringify(error)}`); - reject(error); - }, - 'nativeFile', - action, - [this.path, ...args] - ); - }); - } - - async canRead(): Promise { - const result = await this.execPlugin('canRead'); - //console.log(`[canRead] path=${this.path}, result=${result}`); - return result === 1; - } - - async canWrite(): Promise { - const result = await this.execPlugin('canWrite'); - //console.log(`[canWrite] path=${this.path}, result=${result}`); - return result === 1; - } - - async childByNameExists(name: string): Promise { - try { - const result = await this.execPlugin('childByNameExists', [name]); - //console.log(`[childByNameExists] path=${this.path}, name=${name}, result=${result}`); - return result === 1; - } catch (error) { - console.error(`[childByNameExists] path=${this.path}, name=${name}, error=${error}`); - return null; - } - } - - async createNewFile(): Promise { - try { - const result = await this.execPlugin('createNewFile'); - //console.log(`[createNewFile] path=${this.path}, result=${result}`); - return result === 1; - } catch (error) { - console.error(`[createNewFile] path=${this.path}, error=${error}`); - return false; - } - } - - async delete(): Promise { - try { - const result = await this.execPlugin('delete'); - //console.log(`[delete] path=${this.path}, result=${result}`); - return result === 1; - } catch (error) { - console.error(`[delete] path=${this.path}, error=${error}`); - return false; - } - } - - async exists(): Promise { - try { - const result = await this.execPlugin('exists'); - //console.log(`[exists] path=${this.path}, result=${result}`); - return result === 1; - } catch (error) { - console.error(`[exists] path=${this.path}, error=${error}`); - return false; - } - } - - async getChildByName(name: string): Promise { - try { - const childPath = await this.execPlugin('getChildByName', [name]); - //console.log(`[getChildByName] path=${this.path}, name=${name}, childPath=${childPath}`); - if (childPath && childPath !== "") { - return new NativeFileWrapper(childPath,()=>{}); - } - return null; - } catch (error) { - console.error(`[getChildByName] path=${this.path}, name=${name}, error=${error}`); - return null; - } - } - - async getLength(): Promise { - try { - const result = await this.execPlugin('getLength'); - //console.log(`[getLength] path=${this.path}, length=${result}`); - return result; - } catch (error) { - console.error(`[getLength] path=${this.path}, error=${error}`); - return 0; - } - } - - async getName(): Promise { - try { - const name = await this.execPlugin('getName'); - //console.log(`[getName] path=${this.path}, name=${name}`); - return name; - } catch (error) { - console.error(`[getName] path=${this.path}, error=${error}`); - throw new Error(`Failed to read file name: ${error}`); - } - } - - async getParentFile(): Promise { - try { - const parentPath = await this.execPlugin('getParentFile'); - //console.log(`[getParentFile] path=${this.path}, parentPath=${parentPath}`); - if (parentPath && parentPath !== "") { - return new NativeFileWrapper(parentPath,()=>{}); - } - return null; - } catch (error) { - console.error(`[getParentFile] path=${this.path}, error=${error}`); - return null; - } - } - - async isDirectory(): Promise { - try { - const result = await this.execPlugin('isDirectory'); - //console.log(`[isDirectory] path=${this.path}, result=${result}`); - return result === 1; - } catch (error) { - console.error(`[isDirectory] path=${this.path}, error=${error}`); - return false; - } - } - - async isFile(): Promise { - try { - const result = await this.execPlugin('isFile'); - //console.log(`[isFile] path=${this.path}, result=${result}`); - return result === 1; - } catch (error) { - console.error(`[isFile] path=${this.path}, error=${error}`); - return false; - } - } - - async isLink(): Promise { - try { - const result = await this.execPlugin('isLink'); - //console.log(`[isLink] path=${this.path}, result=${result}`); - return result === 1; - } catch (error) { - console.error(`[isLink] path=${this.path}, error=${error}`); - return false; - } - } - - async isNative(): Promise { - try { - const result = await this.execPlugin('isNative'); - //console.log(`[isNative] path=${this.path}, result=${result}`); - return result === 1; - } catch (error) { - console.error(`[isNative] path=${this.path}, error=${error}`); - return true; - } - } - - async isUnixLike(): Promise { - try { - const result = await this.execPlugin('isUnixLike'); - //console.log(`[isUnixLike] path=${this.path}, result=${result}`); - return result === 1; - } catch (error) { - console.error(`[isUnixLike] path=${this.path}, error=${error}`); - return true; - } - } - - async listFiles(): Promise { - try { - const paths: string[] = await this.execPlugin('listFiles'); - //console.log(`[listFiles] path=${this.path}, files=${JSON.stringify(paths)}`); - return paths.map(path => new NativeFileWrapper(path,()=>{})); - } catch (error) { - console.error(`[listFiles] path=${this.path}, error=${error}`); - return []; - } - } - - async mkdir(): Promise { - try { - const result = await this.execPlugin('mkdir'); - //console.log(`[mkdir] path=${this.path}, result=${result}`); - return result === 1; - } catch (error) { - console.error(`[mkdir] path=${this.path}, error=${error}`); - return false; - } - } - - async mkdirs(): Promise { - try { - const result = await this.execPlugin('mkdirs'); - //console.log(`[mkdirs] path=${this.path}, result=${result}`); - return result === 1; - } catch (error) { - console.error(`[mkdirs] path=${this.path}, error=${error}`); - return false; - } - } - - async readText(encoding: string = "UTF-8"): Promise { - try { - const content = await this.execPlugin('readText', [encoding]); - //console.log(`[readText] path=${this.path}, content length=${content?.length}`); - return content; - } catch (error) { - console.error(`[readText] path=${this.path}, error=${error}`); - throw new Error(`Failed to read file: ${error}`); - } - } - - async toUri(): Promise { - try { - //console.log(`[toUri] path=${this.path}, uri=${uri}`); - return await this.execPlugin('toUri'); - } catch (error) { - console.error(`[toUri] path=${this.path}, error=${error}`); - return `file://${this.path}`; - } - } - - async writeText(text: string, encoding: string = "UTF-8"): Promise { - try { - await this.execPlugin('writeText', [text, encoding]); - //console.log(`[writeText] path=${this.path}, text length=${text.length}`); - } catch (error) { - console.error(`[writeText] path=${this.path}, error=${error}`); - throw new Error(`Failed to write file: ${error}`); - } - } - - getPath(): string { - //console.log(`[getPath] returning path=${this.path}`); - return this.path!!; - } + private path: string | undefined; + + //always check if fileobject is ready before calling any class function + ready: Promise; + + private removePrefix(str: string, prefix: string): string { + return str.startsWith(prefix) ? str.slice(prefix.length) : str; + } + + constructor( + absolutePathOrUri: string, + onReady: (obj: NativeFileWrapper) => void, + ) { + this.ready = (async () => { + let temp = absolutePathOrUri; + + if (absolutePathOrUri.startsWith("cdvfile://")) { + temp = await new Promise((resolve, reject) => { + // @ts-ignore + window.resolveLocalFileSystemURL( + absolutePathOrUri, + (entry: any) => resolve(entry.toURL()), + reject, + ); + }); + } + + this.path = this.removePrefix(temp, "file://"); + + onReady(this); + })(); + } + + private execPlugin(action: string, args: any[] = []): Promise { + //console.log(`[NativeFileWrapper] execPlugin called: action=${action}, args=${JSON.stringify(args)}`); + return new Promise((resolve, reject) => { + cordova.exec( + (result: any) => { + //console.log(`[NativeFileWrapper] execPlugin success: action=${action}, result=${JSON.stringify(result)}`); + resolve(result); + }, + (error: any) => { + console.error( + `[NativeFileWrapper] execPlugin error: action=${action}, error=${JSON.stringify(error)}`, + ); + reject(error); + }, + "nativeFile", + action, + [this.path, ...args], + ); + }); + } + + async canRead(): Promise { + const result = await this.execPlugin("canRead"); + //console.log(`[canRead] path=${this.path}, result=${result}`); + return result === 1; + } + + async canWrite(): Promise { + const result = await this.execPlugin("canWrite"); + //console.log(`[canWrite] path=${this.path}, result=${result}`); + return result === 1; + } + + async childByNameExists(name: string): Promise { + try { + const result = await this.execPlugin("childByNameExists", [name]); + //console.log(`[childByNameExists] path=${this.path}, name=${name}, result=${result}`); + return result === 1; + } catch (error) { + console.error( + `[childByNameExists] path=${this.path}, name=${name}, error=${error}`, + ); + return null; + } + } + + async createNewFile(): Promise { + try { + const result = await this.execPlugin("createNewFile"); + //console.log(`[createNewFile] path=${this.path}, result=${result}`); + return result === 1; + } catch (error) { + console.error(`[createNewFile] path=${this.path}, error=${error}`); + return false; + } + } + + async delete(): Promise { + try { + const result = await this.execPlugin("delete"); + //console.log(`[delete] path=${this.path}, result=${result}`); + return result === 1; + } catch (error) { + console.error(`[delete] path=${this.path}, error=${error}`); + return false; + } + } + + async exists(): Promise { + try { + const result = await this.execPlugin("exists"); + //console.log(`[exists] path=${this.path}, result=${result}`); + return result === 1; + } catch (error) { + console.error(`[exists] path=${this.path}, error=${error}`); + return false; + } + } + + async getChildByName(name: string): Promise { + try { + const childPath = await this.execPlugin("getChildByName", [name]); + //console.log(`[getChildByName] path=${this.path}, name=${name}, childPath=${childPath}`); + if (childPath && childPath !== "") { + return new NativeFileWrapper(childPath, () => {}); + } + return null; + } catch (error) { + console.error( + `[getChildByName] path=${this.path}, name=${name}, error=${error}`, + ); + return null; + } + } + + async getLength(): Promise { + try { + const result = await this.execPlugin("getLength"); + //console.log(`[getLength] path=${this.path}, length=${result}`); + return result; + } catch (error) { + console.error(`[getLength] path=${this.path}, error=${error}`); + return 0; + } + } + + async getName(): Promise { + try { + const name = await this.execPlugin("getName"); + //console.log(`[getName] path=${this.path}, name=${name}`); + return name; + } catch (error) { + console.error(`[getName] path=${this.path}, error=${error}`); + throw new Error(`Failed to read file name: ${error}`); + } + } + + async getParentFile(): Promise { + try { + const parentPath = await this.execPlugin("getParentFile"); + //console.log(`[getParentFile] path=${this.path}, parentPath=${parentPath}`); + if (parentPath && parentPath !== "") { + return new NativeFileWrapper(parentPath, () => {}); + } + return null; + } catch (error) { + console.error(`[getParentFile] path=${this.path}, error=${error}`); + return null; + } + } + + async isDirectory(): Promise { + try { + const result = await this.execPlugin("isDirectory"); + //console.log(`[isDirectory] path=${this.path}, result=${result}`); + return result === 1; + } catch (error) { + console.error(`[isDirectory] path=${this.path}, error=${error}`); + return false; + } + } + + async isFile(): Promise { + try { + const result = await this.execPlugin("isFile"); + //console.log(`[isFile] path=${this.path}, result=${result}`); + return result === 1; + } catch (error) { + console.error(`[isFile] path=${this.path}, error=${error}`); + return false; + } + } + + async isLink(): Promise { + try { + const result = await this.execPlugin("isLink"); + //console.log(`[isLink] path=${this.path}, result=${result}`); + return result === 1; + } catch (error) { + console.error(`[isLink] path=${this.path}, error=${error}`); + return false; + } + } + + async isNative(): Promise { + try { + const result = await this.execPlugin("isNative"); + //console.log(`[isNative] path=${this.path}, result=${result}`); + return result === 1; + } catch (error) { + console.error(`[isNative] path=${this.path}, error=${error}`); + return true; + } + } + + async isUnixLike(): Promise { + try { + const result = await this.execPlugin("isUnixLike"); + //console.log(`[isUnixLike] path=${this.path}, result=${result}`); + return result === 1; + } catch (error) { + console.error(`[isUnixLike] path=${this.path}, error=${error}`); + return true; + } + } + + async listFiles(): Promise { + try { + const paths: string[] = await this.execPlugin("listFiles"); + //console.log(`[listFiles] path=${this.path}, files=${JSON.stringify(paths)}`); + return paths.map((path) => new NativeFileWrapper(path, () => {})); + } catch (error) { + console.error(`[listFiles] path=${this.path}, error=${error}`); + return []; + } + } + + async mkdir(): Promise { + try { + const result = await this.execPlugin("mkdir"); + //console.log(`[mkdir] path=${this.path}, result=${result}`); + return result === 1; + } catch (error) { + console.error(`[mkdir] path=${this.path}, error=${error}`); + return false; + } + } + + async mkdirs(): Promise { + try { + const result = await this.execPlugin("mkdirs"); + //console.log(`[mkdirs] path=${this.path}, result=${result}`); + return result === 1; + } catch (error) { + console.error(`[mkdirs] path=${this.path}, error=${error}`); + return false; + } + } + + async readText(encoding: string = "UTF-8"): Promise { + try { + const content = await this.execPlugin("readText", [encoding]); + //console.log(`[readText] path=${this.path}, content length=${content?.length}`); + return content; + } catch (error) { + console.error(`[readText] path=${this.path}, error=${error}`); + throw new Error(`Failed to read file: ${error}`); + } + } + + async toUri(): Promise { + try { + //console.log(`[toUri] path=${this.path}, uri=${uri}`); + return await this.execPlugin("toUri"); + } catch (error) { + console.error(`[toUri] path=${this.path}, error=${error}`); + return `file://${this.path}`; + } + } + + async writeText(text: string, encoding: string = "UTF-8"): Promise { + try { + await this.execPlugin("writeText", [text, encoding]); + //console.log(`[writeText] path=${this.path}, text length=${text.length}`); + } catch (error) { + console.error(`[writeText] path=${this.path}, error=${error}`); + throw new Error(`Failed to write file: ${error}`); + } + } + + getPath(): string { + //console.log(`[getPath] returning path=${this.path}`); + return this.path!!; + } } diff --git a/src/fileSystem/SAFDocumentFile.ts b/src/fileSystem/SAFDocumentFile.ts index afe0200fc..c3d9a6313 100644 --- a/src/fileSystem/SAFDocumentFile.ts +++ b/src/fileSystem/SAFDocumentFile.ts @@ -7,236 +7,238 @@ import helpers from "utils/helpers"; // @ts-ignore import Url from "utils/Url"; - import { FileObject } from "./fileObject"; declare const sdcard: any; //alternative for externalFs.js export class SAFDocumentFile implements FileObject { - constructor(private uri: string) {} - - async canRead(): Promise { - const stat = await this.stat(); - return !!stat.canRead; - } - - async canWrite(): Promise { - const stat = await this.stat(); - return !!stat.canWrite; - } - - async childByNameExists(name: string): Promise { - const children = await this.listFiles(); - return children.some(c => c.getName().then(n => n === name)); - } - - async createNewFile(): Promise { - try { - await this.writeText(""); - return true; - } catch { - return false; - } - } - - async delete(): Promise { - return new Promise((resolve, reject) => { - sdcard.delete(this.uri, () => resolve(true), reject); - }); - } - - async exists(): Promise { - try { - await this.stat(); - return true; - } catch { - return false; - } - } - - async getChildByName(name: string): Promise { - const children = await this.listFiles(); - for (const child of children) { - if (await child.getName() === name) return child; - } - return null; - } - - async getLength(): Promise { - const stat = await this.stat(); - return stat.size ?? 0; - } - - async getName(): Promise { - const parts = this.uri.split("/"); - return parts[parts.length - 1] || ""; - } - - async getParentFile(): Promise { - const parent = Url.dirname(this.uri); - if (!parent || parent === this.uri) return null; - return new SAFDocumentFile(parent); - } - - async isDirectory(): Promise { - const stat = await this.stat(); - return stat.isDirectory === true; - } - - async isFile(): Promise { - const stat = await this.stat(); - return stat.isFile === true; - } - - async isLink(): Promise { - return false; - } - - async isNative(): Promise { - return true; - } - - async isUnixLike(): Promise { - return false; - } - - async listFiles(): Promise { - const uri = await this.formatUri(this.uri); - return new Promise((resolve, reject) => { - sdcard.listDir( - uri, - (entries: any[]) => { - const files = entries.map((e) => new SAFDocumentFile(e.url || e.uri)); - resolve(files); - }, - reject, - ); - }); - } - - async mkdir(): Promise { - const parent = await this.getParentFile(); - if (!parent) return false; - return new Promise((resolve, reject) => { - // @ts-ignore - sdcard.createDir(parent.uri, this.getName(), () => resolve(true), reject); - }); - } - - async mkdirs(): Promise { - // Simplified version that only creates one level - return this.mkdir(); - } - - async readText(encoding = "utf8"): Promise { - const uri = await this.formatUri(this.uri); - return new Promise((resolve, reject) => { - sdcard.read( - uri, - async (data: ArrayBuffer) => { - const text = await decode(data, encoding); - resolve(text); - }, - reject, - ); - }); - } - - async writeText(text: string, encoding = "utf8"): Promise { - const encoded = await encode(text, encoding); - return new Promise((resolve, reject) => { - sdcard.write(this.uri, encoded, resolve, reject); - }); - } - - async toUri(): Promise { - return this.uri; - } - - // ---- Extra helpers translated from externalFs ---- - - private async formatUri(uri: string): Promise { - return new Promise((resolve, reject) => { - sdcard.formatUri(uri, resolve, reject); - }); - } - - private async stat(): Promise { - const storageList = helpers.parseJSON(localStorage.getItem("storageList")); - - if (Array.isArray(storageList)) { - const storage = storageList.find((s) => s.uri === this.uri); - if (storage) { - const stat = { - size: 0, - name: storage.name, - type: "dir", - canRead: true, - canWrite: true, - modifiedDate: new Date(), - isDirectory: true, - isFile: false, - url: this.uri, - }; - - helpers.defineDeprecatedProperty( - stat, - "uri", - function () { - // @ts-ignore - return this.url; - }, - function (value:any) { - // @ts-ignore - this.url = value; - }, - ); - - return stat; - } - } - - const uri = await this.formatUri(this.uri); - return new Promise((resolve, reject) => { - sdcard.stats( - uri, - (stats: any) => { - helpers.defineDeprecatedProperty( - stats, - "uri", - function () { - // @ts-ignore - return this.url; - }, - function (val:any) { - // @ts-ignore - this.url = val; - }, - ); - resolve(stats); - }, - reject, - ); - }); - } - - // ---- Optional static helpers for mount management ---- - - static async listStorages(): Promise { - return new Promise((resolve, reject) => { - sdcard.listStorages(resolve, reject); - }); - } - - static async getStorageAccessPermission(uuid: string, name: string): Promise { - return new Promise((resolve, reject) => { - setTimeout(() => loader.destroy(), 100); - sdcard.getStorageAccessPermission(uuid, resolve, reject); - }); - } - - static test(url: string): boolean { - return /^content:/.test(url); - } + constructor(private uri: string) {} + + async canRead(): Promise { + const stat = await this.stat(); + return !!stat.canRead; + } + + async canWrite(): Promise { + const stat = await this.stat(); + return !!stat.canWrite; + } + + async childByNameExists(name: string): Promise { + const children = await this.listFiles(); + return children.some((c) => c.getName().then((n) => n === name)); + } + + async createNewFile(): Promise { + try { + await this.writeText(""); + return true; + } catch { + return false; + } + } + + async delete(): Promise { + return new Promise((resolve, reject) => { + sdcard.delete(this.uri, () => resolve(true), reject); + }); + } + + async exists(): Promise { + try { + await this.stat(); + return true; + } catch { + return false; + } + } + + async getChildByName(name: string): Promise { + const children = await this.listFiles(); + for (const child of children) { + if ((await child.getName()) === name) return child; + } + return null; + } + + async getLength(): Promise { + const stat = await this.stat(); + return stat.size ?? 0; + } + + async getName(): Promise { + const parts = this.uri.split("/"); + return parts[parts.length - 1] || ""; + } + + async getParentFile(): Promise { + const parent = Url.dirname(this.uri); + if (!parent || parent === this.uri) return null; + return new SAFDocumentFile(parent); + } + + async isDirectory(): Promise { + const stat = await this.stat(); + return stat.isDirectory === true; + } + + async isFile(): Promise { + const stat = await this.stat(); + return stat.isFile === true; + } + + async isLink(): Promise { + return false; + } + + async isNative(): Promise { + return true; + } + + async isUnixLike(): Promise { + return false; + } + + async listFiles(): Promise { + const uri = await this.formatUri(this.uri); + return new Promise((resolve, reject) => { + sdcard.listDir( + uri, + (entries: any[]) => { + const files = entries.map((e) => new SAFDocumentFile(e.url || e.uri)); + resolve(files); + }, + reject, + ); + }); + } + + async mkdir(): Promise { + const parent = await this.getParentFile(); + if (!parent) return false; + return new Promise((resolve, reject) => { + // @ts-ignore + sdcard.createDir(parent.uri, this.getName(), () => resolve(true), reject); + }); + } + + async mkdirs(): Promise { + // Simplified version that only creates one level + return this.mkdir(); + } + + async readText(encoding = "utf8"): Promise { + const uri = await this.formatUri(this.uri); + return new Promise((resolve, reject) => { + sdcard.read( + uri, + async (data: ArrayBuffer) => { + const text = await decode(data, encoding); + resolve(text); + }, + reject, + ); + }); + } + + async writeText(text: string, encoding = "utf8"): Promise { + const encoded = await encode(text, encoding); + return new Promise((resolve, reject) => { + sdcard.write(this.uri, encoded, resolve, reject); + }); + } + + async toUri(): Promise { + return this.uri; + } + + // ---- Extra helpers translated from externalFs ---- + + private async formatUri(uri: string): Promise { + return new Promise((resolve, reject) => { + sdcard.formatUri(uri, resolve, reject); + }); + } + + private async stat(): Promise { + const storageList = helpers.parseJSON(localStorage.getItem("storageList")); + + if (Array.isArray(storageList)) { + const storage = storageList.find((s) => s.uri === this.uri); + if (storage) { + const stat = { + size: 0, + name: storage.name, + type: "dir", + canRead: true, + canWrite: true, + modifiedDate: new Date(), + isDirectory: true, + isFile: false, + url: this.uri, + }; + + helpers.defineDeprecatedProperty( + stat, + "uri", + function () { + // @ts-ignore + return this.url; + }, + function (value: any) { + // @ts-ignore + this.url = value; + }, + ); + + return stat; + } + } + + const uri = await this.formatUri(this.uri); + return new Promise((resolve, reject) => { + sdcard.stats( + uri, + (stats: any) => { + helpers.defineDeprecatedProperty( + stats, + "uri", + function () { + // @ts-ignore + return this.url; + }, + function (val: any) { + // @ts-ignore + this.url = val; + }, + ); + resolve(stats); + }, + reject, + ); + }); + } + + // ---- Optional static helpers for mount management ---- + + static async listStorages(): Promise { + return new Promise((resolve, reject) => { + sdcard.listStorages(resolve, reject); + }); + } + + static async getStorageAccessPermission( + uuid: string, + name: string, + ): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => loader.destroy(), 100); + sdcard.getStorageAccessPermission(uuid, resolve, reject); + }); + } + + static test(url: string): boolean { + return /^content:/.test(url); + } } diff --git a/src/fileSystem/fileObject.ts b/src/fileSystem/fileObject.ts index 2168f0231..ad6ef444b 100644 --- a/src/fileSystem/fileObject.ts +++ b/src/fileSystem/fileObject.ts @@ -4,138 +4,138 @@ */ export interface FileObject { - /** - * Lists all files and directories within this directory. - * @returns A promise resolving to an array of child `FileObject`s. - * @throws If the current object is not a directory. - */ - listFiles(): Promise; - - /** - * Checks if this object represents a directory. - * @returns A promise resolving to `true` if it's a directory, otherwise `false`. - */ - isDirectory(): Promise; - - /** - * Checks if this object represents a regular file. - * @returns A promise resolving to `true` if it's a file, otherwise `false`. - */ - isFile(): Promise; - - /** - * Checks if this object represents a symbolic link or alias. - * @returns A promise resolving to `true` if it's a link, otherwise `false`. - */ - isLink(): Promise; - - /** - * Gets the name of this file or directory (not the full path). - * @returns The name as a string. - */ - getName(): Promise - - /** - * Checks whether the file or directory is readable. - * @returns A promise resolving to `true` if it can be read, otherwise `false`. - */ - canRead(): Promise; - - /** - * Checks whether the file or directory is writable. - * @returns A promise resolving to `true` if it can be written to, otherwise `false`. - */ - canWrite(): Promise; - - /** - * Determines if this file is backed by the native filesystem and - * can be accessed using `java.io.File` or similar APIs. - * @returns A promise resolving to `true` if it’s a native file. - */ - isNative(): Promise; - - /** - * Determines whether the file path uses a Unix-style format (forward slashes, starting with `/`). - * @returns A promise resolving to `true` if the path is Unix-like. - */ - isUnixLike(): Promise; - - /** - * Gets the file size in bytes. - * @returns A promise resolving to the file’s size. - */ - getLength(): Promise; - - /** - * Checks whether this file or directory exists. - * @returns A promise resolving to `true` if it exists, otherwise `false`. - */ - exists(): Promise; - - /** - * Creates a new empty file at this path. - * @returns A promise resolving to `true` if the file was created, `false` if it already exists. - */ - createNewFile(): Promise; - - /** - * Creates this directory (non-recursively). - * Fails if the parent directory does not exist. - * @returns A promise resolving to `true` if created successfully. - */ - mkdir(): Promise; - - /** - * Creates this directory and all missing parent directories if necessary. - * @returns A promise resolving to `true` if created successfully. - */ - mkdirs(): Promise; - - /** - * Writes text content to this file. - * @param text The text to write. - * @param encoding Optional text encoding (e.g., `"utf-8"`). Defaults to UTF-8. - */ - writeText(text: string, encoding?: string): Promise; - - /** - * Reads the entire content of this file as text. - * @param encoding Optional text encoding (e.g., `"utf-8"`). Defaults to UTF-8. - * @returns A promise resolving to the file’s text content. - */ - readText(encoding?: string): Promise; - - /** - * Deletes this file or directory. - * @returns A promise resolving to `true` if deleted successfully, otherwise `false`. - */ - delete(): Promise; - - /** - * Returns a URI representation of this file (e.g., `file://`, `content://`, or custom scheme). - * @returns A promise resolving to the file’s URI string. - */ - toUri(): Promise; - - /** - * Checks whether a child with the given name exists inside this directory. - * @param name The name of the child file or directory. - * @returns A promise resolving to `true` if it exists, `false` if not, or `null` if unknown. - */ - childByNameExists(name: string): Promise; - - /** - * Gets a `FileObject` representing a child entry with the given name. - * The child may or may not exist yet. - * @param name The name of the child. - * @returns A promise resolving to a `FileObject`, or `null` if the child is impossible - */ - getChildByName(name: string): Promise; - - /** - * Returns the parent directory of this file. - * @returns A promise resolving to the parent `FileObject`, or `null` if there’s no parent. - * @throws If unable to determine the parent. - */ - getParentFile(): Promise; + /** + * Lists all files and directories within this directory. + * @returns A promise resolving to an array of child `FileObject`s. + * @throws If the current object is not a directory. + */ + listFiles(): Promise; + + /** + * Checks if this object represents a directory. + * @returns A promise resolving to `true` if it's a directory, otherwise `false`. + */ + isDirectory(): Promise; + + /** + * Checks if this object represents a regular file. + * @returns A promise resolving to `true` if it's a file, otherwise `false`. + */ + isFile(): Promise; + + /** + * Checks if this object represents a symbolic link or alias. + * @returns A promise resolving to `true` if it's a link, otherwise `false`. + */ + isLink(): Promise; + + /** + * Gets the name of this file or directory (not the full path). + * @returns The name as a string. + */ + getName(): Promise; + + /** + * Checks whether the file or directory is readable. + * @returns A promise resolving to `true` if it can be read, otherwise `false`. + */ + canRead(): Promise; + + /** + * Checks whether the file or directory is writable. + * @returns A promise resolving to `true` if it can be written to, otherwise `false`. + */ + canWrite(): Promise; + + /** + * Determines if this file is backed by the native filesystem and + * can be accessed using `java.io.File` or similar APIs. + * @returns A promise resolving to `true` if it’s a native file. + */ + isNative(): Promise; + + /** + * Determines whether the file path uses a Unix-style format (forward slashes, starting with `/`). + * @returns A promise resolving to `true` if the path is Unix-like. + */ + isUnixLike(): Promise; + + /** + * Gets the file size in bytes. + * @returns A promise resolving to the file’s size. + */ + getLength(): Promise; + + /** + * Checks whether this file or directory exists. + * @returns A promise resolving to `true` if it exists, otherwise `false`. + */ + exists(): Promise; + + /** + * Creates a new empty file at this path. + * @returns A promise resolving to `true` if the file was created, `false` if it already exists. + */ + createNewFile(): Promise; + + /** + * Creates this directory (non-recursively). + * Fails if the parent directory does not exist. + * @returns A promise resolving to `true` if created successfully. + */ + mkdir(): Promise; + + /** + * Creates this directory and all missing parent directories if necessary. + * @returns A promise resolving to `true` if created successfully. + */ + mkdirs(): Promise; + + /** + * Writes text content to this file. + * @param text The text to write. + * @param encoding Optional text encoding (e.g., `"utf-8"`). Defaults to UTF-8. + */ + writeText(text: string, encoding?: string): Promise; + + /** + * Reads the entire content of this file as text. + * @param encoding Optional text encoding (e.g., `"utf-8"`). Defaults to UTF-8. + * @returns A promise resolving to the file’s text content. + */ + readText(encoding?: string): Promise; + + /** + * Deletes this file or directory. + * @returns A promise resolving to `true` if deleted successfully, otherwise `false`. + */ + delete(): Promise; + + /** + * Returns a URI representation of this file (e.g., `file://`, `content://`, or custom scheme). + * @returns A promise resolving to the file’s URI string. + */ + toUri(): Promise; + + /** + * Checks whether a child with the given name exists inside this directory. + * @param name The name of the child file or directory. + * @returns A promise resolving to `true` if it exists, `false` if not, or `null` if unknown. + */ + childByNameExists(name: string): Promise; + + /** + * Gets a `FileObject` representing a child entry with the given name. + * The child may or may not exist yet. + * @param name The name of the child. + * @returns A promise resolving to a `FileObject`, or `null` if the child is impossible + */ + getChildByName(name: string): Promise; + + /** + * Returns the parent directory of this file. + * @returns A promise resolving to the parent `FileObject`, or `null` if there’s no parent. + * @throws If unable to determine the parent. + */ + getParentFile(): Promise; } diff --git a/src/lib/Log.ts b/src/lib/Log.ts index 12108c8e3..6f4236f79 100644 --- a/src/lib/Log.ts +++ b/src/lib/Log.ts @@ -2,27 +2,39 @@ * Android.util.Log */ export class Log { - constructor(private tag: string) {} + constructor(private tag: string) {} - private format(level: string, message: any): string { - const time = new Date().toISOString(); - return `[${time}] [${this.tag}] [${level}] ${message}`; - } + private format(level: string, message: any): string { + const time = new Date().toISOString(); + return `[${time}] [${this.tag}] [${level}] ${message}`; + } - i(message: any, ...args: any[]): void { - console.log(`%c${this.format("INFO", message)}`, "color: #00aaff", ...args); - } + i(message: any, ...args: any[]): void { + console.log(`%c${this.format("INFO", message)}`, "color: #00aaff", ...args); + } - w(message: any, ...args: any[]): void { - console.warn(`%c${this.format("WARN", message)}`, "color: #ffaa00", ...args); - } + w(message: any, ...args: any[]): void { + console.warn( + `%c${this.format("WARN", message)}`, + "color: #ffaa00", + ...args, + ); + } - e(message: any, ...args: any[]): void { - console.error(`%c${this.format("ERROR", message)}`, "color: #ff4444", ...args); - } + e(message: any, ...args: any[]): void { + console.error( + `%c${this.format("ERROR", message)}`, + "color: #ff4444", + ...args, + ); + } - d(message: any, ...args: any[]): void { - //TODO: Only show debug messages in debug mode - console.debug(`%c${this.format("DEBUG", message)}`, "color: #999999", ...args); - } + d(message: any, ...args: any[]): void { + //TODO: Only show debug messages in debug mode + console.debug( + `%c${this.format("DEBUG", message)}`, + "color: #999999", + ...args, + ); + } } diff --git a/src/lib/acode.js b/src/lib/acode.js index 0cca3591d..d4702d042 100644 --- a/src/lib/acode.js +++ b/src/lib/acode.js @@ -43,8 +43,10 @@ import encodings, { decode, encode } from "utils/encodings"; import helpers from "utils/helpers"; import KeyboardEvent from "utils/keyboardEvent"; import Url from "utils/Url"; +import { FileServer } from "../fileServer/FileServer"; +import { NativeFileWrapper } from "../fileSystem/NativeFileWrapper"; +import { SAFDocumentFile } from "../fileSystem/SAFDocumentFile"; import constants from "./constants"; -import {NativeFileWrapper} from "../fileSystem/NativeFileWrapper"; export default class Acode { #modules = {}; @@ -120,7 +122,9 @@ export default class Acode { }, }; - this.define("nativeFile",NativeFileWrapper); + this.define("nativeFile", NativeFileWrapper); + this.define("SAFDocumentFile", SAFDocumentFile); + this.define("fileServer", FileServer); this.define("Url", Url); this.define("page", Page); this.define("Color", Color); From b0857c3b63b83edbd62173c959341a03001cc78c Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Thu, 30 Oct 2025 19:00:08 +0530 Subject: [PATCH 5/9] feat: working http server --- src/fileServer/fileServer.ts | 49 -------------- src/lib/Log.ts | 2 +- src/lib/acode.js | 4 +- src/lib/fileServer.ts | 122 +++++++++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 51 deletions(-) delete mode 100644 src/fileServer/fileServer.ts create mode 100644 src/lib/fileServer.ts diff --git a/src/fileServer/fileServer.ts b/src/fileServer/fileServer.ts deleted file mode 100644 index e0f2798b8..000000000 --- a/src/fileServer/fileServer.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { FileObject } from "../fileSystem/fileObject"; -import { Log } from "../lib/Log"; - -class FileServer { - private readonly file: FileObject; - private readonly port: number; - private httpServer: Server | undefined; - private readonly log: Log = new Log("fileServer"); - - constructor(port: number, file: FileObject) { - this.file = file; - this.port = port; - } - - start(onSuccess: (msg: any) => void, onError: (err: any) => void): void { - this.httpServer = CreateServer(this.port, onSuccess, onError); - - // @ts-ignore - httpServer.setOnRequestHandler(this.handleRequest.bind(this)); - } - - private handleRequest(req: { requestId: string; path: string }): void { - this.log.d("Request received:", req); - // handle file serving logic here - this.log.d("Received request:", req.requestId); - this.log.d("Request Path", req.path); - this.sendText("This is a test", req.requestId, null); - this.log.d("Response sent"); - } - - private sendText( - text: string, - id: string, - mimeType: string | null | undefined, - ) { - this.httpServer?.send( - id, - { - status: 200, - body: text, - headers: { - "Content-Type": mimeType || "text/html", - }, - }, - () => {}, - this.log.e, - ); - } -} diff --git a/src/lib/Log.ts b/src/lib/Log.ts index 6f4236f79..08160c457 100644 --- a/src/lib/Log.ts +++ b/src/lib/Log.ts @@ -31,7 +31,7 @@ export class Log { d(message: any, ...args: any[]): void { //TODO: Only show debug messages in debug mode - console.debug( + console.log( `%c${this.format("DEBUG", message)}`, "color: #999999", ...args, diff --git a/src/lib/acode.js b/src/lib/acode.js index d4702d042..a69e089b8 100644 --- a/src/lib/acode.js +++ b/src/lib/acode.js @@ -43,10 +43,11 @@ import encodings, { decode, encode } from "utils/encodings"; import helpers from "utils/helpers"; import KeyboardEvent from "utils/keyboardEvent"; import Url from "utils/Url"; -import { FileServer } from "../fileServer/FileServer"; import { NativeFileWrapper } from "../fileSystem/NativeFileWrapper"; import { SAFDocumentFile } from "../fileSystem/SAFDocumentFile"; import constants from "./constants"; +import { FileServer } from "./fileServer"; +import { Log } from "./Log"; export default class Acode { #modules = {}; @@ -125,6 +126,7 @@ export default class Acode { this.define("nativeFile", NativeFileWrapper); this.define("SAFDocumentFile", SAFDocumentFile); this.define("fileServer", FileServer); + this.define("log", Log); this.define("Url", Url); this.define("page", Page); this.define("Color", Color); diff --git a/src/lib/fileServer.ts b/src/lib/fileServer.ts new file mode 100644 index 000000000..719b361f6 --- /dev/null +++ b/src/lib/fileServer.ts @@ -0,0 +1,122 @@ +import { FileObject } from "../fileSystem/fileObject"; +import { Log } from "./Log"; + +export class FileServer { + private readonly file: FileObject; + private readonly port: number; + private httpServer: Server | undefined; + private readonly log: Log = new Log("fileServer"); + + constructor(port: number, file: FileObject) { + this.file = file; + this.port = port; + } + + start(onSuccess: (msg: any) => void, onError: (err: any) => void): void { + this.httpServer = CreateServer(this.port, onSuccess, onError); + + // @ts-ignore + this.httpServer.setOnRequestHandler(this.handleRequest.bind(this)); + } + + private async handleRequest(req: { + requestId: string; + path: string; + }): Promise { + this.log.d("Request received:", req); + this.log.d("Received request:", req.requestId); + this.log.d("Request Path", req.path); + + if (await this.file.isFile()) { + this.sendText( + (await this.file?.readText()) ?? "null", + req.requestId, + this.getMimeType(await this.file.getName()), + ); + return; + } + + if (req.path === "/") { + const indexFile = await this.file.getChildByName("index.html"); + if ((await indexFile?.exists()) && (await indexFile?.canRead())) { + this.sendText( + (await indexFile?.readText()) ?? "null", + req.requestId, + this.getMimeType(await indexFile!!.getName()), + ); + } else { + this.sendText("404 index file not found", req.requestId, "text/plain"); + } + return; + } + + let targetFile: FileObject | null = null; + + for (const name of req.path.split("/")) { + if (!name) continue; // skip empty parts like leading or trailing "/" + + if (targetFile === null) { + targetFile = await this.file.getChildByName(name); + } else { + targetFile = await targetFile.getChildByName(name); + } + + if (targetFile === null) { + // Stop early if file is missing + break; + } + } + + if (targetFile == null || !(await targetFile!!.exists())) { + this.sendText( + "404 file not found: " + req.path, + req.requestId, + "text/plain", + ); + return; + } + + this.sendText( + (await targetFile?.readText()) ?? "null", + req.requestId, + this.getMimeType(await targetFile.getName()), + ); + } + + private sendText( + text: string, + id: string, + mimeType: string | null | undefined, + ) { + this.httpServer?.send( + id, + { + status: 200, + body: text, + headers: { + "Content-Type": mimeType || "text/html", + }, + }, + () => {}, + this.log.e, + ); + } + + private getMimeType(filename: string): string { + const ext = filename.split(".").pop()?.toLowerCase(); + const map: Record = { + html: "text/html", + css: "text/css", + js: "application/javascript", + json: "application/json", + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + gif: "image/gif", + svg: "image/svg+xml", + txt: "text/plain", + xml: "text/xml", + }; + return map[ext ?? "text/plain"]; + } +} From e53eb4d8f7c8c56fd52730e0f6361da1022d9d1d Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Thu, 30 Oct 2025 19:07:08 +0530 Subject: [PATCH 6/9] feat: added sanity check --- src/fileSystem/NativeFileWrapper.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/fileSystem/NativeFileWrapper.ts b/src/fileSystem/NativeFileWrapper.ts index db1cee2b3..8a9dd0b52 100644 --- a/src/fileSystem/NativeFileWrapper.ts +++ b/src/fileSystem/NativeFileWrapper.ts @@ -1,3 +1,4 @@ +import { Exception } from "sass"; import { FileObject } from "./fileObject"; declare var cordova: any; @@ -20,12 +21,14 @@ export class NativeFileWrapper implements FileObject { this.ready = (async () => { let temp = absolutePathOrUri; + //NOTE: only cvfiles are supported which are backed by the native filesystem files with http:// is not supported if (absolutePathOrUri.startsWith("cdvfile://")) { temp = await new Promise((resolve, reject) => { // @ts-ignore window.resolveLocalFileSystemURL( absolutePathOrUri, - (entry: any) => resolve(entry.toURL()), + // nativeURL + (entry: any) => resolve(entry.nativeURL()), reject, ); }); @@ -33,6 +36,12 @@ export class NativeFileWrapper implements FileObject { this.path = this.removePrefix(temp, "file://"); + if (!this.path.endsWith("/")) { + throw new Error( + `Path "${absolutePathOrUri}" converted to "${this.path}" which is invalid since it does not start with / `, + ); + } + onReady(this); })(); } From b6d39abffa379d033baf5fadeeeaea7acac0463d Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Sat, 1 Nov 2025 12:54:00 +0530 Subject: [PATCH 7/9] fix: check --- src/fileSystem/NativeFileWrapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fileSystem/NativeFileWrapper.ts b/src/fileSystem/NativeFileWrapper.ts index 8a9dd0b52..c33a72a31 100644 --- a/src/fileSystem/NativeFileWrapper.ts +++ b/src/fileSystem/NativeFileWrapper.ts @@ -36,7 +36,7 @@ export class NativeFileWrapper implements FileObject { this.path = this.removePrefix(temp, "file://"); - if (!this.path.endsWith("/")) { + if (!this.path.startsWith("/")) { throw new Error( `Path "${absolutePathOrUri}" converted to "${this.path}" which is invalid since it does not start with / `, ); From d858fff394e0f160d2329ae99674f92665e288ca Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Tue, 4 Nov 2025 16:07:26 +0530 Subject: [PATCH 8/9] remove stuff --- src/lib/run.js | 812 ++----------------------------------------------- 1 file changed, 21 insertions(+), 791 deletions(-) diff --git a/src/lib/run.js b/src/lib/run.js index 709217531..e883c2ab8 100644 --- a/src/lib/run.js +++ b/src/lib/run.js @@ -16,11 +16,9 @@ import $_console from "views/console.hbs"; import $_markdown from "views/markdown.hbs"; import constants from "./constants"; import EditorFile from "./editorFile"; -import openFolder, { addedFolder } from "./openFolder"; +import openFolder, {addedFolder} from "./openFolder"; import appSettings from "./settings"; -/**@type {Server} */ -let webServer; /** * Starts the server and run the active file in browser @@ -28,797 +26,29 @@ let webServer; * @param {"inapp"|"browser"} target * @param {Boolean} runFile */ + async function run( - isConsole = false, - target = appSettings.value.previewMode, - runFile = false, + isConsole = false, + target = appSettings.value.previewMode, + runFile = false, ) { - if (!isConsole && !runFile) { - const { serverPort, previewPort, previewMode, disableCache, host } = - appSettings.value; - if (serverPort !== previewPort) { - const src = `http://${host}:${previewPort}`; - if (previewMode === "browser") { - system.openInBrowser(src); - return; - } - - browser.open(src); - return; - } - } - - /** @type {EditorFile} */ - const activeFile = isConsole ? null : editorManager.activeFile; - if (!isConsole && !(await activeFile?.canRun())) return; - - if (!isConsole && !localStorage.__init_runPreview) { - localStorage.__init_runPreview = true; - tutorial("run-preview", strings["preview info"]); - } - - const uuid = helpers.uuid(); - - let isLoading = false; - let isFallback = false; - let filename, pathName, extension; - let port = appSettings.value.serverPort; - let EXECUTING_SCRIPT = uuid + "_script.js"; - const MIMETYPE_HTML = mimeType.lookup("html"); - const CONSOLE_SCRIPT = uuid + "_console.js"; - const MARKDOWN_STYLE = uuid + "_md.css"; - const queue = []; - - if (activeFile) { - filename = activeFile.filename; - pathName = activeFile.location; - extension = Url.extname(filename); - - if (!pathName && activeFile.uri) { - pathName = Url.dirname(activeFile.uri); - } - } - - if (runFile && extension === "svg") { - try { - const fs = fsOperation(activeFile.uri); - const res = await fs.readFile(); - let text = new TextDecoder().decode(res); - - if (!/^<\?xml/.test(text)) { - text = `\n` + text; - } - - const blob = new Blob([text], { type: mimeType.lookup(extension) }); - const url = URL.createObjectURL(blob); - - box( - filename, - `
- ${filename} -
`, - ); - } catch (err) { - helpers.error(err); - } - return; - } - - if (!runFile && filename !== "index.html" && pathName) { - const folder = openFolder.find(activeFile.uri); - - if (folder) { - const { url } = folder; - const fs = fsOperation(Url.join(url, "index.html")); - - try { - if (await fs.exists()) { - filename = "index.html"; - extension = "html"; - pathName = url; - start(); - return; - } - - next(); - return; - } catch (err) { - helpers.error(err); - return; - } - } - } - - next(); - - function next() { - if (extension === ".js" || isConsole) startConsole(); - else start(); - } - - function startConsole() { - if (!isConsole) EXECUTING_SCRIPT = activeFile.filename; - isConsole = true; - target = "inapp"; - filename = "console.html"; - - //this extra www is incorrect because asset_directory itself has www - //but keeping it in case something depends on it - pathName = `${ASSETS_DIRECTORY}www/`; - port = constants.CONSOLE_PORT; - - start(); - } - - function start() { - if (target === "browser") { - system.isPowerSaveMode((res) => { - if (res) { - alert(strings.info, strings["powersave mode warning"]); - } else { - startServer(); - } - }, startServer); - } else { - startServer(); - } - } - - function startServer() { - //isFallback = true; - webServer?.stop(); - webServer = CreateServer(port, openBrowser, onError); - webServer.setOnRequestHandler(handleRequest); - - function onError(err) { - if (err === "Server already running") { - openBrowser(); - } else { - ++port; - start(); - } - } - } - - /** - * Requests handler - * @param {object} req - * @param {string} req.requestId - * @param {string} req.path - */ - async function handleRequest(req) { - const reqId = req.requestId; - let reqPath = req.path.substring(1); - - console.log(`XREQPATH ${reqPath}`); - console.log(req); - - if (!reqPath || (reqPath.endsWith("/") && reqPath.length === 1)) { - reqPath = getRelativePath(); - } - - console.log(`XREQPATH1 ${reqPath}`); - - const ext = Url.extname(reqPath); - let url = null; - - switch (reqPath) { - case CONSOLE_SCRIPT: - if ( - isConsole || - appSettings.value.console === appSettings.CONSOLE_LEGACY - ) { - url = `${ASSETS_DIRECTORY}/build/console.js`; - } else { - url = `${DATA_STORAGE}/eruda.js`; - } - sendFileContent(url, reqId, "application/javascript"); - break; - - case EXECUTING_SCRIPT: { - const text = activeFile?.session.getValue() || ""; - sendText(text, reqId, "application/javascript"); - break; - } - - case MARKDOWN_STYLE: - url = appSettings.value.markdownStyle; - if (url) sendFileContent(url, reqId, "text/css"); - else sendText("img {max-width: 100%;}", reqId, "text/css"); - break; - - default: - sendByExt(); - break; - } - - async function sendByExt() { - if (isConsole) { - if (reqPath === "console.html") { - sendText( - mustache.render($_console, { - CONSOLE_SCRIPT, - EXECUTING_SCRIPT, - }), - reqId, - MIMETYPE_HTML, - ); - return; - } - - if (reqPath === "favicon.ico") { - sendIco(ASSETS_DIRECTORY, reqId); - return; - } - } - - if (activeFile.mode === "single") { - if (filename === reqPath) { - sendText( - activeFile.session.getValue(), - reqId, - mimeType.lookup(filename), - ); - } else { - error(reqId); - } - return; - } - - let url = activeFile.uri; - - let file = activeFile.SAFMode === "single" ? activeFile : null; - - if (pathName) { - const projectFolder = addedFolder[0]; - const query = url.split("?")[1]; - let rootFolder = ""; - - if ( - projectFolder !== undefined && - pathName.includes(projectFolder.url) - ) { - rootFolder = projectFolder.url; - } else { - rootFolder = pathName; - } - - if ( - (rootFolder.startsWith("ftp:") || rootFolder.startsWith("sftp:")) && - rootFolder.includes("?") - ) { - rootFolder = rootFolder.split("?")[0]; - } - - rootFolder = rootFolder.replace(/\/+$/, ""); // remove trailing slash - reqPath = reqPath.replace(/^\/+/, ""); // remove leading slash - - const rootParts = rootFolder.split("/"); - const pathParts = reqPath.split("/"); - - if (pathParts[0] === rootParts[rootParts.length - 1]) { - pathParts.shift(); - } - - function removePrefix(str, prefix) { - if (str.startsWith(prefix)) { - return str.slice(prefix.length); - } - return str; - } - - function findOverlap(a, b) { - // Start with the smallest possible overlap (1 character) and increase - let maxOverlap = ""; - - // Check all possible overlapping lengths - for (let i = 1; i <= Math.min(a.length, b.length); i++) { - // Get the ending substring of a with length i - const endOfA = a.slice(-i); - // Get the starting substring of b with length i - const startOfB = b.slice(0, i); - - // If they match, we have a potential overlap - if (endOfA === startOfB) { - maxOverlap = endOfA; - } - } - - return maxOverlap; - } - - console.log(`RootFolder ${rootFolder}`); - console.log(`PARTS ${pathParts.join("/")}`); - - let fullPath; - // Skip overlap detection for GitHub URIs as it causes path corruption - if (rootFolder.startsWith("gh://")) { - fullPath = Url.join(rootFolder, pathParts.join("/")); - } else { - const overlap = findOverlap(rootFolder, pathParts.join("/")); - if (overlap !== "") { - fullPath = Url.join( - rootFolder, - removePrefix(pathParts.join("/"), overlap), - ); - } else { - fullPath = Url.join(rootFolder, pathParts.join("/")); - } - } - - console.log(`Full PATH ${fullPath}`); - - const urlFile = fsOperation(fullPath); - - // Skip stat check for GitHub URIs as they are handled differently - if (!fullPath.startsWith("gh://")) { - const stats = await urlFile.stat(); + if (!isConsole && !runFile) { + const {serverPort, previewPort, previewMode, disableCache, host} = + appSettings.value; + if (serverPort !== previewPort) { + const src = `http://${host}:${previewPort}`; + if (previewMode === "browser") { + system.openInBrowser(src); + return; + } + + browser.open(src); + return; + } + } - if (!stats.exists) { - error(reqId); - return; - } - if (!stats.isFile) { - if (fullPath.endsWith("/")) { - fullPath += "index.html"; - } else { - fullPath += "/index.html"; - } - } - } - - // Add back the query if present - url = query ? `${fullPath}?${query}` : fullPath; - - file = editorManager.getFile(url, "uri"); - } else if (!activeFile.uri) { - file = activeFile; - } - - switch (ext) { - case ".htm": - case ".html": - if (file && file.loaded && file.isUnsaved) { - sendHTML(file.session.getValue(), reqId); - } else { - sendFileContent(url, reqId, MIMETYPE_HTML); - } - break; - - case ".md": - if (file) { - const html = markdownIt({ html: true }) - .use(MarkdownItGitHubAlerts) - .use(anchor, { - slugify: (s) => - s - .trim() - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-"), - }) - .use(markdownItTaskLists) - .use(markdownItFootnote) - .render(file.session.getValue()); - const doc = mustache.render($_markdown, { - html, - filename, - MARKDOWN_STYLE, - }); - sendText(doc, reqId, MIMETYPE_HTML); - } - break; - - default: - if (file && file.loaded && file.isUnsaved) { - if (file.filename.endsWith(".html")) { - sendHTML(file.session.getValue(), reqId); - } else { - sendText( - file.session.getValue(), - reqId, - mimeType.lookup(file.filename), - ); - } - } else if (url) { - if (reqPath === "favicon.ico") { - sendIco(ASSETS_DIRECTORY, reqId); - } else { - sendFile(url, reqId); - } - } else { - error(reqId); - } - break; - } - } - } - - /** - * Sends 404 error - * @param {string} id - */ - function error(id) { - webServer?.send(id, { - status: 404, - body: "File not found!", - }); - } - - /** - * Sends favicon - * @param {string} assets - * @param {string} reqId - */ - function sendIco(assets, reqId) { - const ico = Url.join(assets, "favicon.ico"); - sendFile(ico, reqId); - } - - /** - * Sends HTML file - * @param {string} text - * @param {string} id - */ - function sendHTML(text, id) { - const js = ` - - `; - text = text.replace(/><\/script>/g, ' crossorigin="anonymous">'); - const part = text.split(""); - if (part.length === 2) { - text = `${part[0]}${js}${part[1]}`; - } else if (//i.test(text)) { - text = text.replace("", `${js}`); - } else { - text = `${js}` + text; - } - sendText(text, id); - } - - /** - * Sends file - * @param {string} path - * @param {string} id - * @returns - */ - async function sendFile(path, id) { - if (isLoading) { - queue.push(() => { - sendFile(path, id); - }); - return; - } - - isLoading = true; - const protocol = Url.getProtocol(path); - const ext = Url.extname(path); - const mimetype = mimeType.lookup(ext); - if (/s?ftp:/.test(protocol)) { - const cacheFile = Url.join( - CACHE_STORAGE, - protocol.slice(0, -1) + path.hashCode(), - ); - const fs = fsOperation(path); - try { - await fs.readFile(); // Because reading the remote file will create cache file - path = cacheFile; - } catch (err) { - error(id); - isLoading = false; - return; - } - } else if (protocol === "content:") { - path = await new Promise((resolve, reject) => { - sdcard.formatUri(path, resolve, reject); - }); - } else if (!/^file:/.test(protocol)) { - const fileContent = await fsOperation(path).readFile(); - const tempFileName = path.hashCode(); - const tempFile = Url.join(CACHE_STORAGE, tempFileName); - if (!(await fsOperation(tempFile).exists())) { - await fsOperation(CACHE_STORAGE).createFile(tempFileName, fileContent); - } else { - await fsOperation(tempFile).writeFile(fileContent); - } - path = tempFile; - } - - webServer?.send(id, { - status: 200, - path, - headers: { - "Content-Type": mimetype, - }, - }); - - isLoading = false; - const action = queue.splice(-1, 1)[0]; - if (typeof action === "function") action(); - } - - /** - * Sends file content - * @param {string} url - * @param {string} id - * @param {string} mime - * @param {(txt: string) => string} processText - * @returns - */ - async function sendFileContent(url, id, mime, processText) { - let fs = fsOperation(url); - - if (!(await fs.exists())) { - const xfs = fsOperation(Url.join(pathName, filename)); - - if (await xfs.exists()) { - fs = xfs; - isFallback = true; - console.log(`fallback ${Url.join(pathName, filename)}`); - } else { - console.log(`${url} doesnt exists`); - error(id); - } - - return; - } - - let text = await fs.readFile(appSettings.value.defaultFileEncoding); - text = processText ? processText(text) : text; - if (mime === MIMETYPE_HTML) { - sendHTML(text, id); - } else { - sendText(text, id, mime); - } - } - - /** - * Sends text - * @param {string} text - * @param {string} id - * @param {string} mimeType - * @param {(txt: string) => string} processText - */ - function sendText(text, id, mimeType, processText) { - webServer?.send(id, { - status: 200, - body: processText ? processText(text) : text, - headers: { - "Content-Type": mimeType || "text/html", - }, - }); - } - - function makeUriAbsoluteIfNeeded(uri) { - const termuxRootEncoded = - "content://com.termux.documents/tree/%2Fdata%2Fdata%2Fcom.termux%2Ffiles%2Fhome"; - const termuxRootDecoded = "/data/data/com.termux/files/home"; - - if (uri.startsWith(termuxRootEncoded)) { - // Extract subpath after `::` if already absolute - if (uri.includes("::")) return uri; - - const decodedPath = decodeURIComponent(uri.split("tree/")[1] || ""); - return `${termuxRootEncoded}::${decodedPath}/`; - } - - return uri; - } - - function getRelativePath() { - // Get the project url - const projectFolder = addedFolder[0]; - - // FIXED: Better root folder determination for Termux URIs - let rootFolder = pathName; - - // Special handling for Termux URIs - extract the actual root from the URI structure - if ( - activeFile && - activeFile.uri && - activeFile.uri.includes("com.termux.documents") && - activeFile.uri.includes("tree/") - ) { - // Extract the tree part and decode it to get the actual root path - const treeMatch = activeFile.uri.match(/tree\/([^:]+)/); - if (treeMatch) { - try { - const decodedRoot = decodeURIComponent(treeMatch[1]); - rootFolder = decodedRoot; - console.log(`DEBUG - Termux root folder set to: ${rootFolder}`); - } catch (e) { - console.error("Error decoding Termux root:", e); - } - } - } else if ( - projectFolder !== undefined && - pathName && - pathName.includes(projectFolder.url) - ) { - rootFolder = projectFolder.url; - } - - //make the uri absolute if necessary - rootFolder = makeUriAbsoluteIfNeeded(rootFolder); - - // Parent of the file - let filePath = pathName; - - if (rootFolder.startsWith("ftp:") || rootFolder.startsWith("sftp:")) { - if (rootFolder.includes("?")) { - rootFolder = rootFolder.split("?")[0]; - } - } - - //remove the query string if present this is needs to be removed because the url is not valid - if (filePath.startsWith("ftp:") || rootFolder.startsWith("sftp:")) { - if (filePath.includes("?")) { - filePath = filePath.split("?")[0]; - } - } - - // Create full file path - let temp = Url.join(filePath, filename); - - // Special handling for Termux URIs - if (temp.includes("com.termux.documents") && temp.includes("::")) { - try { - const [, realPath] = temp.split("::"); - - console.log(`DEBUG - realPath: ${realPath}`); - console.log(`DEBUG - rootFolder: ${rootFolder}`); - - // Ensure rootFolder doesn't have trailing slash for comparison - const normalizedRoot = rootFolder.replace(/\/+$/, ""); - - // Check if realPath starts with rootFolder - if (realPath.startsWith(normalizedRoot)) { - // Remove the rootFolder from the beginning of realPath - let relativePath = realPath.substring(normalizedRoot.length); - - // Remove leading slash if present - relativePath = relativePath.replace(/^\/+/, ""); - - console.log(`DEBUG - relativePath: ${relativePath}`); - - if (relativePath) { - return relativePath; - } - } - } catch (e) { - console.error("Error handling Termux URI:", e); - } - } - - // Handle other content:// URIs - if (temp.includes("content://") && temp.includes("::")) { - try { - // Get the part after :: which contains the actual file path - const afterDoubleColon = temp.split("::")[1]; - - if (afterDoubleColon) { - // Extract the rootFolder's content path if it has :: - let rootFolderPath = rootFolder; - if (rootFolder.includes("::")) { - rootFolderPath = rootFolder.split("::")[1]; - } - - // If rootFolder doesn't have ::, try to extract the last part of the path - if (!rootFolderPath.includes("::")) { - const rootParts = rootFolder.split("/"); - const lastPart = rootParts[rootParts.length - 1]; - - // Check if the lastPart is encoded - if (lastPart.includes("%3A")) { - // Try to decode it - try { - const decoded = decodeURIComponent(lastPart); - rootFolderPath = decoded; - } catch (e) { - console.error("Error decoding URI component:", e); - rootFolderPath = lastPart; - } - } else { - rootFolderPath = lastPart; - } - } - - // Use direct string replacement instead of path component comparison - const normalizedRoot = rootFolderPath.replace(/\/+$/, ""); - if (afterDoubleColon.startsWith(normalizedRoot)) { - let relativePath = afterDoubleColon.substring( - normalizedRoot.length, - ); - // Remove leading slash if present - relativePath = relativePath.replace(/^\/+/, ""); - - if (relativePath) { - return relativePath; - } - } - } - } catch (e) { - console.error("Error parsing content URI:", e); - } - } - - // For regular paths or if content:// URI parsing failed - // Try to find a common prefix between rootFolder and temp - // and remove it from temp - try { - const rootParts = rootFolder.split("/"); - const tempParts = temp.split("/"); - - let commonIndex = 0; - for (let i = 0; i < Math.min(rootParts.length, tempParts.length); i++) { - if (rootParts[i] === tempParts[i]) { - commonIndex = i + 1; - } else { - break; - } - } - - if (commonIndex > 0) { - return tempParts.slice(commonIndex).join("/"); - } - } catch (e) { - console.error("Error finding common path:", e); - } - - // If all else fails, just return the filename - if (filename) { - return filename; - } - - console.log("Unable to determine relative path, returning full path"); - return temp; - } - - /** - * Opens the preview in browser - */ - function openBrowser() { - let url = ""; - if (pathName === null && !activeFile.location) { - url = `http://localhost:${port}/__unsaved_file__`; - } else { - url = `http://localhost:${port}/${getRelativePath()}`; - } - - if (target === "browser") { - system.openInBrowser(url); - return; - } - - browser.open(url, isConsole); - } } -export default run; + +export default run; \ No newline at end of file From 78e6a132c336da2ba4414f2056700e26727d69e5 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Wed, 5 Nov 2025 15:09:42 +0530 Subject: [PATCH 9/9] feat: simple implementation of run.js --- src/fileSystem/SAFDocumentFile.ts | 6 +++- src/lib/run.js | 53 +++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/fileSystem/SAFDocumentFile.ts b/src/fileSystem/SAFDocumentFile.ts index c3d9a6313..c5909b506 100644 --- a/src/fileSystem/SAFDocumentFile.ts +++ b/src/fileSystem/SAFDocumentFile.ts @@ -67,8 +67,12 @@ export class SAFDocumentFile implements FileObject { return stat.size ?? 0; } + private removeSuffix(str:string, suffix:string) { + return str.endsWith(suffix) ? str.slice(0, -suffix.length) : str; + } + async getName(): Promise { - const parts = this.uri.split("/"); + const parts = this.removeSuffix(this.uri,"/").split("/"); return parts[parts.length - 1] || ""; } diff --git a/src/lib/run.js b/src/lib/run.js index e883c2ab8..e24ce91bd 100644 --- a/src/lib/run.js +++ b/src/lib/run.js @@ -18,6 +18,9 @@ import constants from "./constants"; import EditorFile from "./editorFile"; import openFolder, {addedFolder} from "./openFolder"; import appSettings from "./settings"; +import {Log} from "./Log"; +import {SAFDocumentFile} from "../fileSystem/SAFDocumentFile"; +import {FileServer} from "./fileServer"; /** @@ -47,6 +50,56 @@ async function run( } } + const activeFile = editorManager.activeFile; + if(!await activeFile?.canRun()){ + //can not run + return; + } + + const log = new Log("Code Runner") + log.d(activeFile.uri) + + //todo use NativeFileObject for file:// uri + const documentFile = new SAFDocumentFile(activeFile.uri) + + log.d(await documentFile.getName()) + log.d(await documentFile.readText()) + + const fileParent = await documentFile.getParentFile() + log.d(await fileParent.uri) + + const port = 8080 + let fileServer; + + let url = `http://localhost:${port}/${await documentFile.getName()}`; + + if (!await fileParent.exists()){ + log.d("No file parent") + fileServer = new FileServer(port,documentFile) + }else{ + log.d(await fileParent.getName()) + fileServer = new FileServer(port,fileParent) + } + + + fileServer.start((msg)=>{ + //success + log.d(msg) + + if (target === "browser") { + system.openInBrowser(url); + }else{ + browser.open(url,false); + } + + + },(err)=>{ + //error + log.e(err) + }) + + + }