mirror of
https://github.com/Quad4-Software/MeshChatX.git
synced 2026-08-18 09:49:09 -04:00
feat: introduce portable data directory support with MESHCHAT_DATA_DIR and related CLI flags
This commit is contained in:
parent
2603a45cb9
commit
41511fc014
11 changed files with 176 additions and 13 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
BIN
meshchatx.rsm
BIN
meshchatx.rsm
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
51
tests/backend/test_data_dir_roots.py
Normal file
51
tests/backend/test_data_dir_roots.py
Normal 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")
|
||||
Loading…
Add table
Add a link
Reference in a new issue