feat: introduce portable data directory support with MESHCHAT_DATA_DIR and related CLI flags

This commit is contained in:
Ivan 2026-07-27 06:23:42 -05:00
parent 2603a45cb9
commit 41511fc014
No known key found for this signature in database
11 changed files with 176 additions and 13 deletions

View file

@ -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 |

View file

@ -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 | `<data-dir>/storage` and `<data-dir>/.reticulum` when using `--data-dir` / `MESHCHAT_DATA_DIR` |
| Desktop Electron data | `~/.reticulum-meshchatx` and `~/.reticulum` unless overridden at launch |
| Per-identity database | `<storage>/identities/<identity_hash>/database.db` |
| Docker volume | `meshchatx-config` mounted at `/config` |

View file

@ -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:

View file

@ -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)) {

Binary file not shown.

View file

@ -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 <dir>/storage and <dir>/.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

View file

@ -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:

View file

@ -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 | `<data-dir>/storage` and `<data-dir>/.reticulum` when using `--data-dir` / `MESHCHAT_DATA_DIR` |
| Desktop Electron data | `~/.reticulum-meshchatx` and `~/.reticulum` unless overridden at launch |
| Per-identity database | `<storage>/identities/<identity_hash>/database.db` |
| Docker volume | `meshchatx-config` mounted at `/config` |

View file

@ -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:

View file

@ -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,

View file

@ -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")