diff --git a/docs/agents/overview.md b/docs/agents/overview.md index d7bd3b41..8539e449 100644 --- a/docs/agents/overview.md +++ b/docs/agents/overview.md @@ -276,6 +276,7 @@ Common overrides (CLI flags usually mirror these): | `MESHCHAT_HEADLESS` / `--headless` | Do not auto-launch a browser | | `MESHCHAT_STORAGE_DIR` / `--storage-dir` | App storage root | | `MESHCHAT_RETICULUM_CONFIG_DIR` / `--reticulum-config-dir` | Reticulum config dir | +| `MESHCHAT_DATA_DIR` / `--data-dir` | Portable root for storage + Reticulum when those two are unset | | `MESHCHAT_PUBLIC_DIR` / `--public-dir` | Frontend assets dir | | `MESHCHAT_AUTH` / `--auth` | Enable web auth | | `MESHCHAT_NO_HTTPS` / `--no-https` | HTTP instead of HTTPS | diff --git a/docs/en/getting-started.md b/docs/en/getting-started.md index 4c05c2a9..e9154da6 100644 --- a/docs/en/getting-started.md +++ b/docs/en/getting-started.md @@ -79,10 +79,12 @@ Use the search bar to query both sets at once. MeshChatX guide text is currently ## Storage locations -| Data | Typical path | +| Data | Typical path (CLI default) | | --------------------- | -------------------------------------------------- | -| MeshChatX app data | `~/.reticulum-meshchatx/` on Linux and macOS | -| Reticulum config | `~/.reticulum/` | +| MeshChatX app data | `./storage` (or `--storage-dir` / `MESHCHAT_STORAGE_DIR`) | +| Reticulum config | `~/.reticulum` (or `--reticulum-config-dir` / `MESHCHAT_RETICULUM_CONFIG_DIR`) | +| Portable bundle | `/storage` and `/.reticulum` when using `--data-dir` / `MESHCHAT_DATA_DIR` | +| Desktop Electron data | `~/.reticulum-meshchatx` and `~/.reticulum` unless overridden at launch | | Per-identity database | `/identities//database.db` | | Docker volume | `meshchatx-config` mounted at `/config` | diff --git a/docs/en/installation.md b/docs/en/installation.md index aa55278d..83b7a7f6 100644 --- a/docs/en/installation.md +++ b/docs/en/installation.md @@ -139,8 +139,9 @@ Common flags and environment variables: | `--ssl-key` | `MESHCHAT_SSL_KEY` | auto | TLS private key path | | `--headless` | `MESHCHAT_HEADLESS` | false | Do not open a browser | | `--auth` | `MESHCHAT_AUTH` | false | Require HTTP basic auth for the UI | -| `--storage-dir` | `MESHCHAT_STORAGE_DIR` | `./storage` | Application data directory | -| `--reticulum-config-dir` | (see `--help`) | `~/.reticulum` | Reticulum configuration | +| `--storage-dir` | `MESHCHAT_STORAGE_DIR` | `./storage` | Application data directory | +| `--reticulum-config-dir` | `MESHCHAT_RETICULUM_CONFIG_DIR` | `~/.reticulum` | Reticulum configuration | +| `--data-dir` | `MESHCHAT_DATA_DIR` | none | Portable root (`storage` + `.reticulum` subdirs when the two paths above are unset) | | `--identity-file` | `MESHCHAT_IDENTITY_FILE` | none | Load identity from file | | `--rns-log-level` | `MESHCHAT_RNS_LOG_LEVEL` | none | Reticulum log level | | `--auto-recover` | `MESHCHAT_AUTO_RECOVER` | false | Attempt SQLite recovery on start | @@ -149,6 +150,35 @@ Common flags and environment variables: CLI flags override environment variables when both are set. +### Portable installs (removable media, Tails, USB sticks) + +MeshChatX already supports relocating all persistent state off the home directory. Use either explicit paths or a single data root: + +```bash +export PERSIST="/media/amnesia/Persistent/meshchatx" +mkdir -p "$PERSIST" + +meshchatx --headless \ + --data-dir="$PERSIST" +``` + +That creates and uses `$PERSIST/storage` for MeshChatX (identities, SQLite, plugins) and `$PERSIST/.reticulum` for Reticulum interfaces and transport config. You can set the same layout with environment variables: + +```bash +export MESHCHAT_DATA_DIR="$PERSIST" +meshchatx --headless +``` + +Equivalent explicit form (overrides any `--data-dir` subpaths when you set these yourself): + +```bash +meshchatx --headless \ + --storage-dir="$PERSIST/storage" \ + --reticulum-config-dir="$PERSIST/.reticulum" +``` + +On Windows portable Electron builds, storage and Reticulum config default next to the `.exe` when `PORTABLE_EXECUTABLE_DIR` is set. On Linux and macOS desktop builds, Electron still defaults to `~/.reticulum-meshchatx` and `~/.reticulum` unless you pass the flags above (or set `MESHCHAT_DATA_DIR` / `MESHCHAT_STORAGE_DIR` / `MESHCHAT_RETICULUM_CONFIG_DIR` in the environment before launch). + ## Reticulum manual bundle The Reticulum HTML manual is fetched from the upstream website **master** branch at build time by default (clearnet ZIP). There is no in-app clearnet refresh. After cloning the repository, or before packaging a release, run: diff --git a/electron/shellPathGuard.js b/electron/shellPathGuard.js index 1313190a..45008919 100644 --- a/electron/shellPathGuard.js +++ b/electron/shellPathGuard.js @@ -93,6 +93,14 @@ function isAllowedShellPath(targetPath, ctx) { const userArgv = ctx.getUserProvidedArguments(process.argv); add(parseArgvFlag(userArgv, "--storage-dir")); add(parseArgvFlag(userArgv, "--reticulum-config-dir")); + const dataDir = + parseArgvFlag(userArgv, "--data-dir") || + (process.env.MESHCHAT_DATA_DIR && String(process.env.MESHCHAT_DATA_DIR).trim()) || + null; + if (dataDir) { + add(path.join(dataDir, "storage")); + add(path.join(dataDir, ".reticulum")); + } for (const root of roots) { if (root && isResolvedPathUnderRoot(resolved, root)) { diff --git a/meshchatx.rsm b/meshchatx.rsm index 9433f4d4..01ffb550 100644 Binary files a/meshchatx.rsm and b/meshchatx.rsm differ diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py index 6b6b0b12..afcb7bed 100644 --- a/meshchatx/meshchat.py +++ b/meshchatx/meshchat.py @@ -289,6 +289,7 @@ from meshchatx.src.path_utils import ( get_file_path, is_path_within_dir, resolve_log_dir, + resolve_meshchat_data_roots, safe_path_under_dir, ) from meshchatx.src.path_utils import ( @@ -10504,6 +10505,16 @@ def main(): type=str, help="Restore the database from the given path (zip or db file) and exit.", ) + parser.add_argument( + "--data-dir", + type=str, + default=os.environ.get("MESHCHAT_DATA_DIR"), + help=( + "Portable data root: uses /storage and /.reticulum when " + "--storage-dir and --reticulum-config-dir are not set. " + "Can also be set via MESHCHAT_DATA_DIR." + ), + ) parser.add_argument( "--reticulum-config-dir", type=str, @@ -10604,6 +10615,12 @@ def main(): if args.no_crash_recovery: recovery.disable() + args.storage_dir, args.reticulum_config_dir = resolve_meshchat_data_roots( + data_dir=args.data_dir, + storage_dir=args.storage_dir, + reticulum_config_dir=args.reticulum_config_dir, + ) + planned_storage_dir = args.storage_dir if not planned_storage_dir: # On Android, prefer user-accessible external storage diff --git a/meshchatx/src/backend/http/routes/interfaces.py b/meshchatx/src/backend/http/routes/interfaces.py index 8a85921c..8a4831e8 100644 --- a/meshchatx/src/backend/http/routes/interfaces.py +++ b/meshchatx/src/backend/http/routes/interfaces.py @@ -1859,9 +1859,6 @@ def register_interfaces_routes(routes, app): status=500, ) - # preview importable interfaces - - # preview importable interfaces @routes.post("/api/v1/reticulum/interfaces/import-preview") async def import_interfaces_preview(request): try: diff --git a/meshchatx/src/frontend/public/meshchatx-docs/en/getting-started.md b/meshchatx/src/frontend/public/meshchatx-docs/en/getting-started.md index 4c05c2a9..e9154da6 100644 --- a/meshchatx/src/frontend/public/meshchatx-docs/en/getting-started.md +++ b/meshchatx/src/frontend/public/meshchatx-docs/en/getting-started.md @@ -79,10 +79,12 @@ Use the search bar to query both sets at once. MeshChatX guide text is currently ## Storage locations -| Data | Typical path | +| Data | Typical path (CLI default) | | --------------------- | -------------------------------------------------- | -| MeshChatX app data | `~/.reticulum-meshchatx/` on Linux and macOS | -| Reticulum config | `~/.reticulum/` | +| MeshChatX app data | `./storage` (or `--storage-dir` / `MESHCHAT_STORAGE_DIR`) | +| Reticulum config | `~/.reticulum` (or `--reticulum-config-dir` / `MESHCHAT_RETICULUM_CONFIG_DIR`) | +| Portable bundle | `/storage` and `/.reticulum` when using `--data-dir` / `MESHCHAT_DATA_DIR` | +| Desktop Electron data | `~/.reticulum-meshchatx` and `~/.reticulum` unless overridden at launch | | Per-identity database | `/identities//database.db` | | Docker volume | `meshchatx-config` mounted at `/config` | diff --git a/meshchatx/src/frontend/public/meshchatx-docs/en/installation.md b/meshchatx/src/frontend/public/meshchatx-docs/en/installation.md index aa55278d..83b7a7f6 100644 --- a/meshchatx/src/frontend/public/meshchatx-docs/en/installation.md +++ b/meshchatx/src/frontend/public/meshchatx-docs/en/installation.md @@ -139,8 +139,9 @@ Common flags and environment variables: | `--ssl-key` | `MESHCHAT_SSL_KEY` | auto | TLS private key path | | `--headless` | `MESHCHAT_HEADLESS` | false | Do not open a browser | | `--auth` | `MESHCHAT_AUTH` | false | Require HTTP basic auth for the UI | -| `--storage-dir` | `MESHCHAT_STORAGE_DIR` | `./storage` | Application data directory | -| `--reticulum-config-dir` | (see `--help`) | `~/.reticulum` | Reticulum configuration | +| `--storage-dir` | `MESHCHAT_STORAGE_DIR` | `./storage` | Application data directory | +| `--reticulum-config-dir` | `MESHCHAT_RETICULUM_CONFIG_DIR` | `~/.reticulum` | Reticulum configuration | +| `--data-dir` | `MESHCHAT_DATA_DIR` | none | Portable root (`storage` + `.reticulum` subdirs when the two paths above are unset) | | `--identity-file` | `MESHCHAT_IDENTITY_FILE` | none | Load identity from file | | `--rns-log-level` | `MESHCHAT_RNS_LOG_LEVEL` | none | Reticulum log level | | `--auto-recover` | `MESHCHAT_AUTO_RECOVER` | false | Attempt SQLite recovery on start | @@ -149,6 +150,35 @@ Common flags and environment variables: CLI flags override environment variables when both are set. +### Portable installs (removable media, Tails, USB sticks) + +MeshChatX already supports relocating all persistent state off the home directory. Use either explicit paths or a single data root: + +```bash +export PERSIST="/media/amnesia/Persistent/meshchatx" +mkdir -p "$PERSIST" + +meshchatx --headless \ + --data-dir="$PERSIST" +``` + +That creates and uses `$PERSIST/storage` for MeshChatX (identities, SQLite, plugins) and `$PERSIST/.reticulum` for Reticulum interfaces and transport config. You can set the same layout with environment variables: + +```bash +export MESHCHAT_DATA_DIR="$PERSIST" +meshchatx --headless +``` + +Equivalent explicit form (overrides any `--data-dir` subpaths when you set these yourself): + +```bash +meshchatx --headless \ + --storage-dir="$PERSIST/storage" \ + --reticulum-config-dir="$PERSIST/.reticulum" +``` + +On Windows portable Electron builds, storage and Reticulum config default next to the `.exe` when `PORTABLE_EXECUTABLE_DIR` is set. On Linux and macOS desktop builds, Electron still defaults to `~/.reticulum-meshchatx` and `~/.reticulum` unless you pass the flags above (or set `MESHCHAT_DATA_DIR` / `MESHCHAT_STORAGE_DIR` / `MESHCHAT_RETICULUM_CONFIG_DIR` in the environment before launch). + ## Reticulum manual bundle The Reticulum HTML manual is fetched from the upstream website **master** branch at build time by default (clearnet ZIP). There is no in-app clearnet refresh. After cloning the repository, or before packaging a release, run: diff --git a/meshchatx/src/path_utils.py b/meshchatx/src/path_utils.py index 9a0cfa74..67bd9aff 100644 --- a/meshchatx/src/path_utils.py +++ b/meshchatx/src/path_utils.py @@ -93,6 +93,31 @@ def resolve_log_dir(): return None +def resolve_meshchat_data_roots( + *, + data_dir: str | None, + storage_dir: str | None, + reticulum_config_dir: str | None, +) -> tuple[str | None, str | None]: + """Apply MESHCHAT_DATA_DIR / --data-dir when explicit roots are unset. + + Layout under data_dir: + storage/ MeshChatX databases and identity tree + .reticulum/ Reticulum interfaces and transport config + """ + root = (data_dir or os.environ.get("MESHCHAT_DATA_DIR") or "").strip() + if not root: + return storage_dir, reticulum_config_dir + root = os.path.abspath(os.path.expanduser(root)) + out_storage = storage_dir + out_reticulum = reticulum_config_dir + if not out_storage: + out_storage = os.path.join(root, "storage") + if not out_reticulum: + out_reticulum = os.path.join(root, ".reticulum") + return out_storage, out_reticulum + + def request_client_ip( request: web.Request, trusted_proxy_cidrs: str | None = None, diff --git a/tests/backend/test_data_dir_roots.py b/tests/backend/test_data_dir_roots.py new file mode 100644 index 00000000..22e2985e --- /dev/null +++ b/tests/backend/test_data_dir_roots.py @@ -0,0 +1,51 @@ +# SPDX-License-Identifier: 0BSD + +import os + +from meshchatx.src.path_utils import resolve_meshchat_data_roots + + +def test_data_dir_unset_passes_through_explicit_roots(): + storage, rns = resolve_meshchat_data_roots( + data_dir=None, + storage_dir="/app/storage", + reticulum_config_dir="/app/.reticulum", + ) + assert storage == "/app/storage" + assert rns == "/app/.reticulum" + + +def test_data_dir_fills_missing_roots(tmp_path, monkeypatch): + monkeypatch.delenv("MESHCHAT_DATA_DIR", raising=False) + root = tmp_path / "persist" + storage, rns = resolve_meshchat_data_roots( + data_dir=str(root), + storage_dir=None, + reticulum_config_dir=None, + ) + assert storage == os.path.join(str(root.resolve()), "storage") + assert rns == os.path.join(str(root.resolve()), ".reticulum") + + +def test_data_dir_does_not_override_explicit_storage(tmp_path, monkeypatch): + monkeypatch.delenv("MESHCHAT_DATA_DIR", raising=False) + root = tmp_path / "persist" + storage, rns = resolve_meshchat_data_roots( + data_dir=str(root), + storage_dir="/custom/storage", + reticulum_config_dir=None, + ) + assert storage == "/custom/storage" + assert rns == os.path.join(str(root.resolve()), ".reticulum") + + +def test_data_dir_from_env(monkeypatch, tmp_path): + root = tmp_path / "tails" + monkeypatch.setenv("MESHCHAT_DATA_DIR", str(root)) + storage, rns = resolve_meshchat_data_roots( + data_dir=None, + storage_dir=None, + reticulum_config_dir=None, + ) + assert storage == os.path.join(str(root.resolve()), "storage") + assert rns == os.path.join(str(root.resolve()), ".reticulum")