From 52f6e693a0a2ba6a02671b23f6b44dee13fad5e2 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Fri, 10 Oct 2025 15:14:36 +0530 Subject: [PATCH 01/21] 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 02/21] 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 03/21] 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 04/21] 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 05/21] 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 06/21] 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 07/21] 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 08/21] 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 09/21] 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) + }) + + + } From a9deda3b1d1e600523bc0f9df3da57bddc8ef281 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Fri, 7 Nov 2025 11:10:47 +0530 Subject: [PATCH 10/21] feat: improvements --- src/fileSystem/FileObjectBuilder.ts | 22 ++++++ src/fileSystem/NativeFileWrapper.ts | 11 +++ src/fileSystem/SAFDocumentFile.ts | 64 +++++++++++++++++- src/fileSystem/fileObject.ts | 2 + src/lib/run.js | 67 ++++++++++--------- .../file/src/android/documentFile.java | 1 + 6 files changed, 131 insertions(+), 36 deletions(-) create mode 100644 src/fileSystem/FileObjectBuilder.ts diff --git a/src/fileSystem/FileObjectBuilder.ts b/src/fileSystem/FileObjectBuilder.ts new file mode 100644 index 000000000..cf547eb6e --- /dev/null +++ b/src/fileSystem/FileObjectBuilder.ts @@ -0,0 +1,22 @@ +import {FileObject} from "./fileObject"; +import {NativeFileWrapper} from "./NativeFileWrapper"; +import {SAFDocumentFile} from "./SAFDocumentFile"; + +export class FileObjectBuilder { + async build(uri: string): Promise { + if (uri.startsWith("file://") || uri.startsWith("cdvfile://")) { + return new Promise((resolve, reject) => { + const wrapper = new NativeFileWrapper(uri, (nativeFile) => { + resolve(nativeFile); + }); + + // Catch errors from the async ready promise + wrapper.ready.catch(reject); + }); + } + if (uri.startsWith("content://")) { + return new SAFDocumentFile(uri); + } + return null; + } +} diff --git a/src/fileSystem/NativeFileWrapper.ts b/src/fileSystem/NativeFileWrapper.ts index c33a72a31..f4bcd434f 100644 --- a/src/fileSystem/NativeFileWrapper.ts +++ b/src/fileSystem/NativeFileWrapper.ts @@ -46,6 +46,17 @@ export class NativeFileWrapper implements FileObject { })(); } + async isMyChild(fileObject: FileObject): Promise { + if (!(fileObject instanceof NativeFileWrapper)){ + return false + } + if (!(await this.isDirectory())) { + return false; + } + + return fileObject.getPath().includes(this.getPath()); + } + private execPlugin(action: string, args: any[] = []): Promise { //console.log(`[NativeFileWrapper] execPlugin called: action=${action}, args=${JSON.stringify(args)}`); return new Promise((resolve, reject) => { diff --git a/src/fileSystem/SAFDocumentFile.ts b/src/fileSystem/SAFDocumentFile.ts index c5909b506..137adb4c9 100644 --- a/src/fileSystem/SAFDocumentFile.ts +++ b/src/fileSystem/SAFDocumentFile.ts @@ -10,11 +10,60 @@ import Url from "utils/Url"; import { FileObject } from "./fileObject"; declare const sdcard: any; +declare var cordova: any; //alternative for externalFs.js export class SAFDocumentFile implements FileObject { constructor(private uri: string) {} + private execPlugin(action: string, args: any[] = []): Promise { + 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( + `[SAFDocumentFile] execPlugin error: action=${action}, error=${JSON.stringify(error)}`, + ); + reject(error); + }, + "documentFile", + action, + [this.uri, ...args], + ); + }); + } + + //if this fails then... + async isMyChild(fileObject: FileObject): Promise { + if (!(fileObject instanceof SAFDocumentFile)){ + return false + } + if (!(await this.isDirectory())) { + return false; + } + + let current: FileObject | null = fileObject; + + while (current !== null) { + const parent:FileObject | null = await current.getParentFile(); + if (parent === null) { + return false; // Reached root without finding this + } + + const parentUri = await parent.toUri(); + if (parentUri === this.uri) { + return true; // Found a match + } + + current = parent; + } + + return false; + } + async canRead(): Promise { const stat = await this.stat(); return !!stat.canRead; @@ -77,9 +126,18 @@ export class SAFDocumentFile implements FileObject { } async getParentFile(): Promise { - const parent = Url.dirname(this.uri); - if (!parent || parent === this.uri) return null; - return new SAFDocumentFile(parent); + //fixme + if (!this.uri){ + return null + } + + try{ + const result = await this.execPlugin("getParentFile") + return new SAFDocumentFile(result); + }catch (e) { + return null; + } + } async isDirectory(): Promise { diff --git a/src/fileSystem/fileObject.ts b/src/fileSystem/fileObject.ts index ad6ef444b..6f3afbddc 100644 --- a/src/fileSystem/fileObject.ts +++ b/src/fileSystem/fileObject.ts @@ -132,6 +132,8 @@ export interface FileObject { */ getChildByName(name: string): Promise; + isMyChild(fileObject:FileObject): Promise; + /** * Returns the parent directory of this file. * @returns A promise resolving to the parent `FileObject`, or `null` if there’s no parent. diff --git a/src/lib/run.js b/src/lib/run.js index e24ce91bd..e67bf7f3e 100644 --- a/src/lib/run.js +++ b/src/lib/run.js @@ -1,26 +1,8 @@ -import fsOperation from "fileSystem"; -import tutorial from "components/tutorial"; -import alert from "dialogs/alert"; -import box from "dialogs/box"; -import markdownIt from "markdown-it"; -import anchor from "markdown-it-anchor"; -import markdownItFootnote from "markdown-it-footnote"; -import MarkdownItGitHubAlerts from "markdown-it-github-alerts"; -import markdownItTaskLists from "markdown-it-task-lists"; -import mimeType from "mime-types"; -import mustache from "mustache"; import browser from "plugins/browser"; -import helpers from "utils/helpers"; -import Url from "utils/Url"; -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 appSettings from "./settings"; import {Log} from "./Log"; -import {SAFDocumentFile} from "../fileSystem/SAFDocumentFile"; import {FileServer} from "./fileServer"; +import {FileObjectBuilder} from "../fileSystem/FileObjectBuilder"; /** @@ -59,28 +41,35 @@ async function run( 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 fileObjectBuilder = new FileObjectBuilder() + const documentFile = await fileObjectBuilder.build(activeFile.uri); + + const projectFolder = await fileObjectBuilder.build(addedFolder[0].url) + log.d(projectFolder.uri) + + + let root = documentFile + if (await projectFolder.isMyChild(documentFile)){ + root = projectFolder + }else{ + root = await documentFile.getParentFile() + if (root == null || await root.exists()){ + root = documentFile + } + } - const fileParent = await documentFile.getParentFile() - log.d(await fileParent.uri) const port = 8080 let fileServer; - let url = `http://localhost:${port}/${await documentFile.getName()}`; + const path = await buildPathFromFile(documentFile,root) + log.d(`PATH ${path}`) + let url = `http://localhost:${port}/${path}`; - 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) - } + log.d(url) + fileServer = new FileServer(port,root) fileServer.start((msg)=>{ //success @@ -103,5 +92,17 @@ async function run( } +//returns a string without a '/' prefix +async function buildPathFromFile(file, rootDirectory) { + const parts = []; + let current = file; + + while (current !== null && await current.toUri() !== await rootDirectory.toUri()) { + parts.unshift(await current.getName()); // Add to the beginning + current = await current.getParentFile(); + } + + return parts.length === 0 ? await file.getName() : "/" + parts.join("/"); +} export default run; \ No newline at end of file diff --git a/src/plugins/file/src/android/documentFile.java b/src/plugins/file/src/android/documentFile.java index 8dc494f44..2a213f417 100644 --- a/src/plugins/file/src/android/documentFile.java +++ b/src/plugins/file/src/android/documentFile.java @@ -86,6 +86,7 @@ private boolean handleGetName(JSONArray args, CallbackContext cb) throws JSONExc } private boolean handleGetParentFile(JSONArray args, CallbackContext cb) throws JSONException { + android.util.Log.d("DocumentFile.java",args.getString(0)); DocumentFile f = fromUri(args.getString(0)); DocumentFile parent = f != null ? f.getParentFile() : null; cb.success(parent != null ? parent.getUri().toString() : ""); From 07a2a04ad505dd3de66562bc7dbf0b2bf90091da Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Mon, 10 Nov 2025 15:44:58 +0530 Subject: [PATCH 11/21] format --- src/fileSystem/FileObjectBuilder.ts | 36 +++--- src/fileSystem/NativeFileWrapper.ts | 20 ++-- src/fileSystem/SAFDocumentFile.ts | 123 ++++++++++--------- src/fileSystem/fileObject.ts | 2 +- src/lib/run.js | 176 ++++++++++++++-------------- 5 files changed, 176 insertions(+), 181 deletions(-) diff --git a/src/fileSystem/FileObjectBuilder.ts b/src/fileSystem/FileObjectBuilder.ts index cf547eb6e..664bfecb0 100644 --- a/src/fileSystem/FileObjectBuilder.ts +++ b/src/fileSystem/FileObjectBuilder.ts @@ -1,22 +1,22 @@ -import {FileObject} from "./fileObject"; -import {NativeFileWrapper} from "./NativeFileWrapper"; -import {SAFDocumentFile} from "./SAFDocumentFile"; +import { FileObject } from "./fileObject"; +import { NativeFileWrapper } from "./NativeFileWrapper"; +import { SAFDocumentFile } from "./SAFDocumentFile"; export class FileObjectBuilder { - async build(uri: string): Promise { - if (uri.startsWith("file://") || uri.startsWith("cdvfile://")) { - return new Promise((resolve, reject) => { - const wrapper = new NativeFileWrapper(uri, (nativeFile) => { - resolve(nativeFile); - }); + async build(uri: string): Promise { + if (uri.startsWith("file://") || uri.startsWith("cdvfile://")) { + return new Promise((resolve, reject) => { + const wrapper = new NativeFileWrapper(uri, (nativeFile) => { + resolve(nativeFile); + }); - // Catch errors from the async ready promise - wrapper.ready.catch(reject); - }); - } - if (uri.startsWith("content://")) { - return new SAFDocumentFile(uri); - } - return null; - } + // Catch errors from the async ready promise + wrapper.ready.catch(reject); + }); + } + if (uri.startsWith("content://")) { + return new SAFDocumentFile(uri); + } + return null; + } } diff --git a/src/fileSystem/NativeFileWrapper.ts b/src/fileSystem/NativeFileWrapper.ts index f4bcd434f..749c4689d 100644 --- a/src/fileSystem/NativeFileWrapper.ts +++ b/src/fileSystem/NativeFileWrapper.ts @@ -46,16 +46,16 @@ export class NativeFileWrapper implements FileObject { })(); } - async isMyChild(fileObject: FileObject): Promise { - if (!(fileObject instanceof NativeFileWrapper)){ - return false - } - if (!(await this.isDirectory())) { - return false; - } - - return fileObject.getPath().includes(this.getPath()); - } + async isMyChild(fileObject: FileObject): Promise { + if (!(fileObject instanceof NativeFileWrapper)) { + return false; + } + if (!(await this.isDirectory())) { + return false; + } + + return fileObject.getPath().includes(this.getPath()); + } private execPlugin(action: string, args: any[] = []): Promise { //console.log(`[NativeFileWrapper] execPlugin called: action=${action}, args=${JSON.stringify(args)}`); diff --git a/src/fileSystem/SAFDocumentFile.ts b/src/fileSystem/SAFDocumentFile.ts index 137adb4c9..64023dace 100644 --- a/src/fileSystem/SAFDocumentFile.ts +++ b/src/fileSystem/SAFDocumentFile.ts @@ -16,53 +16,53 @@ declare var cordova: any; export class SAFDocumentFile implements FileObject { constructor(private uri: string) {} - private execPlugin(action: string, args: any[] = []): Promise { - 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( - `[SAFDocumentFile] execPlugin error: action=${action}, error=${JSON.stringify(error)}`, - ); - reject(error); - }, - "documentFile", - action, - [this.uri, ...args], - ); - }); - } - - //if this fails then... - async isMyChild(fileObject: FileObject): Promise { - if (!(fileObject instanceof SAFDocumentFile)){ - return false - } - if (!(await this.isDirectory())) { - return false; - } - - let current: FileObject | null = fileObject; - - while (current !== null) { - const parent:FileObject | null = await current.getParentFile(); - if (parent === null) { - return false; // Reached root without finding this - } - - const parentUri = await parent.toUri(); - if (parentUri === this.uri) { - return true; // Found a match - } - - current = parent; - } - - return false; - } + private execPlugin(action: string, args: any[] = []): Promise { + 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( + `[SAFDocumentFile] execPlugin error: action=${action}, error=${JSON.stringify(error)}`, + ); + reject(error); + }, + "documentFile", + action, + [this.uri, ...args], + ); + }); + } + + //if this fails then... + async isMyChild(fileObject: FileObject): Promise { + if (!(fileObject instanceof SAFDocumentFile)) { + return false; + } + if (!(await this.isDirectory())) { + return false; + } + + let current: FileObject | null = fileObject; + + while (current !== null) { + const parent: FileObject | null = await current.getParentFile(); + if (parent === null) { + return false; // Reached root without finding this + } + + const parentUri = await parent.toUri(); + if (parentUri === this.uri) { + return true; // Found a match + } + + current = parent; + } + + return false; + } async canRead(): Promise { const stat = await this.stat(); @@ -116,28 +116,27 @@ 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; - } + private removeSuffix(str: string, suffix: string) { + return str.endsWith(suffix) ? str.slice(0, -suffix.length) : str; + } async getName(): Promise { - const parts = this.removeSuffix(this.uri,"/").split("/"); + const parts = this.removeSuffix(this.uri, "/").split("/"); return parts[parts.length - 1] || ""; } async getParentFile(): Promise { - //fixme - if (!this.uri){ - return null - } - - try{ - const result = await this.execPlugin("getParentFile") - return new SAFDocumentFile(result); - }catch (e) { - return null; - } + //fixme + if (!this.uri) { + return null; + } + try { + const result = await this.execPlugin("getParentFile"); + return new SAFDocumentFile(result); + } catch (e) { + return null; + } } async isDirectory(): Promise { diff --git a/src/fileSystem/fileObject.ts b/src/fileSystem/fileObject.ts index 6f3afbddc..d28285898 100644 --- a/src/fileSystem/fileObject.ts +++ b/src/fileSystem/fileObject.ts @@ -132,7 +132,7 @@ export interface FileObject { */ getChildByName(name: string): Promise; - isMyChild(fileObject:FileObject): Promise; + isMyChild(fileObject: FileObject): Promise; /** * Returns the parent directory of this file. diff --git a/src/lib/run.js b/src/lib/run.js index e67bf7f3e..86ea09480 100644 --- a/src/lib/run.js +++ b/src/lib/run.js @@ -1,9 +1,8 @@ import browser from "plugins/browser"; +import { FileObjectBuilder } from "../fileSystem/FileObjectBuilder"; +import { FileServer } from "./fileServer"; +import { Log } from "./Log"; import appSettings from "./settings"; -import {Log} from "./Log"; -import {FileServer} from "./fileServer"; -import {FileObjectBuilder} from "../fileSystem/FileObjectBuilder"; - /** * Starts the server and run the active file in browser @@ -13,96 +12,93 @@ import {FileObjectBuilder} from "../fileSystem/FileObjectBuilder"; */ 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; - } - } - - const activeFile = editorManager.activeFile; - if(!await activeFile?.canRun()){ - //can not run - return; - } - - const log = new Log("Code Runner") - log.d(activeFile.uri) - - - const fileObjectBuilder = new FileObjectBuilder() - const documentFile = await fileObjectBuilder.build(activeFile.uri); - - const projectFolder = await fileObjectBuilder.build(addedFolder[0].url) - log.d(projectFolder.uri) - - - let root = documentFile - if (await projectFolder.isMyChild(documentFile)){ - root = projectFolder - }else{ - root = await documentFile.getParentFile() - if (root == null || await root.exists()){ - root = documentFile - } - } - - - const port = 8080 - let fileServer; - - const path = await buildPathFromFile(documentFile,root) - log.d(`PATH ${path}`) - let url = `http://localhost:${port}/${path}`; - - log.d(url) - - fileServer = new FileServer(port,root) - - fileServer.start((msg)=>{ - //success - log.d(msg) - - if (target === "browser") { - system.openInBrowser(url); - }else{ - browser.open(url,false); - } - - - },(err)=>{ - //error - log.e(err) - }) - - - - + 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; + } + } + + const activeFile = editorManager.activeFile; + if (!(await activeFile?.canRun())) { + //can not run + return; + } + + const log = new Log("Code Runner"); + log.d(activeFile.uri); + + const fileObjectBuilder = new FileObjectBuilder(); + const documentFile = await fileObjectBuilder.build(activeFile.uri); + + const projectFolder = await fileObjectBuilder.build(addedFolder[0].url); + log.d(projectFolder.uri); + + let root = documentFile; + if (await projectFolder.isMyChild(documentFile)) { + root = projectFolder; + } else { + root = await documentFile.getParentFile(); + if (root == null || (await root.exists())) { + root = documentFile; + } + } + + const port = 8080; + let fileServer; + + const path = await buildPathFromFile(documentFile, root); + log.d(`PATH ${path}`); + let url = `http://localhost:${port}/${path}`; + + log.d(url); + + fileServer = new FileServer(port, root); + + fileServer.start( + (msg) => { + //success + log.d(msg); + + if (target === "browser") { + system.openInBrowser(url); + } else { + browser.open(url, false); + } + }, + (err) => { + //error + log.e(err); + }, + ); } //returns a string without a '/' prefix async function buildPathFromFile(file, rootDirectory) { - const parts = []; - let current = file; - - while (current !== null && await current.toUri() !== await rootDirectory.toUri()) { - parts.unshift(await current.getName()); // Add to the beginning - current = await current.getParentFile(); - } - - return parts.length === 0 ? await file.getName() : "/" + parts.join("/"); + const parts = []; + let current = file; + + while ( + current !== null && + (await current.toUri()) !== (await rootDirectory.toUri()) + ) { + parts.unshift(await current.getName()); // Add to the beginning + current = await current.getParentFile(); + } + + return parts.length === 0 ? await file.getName() : "/" + parts.join("/"); } -export default run; \ No newline at end of file +export default run; From 3222b776813609817315022d5efc99968745ea56 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Mon, 10 Nov 2025 15:51:59 +0530 Subject: [PATCH 12/21] fix: implement better uri parsing (experimental) --- .../file/src/android/documentFile.java | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/plugins/file/src/android/documentFile.java b/src/plugins/file/src/android/documentFile.java index 2a213f417..34b0fd7f6 100644 --- a/src/plugins/file/src/android/documentFile.java +++ b/src/plugins/file/src/android/documentFile.java @@ -51,10 +51,29 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo } private DocumentFile fromUri(String uriStr) { - Uri uri = Uri.parse(uriStr); - return DocumentFile.fromTreeUri(getContext(), uri); + try { + // Decode once if double-encoded + if (uriStr.contains("%25")) { + uriStr = Uri.decode(uriStr); + } + + Uri uri = Uri.parse(uriStr); + + DocumentFile file; + if (DocumentsContract.isTreeUri(uri)) { + file = DocumentFile.fromTreeUri(getContext(), uri); + } else { + file = DocumentFile.fromSingleUri(getContext(), uri); + } + + return file; + } catch (Exception e) { + e.printStackTrace(); + return null; + } } + private boolean handleExists(JSONArray args, CallbackContext cb) throws JSONException { DocumentFile f = fromUri(args.getString(0)); cb.success(f != null && f.exists() ? 1 : 0); From a05519935e91a00a77505d9ebfcff8727aa76a96 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Tue, 11 Nov 2025 15:04:40 +0530 Subject: [PATCH 13/21] add moe logging --- src/fileSystem/SAFDocumentFile.ts | 60 +++++++++++++++++++------------ src/lib/run.js | 4 ++- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/fileSystem/SAFDocumentFile.ts b/src/fileSystem/SAFDocumentFile.ts index 64023dace..04decd847 100644 --- a/src/fileSystem/SAFDocumentFile.ts +++ b/src/fileSystem/SAFDocumentFile.ts @@ -37,34 +37,50 @@ export class SAFDocumentFile implements FileObject { } //if this fails then... - async isMyChild(fileObject: FileObject): Promise { - if (!(fileObject instanceof SAFDocumentFile)) { - return false; - } - if (!(await this.isDirectory())) { - return false; - } + async isMyChild(fileObject: FileObject): Promise { + console.log(`[isMyChild] Checking if`, fileObject, `is a child of`, this); - let current: FileObject | null = fileObject; + if (!(fileObject instanceof SAFDocumentFile)) { + console.log(`[isMyChild] Not an SAFDocumentFile`); + return false; + } - while (current !== null) { - const parent: FileObject | null = await current.getParentFile(); - if (parent === null) { - return false; // Reached root without finding this - } + const isDir = await this.isDirectory(); + if (!isDir) { + console.log(`[isMyChild] This file is not a directory`); + return false; + } - const parentUri = await parent.toUri(); - if (parentUri === this.uri) { - return true; // Found a match - } + let current: FileObject | null = fileObject; - current = parent; - } + while (current !== null) { + console.log(`[isMyChild] Checking parent of`, current); + + const parent: FileObject | null = await current.getParentFile(); + if (parent === null) { + console.log(`[isMyChild] Reached root without finding match`); + return false; + } + + const parentUri = (await parent.toUri())?.replace(/\/+$/, ""); + const thisUri = this.uri?.replace(/\/+$/, ""); + + console.log(`[isMyChild] parentUri=${parentUri}, thisUri=${thisUri}`); + + if (parentUri === thisUri) { + console.log(`[isMyChild] Match found!`); + return true; + } + + current = parent; + } + + console.log(`[isMyChild] No match found after traversal`); + return false; + } - return false; - } - async canRead(): Promise { + async canRead(): Promise { const stat = await this.stat(); return !!stat.canRead; } diff --git a/src/lib/run.js b/src/lib/run.js index 86ea09480..832c20fb5 100644 --- a/src/lib/run.js +++ b/src/lib/run.js @@ -49,10 +49,12 @@ async function run( let root = documentFile; if (await projectFolder.isMyChild(documentFile)) { root = projectFolder; + log.d("Not a child of project folder"); } else { root = await documentFile.getParentFile(); - if (root == null || (await root.exists())) { + if (root == null || (!await root.exists())) { root = documentFile; + log.d("llll") } } From 9b465c227bf94c21400760a9545504dbb3ab993a Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Thu, 13 Nov 2025 16:21:34 +0530 Subject: [PATCH 14/21] fix: isMyChild --- src/fileSystem/SAFDocumentFile.ts | 43 ++++----------- .../file/src/android/documentFile.java | 52 +++++++++++++++++++ 2 files changed, 63 insertions(+), 32 deletions(-) diff --git a/src/fileSystem/SAFDocumentFile.ts b/src/fileSystem/SAFDocumentFile.ts index 04decd847..172105ef7 100644 --- a/src/fileSystem/SAFDocumentFile.ts +++ b/src/fileSystem/SAFDocumentFile.ts @@ -36,50 +36,29 @@ export class SAFDocumentFile implements FileObject { }); } - //if this fails then... async isMyChild(fileObject: FileObject): Promise { - console.log(`[isMyChild] Checking if`, fileObject, `is a child of`, this); - if (!(fileObject instanceof SAFDocumentFile)) { console.log(`[isMyChild] Not an SAFDocumentFile`); return false; } - const isDir = await this.isDirectory(); - if (!isDir) { - console.log(`[isMyChild] This file is not a directory`); - return false; - } - - let current: FileObject | null = fileObject; - - while (current !== null) { - console.log(`[isMyChild] Checking parent of`, current); - - const parent: FileObject | null = await current.getParentFile(); - if (parent === null) { - console.log(`[isMyChild] Reached root without finding match`); - return false; - } + try { + console.log(`[isMyChild] Checking if ${fileObject.uri} is a child of ${this.uri}`); + const result = await this.execPlugin("isMyChild", [fileObject.uri]); - const parentUri = (await parent.toUri())?.replace(/\/+$/, ""); - const thisUri = this.uri?.replace(/\/+$/, ""); + // result will be 1 or 0 (from the native plugin) + const isChild = result === 1 || result === true; - console.log(`[isMyChild] parentUri=${parentUri}, thisUri=${thisUri}`); - - if (parentUri === thisUri) { - console.log(`[isMyChild] Match found!`); - return true; - } - - current = parent; + console.log(`[isMyChild] Result from native =`, isChild); + return isChild; + } catch (err) { + console.error(`[isMyChild] Error:`, err); + return false; } - - console.log(`[isMyChild] No match found after traversal`); - return false; } + async canRead(): Promise { const stat = await this.stat(); return !!stat.canRead; diff --git a/src/plugins/file/src/android/documentFile.java b/src/plugins/file/src/android/documentFile.java index 34b0fd7f6..a936d7598 100644 --- a/src/plugins/file/src/android/documentFile.java +++ b/src/plugins/file/src/android/documentFile.java @@ -14,6 +14,8 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import android.provider.DocumentsContract; + public class documentFile extends CordovaPlugin { @@ -41,6 +43,7 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo case "canWrite": return handleCanWrite(args, callbackContext); case "childByNameExists": return handleChildByNameExists(args, callbackContext); case "getChildByName": return handleGetChildByName(args, callbackContext); + case "isMyChild": return handleIsMyChild(args, callbackContext); case "toUri": return handleToUri(args, callbackContext); default: return false; } @@ -50,6 +53,55 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo } } + private boolean handleIsMyChild(JSONArray args, CallbackContext cb) throws JSONException { + String parentUriStr = args.getString(0); + String childUriStr = args.getString(1); + + try { + Uri parentUri = Uri.parse(parentUriStr); + Uri childUri = Uri.parse(childUriStr); + + boolean result = false; + + // SAF-safe check + if (DocumentsContract.isDocumentUri(getContext(), childUri)) { + try { + result = DocumentsContract.isChildDocument( + getContext().getContentResolver(), + parentUri, + childUri + ); + } catch (Exception e) { + // If that fails, fallback to ID-based check + result = isChildByDocIdFallback(parentUri, childUri); + } + } else { + // Non-SAF fallback: compare normalized paths + String p = parentUri.getPath(); + String c = childUri.getPath(); + if (p != null && c != null && c.startsWith(p)) { + result = true; + } + } + + cb.success(result ? 1 : 0); + } catch (Exception e) { + cb.error("Error: " + e.getMessage()); + } + return true; + } + + /** + * Fallback if DocumentsContract.isChildDocument() fails. + * Checks if the child’s document ID starts with parent’s document ID. + */ + private boolean isChildByDocIdFallback(Uri parentUri, Uri childUri) { + String parentId = DocumentsContract.getDocumentId(parentUri); + String childId = DocumentsContract.getDocumentId(childUri); + return childId != null && parentId != null && childId.startsWith(parentId + "%2F"); + } + + private DocumentFile fromUri(String uriStr) { try { // Decode once if double-encoded From 60cb4e71f6982755cf17e6a92f1312682b7f5508 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Thu, 13 Nov 2025 16:22:31 +0530 Subject: [PATCH 15/21] . --- src/fileSystem/SAFDocumentFile.ts | 40 +++++++++++++++---------------- src/lib/run.js | 6 ++--- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/fileSystem/SAFDocumentFile.ts b/src/fileSystem/SAFDocumentFile.ts index 172105ef7..3797dab29 100644 --- a/src/fileSystem/SAFDocumentFile.ts +++ b/src/fileSystem/SAFDocumentFile.ts @@ -36,30 +36,30 @@ export class SAFDocumentFile implements FileObject { }); } - async isMyChild(fileObject: FileObject): Promise { - if (!(fileObject instanceof SAFDocumentFile)) { - console.log(`[isMyChild] Not an SAFDocumentFile`); - return false; - } - - try { - console.log(`[isMyChild] Checking if ${fileObject.uri} is a child of ${this.uri}`); - const result = await this.execPlugin("isMyChild", [fileObject.uri]); - - // result will be 1 or 0 (from the native plugin) - const isChild = result === 1 || result === true; + async isMyChild(fileObject: FileObject): Promise { + if (!(fileObject instanceof SAFDocumentFile)) { + console.log(`[isMyChild] Not an SAFDocumentFile`); + return false; + } - console.log(`[isMyChild] Result from native =`, isChild); - return isChild; - } catch (err) { - console.error(`[isMyChild] Error:`, err); - return false; - } - } + try { + console.log( + `[isMyChild] Checking if ${fileObject.uri} is a child of ${this.uri}`, + ); + const result = await this.execPlugin("isMyChild", [fileObject.uri]); + // result will be 1 or 0 (from the native plugin) + const isChild = result === 1 || result === true; + console.log(`[isMyChild] Result from native =`, isChild); + return isChild; + } catch (err) { + console.error(`[isMyChild] Error:`, err); + return false; + } + } - async canRead(): Promise { + async canRead(): Promise { const stat = await this.stat(); return !!stat.canRead; } diff --git a/src/lib/run.js b/src/lib/run.js index 832c20fb5..883334ee0 100644 --- a/src/lib/run.js +++ b/src/lib/run.js @@ -49,12 +49,12 @@ async function run( let root = documentFile; if (await projectFolder.isMyChild(documentFile)) { root = projectFolder; - log.d("Not a child of project folder"); + log.d("Not a child of project folder"); } else { root = await documentFile.getParentFile(); - if (root == null || (!await root.exists())) { + if (root == null || !(await root.exists())) { root = documentFile; - log.d("llll") + log.d("llll"); } } From 6c0ca0866d0688295f54d1531a0440a6ed2e92d9 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Thu, 11 Dec 2025 10:29:42 +0530 Subject: [PATCH 16/21] fix: terminal --- package-lock.json | 13 +- package.json | 6 +- src/components/terminal/terminalManager.js | 4 - src/main.js | 1 - src/plugins/terminal/plugin.xml | 29 +- .../terminal/src/android/Executor.java | 535 ++++++++---------- .../src/android/KeepAliveService.java | 117 ++++ .../terminal/src/android/TerminalService.java | 413 -------------- src/plugins/terminal/www/Executor.js | 46 +- 9 files changed, 379 insertions(+), 785 deletions(-) create mode 100644 src/plugins/terminal/src/android/KeepAliveService.java delete mode 100644 src/plugins/terminal/src/android/TerminalService.java diff --git a/package-lock.json b/package-lock.json index c9e575333..2f1835e6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,6 @@ "@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", @@ -4335,10 +4334,6 @@ "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 @@ -4645,9 +4640,9 @@ } }, "node_modules/cordova-android/node_modules/dedent": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", - "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -11208,7 +11203,7 @@ "src/plugins/proot": { "name": "com.foxdebug.acode.rk.exec.proot", "version": "1.0.0", - "dev": true, + "extraneous": true, "license": "MIT" }, "src/plugins/sdcard": { diff --git a/package.json b/package.json index 278957a56..c21cb7834 100644 --- a/package.json +++ b/package.json @@ -34,11 +34,16 @@ }, "cordova-plugin-websocket": {}, "cordova-plugin-buildinfo": {}, + "com.foxdebug.acode.rk.exec.terminal": {}, "cordova-plugin-browser": {}, +<<<<<<< HEAD "cordova-plugin-sftp": {}, "cordova-plugin-system": {}, "com.foxdebug.acode.rk.exec.terminal": {}, "com.foxdebug.acode.rk.exec.proot": {} +======= + "cordova-plugin-system": {} +>>>>>>> parent of 10f82de0 (Terminal Service (#1570)) }, "platforms": [ "android" @@ -63,7 +68,6 @@ "@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", diff --git a/src/components/terminal/terminalManager.js b/src/components/terminal/terminalManager.js index 9c2f424a1..94c1eae94 100644 --- a/src/components/terminal/terminalManager.js +++ b/src/components/terminal/terminalManager.js @@ -588,10 +588,6 @@ class TerminalManager { // Remove from map this.terminals.delete(terminalId); - if (this.getAllTerminals().size <= 0) { - Executor.stopService(); - } - console.log(`Terminal ${terminalId} closed`); } catch (error) { console.error(`Error closing terminal ${terminalId}:`, error); diff --git a/src/main.js b/src/main.js index edafc5a0d..17b32c0cf 100644 --- a/src/main.js +++ b/src/main.js @@ -178,7 +178,6 @@ async function onDeviceReady() { system.requestPermission("android.permission.READ_EXTERNAL_STORAGE"); system.requestPermission("android.permission.WRITE_EXTERNAL_STORAGE"); - system.requestPermission("android.permission.POST_NOTIFICATIONS"); const { versionCode } = BuildInfo; diff --git a/src/plugins/terminal/plugin.xml b/src/plugins/terminal/plugin.xml index fbde6010f..27e78fc14 100644 --- a/src/plugins/terminal/plugin.xml +++ b/src/plugins/terminal/plugin.xml @@ -20,9 +20,9 @@ - - + + @@ -31,25 +31,32 @@ + android:name="com.foxdebug.acode.rk.exec.terminal.AlpineDocumentProvider" + android:authorities="${applicationId}.documents" + android:exported="true" + android:grantUriPermissions="true" + android:icon="@mipmap/ic_launcher" + android:permission="android.permission.MANAGE_DOCUMENTS"> + + + + + + + @@ -57,9 +64,5 @@ - - - -
\ No newline at end of file diff --git a/src/plugins/terminal/src/android/Executor.java b/src/plugins/terminal/src/android/Executor.java index 3e0b4e83d..338560141 100644 --- a/src/plugins/terminal/src/android/Executor.java +++ b/src/plugins/terminal/src/android/Executor.java @@ -3,250 +3,91 @@ import org.apache.cordova.*; import org.json.*; -import android.content.ComponentName; +import org.apache.cordova.CordovaInterface; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CordovaWebView; +import org.apache.cordova.CallbackContext; +import org.apache.cordova.PluginResult; + +import org.json.JSONArray; +import org.json.JSONException; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; + +import java.util.Map; +import java.util.HashMap; +import java.util.TimeZone; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + import android.content.Context; import android.content.Intent; -import android.content.ServiceConnection; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.Message; -import android.os.Messenger; -import android.os.RemoteException; -import android.util.Log; - -import java.util.UUID; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import android.Manifest; -import android.content.pm.PackageManager; import android.os.Build; +import android.content.pm.PackageManager; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; -import android.app.Activity; public class Executor extends CordovaPlugin { - private Messenger serviceMessenger; - private boolean isServiceBound; - private boolean isServiceBinding; // Track if binding is in progress + private final Map processes = new ConcurrentHashMap<>(); + private final Map processInputs = new ConcurrentHashMap<>(); + private final Map processCallbacks = new ConcurrentHashMap<>(); + private Context context; - private Activity activity; - private final Messenger handlerMessenger = new Messenger(new IncomingHandler()); - private CountDownLatch serviceConnectedLatch; - private final java.util.Map callbackContextMap = new java.util.concurrent.ConcurrentHashMap<>(); - - private static final int REQUEST_POST_NOTIFICATIONS = 1001; - - private void askNotificationPermission(Activity context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - if (ContextCompat.checkSelfPermission( - context, Manifest.permission.POST_NOTIFICATIONS) == - PackageManager.PERMISSION_GRANTED) { - } else if (ActivityCompat.shouldShowRequestPermissionRationale( - context, Manifest.permission.POST_NOTIFICATIONS)) { - ActivityCompat.requestPermissions( - context, - new String[]{Manifest.permission.POST_NOTIFICATIONS}, - REQUEST_POST_NOTIFICATIONS - ); - } else { - ActivityCompat.requestPermissions( - context, - new String[]{Manifest.permission.POST_NOTIFICATIONS}, - REQUEST_POST_NOTIFICATIONS - ); - } - } - } - @Override - public void initialize(CordovaInterface cordova, CordovaWebView webView) { - super.initialize(cordova, webView); - this.context = cordova.getContext(); - this.activity = cordova.getActivity(); - askNotificationPermission(activity); - - // Don't bind service immediately - wait until needed - Log.d("Executor", "Plugin initialized - service will be started when needed"); - } - /** - * Ensure service is bound and ready for communication - * Returns true if service is ready, false if binding failed - */ - private boolean ensureServiceBound(CallbackContext callbackContext) { - // If already bound, return immediately - if (isServiceBound && serviceMessenger != null) { - return true; - } + @Override + public void initialize(CordovaInterface cordova, CordovaWebView webView) { + super.initialize(cordova, webView); + this.context = cordova.getContext(); + + } - // If binding is already in progress, wait for it - if (isServiceBinding) { - try { - if (serviceConnectedLatch != null && - serviceConnectedLatch.await(10, TimeUnit.SECONDS)) { - return isServiceBound; - } else { - callbackContext.error("Service binding timeout"); - return false; - } - } catch (InterruptedException e) { - callbackContext.error("Service binding interrupted: " + e.getMessage()); - return false; + private void checkStopService() { + boolean hasRunning = false; + + for (Process p : processes.values()) { + if (p != null && p.isAlive()) { + hasRunning = true; + break; } } - // Start binding process - Log.d("Executor", "Starting service binding..."); - return bindServiceNow(callbackContext); - } - - /** - * Immediately bind to service - */ - private boolean bindServiceNow(CallbackContext callbackContext) { - if (isServiceBinding) { - return false; // Already binding + if (!hasRunning) { + // Stop foreground service + Intent serviceIntent = new Intent(context, KeepAliveService.class); + context.stopService(serviceIntent); } + } - isServiceBinding = true; - serviceConnectedLatch = new CountDownLatch(1); - - Intent intent = new Intent(context, TerminalService.class); - - // Start the service first - context.startService(intent); - - // Then bind to it - boolean bindResult = context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); - - if (!bindResult) { - Log.e("Executor", "Failed to bind to service"); - isServiceBinding = false; - callbackContext.error("Failed to bind to service"); - return false; - } + private void startService(){ + Intent serviceIntent = new Intent(this.context, KeepAliveService.class); - // Wait for connection - try { - if (serviceConnectedLatch.await(10, TimeUnit.SECONDS)) { - Log.d("Executor", "Service bound successfully"); - return isServiceBound; - } else { - Log.e("Executor", "Service binding timeout"); - callbackContext.error("Service binding timeout"); - isServiceBinding = false; - return false; - } - } catch (InterruptedException e) { - Log.e("Executor", "Service binding interrupted: " + e.getMessage()); - callbackContext.error("Service binding interrupted: " + e.getMessage()); - isServiceBinding = false; - return false; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Android 8.0+ needs startForegroundService() + this.context.startForegroundService(serviceIntent); + } else { + // Older versions use startService() + this.context.startService(serviceIntent); } } - private final ServiceConnection serviceConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - Log.d("Executor", "Service connected"); - serviceMessenger = new Messenger(service); - isServiceBound = true; - isServiceBinding = false; - if (serviceConnectedLatch != null) { - serviceConnectedLatch.countDown(); - } - } + - @Override - public void onServiceDisconnected(ComponentName name) { - Log.w("Executor", "Service disconnected"); - serviceMessenger = null; - isServiceBound = false; - isServiceBinding = false; - serviceConnectedLatch = new CountDownLatch(1); - } - }; - - private class IncomingHandler extends Handler { - @Override - public void handleMessage(Message msg) { - Bundle bundle = msg.getData(); - String id = bundle.getString("id"); - String action = bundle.getString("action"); - String data = bundle.getString("data"); - - if (action.equals("exec_result")) { - CallbackContext callbackContext = getCallbackContext(id); - if (callbackContext != null) { - if (bundle.getBoolean("isSuccess", false)) { - callbackContext.success(data); - } else { - callbackContext.error(data); - } - cleanupCallback(id); - } - } else { - String pid = id; - CallbackContext callbackContext = getCallbackContext(pid); - - if (callbackContext != null) { - switch (action) { - case "stdout": - case "stderr": - PluginResult result = new PluginResult(PluginResult.Status.OK, action + ":" + data); - result.setKeepCallback(true); - callbackContext.sendPluginResult(result); - break; - case "exit": - cleanupCallback(pid); - callbackContext.success("exit:" + data); - break; - case "isRunning": - callbackContext.success(data); - cleanupCallback(pid); - break; - } - } - } - } - } @Override public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { - // For actions that don't need the service, handle them directly - if (action.equals("loadLibrary")) { - try { - System.load(args.getString(0)); - callbackContext.success("Library loaded successfully."); - } catch (Exception e) { - callbackContext.error("Failed to load library: " + e.getMessage()); - } - return true; - } + switch (action) { + case "start": - if (action.equals("stopService")) { - stopServiceNow(); - callbackContext.success("Service stopped"); - return true; - } + startService(); - // For all other actions, ensure service is bound first - if (!ensureServiceBound(callbackContext)) { - // Error already sent by ensureServiceBound - return false; - } - switch (action) { - case "start": String cmdStart = args.getString(0); String pid = UUID.randomUUID().toString(); - callbackContextMap.put(pid, callbackContext); - startProcess(pid, cmdStart, args.getString(1)); + startProcess(pid, cmdStart,args.getString(1), callbackContext); return true; case "write": String pidWrite = args.getString(0); @@ -258,14 +99,22 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo stopProcess(pidStop, callbackContext); return true; case "exec": - String execId = UUID.randomUUID().toString(); - callbackContextMap.put(execId, callbackContext); - exec(execId, args.getString(0), args.getString(1)); + + startService(); + + + exec(args.getString(0),args.getString(1), callbackContext); return true; case "isRunning": - String pidCheck = args.getString(0); - callbackContextMap.put(pidCheck, callbackContext); - isProcessRunning(pidCheck); + isProcessRunning(args.getString(0), callbackContext); + return true; + case "loadLibrary": + try { + System.load(args.getString(0)); + callbackContext.success("Library loaded successfully."); + } catch (Exception e) { + callbackContext.error("Failed to load library: " + e.getMessage()); + } return true; default: callbackContext.error("Unknown action: " + action); @@ -273,120 +122,165 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo } } - private void stopServiceNow() { - if (isServiceBound) { - try { - context.unbindService(serviceConnection); - Log.d("Executor", "Service unbound"); - } catch (IllegalArgumentException ignored) { - // already unbound - } - isServiceBound = false; - } - isServiceBinding = false; - - Intent intent = new Intent(context, TerminalService.class); - boolean stopped = context.stopService(intent); - Log.d("Executor", "Service stop result: " + stopped); - - serviceMessenger = null; - if (serviceConnectedLatch == null) { - serviceConnectedLatch = new CountDownLatch(1); - } - } + private void exec(String cmd,String alpine, CallbackContext callbackContext) { + cordova.getThreadPool().execute(() -> { + try { + if (cmd != null && !cmd.isEmpty()) { + String xcmd = cmd; + if(alpine.equals("true")){ + xcmd = "source $PREFIX/init-sandbox.sh "+cmd; + } + + ProcessBuilder builder = new ProcessBuilder("sh", "-c", xcmd); + + // Set environment variables + Map env = builder.environment(); + env.put("PREFIX", context.getFilesDir().getAbsolutePath()); + env.put("NATIVE_DIR", context.getApplicationInfo().nativeLibraryDir); + TimeZone tz = TimeZone.getDefault(); + String timezoneId = tz.getID(); + env.put("ANDROID_TZ", timezoneId); + + try { + int target = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).applicationInfo.targetSdkVersion; + env.put("FDROID", String.valueOf(target <= 28)); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } - private void startProcess(String pid, String cmd, String alpine) { - CallbackContext callbackContext = getCallbackContext(pid); - if (callbackContext != null) { - PluginResult result = new PluginResult(PluginResult.Status.OK, pid); - result.setKeepCallback(true); - callbackContext.sendPluginResult(result); - } - Message msg = Message.obtain(null, TerminalService.MSG_START_PROCESS); - msg.replyTo = handlerMessenger; - Bundle bundle = new Bundle(); - bundle.putString("id", pid); - bundle.putString("cmd", cmd); - bundle.putString("alpine", alpine); - msg.setData(bundle); - try { - serviceMessenger.send(msg); - } catch (RemoteException e) { - CallbackContext errorContext = getCallbackContext(pid); - if (errorContext != null) { - errorContext.error("Failed to start process: " + e.getMessage()); - cleanupCallback(pid); + Process process = builder.start(); + + // Capture stdout + BufferedReader stdOutReader = new BufferedReader( + new InputStreamReader(process.getInputStream())); + StringBuilder stdOut = new StringBuilder(); + String line; + while ((line = stdOutReader.readLine()) != null) { + stdOut.append(line).append("\n"); + } + + // Capture stderr + BufferedReader stdErrReader = new BufferedReader( + new InputStreamReader(process.getErrorStream())); + StringBuilder stdErr = new StringBuilder(); + while ((line = stdErrReader.readLine()) != null) { + stdErr.append(line).append("\n"); + } + + int exitCode = process.waitFor(); + if (exitCode == 0) { + callbackContext.success(stdOut.toString().trim()); + } else { + String errorOutput = stdErr.toString().trim(); + if (errorOutput.isEmpty()) { + errorOutput = "Command exited with code: " + exitCode; + } + callbackContext.error(errorOutput); + } + } else { + callbackContext.error("Expected one non-empty string argument."); } + } catch (Exception e) { + e.printStackTrace(); + callbackContext.error("Exception: " + e.getMessage()); } + }); } - private void exec(String execId, String cmd, String alpine) { - Message msg = Message.obtain(null, TerminalService.MSG_EXEC); - msg.replyTo = handlerMessenger; - Bundle bundle = new Bundle(); - bundle.putString("id", execId); - bundle.putString("cmd", cmd); - bundle.putString("alpine", alpine); - msg.setData(bundle); - try { - serviceMessenger.send(msg); - } catch (RemoteException e) { - CallbackContext callbackContext = getCallbackContext(execId); - if (callbackContext != null) { - callbackContext.error("Failed to execute command: " + e.getMessage()); - cleanupCallback(execId); + private void startProcess(String pid, String cmd,String alpine, CallbackContext callbackContext) { + cordova.getThreadPool().execute(() -> { + try { + String xcmd = cmd; + if(alpine.equals("true")){ + xcmd = "source $PREFIX/init-sandbox.sh "+cmd; + } + ProcessBuilder builder = new ProcessBuilder("sh", "-c", xcmd); + + // Set environment variables + Map env = builder.environment(); + env.put("PREFIX", context.getFilesDir().getAbsolutePath()); + env.put("NATIVE_DIR", context.getApplicationInfo().nativeLibraryDir); + TimeZone tz = TimeZone.getDefault(); + String timezoneId = tz.getID(); + env.put("ANDROID_TZ", timezoneId); + + try { + int target = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).applicationInfo.targetSdkVersion; + env.put("FDROID", String.valueOf(target <= 28)); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + + + + Process process = builder.start(); + + processes.put(pid, process); + processInputs.put(pid, process.getOutputStream()); + processCallbacks.put(pid, callbackContext); + + PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, pid); + pluginResult.setKeepCallback(true); + callbackContext.sendPluginResult(pluginResult); + + // stdout thread + new Thread(() -> streamOutput(process.getInputStream(), pid, "stdout")).start(); + // stderr thread + new Thread(() -> streamOutput(process.getErrorStream(), pid, "stderr")).start(); + + int exitCode = process.waitFor(); + sendPluginMessage(pid, "exit:" + exitCode); + cleanup(pid); + } catch (Exception e) { + callbackContext.error("Failed to start process: " + e.getMessage()); } - } + }); } private void writeToProcess(String pid, String input, CallbackContext callbackContext) { - Message msg = Message.obtain(null, TerminalService.MSG_WRITE_TO_PROCESS); - Bundle bundle = new Bundle(); - bundle.putString("id", pid); - bundle.putString("input", input); - msg.setData(bundle); try { - serviceMessenger.send(msg); - callbackContext.success("Written to process"); - } catch (RemoteException e) { + OutputStream os = processInputs.get(pid); + if (os != null) { + os.write((input + "\n").getBytes()); + os.flush(); + callbackContext.success("Written to process"); + } else { + callbackContext.error("Process not found or closed"); + } + } catch (IOException e) { callbackContext.error("Write error: " + e.getMessage()); } } private void stopProcess(String pid, CallbackContext callbackContext) { - Message msg = Message.obtain(null, TerminalService.MSG_STOP_PROCESS); - Bundle bundle = new Bundle(); - bundle.putString("id", pid); - msg.setData(bundle); - try { - serviceMessenger.send(msg); + Process process = processes.get(pid); + if (process != null) { + process.destroy(); + cleanup(pid); + // Check if we should stop the service + checkStopService(); callbackContext.success("Process terminated"); - } catch (RemoteException e) { - callbackContext.error("Stop error: " + e.getMessage()); + } else { + callbackContext.error("No such process"); } } - private void isProcessRunning(String pid) { - Message msg = Message.obtain(null, TerminalService.MSG_IS_RUNNING); - msg.replyTo = handlerMessenger; - Bundle bundle = new Bundle(); - bundle.putString("id", pid); - msg.setData(bundle); - try { - serviceMessenger.send(msg); - } catch (RemoteException e) { - CallbackContext callbackContext = getCallbackContext(pid); - if (callbackContext != null) { - callbackContext.error("Check running error: " + e.getMessage()); - cleanupCallback(pid); + private void isProcessRunning(String pid, CallbackContext callbackContext) { + Process process = processes.get(pid); + + if (process != null) { + if (process.isAlive()) { + callbackContext.success("running"); + } else { + cleanup(pid); + callbackContext.success("exited"); } + } else { + callbackContext.success("not_found"); } } - private CallbackContext getCallbackContext(String id) { - return callbackContextMap.get(id); - } private void cleanupCallback(String id) { callbackContextMap.remove(id); @@ -395,5 +289,22 @@ private void cleanupCallback(String id) { @Override public void onDestroy() { super.onDestroy(); + Intent serviceIntent = new Intent(context, KeepAliveService.class); + context.stopService(serviceIntent); + } + + private void sendPluginMessage(String pid, String message) { + CallbackContext ctx = processCallbacks.get(pid); + if (ctx != null) { + PluginResult result = new PluginResult(PluginResult.Status.OK, message); + result.setKeepCallback(true); + ctx.sendPluginResult(result); + } + } + + private void cleanup(String pid) { + processes.remove(pid); + processInputs.remove(pid); + processCallbacks.remove(pid); } -} \ No newline at end of file +} diff --git a/src/plugins/terminal/src/android/KeepAliveService.java b/src/plugins/terminal/src/android/KeepAliveService.java new file mode 100644 index 000000000..ca35546b2 --- /dev/null +++ b/src/plugins/terminal/src/android/KeepAliveService.java @@ -0,0 +1,117 @@ +package com.foxdebug.acode.rk.exec.terminal; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.os.Build; +import android.os.IBinder; +import android.os.PowerManager; + +public class KeepAliveService extends Service { + + private static final String CHANNEL_ID = "keepalive_channel"; + private static final int NOTIFICATION_ID = 101; + + private PowerManager.WakeLock wakeLock; + + public static final String ACTION_ACQUIRE = "ACQUIRE_WAKELOCK"; + public static final String ACTION_EXIT = "EXIT_SERVICE"; + + @Override + public void onCreate() { + super.onCreate(); + createNotificationChannel(); + startForeground(NOTIFICATION_ID, buildNotification()); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + + if (intent != null && intent.getAction() != null) { + switch (intent.getAction()) { + + case ACTION_ACQUIRE: + acquireWakeLock(); + break; + + case ACTION_EXIT: + stopSelf(); + break; + } + } + + // Update notification (in case state changed) + startForeground(NOTIFICATION_ID, buildNotification()); + return START_STICKY; + } + + private void acquireWakeLock() { + if (wakeLock == null) { + PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); + wakeLock = pm.newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, + "KeepAliveService:WakeLock"); + wakeLock.setReferenceCounted(false); + } + + if (!wakeLock.isHeld()) { + wakeLock.acquire(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (wakeLock != null && wakeLock.isHeld()) { + wakeLock.release(); + } + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private Notification buildNotification() { + + // Intent: Acquire WakeLock + Intent acquireIntent = new Intent(this, KeepAliveService.class); + acquireIntent.setAction(ACTION_ACQUIRE); + + PendingIntent acquirePending = PendingIntent.getService( + this, 0, acquireIntent, PendingIntent.FLAG_IMMUTABLE); + + // Intent: Exit Service + Intent exitIntent = new Intent(this, KeepAliveService.class); + exitIntent.setAction(ACTION_EXIT); + + PendingIntent exitPending = PendingIntent.getService( + this, 1, exitIntent, PendingIntent.FLAG_IMMUTABLE); + + Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID) + .setContentTitle("KeepAlive Service") + .setContentText("Running in foreground") + .setSmallIcon(android.R.drawable.ic_lock_idle_alarm) + .addAction(0, "Acquire WakeLock", acquirePending) + .addAction(0, "Exit", exitPending) + .setOngoing(true); + + return builder.build(); + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + "KeepAlive", + NotificationManager.IMPORTANCE_LOW); + + NotificationManager manager = getSystemService(NotificationManager.class); + manager.createNotificationChannel(channel); + } + } +} diff --git a/src/plugins/terminal/src/android/TerminalService.java b/src/plugins/terminal/src/android/TerminalService.java deleted file mode 100644 index f8e26c67f..000000000 --- a/src/plugins/terminal/src/android/TerminalService.java +++ /dev/null @@ -1,413 +0,0 @@ -package com.foxdebug.acode.rk.exec.terminal; - -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.Message; -import android.os.Messenger; -import android.os.PowerManager; -import android.os.RemoteException; -import androidx.core.app.NotificationCompat; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; -import java.lang.reflect.Field; - -import java.util.TimeZone; -import java.util.Map; -import java.util.HashMap; - - - -public class TerminalService extends Service { - - public static final int MSG_START_PROCESS = 1; - public static final int MSG_WRITE_TO_PROCESS = 2; - public static final int MSG_STOP_PROCESS = 3; - public static final int MSG_IS_RUNNING = 4; - public static final int MSG_EXEC = 5; - - public static final String CHANNEL_ID = "terminal_exec_channel"; - - public static final String ACTION_EXIT_SERVICE = "com.foxdebug.acode.ACTION_EXIT_SERVICE"; - public static final String ACTION_TOGGLE_WAKE_LOCK = "com.foxdebug.acode.ACTION_TOGGLE_WAKE_LOCK"; - - private final Map processes = new ConcurrentHashMap<>(); - private final Map processInputs = new ConcurrentHashMap<>(); - private final Map clientMessengers = new ConcurrentHashMap<>(); - private final java.util.concurrent.ExecutorService threadPool = Executors.newCachedThreadPool(); - - private final Messenger serviceMessenger = new Messenger(new ServiceHandler()); - - private PowerManager.WakeLock wakeLock; - private boolean isWakeLockHeld = false; - - @Override - public IBinder onBind(Intent intent) { - return serviceMessenger.getBinder(); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (intent != null) { - String action = intent.getAction(); - if (ACTION_EXIT_SERVICE.equals(action)) { - stopForeground(true); - stopSelf(); - return START_NOT_STICKY; - } else if (ACTION_TOGGLE_WAKE_LOCK.equals(action)) { - toggleWakeLock(); - } - } - return START_STICKY; - } - - private class ServiceHandler extends Handler { - @Override - public void handleMessage(Message msg) { - Bundle bundle = msg.getData(); - String id = bundle.getString("id"); - Messenger clientMessenger = msg.replyTo; - - switch (msg.what) { - case MSG_START_PROCESS: - String cmd = bundle.getString("cmd"); - String alpine = bundle.getString("alpine"); - clientMessengers.put(id, clientMessenger); - startProcess(id, cmd, alpine); - break; - case MSG_WRITE_TO_PROCESS: - String input = bundle.getString("input"); - writeToProcess(id, input); - break; - case MSG_STOP_PROCESS: - stopProcess(id); - break; - case MSG_IS_RUNNING: - isProcessRunning(id, clientMessenger); - break; - case MSG_EXEC: - String execCmd = bundle.getString("cmd"); - String execAlpine = bundle.getString("alpine"); - clientMessengers.put(id, clientMessenger); - exec(id, execCmd, execAlpine); - break; - } - } - } - - private void toggleWakeLock() { - if (isWakeLockHeld) { - releaseWakeLock(); - } else { - acquireWakeLock(); - } - updateNotification(); - } - - private void acquireWakeLock() { - if (wakeLock == null) { - PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); - wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "AcodeTerminal:WakeLock"); - } - - if (!isWakeLockHeld) { - wakeLock.acquire(); - isWakeLockHeld = true; - } - } - - private void releaseWakeLock() { - if (wakeLock != null && isWakeLockHeld) { - wakeLock.release(); - isWakeLockHeld = false; - } - } - - private void startProcess(String pid, String cmd, String alpine) { - threadPool.execute(() -> { - try { - String xcmd = alpine.equals("true") ? "source $PREFIX/init-sandbox.sh " + cmd : cmd; - ProcessBuilder builder = new ProcessBuilder("sh", "-c", xcmd); - - Map env = builder.environment(); - env.put("PREFIX", getFilesDir().getAbsolutePath()); - env.put("NATIVE_DIR", getApplicationInfo().nativeLibraryDir); - TimeZone tz = TimeZone.getDefault(); - String timezoneId = tz.getID(); - env.put("ANDROID_TZ", timezoneId); - - try { - int target = getPackageManager().getPackageInfo(getPackageName(), 0).applicationInfo.targetSdkVersion; - env.put("FDROID", String.valueOf(target <= 28)); - } catch (Exception e) { - e.printStackTrace(); - } - - Process process = builder.start(); - processes.put(pid, process); - processInputs.put(pid, process.getOutputStream()); - threadPool.execute(() -> streamOutput(process.getInputStream(), pid, "stdout")); - threadPool.execute(() -> streamOutput(process.getErrorStream(), pid, "stderr")); - threadPool.execute(() -> { - try { - int exitCode = process.waitFor(); - sendMessageToClient(pid, "exit", String.valueOf(exitCode)); - cleanup(pid); - } catch (InterruptedException e) { - e.printStackTrace(); - } - }); - } catch (IOException e) { - e.printStackTrace(); - sendMessageToClient(pid, "stderr", "Failed to start process: " + e.getMessage()); - sendMessageToClient(pid, "exit", "1"); - cleanup(pid); - } - }); - } - - private void exec(String execId, String cmd, String alpine) { - threadPool.execute(() -> { - try { - String xcmd = alpine.equals("true") ? "source $PREFIX/init-sandbox.sh " + cmd : cmd; - ProcessBuilder builder = new ProcessBuilder("sh", "-c", xcmd); - Map env = builder.environment(); - env.put("PREFIX", getFilesDir().getAbsolutePath()); - env.put("NATIVE_DIR", getApplicationInfo().nativeLibraryDir); - TimeZone tz = TimeZone.getDefault(); - String timezoneId = tz.getID(); - env.put("ANDROID_TZ", timezoneId); - - try { - int target = getPackageManager().getPackageInfo(getPackageName(), 0).applicationInfo.targetSdkVersion; - env.put("FDROID", String.valueOf(target <= 28)); - } catch (Exception e) { - e.printStackTrace(); - } - - Process process = builder.start(); - BufferedReader stdOutReader = new BufferedReader( - new InputStreamReader(process.getInputStream())); - StringBuilder stdOut = new StringBuilder(); - String line; - while ((line = stdOutReader.readLine()) != null) { - stdOut.append(line).append("\n"); - } - - BufferedReader stdErrReader = new BufferedReader( - new InputStreamReader(process.getErrorStream())); - StringBuilder stdErr = new StringBuilder(); - while ((line = stdErrReader.readLine()) != null) { - stdErr.append(line).append("\n"); - } - - int exitCode = process.waitFor(); - - if (exitCode == 0) { - sendExecResultToClient(execId, true, stdOut.toString().trim()); - } else { - String errorOutput = stdErr.toString().trim(); - if (errorOutput.isEmpty()) { - errorOutput = "Command exited with code: " + exitCode; - } - sendExecResultToClient(execId, false, errorOutput); - } - - cleanup(execId); - } catch (Exception e) { - sendExecResultToClient(execId, false, "Exception: " + e.getMessage()); - cleanup(execId); - } - }); - } - - private void streamOutput(InputStream inputStream, String pid, String streamType) { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { - String line; - while ((line = reader.readLine()) != null) { - sendMessageToClient(pid, streamType, line); - } - } catch (IOException ignored) { - } - } - - private void sendMessageToClient(String id, String action, String data) { - Messenger clientMessenger = clientMessengers.get(id); - if (clientMessenger != null) { - try { - Message msg = Message.obtain(); - Bundle bundle = new Bundle(); - bundle.putString("id", id); - bundle.putString("action", action); - bundle.putString("data", data); - msg.setData(bundle); - clientMessenger.send(msg); - } catch (RemoteException e) { - cleanup(id); - } - } - } - - private void sendExecResultToClient(String id, boolean isSuccess, String data) { - Messenger clientMessenger = clientMessengers.get(id); - if (clientMessenger != null) { - try { - Message msg = Message.obtain(); - Bundle bundle = new Bundle(); - bundle.putString("id", id); - bundle.putString("action", "exec_result"); - bundle.putString("data", data); - bundle.putBoolean("isSuccess", isSuccess); - msg.setData(bundle); - clientMessenger.send(msg); - } catch (RemoteException e) { - cleanup(id); - } - } - } - - private void writeToProcess(String pid, String input) { - try { - OutputStream os = processInputs.get(pid); - if (os != null) { - os.write((input + "\n").getBytes()); - os.flush(); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - private long getPid(Process process) { - try { - Field f = process.getClass().getDeclaredField("pid"); - f.setAccessible(true); - return f.getLong(process); - } catch (Exception e) { - return -1; - } -} - - - private void stopProcess(String pid) { - Process process = processes.get(pid); - if (process != null) { - try { - Runtime.getRuntime().exec("kill -9 -" + getPid(process)); - } catch (Exception ignored) {} - process.destroy(); - cleanup(pid); - } - } - - private void isProcessRunning(String pid, Messenger clientMessenger) { - Process process = processes.get(pid); - String status = process != null && isProcessAlive(process) ? "running" : "not_found"; - sendMessageToClient(pid, "isRunning", status); - } - - private boolean isProcessAlive(Process process) { - try { - process.exitValue(); - return false; - } catch(IllegalThreadStateException e) { - return true; - } - } - - private void cleanup(String id) { - processes.remove(id); - processInputs.remove(id); - clientMessengers.remove(id); - } - - @Override - public void onCreate() { - super.onCreate(); - createNotificationChannel(); - updateNotification(); - } - - private void createNotificationChannel() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel serviceChannel = new NotificationChannel( - CHANNEL_ID, - "Terminal Executor Channel", - NotificationManager.IMPORTANCE_LOW - ); - NotificationManager manager = getSystemService(NotificationManager.class); - if (manager != null) { - manager.createNotificationChannel(serviceChannel); - } - } - } - - private void updateNotification() { - Intent exitIntent = new Intent(this, TerminalService.class); - exitIntent.setAction(ACTION_EXIT_SERVICE); - PendingIntent exitPendingIntent = PendingIntent.getService(this, 0, exitIntent, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - - Intent wakeLockIntent = new Intent(this, TerminalService.class); - wakeLockIntent.setAction(ACTION_TOGGLE_WAKE_LOCK); - PendingIntent wakeLockPendingIntent = PendingIntent.getService(this, 1, wakeLockIntent, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - - String contentText = "Executor service" + (isWakeLockHeld ? " (wakelock held)" : ""); - String wakeLockButtonText = isWakeLockHeld ? "Release Wake Lock" : "Acquire Wake Lock"; - - int notificationIcon = resolveDrawableId("ic_notification", "ic_launcher_foreground", "ic_launcher"); - - Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("Acode Service") - .setContentText(contentText) - .setSmallIcon(notificationIcon) - .setOngoing(true) - .addAction(notificationIcon, wakeLockButtonText, wakeLockPendingIntent) - .addAction(notificationIcon, "Exit", exitPendingIntent) - .build(); - - startForeground(1, notification); - } - - @Override - public void onDestroy() { - stopForeground(true); - super.onDestroy(); - releaseWakeLock(); - - for (Process process : processes.values()) { - try { - Runtime.getRuntime().exec("kill -9 -" + getPid(process)); - } catch (Exception ignored) {} - process.destroyForcibly(); - } - - processes.clear(); - processInputs.clear(); - clientMessengers.clear(); - threadPool.shutdown(); - } - - private int resolveDrawableId(String... names) { - for (String name : names) { - int id = getResources().getIdentifier(name, "drawable", getPackageName()); - if (id != 0) return id; - } - return android.R.drawable.sym_def_app_icon; - } -} diff --git a/src/plugins/terminal/www/Executor.js b/src/plugins/terminal/www/Executor.js index 78103fafc..4675d12c6 100644 --- a/src/plugins/terminal/www/Executor.js +++ b/src/plugins/terminal/www/Executor.js @@ -30,33 +30,22 @@ const Executor = { */ - start(command, onData) { - this.start(command, onData, false); + start(command,onData){ + this.start(command,onData,false) }, start(command, onData, alpine) { - console.log("start: " + command); - return new Promise((resolve, reject) => { - let first = true; - exec(async (message) => { - console.log(message); - if (first) { - first = false; - await new Promise(resolve => setTimeout(resolve, 100)); + exec( + (message) => { + // Stream stdout, stderr, or exit notifications + if (message.startsWith("stdout:")) return onData("stdout", message.slice(7)); + if (message.startsWith("stderr:")) return onData("stderr", message.slice(7)); + if (message.startsWith("exit:")) return onData("exit", message.slice(5)); + // First message is always the process UUID resolve(message); - } else { - const match = message.match(/^([^:]+):(.*)$/); - if (match) { - const prefix = match[1]; // e.g. "stdout" - const message = match[2].trim(); // output - onData(prefix, message); - } else { - onData("unknown", message); - } - } - }, + }, reject, "Executor", "start", @@ -73,10 +62,9 @@ const Executor = { * @returns {Promise} Resolves once the input is written. * * @example - * Executor.write(uuid, 'ls /sdcard'); + * Executor.write(uuid, 'ls /data'); */ write(uuid, input) { - console.log("write: " + input + " to " + uuid); return new Promise((resolve, reject) => { exec(resolve, reject, "Executor", "write", [uuid, input]); }); @@ -114,12 +102,6 @@ const Executor = { }); }, - stopService() { - return new Promise((resolve, reject) => { - exec(resolve, reject, "Executor", "stopService", []); - }); - }, - /** * Executes a shell command once and waits for it to finish. * Unlike {@link Executor.start}, this does not stream output. @@ -133,8 +115,8 @@ const Executor = { * .then(console.log) * .catch(console.error); */ - execute(command) { - this.execute(command, false); + execute(command){ + this.execute(command,false) } , execute(command, alpine) { @@ -143,7 +125,7 @@ const Executor = { }); }, - loadLibrary(path) { + loadLibrary(path){ return new Promise((resolve, reject) => { exec(resolve, reject, "Executor", "loadLibrary", [path]); }); From 5d6a01968b696beb6f235c01e0e126ea646072fd Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Thu, 11 Dec 2025 10:31:30 +0530 Subject: [PATCH 17/21] fix: package.json --- package.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/package.json b/package.json index c21cb7834..807110f0a 100644 --- a/package.json +++ b/package.json @@ -36,14 +36,9 @@ "cordova-plugin-buildinfo": {}, "com.foxdebug.acode.rk.exec.terminal": {}, "cordova-plugin-browser": {}, -<<<<<<< HEAD "cordova-plugin-sftp": {}, "cordova-plugin-system": {}, - "com.foxdebug.acode.rk.exec.terminal": {}, "com.foxdebug.acode.rk.exec.proot": {} -======= - "cordova-plugin-system": {} ->>>>>>> parent of 10f82de0 (Terminal Service (#1570)) }, "platforms": [ "android" From 50a994f484572c649e8d585703b1ed29e25e31c8 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Thu, 11 Dec 2025 12:41:29 +0530 Subject: [PATCH 18/21] . --- package-lock.json | 7 ++++- package.json | 7 +++-- src/main.js | 1 + .../terminal/src/android/Executor.java | 29 +++++++++++-------- src/plugins/terminal/www/Executor.js | 2 +- 5 files changed, 29 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2f1835e6d..3df5a6262 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", @@ -4334,6 +4335,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 @@ -11203,7 +11208,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 807110f0a..7dbbe1723 100644 --- a/package.json +++ b/package.json @@ -34,11 +34,11 @@ }, "cordova-plugin-websocket": {}, "cordova-plugin-buildinfo": {}, - "com.foxdebug.acode.rk.exec.terminal": {}, "cordova-plugin-browser": {}, "cordova-plugin-sftp": {}, "cordova-plugin-system": {}, - "com.foxdebug.acode.rk.exec.proot": {} + "com.foxdebug.acode.rk.exec.proot": {}, + "com.foxdebug.acode.rk.exec.terminal": {} }, "platforms": [ "android" @@ -63,6 +63,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", @@ -128,4 +129,4 @@ "yargs": "^18.0.0" }, "browserslist": "cover 100%,not android < 5" -} \ No newline at end of file +} diff --git a/src/main.js b/src/main.js index 17b32c0cf..edafc5a0d 100644 --- a/src/main.js +++ b/src/main.js @@ -178,6 +178,7 @@ async function onDeviceReady() { system.requestPermission("android.permission.READ_EXTERNAL_STORAGE"); system.requestPermission("android.permission.WRITE_EXTERNAL_STORAGE"); + system.requestPermission("android.permission.POST_NOTIFICATIONS"); const { versionCode } = BuildInfo; diff --git a/src/plugins/terminal/src/android/Executor.java b/src/plugins/terminal/src/android/Executor.java index 338560141..61ebcef03 100644 --- a/src/plugins/terminal/src/android/Executor.java +++ b/src/plugins/terminal/src/android/Executor.java @@ -14,7 +14,7 @@ import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStreamReader; +import java.io.*; import java.io.OutputStream; import java.util.Map; @@ -281,16 +281,14 @@ private void isProcessRunning(String pid, CallbackContext callbackContext) { } } - - private void cleanupCallback(String id) { - callbackContextMap.remove(id); - } - - @Override - public void onDestroy() { - super.onDestroy(); - Intent serviceIntent = new Intent(context, KeepAliveService.class); - context.stopService(serviceIntent); + private void streamOutput(InputStream inputStream, String pid, String streamType) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + String line; + while ((line = reader.readLine()) != null) { + sendPluginMessage(pid, streamType + ":" + line); + } + } catch (IOException ignored) { + } } private void sendPluginMessage(String pid, String message) { @@ -302,9 +300,16 @@ private void sendPluginMessage(String pid, String message) { } } + @Override + public void onDestroy() { + super.onDestroy(); + //Intent serviceIntent = new Intent(context, KeepAliveService.class); + //context.stopService(serviceIntent); + } + private void cleanup(String pid) { processes.remove(pid); processInputs.remove(pid); processCallbacks.remove(pid); } -} +} \ No newline at end of file diff --git a/src/plugins/terminal/www/Executor.js b/src/plugins/terminal/www/Executor.js index 4675d12c6..845e018f6 100644 --- a/src/plugins/terminal/www/Executor.js +++ b/src/plugins/terminal/www/Executor.js @@ -31,7 +31,7 @@ const Executor = { start(command,onData){ - this.start(command,onData,false) + this.start(command,onData,true) }, start(command, onData, alpine) { From 591f91d279d5f511df7d5a9261172fb1baac674c Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Sun, 14 Dec 2025 13:24:05 +0530 Subject: [PATCH 19/21] feat: merge main into fileobject-feature --- package-lock.json | 44 ++++++-------------------------------------- 1 file changed, 6 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7aa866783..faeea04e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -132,7 +132,6 @@ "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2963,7 +2962,8 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/markdown-it": { "version": "14.1.2", @@ -2980,7 +2980,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/node": { "version": "24.2.1", @@ -3293,8 +3294,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", @@ -3325,7 +3325,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3359,7 +3358,6 @@ "version": "8.12.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -3740,7 +3738,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001733", "electron-to-chromium": "^1.5.199", @@ -3843,7 +3840,6 @@ "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", @@ -5129,23 +5125,6 @@ "reusify": "^1.0.4" } }, - "node_modules/figures": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/file": { "resolved": "src/plugins/file", "link": true @@ -6045,7 +6024,6 @@ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "license": "MIT", - "peer": true, "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", @@ -6935,7 +6913,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", @@ -7090,7 +7067,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7257,7 +7233,6 @@ "version": "6.12.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -8090,7 +8065,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8247,10 +8221,6 @@ "node": ">=8" } }, - "node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, "node_modules/tuf-js": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.0.0.tgz", @@ -8500,7 +8470,6 @@ "integrity": "sha512-B4t+nJqytPeuZlHuIKTbalhljIFXeNRqrUGAQgTGlfOl2lXXKXw+yZu6bicycP+PUlM44CxBjCFD6aciKFT3LQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -8550,7 +8519,6 @@ "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", @@ -8990,4 +8958,4 @@ "license": "Apache-2.0" } } -} \ No newline at end of file +} From bd8a2fa023679bd79e011db8a6b9aaf2eebdc37a Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Sun, 14 Dec 2025 15:24:23 +0530 Subject: [PATCH 20/21] feat: reuse legacy code --- package.json | 6 +- src/lib/fileServer.ts | 122 ------- src/lib/run.js | 807 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 753 insertions(+), 182 deletions(-) delete mode 100644 src/lib/fileServer.ts diff --git a/package.json b/package.json index 1c27bf1b2..ee69a4228 100644 --- a/package.json +++ b/package.json @@ -39,9 +39,7 @@ "com.foxdebug.acode.rk.exec.proot": {}, "cordova-plugin-sftp": {}, "cordova-plugin-system": {}, - "com.foxdebug.acode.rk.file": {}, - "com.foxdebug.acode.rk.exec.proot": {}, - "com.foxdebug.acode.rk.exec.terminal": {} + "com.foxdebug.acode.rk.file": {} }, "platforms": [ "android" @@ -136,4 +134,4 @@ "yargs": "^18.0.0" }, "browserslist": "cover 100%,not android < 5" -} \ No newline at end of file +} diff --git a/src/lib/fileServer.ts b/src/lib/fileServer.ts deleted file mode 100644 index 719b361f6..000000000 --- a/src/lib/fileServer.ts +++ /dev/null @@ -1,122 +0,0 @@ -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"]; - } -} diff --git a/src/lib/run.js b/src/lib/run.js index 883334ee0..226155627 100644 --- a/src/lib/run.js +++ b/src/lib/run.js @@ -1,8 +1,27 @@ +import fsOperation from "fileSystem"; +import tutorial from "components/tutorial"; +import alert from "dialogs/alert"; +import box from "dialogs/box"; +import markdownIt from "markdown-it"; +import anchor from "markdown-it-anchor"; +import markdownItFootnote from "markdown-it-footnote"; +import MarkdownItGitHubAlerts from "markdown-it-github-alerts"; +import markdownItTaskLists from "markdown-it-task-lists"; +import mimeType from "mime-types"; +import mustache from "mustache"; import browser from "plugins/browser"; -import { FileObjectBuilder } from "../fileSystem/FileObjectBuilder"; -import { FileServer } from "./fileServer"; -import { Log } from "./Log"; +import helpers from "utils/helpers"; +import Url from "utils/Url"; +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 appSettings from "./settings"; +import { SAFDocumentFile } from "../fileSystem/SAFDocumentFile"; + +/**@type {Server} */ +let webServer; /** * Starts the server and run the active file in browser @@ -10,7 +29,6 @@ import appSettings from "./settings"; * @param {"inapp"|"browser"} target * @param {Boolean} runFile */ - async function run( isConsole = false, target = appSettings.value.previewMode, @@ -31,76 +49,753 @@ async function run( } } - const activeFile = editorManager.activeFile; - if (!(await activeFile?.canRun())) { - //can not run + /** @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; } - const log = new Log("Code Runner"); - log.d(activeFile.uri); + if (!runFile && filename !== "index.html" && pathName) { + const folder = openFolder.find(activeFile.uri); - const fileObjectBuilder = new FileObjectBuilder(); - const documentFile = await fileObjectBuilder.build(activeFile.uri); + if (folder) { + const { url } = folder; + const fs = fsOperation(Url.join(url, "index.html")); - const projectFolder = await fileObjectBuilder.build(addedFolder[0].url); - log.d(projectFolder.uri); + try { + if (await fs.exists()) { + filename = "index.html"; + extension = "html"; + pathName = url; + start(); + return; + } - let root = documentFile; - if (await projectFolder.isMyChild(documentFile)) { - root = projectFolder; - log.d("Not a child of project folder"); - } else { - root = await documentFile.getParentFile(); - if (root == null || !(await root.exists())) { - root = documentFile; - log.d("llll"); + next(); + return; + } catch (err) { + helpers.error(err); + return; + } } } - const port = 8080; - let fileServer; + 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"; - const path = await buildPathFromFile(documentFile, root); - log.d(`PATH ${path}`); - let url = `http://localhost:${port}/${path}`; + //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; - log.d(url); + start(); + } - fileServer = new FileServer(port, root); + function start() { + if (target === "browser") { + system.isPowerSaveMode((res) => { + if (res) { + alert(strings.info, strings["powersave mode warning"]); + } else { + startServer(); + } + }, startServer); + } else { + startServer(); + } + } - fileServer.start( - (msg) => { - //success - log.d(msg); + function startServer() { + //isFallback = true; + webServer?.stop(); + webServer = CreateServer(port, openBrowser, onError); + webServer.setOnRequestHandler(handleRequest); - if (target === "browser") { - system.openInBrowser(url); + function onError(err) { + if (err === "Server already running") { + openBrowser(); } else { - browser.open(url, false); - } - }, - (err) => { - //error - log.e(err); - }, - ); -} + ++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 = await 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 + + // Use SAFDocumentFile for Android content URIs + let fullPath; + + if (rootFolder.startsWith("content://")) { + try { + // For SAF URIs, reconstruct the path properly + const rootFile = new SAFDocumentFile(rootFolder); + + // Split the request path to navigate through directories + const pathParts = reqPath.split("/").filter(p => p); + + // Start from root and navigate to the requested file + let currentFile = rootFile; + for (const part of pathParts) { + const child = await currentFile.getChildByName(part); + if (!child) { + console.error(`Could not find child: ${part}`); + error(reqId); + return; + } + currentFile = child; + } + + fullPath = await currentFile.toUri(); + console.log(`SAF Full PATH: ${fullPath}`); + } catch (err) { + console.error("Error navigating SAF path:", err); + error(reqId); + return; + } + } else { + // Original path handling for non-SAF URIs + 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) { + let maxOverlap = ""; + for (let i = 1; i <= Math.min(a.length, b.length); i++) { + const endOfA = a.slice(-i); + const startOfB = b.slice(0, i); + if (endOfA === startOfB) { + maxOverlap = endOfA; + } + } + return maxOverlap; + } + + console.log(`RootFolder ${rootFolder}`); + console.log(`PARTS ${pathParts.join("/")}`); + + // 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 (!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!", + }); + } -//returns a string without a '/' prefix -async function buildPathFromFile(file, rootDirectory) { - const parts = []; - let current = file; + /** + * 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(); + } - while ( - current !== null && - (await current.toUri()) !== (await rootDirectory.toUri()) - ) { - parts.unshift(await current.getName()); // Add to the beginning - current = await current.getParentFile(); + /** + * 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; } - return parts.length === 0 ? await file.getName() : "/" + parts.join("/"); + async function getRelativePath() { + const projectFolder = addedFolder[0]; + let rootFolder = pathName; + + // Use SAFDocumentFile for Android content URIs + if (activeFile && activeFile.uri && activeFile.uri.startsWith("content://")) { + try { + console.log(`DEBUG - Using SAFDocumentFile for URI: ${activeFile.uri}`); + + const rootFile = new SAFDocumentFile(rootFolder); + const currentFile = new SAFDocumentFile(Url.join(pathName, filename)); + + // Get clean filename + const fileName = await currentFile.getName(); + console.log(`DEBUG - SAF fileName: ${fileName}`); + + // Check if current file is a child of root + const isChild = await rootFile.isMyChild(currentFile); + console.log(`DEBUG - Is child of root: ${isChild}`); + + if (isChild) { + // Build relative path by walking up to root + const relativeParts = []; + let current = currentFile; + + // Add current file name first + relativeParts.unshift(fileName); + + // Walk up the tree until we reach root + let parent = await current.getParentFile(); + while (parent) { + const parentUri = await parent.toUri(); + console.log(`DEBUG - Checking parent URI: ${parentUri}`); + + // Normalize URIs for comparison + const normalizedParentUri = parentUri.replace(/\/+$/, ''); + const normalizedRootFolder = rootFolder.replace(/\/+$/, ''); + + if (normalizedParentUri === normalizedRootFolder) { + console.log(`DEBUG - Reached root folder`); + break; + } + + const parentName = await parent.getName(); + console.log(`DEBUG - Adding parent to path: ${parentName}`); + relativeParts.unshift(parentName); + + current = parent; + parent = await current.getParentFile(); + } + + const relativePath = relativeParts.join('/'); + console.log(`DEBUG - Final SAF relative path: ${relativePath}`); + return relativePath; + } else { + // If not a child, just return the filename + console.log(`DEBUG - Not a child, returning filename: ${fileName}`); + return fileName; + } + } catch (e) { + console.error('Error using SAFDocumentFile:', e); + // Fall through to existing logic + } + } + + // Original logic for non-SAF URIs + if ( + projectFolder !== undefined && + pathName && + pathName.includes(projectFolder.url) + ) { + rootFolder = projectFolder.url; + } + + rootFolder = makeUriAbsoluteIfNeeded(rootFolder); + + let filePath = pathName; + + if (rootFolder.startsWith("ftp:") || rootFolder.startsWith("sftp:")) { + if (rootFolder.includes("?")) { + rootFolder = rootFolder.split("?")[0]; + } + } + + if (filePath.startsWith("ftp:") || rootFolder.startsWith("sftp:")) { + if (filePath.includes("?")) { + filePath = filePath.split("?")[0]; + } + } + + let temp = Url.join(filePath, filename); + + // Try to find a common prefix between rootFolder and 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 + */ + async function openBrowser() { + let url = ""; + if (pathName === null && !activeFile.location) { + url = `http://localhost:${port}/__unsaved_file__`; + } else { + const relativePath = await getRelativePath(); + url = `http://localhost:${port}${relativePath}`; + } + + if (target === "browser") { + system.openInBrowser(url); + return; + } + + browser.open(url, isConsole); + } } -export default run; +export default run; \ No newline at end of file From de6ca257a151cda83ba5d25308105a0e08d19b25 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Sun, 14 Dec 2025 15:30:05 +0530 Subject: [PATCH 21/21] format --- src/lib/run.js | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/lib/run.js b/src/lib/run.js index 226155627..3c39fce1e 100644 --- a/src/lib/run.js +++ b/src/lib/run.js @@ -14,11 +14,11 @@ import helpers from "utils/helpers"; import Url from "utils/Url"; import $_console from "views/console.hbs"; import $_markdown from "views/markdown.hbs"; +import { SAFDocumentFile } from "../fileSystem/SAFDocumentFile"; import constants from "./constants"; import EditorFile from "./editorFile"; import openFolder, { addedFolder } from "./openFolder"; import appSettings from "./settings"; -import { SAFDocumentFile } from "../fileSystem/SAFDocumentFile"; /**@type {Server} */ let webServer; @@ -303,7 +303,7 @@ async function run( const rootFile = new SAFDocumentFile(rootFolder); // Split the request path to navigate through directories - const pathParts = reqPath.split("/").filter(p => p); + const pathParts = reqPath.split("/").filter((p) => p); // Start from root and navigate to the requested file let currentFile = rootFile; @@ -494,20 +494,22 @@ async function run( theme: 'dark' }); - ${target === "inapp" - ? "eruda._shadowRoot.querySelector('.eruda-entry-btn').style.display = 'none';" - : "" - } + ${ + target === "inapp" + ? "eruda._shadowRoot.querySelector('.eruda-entry-btn').style.display = 'none';" + : "" + } sessionStorage.setItem('__console_available', true); document.addEventListener('showconsole', function () {eruda.show()}); document.addEventListener('hideconsole', function () {eruda.hide()}); }else if(document.querySelector('c-toggler')){ - ${target === "inapp" || - (target !== "inapp" && !appSettings.value.showConsoleToggler) - ? "document.querySelector('c-toggler').style.display = 'none';" - : "" - } + ${ + target === "inapp" || + (target !== "inapp" && !appSettings.value.showConsoleToggler) + ? "document.querySelector('c-toggler').style.display = 'none';" + : "" + } } setTimeout(function(){ var scripts = document.querySelectorAll('.${uuid}'); @@ -660,7 +662,11 @@ async function run( let rootFolder = pathName; // Use SAFDocumentFile for Android content URIs - if (activeFile && activeFile.uri && activeFile.uri.startsWith("content://")) { + if ( + activeFile && + activeFile.uri && + activeFile.uri.startsWith("content://") + ) { try { console.log(`DEBUG - Using SAFDocumentFile for URI: ${activeFile.uri}`); @@ -690,8 +696,8 @@ async function run( console.log(`DEBUG - Checking parent URI: ${parentUri}`); // Normalize URIs for comparison - const normalizedParentUri = parentUri.replace(/\/+$/, ''); - const normalizedRootFolder = rootFolder.replace(/\/+$/, ''); + const normalizedParentUri = parentUri.replace(/\/+$/, ""); + const normalizedRootFolder = rootFolder.replace(/\/+$/, ""); if (normalizedParentUri === normalizedRootFolder) { console.log(`DEBUG - Reached root folder`); @@ -706,7 +712,7 @@ async function run( parent = await current.getParentFile(); } - const relativePath = relativeParts.join('/'); + const relativePath = relativeParts.join("/"); console.log(`DEBUG - Final SAF relative path: ${relativePath}`); return relativePath; } else { @@ -715,7 +721,7 @@ async function run( return fileName; } } catch (e) { - console.error('Error using SAFDocumentFile:', e); + console.error("Error using SAFDocumentFile:", e); // Fall through to existing logic } } @@ -798,4 +804,4 @@ async function run( } } -export default run; \ No newline at end of file +export default run;