feat: pos media sync — incremental Music→USB sync (mp3/mp4) + shared lib/usb-lib.sh
gates / consistency-and-conventions (push) Successful in 46s
gates / consistency-and-conventions (push) Successful in 46s
This commit is contained in:
@@ -123,3 +123,4 @@ summary (newest last).
|
||||
- **2026-08-14** — Convention-drift maintenance fix session (completed the audit backlog `MAINTENANCE.md`, M-001..M-023, all VERIFIED; gate `scripts/lint-conventions.sh` + `make lint` now 0 FAIL / 0 WARN; `make gen && make check` green). P0 bugs: M-002/003/004 added `docker-compose docker-vbox network-hotspot` to `INTERACTIVE_CMDS` (stdin/log-pipe prompt swallow); M-005 `install.sh --steps` now expands documented `N-M` ranges via `normalize_steps_spec()` (dry-run verified); M-006 feature-vs-docs decision: `--send`/`--markdown` **not restored** (health is a console-only reporter by design since fe7708f; scheduler `NOTIFY=always` covers delivery) — 5 docs corrected instead; M-007 `lib/notify.sh:57` fallback routed to stderr (stdout-leak on standalone source). P1: M-008..M-014 deps guards moved **before** `-h|--help` in docker-health/docker-ps (converted to `command -v X || err`), network-scan, share-usb-server, media-mp3/mp4 (guards before help with a `--dry-run` pre-scan preserving the documented no-deps preview); system-health documented as the sanctioned graceful-degradation no-guard pattern in DEV.md — lint refined accordingly (`first_guard_line` only matches real guards; `first_line` skips comments; precision fixes, not weakenings); M-015 system-firewall gained `usage()`+`-h|--help` (root-gated first; verified via sudo); M-016 `ffmpeg` added to preinstall PACKAGES. P2: M-017/M-018 autostart + usb-automount gained the feature-template preamble (flags.sh load, usage); M-019 `chmod +x apps/media/scrcpy.sh`; M-020 `SCALE_DIR`/`CONFIG_ENV` `:-` seams in pos-docker-compose (verified via overrides; follow-on fix: `DIM` color var missing from common.sh crashed `pos docker compose config` — added it); M-021 `CONFIG_DIR` centralized as the canonical XDG-aware seam in common.sh, per-file duplicates dropped (standalone-sourced notify.sh/config-ui.sh/matrix+telegram tools keep an identical guarded copy — "no shared lib? inline fallbacks"); M-022 `plugin_*` prefix collision resolved by renaming the internal registry helpers to `ent_plugin_*` (the documented plugin-authoring API `plugin_have`/`plugin_require`/`plugin_load_config`/`plugin_http_json` kept for user plugins); M-023 six tools (pos-config, pos-tree, pos-entertainment-{config,enable,disable,status}) now filename-referenced in DOC/POS.md. Hand-maintained AGENT_Context line-count rows bumped (install.sh 223→248, preinstall 75→76, common.sh 144→151, notify.sh 76→87 stale-corrected, autostart 14→50, usb-automount 134→138); `make lint` target wired in the Makefile. `MAINTENANCE.md` kept as the working record (uncommitted by design).
|
||||
- **2026-08-14** — `pos system backup` optional encryption (`--no-encrypt` flag + `BACKUP_ENCRYPT=0` env, flag-or-env — user chose "Flag + env only"): plain path keeps a verified `.tar.gz` with no password prompt (headless/cron safe); encrypt path unchanged (prompt → gpg AES-256 → decrypt-verify; the gpg dep-guard moved into the encrypt branch so plain backups no longer require `gnupg`). Arg parsing rewritten as a loop over `"$@"` so `pos system backup <folder> --no-encrypt` works with the flag after the folder; usage() documents all three forms + the plain artifact name; `# POS_FLAGS: --service --no-encrypt`; `config/system.env` template gains `#BACKUP_ENCRYPT=0`; POS.md row + howto/system.md section updated. Verified: stub suite +2 cases (T18 flag / T19 env: plain .tar.gz artifact, gpg never called via `$GPG_CALLED`, USB copy + sha256 of the plain archive, notify wording) — 65/65 green; `bash -n`, `make gen && make check`, `make lint` 0 FAIL / 0 WARN.
|
||||
- **2026-08-14** — CI green-check via plain git (no SSH to the runner, no API tokens — user chose "CI tags + git ls-remote" + "scripts/ci-status.sh helper"): `.gitea/workflows/lint.yml` scoped to `on: push: branches: [main]` (tag pushes no longer re-trigger it) and the gate step now reports its own outcome as a lightweight tag — `ci-ok/$GITHUB_SHA` on success / `ci-fail/$GITHUB_SHA` on failure, pushed over HTTP with the jobs automatic `GITEA_TOKEN` to `http://oauth2:${GITEA_TOKEN}@gitea.skink-platy.ts.net:3000/admin/Linux_post_install.git` (runner container already host-maps that hostname to 100.111.241.54); `steps.gates.conclusion` decides ok/fail, `if: always()` (guarded to `push` events) covers failed gate runs, and an existing-tag guard makes re-runs idempotent. New executable `scripts/ci-status.sh [--wait] [<sha>]` reads the tags via `git ls-remote` (origin, `CI_STATUS_REMOTE` override): GREEN (0) / RED (1) / PENDING (2); `--wait` polls every 10s up to 10 min. DEV.md §CI gains a "Checking green without SSH" bullet. Verified: `bash -n`, yaml-parse OK, `make gen && make check`, `make lint` 0 FAIL / 0 WARN; first live-tag verification pending the push (fallback if Gitea clamps token-push: PAT as workflow secret).
|
||||
- **2026-08-14** — `pos media sync` (`bin/pos-media-sync`) — incremental Music → USB sync, plus the shared USB layer it builds on. **New lib `lib/usb-lib.sh`** (194 lines, installed by install.sh): `usb_detect` (lsblk JSON, TRAN + lsusb/by-id cross-check → `USB_MOUNTED`/`USB_UNMOUNTED`), `usb_related_present`, `usb_mount_offer` (`/media/<label>` mount-offer, `usb-automount` scheme), `usb_pick_root <prefix> <subfolder> <giveup-msg>` (detect → mount-offer → single/multi picker → `USB_ROOT`); seams `USB_MOUNT_BASE`/`USB_BYID` with `BACKUP_MOUNT_BASE`/`BACKUP_USB_BYID` aliases so existing `system.env` lines keep working; TRAN-fallback warning deduped to once per scan. `pos-system-backup` refactored onto it (216 lines, was 364) — re-ran the backup stub suite: 65/65 green. **Sync tool**: add/update only, never deletes (user choice); `--mp3`/`--mp4` filter (neither = both), `--source <dir>` (default `MEDIA_SYNC_SOURCE`/`$HOME/Music`), `--dry-run` preview with counts; copies missing/changed (size/mtime) files into `<usb>/Music/` (`MEDIA_SYNC_DEST`) preserving the tree via `cp --preserve=timestamps`; result notified via `lib/notify.sh`; `media-sync` added to `INTERACTIVE_CMDS`; deps guards (`lsblk`/`jq`) before `-h|--help`. Docs: POS.md media row, howto/media.md section, SCRIPTS.md lib section + Phase-2 lib list, system.env seams, DEV.md env-seam registry, AGENT_Context Common Tasks + hand-maintained lib row (+usb-lib 194) + gen'd tree/dispatch/filetable/flags. Verified: new stub suite `/tmp/opencode/msync-run.sh` 46/46 green (fresh/no-op/update/filter/dry-run/multi-stick/mount-offer/no-USB skip/never-delete/--source/TRAN-fallback/notify) — caught and fixed an inverted `needs_copy` return; `make gen && make check`, `make lint` 0 FAIL / 0 WARN; dispatch via `pos media sync --help` + `pos media` listing.
|
||||
|
||||
@@ -10,19 +10,19 @@
|
||||
|
||||
<!-- GEN:START docmap -->
|
||||
| ## 1. Project Overview | 28–43 |
|
||||
| ## 2. Directory Structure | 44–193 |
|
||||
| ## 3. Installation Flow | 194–245 |
|
||||
| ## 4. The `pos` CLI System | 246–316 |
|
||||
| ## 5. Shared Library — `lib/common.sh` | 317–348 |
|
||||
| ## 6. Docker Compose / ScaleTail | 349–391 |
|
||||
| ## 7. Optional Apps (`apps/`) | 392–421 |
|
||||
| ## 8. Entertainment Module | 422–435 |
|
||||
| ## 9. Systemd Services | 436–447 |
|
||||
| ## 10. Configuration Files | 448–474 |
|
||||
| ## 11. Coding Conventions | 475–507 |
|
||||
| ## 12. Development Workflow | 508–560 |
|
||||
| ## 13. Key File Quick Reference | 561–621 |
|
||||
| ## 14. Common Tasks for Agents | 622–652 |
|
||||
| ## 2. Directory Structure | 44–194 |
|
||||
| ## 3. Installation Flow | 195–246 |
|
||||
| ## 4. The `pos` CLI System | 247–318 |
|
||||
| ## 5. Shared Library — `lib/common.sh` | 319–350 |
|
||||
| ## 6. Docker Compose / ScaleTail | 351–393 |
|
||||
| ## 7. Optional Apps (`apps/`) | 394–423 |
|
||||
| ## 8. Entertainment Module | 424–437 |
|
||||
| ## 9. Systemd Services | 438–449 |
|
||||
| ## 10. Configuration Files | 450–476 |
|
||||
| ## 11. Coding Conventions | 477–509 |
|
||||
| ## 12. Development Workflow | 510–562 |
|
||||
| ## 13. Key File Quick Reference | 563–625 |
|
||||
| ## 14. Common Tasks for Agents | 626–657 |
|
||||
<!-- GEN:END docmap -->
|
||||
|
||||
## 1. Project Overview
|
||||
@@ -78,6 +78,7 @@ Linux_post_install/
|
||||
│ ├── pos-entertainment-status # Show enabled plugins and scheduler state
|
||||
│ ├── pos-media-mp3 # Download audio as MP3 (yt-dlp)
|
||||
│ ├── pos-media-mp4 # Download video as MP4 (smart/interactive format select)
|
||||
│ ├── pos-media-sync # Incremental Music → USB sync (mp3/mp4, add/update only)
|
||||
│ ├── pos-network-checkport # Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view
|
||||
│ ├── pos-network-download # aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits)
|
||||
│ ├── pos-network-hotspot # Wi-Fi hotspot via create_ap + wihotspot-gui
|
||||
@@ -279,6 +280,7 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst
|
||||
| entertainment | status | `pos-entertainment-status` | Show enabled plugins and scheduler state |
|
||||
| media | mp3 | `pos-media-mp3` | Download audio as MP3 (yt-dlp) |
|
||||
| media | mp4 | `pos-media-mp4` | Download video as MP4 (smart/interactive format select) |
|
||||
| media | sync | `pos-media-sync` | Incremental Music → USB sync (mp3/mp4, add/update only) |
|
||||
| network | checkport | `pos-network-checkport` | Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view |
|
||||
| network | download | `pos-network-download` | aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits) |
|
||||
| network | hotspot | `pos-network-hotspot` | Wi-Fi hotspot via create_ap + wihotspot-gui |
|
||||
@@ -572,6 +574,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||
| `lib/entertainment-plugin-lib.sh` | 67 | Message-safe helpers for plugins (config load, require, fetch+retry) — plugins MAY source it |
|
||||
| `lib/scheduler-lib.sh` | 760 | Scheduler lib (job parsing, notify policies, per-job user timers via user-timers-lib, legacy migrate) |
|
||||
| `lib/user-timers-lib.sh` | 112 | Shared systemd **user** timer machinery (interval→OnCalendar, unit pair writer, linger) |
|
||||
| `lib/usb-lib.sh` | 194 | Shared USB-storage detection + pick flow (detect/mount-offer/`usb_pick_root`) — used by `pos system backup` + `pos media sync` |
|
||||
| `bin/flag-reader` | 58 | Inspect flags (list/status/`--raw`) |
|
||||
| `bin/flag-set` | 21 | Set a flag (optionally with a value) |
|
||||
| `bin/flag-clear` | 21 | Unset a flag |
|
||||
@@ -596,6 +599,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||
| `bin/pos-entertainment-status` | 62 | Show enabled plugins and scheduler state |
|
||||
| `bin/pos-media-mp3` | 86 | Download audio as MP3 (yt-dlp) |
|
||||
| `bin/pos-media-mp4` | 132 | Download video as MP4 (smart/interactive format select) |
|
||||
| `bin/pos-media-sync` | 138 | Incremental Music → USB sync (mp3/mp4, add/update only) |
|
||||
| `bin/pos-network-checkport` | 496 | Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view |
|
||||
| `bin/pos-network-download` | 951 | aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits) |
|
||||
| `bin/pos-network-hotspot` | 93 | Wi-Fi hotspot via create_ap + wihotspot-gui |
|
||||
@@ -607,13 +611,13 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||
| `bin/pos-share-smb-server` | 253 | Manage the Samba server (status, share/unshare exports, users, enable/disable) |
|
||||
| `bin/pos-share-usb-server` | 218 | USB Redirector server control (--ls, --share; prompts when args omitted) |
|
||||
| `bin/pos-ssh-load-keys` | 31 | Load all SSH keys into the agent |
|
||||
| `bin/pos-system-backup` | 364 | Encrypted (AES-256) folder snapshots (tar + gpg) |
|
||||
| `bin/pos-system-backup` | 216 | Encrypted (AES-256) folder snapshots (tar + gpg) |
|
||||
| `bin/pos-system-firewall` | 308 | Interactive UFW management |
|
||||
| `bin/pos-system-health` | 209 | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL |
|
||||
| `bin/pos-system-schedule` | 81 | Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently |
|
||||
| `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
|
||||
| `bin/pos-tree` | 112 | Show the pos CLI command tree: categories, commands, and subcommands |
|
||||
| `completions/pos.bash` | 292 | Dynamic bash completion |
|
||||
| `completions/pos.bash` | 293 | Dynamic bash completion |
|
||||
<!-- GEN:END filetable -->
|
||||
| `apps/install.sh` | 171 | App install/uninstall picker/orchestrator |
|
||||
|
||||
@@ -643,6 +647,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||
| Modify NFS share logic | Edit `bin/pos-share-nfs-server` / `bin/pos-share-nfs-client` |
|
||||
| Modify SMB share logic | Edit `bin/pos-share-smb-server` / `bin/pos-share-smb-client` |
|
||||
| Modify scrcpy mirroring logic | Edit `bin/pos-communication-scrcpy` (config scope `scrcpy` via `pos config scrcpy`; `SCRCPY_*` keys in `~/.config/linux_post_install/scrcpy.env`) |
|
||||
| Modify Music→USB sync logic | Edit `bin/pos-media-sync` / shared USB layer `lib/usb-lib.sh` (seams `MEDIA_SYNC_SOURCE`/`MEDIA_SYNC_DEST`/`USB_MOUNT_BASE`/`USB_BYID` in `~/.config/linux_post_install/system.env`) |
|
||||
| Modify the scheduler / scheduled jobs | Edit `bin/pos-system-schedule` / `lib/scheduler-lib.sh` (jobs in `~/.config/linux_post_install/schedule.d/`) |
|
||||
| Modify AI/Gemini logic | Edit `bin/pos-ai-gemini` (config scope `ai` via `pos config ai`; `AI_GEMINI_API_KEY`/`AI_GEMINI_MODEL` in `~/.config/linux_post_install/ai.env`) |
|
||||
| Modify UFW/firewall logic | Edit `bin/pos-system-firewall` |
|
||||
|
||||
+2
-2
@@ -154,7 +154,7 @@ Two kinds of config, don't mix them up:
|
||||
|
||||
- **Machine defaults shipped by the installer:** place the file in `config/` and add copy logic to `postinstall.sh`. If it contains secrets, add to `.gitignore` and document in `DOC/`.
|
||||
- **Runtime tool config set by the user:** `~/.config/linux_post_install/<tool>.env` with `chmod 600`. Load it with env-var precedence (flags > environment > file). Patterns: `pos-docker-compose` (`compose.env`), `pos-communication-telegram-sender` (`telegram.env`, edited via `pos config telegram` — token masked), and the shared ones below. Never store tokens in the repo.
|
||||
- `system.env` — shared "system" settings loaded by `pos-system-*` tools via `load_system_env()` in `lib/common.sh` (currently `BACKUP_SERVICE_ROOTS`, `BACKUP_USB_ROOT`, `BACKUP_MOUNT_BASE`, `BACKUP_USB_BYID`, `HEALTH_BACKUP_MAX_AGE_DAYS`). Env already exported wins over the file.
|
||||
- `system.env` — shared "system" settings loaded via `load_system_env()` in `lib/common.sh` (currently `BACKUP_SERVICE_ROOTS`, `BACKUP_USB_ROOT`, `BACKUP_MOUNT_BASE`, `BACKUP_USB_BYID`, `HEALTH_BACKUP_MAX_AGE_DAYS`, plus `USB_MOUNT_BASE`/`USB_BYID`/`MEDIA_SYNC_SOURCE`/`MEDIA_SYNC_DEST` for `pos media sync`). Env already exported wins over the file.
|
||||
- `notify.env` — alerting platform selection (`NOTIFY_PLATFORM=telegram,matrix`), read by `lib/notify.sh`.
|
||||
|
||||
### 5. Add SSH keys (if needed)
|
||||
@@ -193,7 +193,7 @@ make lint # convention gate (scripts/lint-conventions.sh)
|
||||
|
||||
`make check` only proves syntax, exec bits, doc sync and dispatch — not behaviour. For tools that need `sudo`, systemd, or binaries absent from the dev box (samba, usbsrv, …), test them end-to-end with two patterns:
|
||||
|
||||
- **Env-overridable paths.** Anything that touches a system config location gets an env override whose default is the real path — the seam that lets the tool be exercised against temp files. Precedents: `FLAGS_DIR` (`lib/flags.sh`), `SMB_CONF` (`bin/pos-share-smb-server`, default `/etc/samba/smb.conf`), `SMB_CREDS_DIR`/`UNIT_DIR` (`bin/pos-share-smb-client`), `USER_SYSTEMD_DIR` (`bin/pos-network-download`, `bin/pos-communication-{telegram,matrix}-listener`, `lib/scheduler-lib.sh` — write it as `${USER_SYSTEMD_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user}`), and the scheduler's `SCHEDULE_DIR`/`SCHEDULE_STATE_DIR`/`SCHEDULE_LOG_DIR`/`SCHED_LEGACY_ENV` (`lib/scheduler-lib.sh`). Pick a short tool-specific name and don't advertise it in `usage()` — it's a test seam, not user-facing. **Gotcha (session-learned):** a `VAR="${XDG…:-…}"` without the leading `VAR:-` *overrides* the seam — the stub run then silently writes to the real `$HOME` path and every assertion passes while the bug hides. The override must be written first, then tested with `VAR=/tmp/x …` and a check that the real path is untouched.
|
||||
- **Env-overridable paths.** Anything that touches a system config location gets an env override whose default is the real path — the seam that lets the tool be exercised against temp files. Precedents: `FLAGS_DIR` (`lib/flags.sh`), `SMB_CONF` (`bin/pos-share-smb-server`, default `/etc/samba/smb.conf`), `SMB_CREDS_DIR`/`UNIT_DIR` (`bin/pos-share-smb-client`), `USER_SYSTEMD_DIR` (`bin/pos-network-download`, `bin/pos-communication-{telegram,matrix}-listener`, `lib/scheduler-lib.sh` — write it as `${USER_SYSTEMD_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user}`), the scheduler's `SCHEDULE_DIR`/`SCHEDULE_STATE_DIR`/`SCHEDULE_LOG_DIR`/`SCHED_LEGACY_ENV` (`lib/scheduler-lib.sh`), and the USB layer shared by `pos system backup` + `pos media sync`: `USB_MOUNT_BASE` (default `/media`, keeps `BACKUP_MOUNT_BASE` as an alias) and `USB_BYID` (default `/dev/disk/by-id`, keeps `BACKUP_USB_BYID` as an alias) — both guarded as `VAR="${VAR:-${BACKUP_…:-default}}"` in `lib/usb-lib.sh` so existing config lines keep working. Pick a short tool-specific name and don't advertise it in `usage()` — it's a test seam, not user-facing. **Gotcha (session-learned):** a `VAR="${XDG…:-…}"` without the leading `VAR:-` *overrides* the seam — the stub run then silently writes to the real `$HOME` path and every assertion passes while the bug hides. The override must be written first, then tested with `VAR=/tmp/x …` and a check that the real path is untouched.
|
||||
- **Stub PATH.** Create a temp dir with fake binaries, then run the tool with `PATH="$stubs:$PATH"`: fake `sudo` → `exec "$@"`; fake `systemctl`/`smbcontrol`/`mount.cifs` → echo their args; fake `testparm` → `cat` the file back (so validation passes); fake `systemd-escape` → print a fixed name. Assert on output **and** exit codes — happy path plus each failure path (`err` sets rc=1).
|
||||
- **Interactive prompts** (`read … </dev/tty`): drive them with a PTY — `printf 'answer\n' | script -qec "cmd" /dev/null` — then assert the side effect (e.g. the chmod-600 creds file lands with the right mode).
|
||||
|
||||
|
||||
@@ -206,6 +206,7 @@ The standalone `vbox` command still works and forwards to `pos docker vbox` (see
|
||||
|---------|------|---------|---------------|
|
||||
| `pos media mp3 <url>` | `bin/pos-media-mp3` | Download audio as MP3 via yt-dlp, with thumbnail + metadata | Output to `~/Music/%(title)s.%(ext)s`, `--audio-quality 0` |
|
||||
| `pos media mp4 <url>` | `bin/pos-media-mp4` | Download video via yt-dlp with **interactive format selection** | Lists formats (`yt-dlp -F`), asks for a format ID, saves to `~/Videos/` |
|
||||
| `pos media sync [--mp3\|--mp4]` | `bin/pos-media-sync` | Incremental Music → USB sync (add/update only — never deletes) | Copies mp3/mp4 from `$HOME/Music` (or `--source <dir>`) into `<usb>/Music/`, preserving the tree; missing or changed (size/mtime) files are copied, identical ones skipped. Same USB detection as `pos system backup` (lsblk TRAN + lsusb/by-id, mount offer for unmounted sticks, multi-stick picker). `--mp3`/`--mp4` filter by extension, neither = both; `--dry-run` previews. Config: `MEDIA_SYNC_SOURCE`, `MEDIA_SYNC_DEST`, shared `USB_MOUNT_BASE`/`USB_BYID` from `~/.config/linux_post_install/system.env`. Result notified via `lib/notify.sh` |
|
||||
|
||||
### system
|
||||
|
||||
|
||||
+8
-1
@@ -38,7 +38,7 @@ The phases:
|
||||
| # | Phase | Script/action |
|
||||
|---|-------|----------------|
|
||||
| 1 | preinstall | `preinstall.sh` — apt packages + yt-dlp |
|
||||
| 2 | scripts | Copies `bin/*` → `/usr/local/bin/` (755), `lib/common.sh` + `lib/flags.sh` + `lib/notify.sh` + `lib/entertainment-lib.sh` + `lib/entertainment-plugin-lib.sh` + `lib/scheduler-lib.sh` + `lib/config-ui.sh` + `lib/user-timers-lib.sh` → `/usr/local/bin/` (644). Copies precompiled arch binaries from `x64_bin/` (or `arm64_bin/`) → `/usr/local/bin/`. With `--feature`: also installs `features/*` (see below) |
|
||||
| 2 | scripts | Copies `bin/*` → `/usr/local/bin/` (755), `lib/common.sh` + `lib/flags.sh` + `lib/notify.sh` + `lib/entertainment-lib.sh` + `lib/entertainment-plugin-lib.sh` + `lib/scheduler-lib.sh` + `lib/config-ui.sh` + `lib/user-timers-lib.sh` + `lib/usb-lib.sh` → `/usr/local/bin/` (644). Copies precompiled arch binaries from `x64_bin/` (or `arm64_bin/`) → `/usr/local/bin/`. With `--feature`: also installs `features/*` (see below) |
|
||||
| 3 | postinstall | `postinstall.sh` — PATH, completion, SSH keys, systemd |
|
||||
| 4 | scalepoint | Shallow-clones ScaleTail templates to `/usr/local/share/linux_post_install/scale-tail` |
|
||||
| 5 (opt) | apps | `apps/install.sh` when `--apps` (interactive) or `--full` (all, non-interactive) |
|
||||
@@ -207,6 +207,13 @@ Sourced by `bin/pos-entertainment-send|config|enable|disable|status` (after `lib
|
||||
|
||||
---
|
||||
|
||||
## lib/usb-lib.sh — shared USB-storage detection
|
||||
|
||||
**File:** `lib/usb-lib.sh` (installed to `/usr/local/bin/usb-lib.sh`)
|
||||
**Purpose:** the one copy of the USB-storage machinery shared by `pos system backup`'s post-verify USB copy and `pos media sync` — `usb_detect` (lsblk JSON, TRAN + lsusb/by-id cross-check → `USB_MOUNTED`/`USB_UNMOUNTED`), `usb_related_present`, `usb_mount_offer` (mount an unmounted stick at `/media/<label>`, `usb-automount` scheme), and `usb_pick_root` (detect → mount-offer → single/multi picker → `USB_ROOT`). Defines only `usb_*`; seams `USB_MOUNT_BASE` (default `/media`, alias `BACKUP_MOUNT_BASE`) and `USB_BYID` (default `/dev/disk/by-id`, alias `BACKUP_USB_BYID`) keep existing config lines working.
|
||||
|
||||
---
|
||||
|
||||
## features/autostart.sh — boot-time feature
|
||||
|
||||
**File:** `features/autostart.sh` (installed to `/usr/local/bin/autostart.sh` by `./install.sh --feature`)
|
||||
|
||||
+52
-1
@@ -1,11 +1,13 @@
|
||||
# How-To: `pos media`
|
||||
|
||||
Download audio and video from the web via `yt-dlp`. Tools: `mp3`, `mp4`.
|
||||
Download audio and video from the web via `yt-dlp`, and sync your library to a
|
||||
USB stick. Tools: `mp3`, `mp4`, `sync`.
|
||||
|
||||
| Tool | What it does |
|
||||
|------|--------------|
|
||||
| `pos media mp3` | Download audio, convert to MP3 |
|
||||
| `pos media mp4` | Download video with smart/interactive format selection |
|
||||
| `pos media sync` | Incrementally copy `~/Music` onto a USB stick (mp3/mp4) |
|
||||
|
||||
Requires `yt-dlp` and `ffmpeg` (`sudo apt install yt-dlp ffmpeg`); the tools
|
||||
fail with a clean error message instead of a raw `command not found` if either
|
||||
@@ -84,6 +86,55 @@ video+audio and merges them.
|
||||
|
||||
---
|
||||
|
||||
## `pos media sync` — music onto a USB stick
|
||||
|
||||
```bash
|
||||
pos media sync # copy everything (mp3 + mp4) from ~/Music to the stick
|
||||
pos media sync --mp3 # only the .mp3 files
|
||||
pos media sync --mp4 # only the .mp4 files
|
||||
```
|
||||
|
||||
Detects connected USB storage exactly like `pos system backup` (same shared
|
||||
`lib/usb-lib.sh`): a plugged-in but unmounted stick is offered a mount first
|
||||
(`/media/<label>`, world-writable, mirrors `usb-automount`), multiple sticks
|
||||
are listed for you to pick, and if nothing is plugged in it re-scans after you
|
||||
press Enter. Files are mirrored into `<usb>/Music/` (change with
|
||||
`MEDIA_SYNC_DEST`) preserving the artist/album tree.
|
||||
|
||||
**Sync semantics — add/update only, never delete.** Files missing on the stick
|
||||
are copied; files whose size or mtime changed are overwritten; everything
|
||||
identical is skipped. Files on the stick that are no longer in the source are
|
||||
**left alone** — a playback stick can never lose files to a mirror mistake.
|
||||
Copies keep the source timestamps (`cp --preserve=timestamps`), so a re-run is
|
||||
a no-op. Preview before copying with `--dry-run`:
|
||||
|
||||
```bash
|
||||
pos media sync --mp4 --dry-run # shows "would copy" list + counts, copies nothing
|
||||
```
|
||||
|
||||
| Flag | Meaning |
|
||||
|------|---------|
|
||||
| `--mp3` | Sync only `*.mp3` (neither flag = both) |
|
||||
| `--mp4` | Sync only `*.mp4` (neither flag = both) |
|
||||
| `--source <dir>` | Source folder (default `~/Music`) |
|
||||
| `--dry-run` | Preview what would be copied, copy nothing |
|
||||
|
||||
When it finishes it announces the result via `lib/notify.sh`
|
||||
(`Music sync completed: N added, M updated → <usb>/Music`).
|
||||
|
||||
**Recipes:**
|
||||
```bash
|
||||
pos media sync # keep the car stick up to date (both formats)
|
||||
pos media sync --mp3 --dry-run # check what a new batch will bring first
|
||||
pos media sync --source /data/Music # sync a library that lives elsewhere
|
||||
```
|
||||
|
||||
Config (all in `~/.config/linux_post_install/system.env` or exported):
|
||||
`MEDIA_SYNC_SOURCE` (default `$HOME/Music`), `MEDIA_SYNC_DEST` (default
|
||||
`Music`), plus the shared `USB_MOUNT_BASE` / `USB_BYID` seams.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Format list is empty / download fails → the site or age-gate requires
|
||||
|
||||
@@ -256,7 +256,7 @@ MAIN_LOG="$LOG_DIR/pos.log"
|
||||
log_cmd() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $* → exit $2" >> "$MAIN_LOG"; }
|
||||
|
||||
# Commands that read from stdin interactively — only log invocation
|
||||
INTERACTIVE_CMDS="docker-compose docker-vbox network-hotspot system-firewall media-mp4 system-backup share-usb-server share-smb-server share-smb-client communication-telegram-listener communication-matrix-listener ai-gemini system-schedule entertainment-config config"
|
||||
INTERACTIVE_CMDS="docker-compose docker-vbox network-hotspot system-firewall media-mp4 media-sync system-backup share-usb-server share-smb-server share-smb-client communication-telegram-listener communication-matrix-listener ai-gemini system-schedule entertainment-config config"
|
||||
|
||||
for ((i=n-1; i>=0; i--)); do
|
||||
cmd="pos"
|
||||
|
||||
Executable
+138
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# POS: media sync — Incremental Music → USB sync (mp3/mp4, add/update only)
|
||||
# POS_FLAGS: --mp3 --mp4 --source --dry-run
|
||||
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
source "$(dirname "$0")/../lib/notify.sh" 2>/dev/null || source "$(dirname "$0")/notify.sh"
|
||||
source "$(dirname "$0")/../lib/usb-lib.sh" 2>/dev/null || source "$(dirname "$0")/usb-lib.sh"
|
||||
|
||||
load_system_env
|
||||
|
||||
# Deps guards sit before -h|--help (help also errors on a box missing the deps).
|
||||
command -v lsblk &>/dev/null || err "lsblk not found (util-linux) — needed to detect USB storage"
|
||||
command -v jq &>/dev/null || err "jq not found — needed to detect USB storage (sudo apt install jq)"
|
||||
|
||||
SRC="${MEDIA_SYNC_SOURCE:-$HOME/Music}"
|
||||
DEST_DIR="${MEDIA_SYNC_DEST:-Music}"
|
||||
MP3=0
|
||||
MP4=0
|
||||
DRY_RUN=0
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: pos media sync [options]
|
||||
|
||||
Incrementally copy your music onto a USB stick. Add/update only — files on
|
||||
the stick that are no longer in the source are left alone, never deleted.
|
||||
USB storage is detected like \`pos system backup\` (lsblk TRAN + lsusb/by-id
|
||||
corroboration; unmounted sticks get a mount offer first) and you pick which
|
||||
one to sync to. The source tree is mirrored under <usb>/$DEST_DIR.
|
||||
|
||||
Options:
|
||||
--mp3 Sync only *.mp3 files
|
||||
--mp4 Sync only *.mp4 files
|
||||
--source <dir> Source folder (default: $SRC)
|
||||
--dry-run Preview what would be copied (copies nothing)
|
||||
-h, --help This help
|
||||
|
||||
Examples:
|
||||
pos media sync
|
||||
pos media sync --mp3
|
||||
pos media sync --mp4 --dry-run
|
||||
pos media sync --source /data/Music
|
||||
|
||||
Environment:
|
||||
MEDIA_SYNC_SOURCE Source folder (default: \$HOME/Music)
|
||||
MEDIA_SYNC_DEST Subfolder on the USB stick (default: Music)
|
||||
USB_MOUNT_BASE Where to mount an unmounted stick (default: /media)
|
||||
USB_BYID by-id dir used to corroborate USB detection
|
||||
(default: /dev/disk/by-id)
|
||||
(loaded from ~/.config/linux_post_install/system.env unless exported)
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage ;;
|
||||
--mp3) MP3=1; shift ;;
|
||||
--mp4) MP4=1; shift ;;
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
--source)
|
||||
[ $# -ge 2 ] || err "--source needs a value: pos media sync --source <dir>"
|
||||
SRC="$2"; shift 2 ;;
|
||||
--source=*) SRC="${1#--source=}"; shift ;;
|
||||
-*) err "Unknown option: $1 (see --help)" ;;
|
||||
*) err "Unexpected argument: $1 (see --help)" ;;
|
||||
esac
|
||||
done
|
||||
[ "$MP3" -eq 1 ] || [ "$MP4" -eq 1 ] || { MP3=1; MP4=1; }
|
||||
|
||||
[ -d "$SRC" ] || err "Source not found: $SRC"
|
||||
|
||||
trap 'notify_send "Music sync FAILED"' ERR
|
||||
|
||||
section "Music sync"
|
||||
echo "Source : $SRC"
|
||||
if [ "$MP3" -eq 1 ] && [ "$MP4" -eq 1 ]; then
|
||||
echo "Filter : mp3 + mp4"
|
||||
find_expr=(-type f \( -iname '*.mp3' -o -iname '*.mp4' \))
|
||||
elif [ "$MP3" -eq 1 ]; then
|
||||
echo "Filter : mp3 only"
|
||||
find_expr=(-type f -iname '*.mp3')
|
||||
else
|
||||
echo "Filter : mp4 only"
|
||||
find_expr=(-type f -iname '*.mp4')
|
||||
fi
|
||||
|
||||
usb_pick_root "Sync to" "$DEST_DIR" "no sync performed" || {
|
||||
log "Skipped — no sync performed"
|
||||
exit 0
|
||||
}
|
||||
dest_root="${USB_ROOT%/}/$DEST_DIR"
|
||||
echo "Target : $dest_root"
|
||||
|
||||
# Does the destination need this file copied? Missing, or size/mtime differs.
|
||||
needs_copy() {
|
||||
local src="$1" dst="$2" ss="" ds="" sm="" dm=""
|
||||
[ -f "$dst" ] || return 0
|
||||
ss="$(stat -c %s "$src" 2>/dev/null || printf 0)"
|
||||
ds="$(stat -c %s "$dst" 2>/dev/null || printf 0)"
|
||||
sm="$(stat -c %Y "$src" 2>/dev/null || printf 0)"
|
||||
dm="$(stat -c %Y "$dst" 2>/dev/null || printf 0)"
|
||||
[ "$ss" = "$ds" ] && [ "$sm" -le "$dm" ] && return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
added=0
|
||||
updated=0
|
||||
unchanged=0
|
||||
while IFS= read -r f; do
|
||||
rel="${f#"$SRC/"}"
|
||||
dest="$dest_root/$rel"
|
||||
|
||||
if needs_copy "$f" "$dest"; then
|
||||
if [ -f "$dest" ]; then
|
||||
updated=$((updated + 1))
|
||||
else
|
||||
added=$((added + 1))
|
||||
fi
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
log "would copy $rel"
|
||||
else
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
cp --preserve=timestamps "$f" "$dest"
|
||||
echo " + $rel"
|
||||
fi
|
||||
else
|
||||
unchanged=$((unchanged + 1))
|
||||
fi
|
||||
done < <(find "$SRC" "${find_expr[@]}" | sort)
|
||||
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
echo "DRY RUN — nothing copied. Would sync: ${added} new, ${updated} updated, ${unchanged} unchanged → $dest_root"
|
||||
else
|
||||
ok "Sync complete: ${added} added, ${updated} updated, ${unchanged} unchanged → $dest_root"
|
||||
notify_send "Music sync completed: ${added} added, ${updated} updated → $dest_root"
|
||||
fi
|
||||
+16
-164
@@ -6,6 +6,7 @@ set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
source "$(dirname "$0")/../lib/notify.sh" 2>/dev/null || source "$(dirname "$0")/notify.sh"
|
||||
source "$(dirname "$0")/../lib/usb-lib.sh" 2>/dev/null || source "$(dirname "$0")/usb-lib.sh"
|
||||
|
||||
load_system_env
|
||||
EFF_ROOTS="${BACKUP_SERVICE_ROOTS:-/srv $HOME/srv}"
|
||||
@@ -42,7 +43,10 @@ Environment:
|
||||
BACKUP_USB_ROOT USB root to copy finished backups to
|
||||
(default: auto-detect mounted USB storage)
|
||||
BACKUP_MOUNT_BASE Where to mount an unmounted USB stick offered
|
||||
during the copy (default: /media)
|
||||
during the copy (default: /media; alias of
|
||||
USB_MOUNT_BASE)
|
||||
BACKUP_USB_BYID by-id dir used to corroborate USB detection
|
||||
(default: /dev/disk/by-id; alias of USB_BYID)
|
||||
(loaded from ~/.config/linux_post_install/system.env unless exported)
|
||||
EOF
|
||||
exit 0
|
||||
@@ -55,79 +59,28 @@ command -v tar &>/dev/null || err "tar not found"
|
||||
# the archive was being made is found. The copy lands in <usb>/backups/
|
||||
# and the transfer is proven 100% (sha256 source vs copy) before any
|
||||
# success is announced. BACKUP_USB_ROOT pins the root and skips
|
||||
# detection; otherwise USB storage is auto-detected.
|
||||
#
|
||||
# "Is it really USB?" — lsblk's TRAN column decides per device (rm==1
|
||||
# card readers / hot-swap bays report sata and are skipped). When TRAN
|
||||
# is unavailable, /dev/disk/by-id/usb-* symlinks and lsusb text
|
||||
# corroborate. A stick that is plugged in but not mounted (the CLI-box
|
||||
# case) is offered a mount (sudo, usb-automount scheme: /media/<label>,
|
||||
# fallback /media/usb-<devname>) before the copy; cron/EOF skips.
|
||||
# detection; otherwise usb_pick_root (lib/usb-lib.sh) auto-detects,
|
||||
# offers to mount unmounted sticks, and picks the target.
|
||||
usb_copy_offer() {
|
||||
local archive="$1" pass=0 resp="" i=0 root="" dest_dir="" dest="" src_sum="" dst_sum=""
|
||||
local -a roots=()
|
||||
local offered=0
|
||||
local archive="$1" root="" dest_dir="" dest="" src_sum="" dst_sum=""
|
||||
|
||||
command -v lsblk &>/dev/null || { warn "lsblk not found — USB copy skipped"; return 0; }
|
||||
command -v jq &>/dev/null || { warn "jq not found — USB copy skipped"; return 0; }
|
||||
|
||||
section "USB copy"
|
||||
|
||||
while :; do
|
||||
pass=$((pass + 1))
|
||||
roots=()
|
||||
|
||||
if [ -n "${BACKUP_USB_ROOT:-}" ]; then
|
||||
roots+=("$BACKUP_USB_ROOT")
|
||||
else
|
||||
detect_usb
|
||||
roots=("${USB_MOUNTED[@]}")
|
||||
|
||||
if [ ${#roots[@]} -eq 0 ] && [ ${#USB_UNMOUNTED[@]} -gt 0 ]; then
|
||||
if [ "$offered" -ge 1 ]; then
|
||||
warn "USB stick still not mounted — backup stays local: $archive"
|
||||
return 0
|
||||
fi
|
||||
offered=1
|
||||
mount_offer "${USB_UNMOUNTED[@]}" || {
|
||||
log "Skipped — backup stays local: $archive"
|
||||
return 0
|
||||
}
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
[ ${#roots[@]} -gt 0 ] && break
|
||||
|
||||
# None found — offer one re-scan before giving up.
|
||||
if [ "$pass" -ge 2 ]; then
|
||||
warn "Still no USB storage detected — backup stays local: $archive"
|
||||
return 0
|
||||
fi
|
||||
warn "No USB storage detected"
|
||||
read -rp "Plug a USB drive in now and press Enter to re-check (or 's' to skip): " resp || return 0
|
||||
case "$resp" in
|
||||
s|S) log "Skipped — backup stays local: $archive"; return 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ ${#roots[@]} -eq 1 ]; then
|
||||
root="${roots[0]}"
|
||||
if [ -n "${BACKUP_USB_ROOT:-}" ]; then
|
||||
root="$BACKUP_USB_ROOT"
|
||||
if ! confirm "Copy backup to ${root%/}/backups/?" n; then
|
||||
log "Skipped — backup stays local: $archive"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
echo "Multiple USB storages found:"
|
||||
for i in "${!roots[@]}"; do
|
||||
printf "%2d) %s\n" "$((i + 1))" "${roots[$i]}"
|
||||
done
|
||||
read -rp "Copy backup to which one? [1-${#roots[@]}] (0 = skip): " resp || return 0
|
||||
if ! [[ "$resp" =~ ^[0-9]+$ ]] || (( resp < 1 || resp > ${#roots[@]} )); then
|
||||
usb_pick_root "Copy backup to" "backups" "backup stays local: $archive" || {
|
||||
log "Skipped — backup stays local: $archive"
|
||||
return 0
|
||||
fi
|
||||
root="${roots[$((resp - 1))]}"
|
||||
}
|
||||
root="$USB_ROOT"
|
||||
fi
|
||||
|
||||
dest_dir="${root%/}/backups"
|
||||
@@ -153,110 +106,9 @@ usb_copy_offer() {
|
||||
notify_send "Backup copied to USB: $dest (sha256 verified)"
|
||||
}
|
||||
|
||||
# ── USB detection ────────────────────────────────────────────────
|
||||
# Fills USB_MOUNTED (mountpoints, one per removable USB storage) and
|
||||
# USB_UNMOUNTED ("path|label|size|model" entries) from lsblk JSON.
|
||||
# TRAN=="usb" is the per-device deciding signal; when TRAN is empty,
|
||||
# usb_related_present() corroborates. Non-USB removables are skipped.
|
||||
detect_usb() {
|
||||
local out path="" mp="" label="" size="" model="" tran="" type="" children=""
|
||||
USB_MOUNTED=()
|
||||
USB_UNMOUNTED=()
|
||||
out="$(lsblk -J -o NAME,PATH,LABEL,MOUNTPOINT,RM,TYPE,TRAN,SIZE,MODEL 2>/dev/null)" || return 0
|
||||
|
||||
while IFS=$'\x1f' read -r path mp label size model tran type children; do
|
||||
[ -n "$path" ] || continue
|
||||
if [ "$tran" = "usb" ]; then
|
||||
:
|
||||
elif [ -z "$tran" ] && usb_related_present; then
|
||||
warn "TRAN unavailable — assuming USB (lsusb/by-id corroboration)"
|
||||
else
|
||||
continue
|
||||
fi
|
||||
if [ -n "$mp" ]; then
|
||||
USB_MOUNTED+=("$mp")
|
||||
elif [ "$children" = "0" ]; then
|
||||
USB_UNMOUNTED+=("$path|$label|$size|$model")
|
||||
fi
|
||||
done < <(printf '%s' "$out" | jq -r '
|
||||
.. | objects
|
||||
| select(.rm == true and (.type == "part" or .type == "disk"))
|
||||
| [.path, (.mountpoint // ""), (.label // ""), (.size // ""),
|
||||
(.model // ""), (.tran // ""), (.type // ""),
|
||||
((.children // []) | length)]
|
||||
| join("\u001f")')
|
||||
}
|
||||
|
||||
# Whole-system "is any USB storage attached?" — udev by-id usb-* symlinks
|
||||
# are definitive; lsusb text is a secondary hint for storage-ish devices.
|
||||
# BACKUP_USB_BYID points at the by-id dir (test seam; default real one).
|
||||
usb_related_present() {
|
||||
[ -n "$(ls "${BACKUP_USB_BYID:-/dev/disk/by-id}"/usb-* 2>/dev/null)" ] && return 0
|
||||
command -v lsusb &>/dev/null \
|
||||
&& lsusb 2>/dev/null | grep -qiE 'mass storage|card reader|flash disk|usb.*(disk|drive|storage)|reader|external'
|
||||
}
|
||||
|
||||
# ── Mount an unmounted USB stick (CLI-box case) ──────────────────
|
||||
# Asks to mount a detected-but-unmounted device (sudo, mirroring
|
||||
# usb-automount: /media/<label>, fallback /media/usb-<devname>, -o
|
||||
# umask=000 world-writable). Returns 0 → caller re-scans (mounted now
|
||||
# or the user will mount manually); 1 → skipped/EOF (backup stays local).
|
||||
mount_offer() {
|
||||
local -a devs=("$@")
|
||||
local i=0 dev="" label="" size="" model="" mp="" resp="" n=""
|
||||
local -a chosen=()
|
||||
|
||||
if [ ${#devs[@]} -gt 1 ]; then
|
||||
echo "Multiple unmounted USB storages found:"
|
||||
for i in "${!devs[@]}"; do
|
||||
IFS='|' read -r dev label size model <<< "${devs[$i]}"
|
||||
printf "%2d) %s (%s, %s)\n" "$((i + 1))" "$dev" "$size" "${model:-no label}"
|
||||
done
|
||||
read -rp "Copy backup to which one? [1-${#devs[@]}] (0 = skip): " resp || return 1
|
||||
if ! [[ "$resp" =~ ^[0-9]+$ ]] || (( resp < 1 || resp > ${#devs[@]} )); then
|
||||
return 1
|
||||
fi
|
||||
chosen=("${devs[$((resp - 1))]}")
|
||||
else
|
||||
chosen=("${devs[0]}")
|
||||
fi
|
||||
|
||||
IFS='|' read -r dev label size model <<< "${chosen[0]}"
|
||||
label="${label//\//_}"
|
||||
mp="${BACKUP_MOUNT_BASE:-/media}/${label:-usb-$(basename "$dev")}"
|
||||
if [ -d "$mp" ] && mountpoint -q "$mp" 2>/dev/null; then
|
||||
n=2
|
||||
while [ -d "${mp}-${n}" ] && mountpoint -q "${mp}-${n}" 2>/dev/null; do
|
||||
n=$((n + 1))
|
||||
done
|
||||
mp="${mp}-${n}"
|
||||
fi
|
||||
|
||||
echo "Found USB storage not mounted: $dev ($size, ${model:-no label})"
|
||||
if confirm "Mount it at $mp (world-writable) so the backup can go there?" n; then
|
||||
run sudo mkdir -p "$mp"
|
||||
if ! run sudo mount -o umask=000 "$dev" "$mp"; then
|
||||
run sudo mount "$dev" "$mp" || true
|
||||
fi
|
||||
if [ "${DRY_RUN:-0}" -eq 1 ] || mountpoint -q "$mp" 2>/dev/null; then
|
||||
ok "Mounted $dev at $mp"
|
||||
return 0
|
||||
fi
|
||||
warn "Mount failed — do it manually, then we'll copy:"
|
||||
log " sudo mkdir -p $mp && sudo mount $dev $mp"
|
||||
else
|
||||
warn "Mount it manually, then we'll copy:"
|
||||
log " sudo mkdir -p $mp"
|
||||
log " sudo mount $dev $mp"
|
||||
fi
|
||||
|
||||
read -rp "Press Enter once mounted (or 's' to skip): " resp || return 1
|
||||
case "$resp" in
|
||||
s|S) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── USB detection / mount offer / pick flow ─────────────────────
|
||||
# Shared with pos-media-sync: lib/usb-lib.sh (usb_detect /
|
||||
# usb_related_present / usb_mount_offer / usb_pick_root).
|
||||
SERVICE=0
|
||||
ENCRYPT=1
|
||||
[ "${BACKUP_ENCRYPT:-1}" = "0" ] && ENCRYPT=0
|
||||
|
||||
@@ -10,6 +10,7 @@ _pos_flags[communication-telegram-sender]="--type --caption --parse-mode --no-pr
|
||||
_pos_flags[entertainment-send]="--print --markdown"
|
||||
_pos_flags[media-mp3]="--output --no-playlist --cookies --by-artist --dry-run"
|
||||
_pos_flags[media-mp4]="--format --best --worst --output --no-playlist --cookies --dry-run"
|
||||
_pos_flags[media-sync]="--mp3 --mp4 --source --dry-run"
|
||||
_pos_flags[network-checkport]="--tcp --udp --ping --no-banner --versions --timeout"
|
||||
_pos_flags[network-download]="--dir --out --split --seed --force --upload --gid --tmux"
|
||||
_pos_flags[network-hotspot]="--foreground"
|
||||
|
||||
+16
-7
@@ -1,8 +1,8 @@
|
||||
# ~/.config/linux_post_install/system.env — shared "system" tool config
|
||||
#
|
||||
# Loaded by `pos system health` and `pos system backup` via load_system_env()
|
||||
# with precedence: already-exported environment > this file > defaults.
|
||||
# Commented lines are defaults — uncomment to override.
|
||||
# Loaded by `pos system health`, `pos system backup` and `pos media sync` via
|
||||
# load_system_env() with precedence: already-exported environment > this file
|
||||
# > defaults. Commented lines are defaults — uncomment to override.
|
||||
|
||||
# Roots scanned by `pos system backup --service` and by the backup-age check
|
||||
# in `pos system health`. Default: /srv $HOME/srv
|
||||
@@ -19,9 +19,18 @@
|
||||
# cron). The copy lands in <root>/backups/ and is still sha256-verified.
|
||||
#BACKUP_USB_ROOT=/mnt/usb
|
||||
|
||||
# Where `pos system backup` mounts an unmounted stick it found. Default: /media
|
||||
#BACKUP_MOUNT_BASE=/media
|
||||
# Where `pos system backup` / `pos media sync` mount an unmounted stick they
|
||||
# found. Default: /media (BACKUP_MOUNT_BASE is kept as an alias)
|
||||
#USB_MOUNT_BASE=/media
|
||||
|
||||
# Dir holding by-id symlinks used to cross-check devices that show no TRAN in
|
||||
# lsblk. Default: /dev/disk/by-id
|
||||
#BACKUP_USB_BYID=/dev/disk/by-id
|
||||
# lsblk. Shared by `pos system backup` and `pos media sync`. Default:
|
||||
# /dev/disk/by-id (BACKUP_USB_BYID is kept as an alias)
|
||||
#USB_BYID=/dev/disk/by-id
|
||||
|
||||
# Source folder synced by `pos media sync`. Default: $HOME/Music
|
||||
#MEDIA_SYNC_SOURCE=/home/you/Music
|
||||
|
||||
# Subfolder on the USB stick that `pos media sync` mirrors the source into.
|
||||
# Default: Music
|
||||
#MEDIA_SYNC_DEST=Music
|
||||
|
||||
+1
-1
@@ -140,7 +140,7 @@ if should_run 2 scripts; then
|
||||
done
|
||||
lib_count=0
|
||||
lib_names=""
|
||||
for lf in common.sh flags.sh notify.sh entertainment-lib.sh scheduler-lib.sh config-ui.sh user-timers-lib.sh entertainment-plugin-lib.sh; do
|
||||
for lf in common.sh flags.sh notify.sh entertainment-lib.sh scheduler-lib.sh config-ui.sh user-timers-lib.sh entertainment-plugin-lib.sh usb-lib.sh; do
|
||||
run sudo install -m 644 "lib/$lf" "/usr/local/bin/$lf"
|
||||
lib_count=$((lib_count + 1))
|
||||
lib_names+="$lf "
|
||||
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
# lib/usb-lib.sh — shared USB-storage detection + pick flow. Used by
|
||||
# pos-system-backup (post-verify USB copy) and pos-media-sync (Music sync).
|
||||
# Requires common.sh helpers: log/warn/ok/section/confirm/run. Defines ONLY
|
||||
# usb_* functions plus the USB_MOUNTED / USB_UNMOUNTED / USB_ROOT globals.
|
||||
#
|
||||
# Seams (env overrides; the BACKUP_* names are kept as aliases so existing
|
||||
# system.env lines keep working):
|
||||
# USB_MOUNT_BASE where an unmounted stick is offered to mount (default /media)
|
||||
# USB_BYID /dev/disk/by-id dir consulted by usb_related_present
|
||||
#
|
||||
# API:
|
||||
# usb_detect fill USB_MOUNTED (mountpoints, one per
|
||||
# removable USB storage) and USB_UNMOUNTED
|
||||
# ("path|label|size|model" entries)
|
||||
# usb_related_present whole-system "is any USB storage attached?"
|
||||
# usb_mount_offer <devs...> offer to mount detected-but-unmounted sticks
|
||||
# (sudo, mirrors usb-automount: /media/<label>,
|
||||
# fallback /media/usb-<devname>, umask=000);
|
||||
# returns 0 = mounted (or user will do it
|
||||
# manually), 1 = skipped/EOF
|
||||
# usb_pick_root <confirm-prefix> <subfolder> <giveup-msg>
|
||||
# full detect → mount-offer → pick loop; returns
|
||||
# 0 with USB_ROOT set to the chosen mountpoint,
|
||||
# 1 = skipped. Single-stick: confirm
|
||||
# "<confirm-prefix> <root>/<subfolder>?" first.
|
||||
|
||||
USB_MOUNT_BASE="${USB_MOUNT_BASE:-${BACKUP_MOUNT_BASE:-/media}}"
|
||||
USB_BYID="${USB_BYID:-${BACKUP_USB_BYID:-/dev/disk/by-id}}"
|
||||
|
||||
# ── USB detection ────────────────────────────────────────────────
|
||||
# TRAN=="usb" is the per-device deciding signal; when TRAN is empty,
|
||||
# usb_related_present() corroborates. Non-USB removables are skipped. The
|
||||
# "TRAN unavailable" warning fires once per scan, not per device.
|
||||
usb_detect() {
|
||||
local out path="" mp="" label="" size="" model="" tran="" type="" children=""
|
||||
local warned=0
|
||||
USB_MOUNTED=()
|
||||
USB_UNMOUNTED=()
|
||||
out="$(lsblk -J -o NAME,PATH,LABEL,MOUNTPOINT,RM,TYPE,TRAN,SIZE,MODEL 2>/dev/null)" || return 0
|
||||
|
||||
while IFS=$'\x1f' read -r path mp label size model tran type children; do
|
||||
[ -n "$path" ] || continue
|
||||
if [ "$tran" = "usb" ]; then
|
||||
:
|
||||
elif [ -z "$tran" ] && usb_related_present; then
|
||||
if [ "$warned" -eq 0 ]; then
|
||||
warn "TRAN unavailable — assuming USB (lsusb/by-id corroboration)"
|
||||
warned=1
|
||||
fi
|
||||
else
|
||||
continue
|
||||
fi
|
||||
if [ -n "$mp" ]; then
|
||||
USB_MOUNTED+=("$mp")
|
||||
elif [ "$children" = "0" ]; then
|
||||
USB_UNMOUNTED+=("$path|$label|$size|$model")
|
||||
fi
|
||||
done < <(printf '%s' "$out" | jq -r '
|
||||
.. | objects
|
||||
| select(.rm == true and (.type == "part" or .type == "disk"))
|
||||
| [.path, (.mountpoint // ""), (.label // ""), (.size // ""),
|
||||
(.model // ""), (.tran // ""), (.type // ""),
|
||||
((.children // []) | length)]
|
||||
| join("\u001f")')
|
||||
}
|
||||
|
||||
# Whole-system "is any USB storage attached?" — udev by-id usb-* symlinks
|
||||
# are definitive; lsusb text is a secondary hint for storage-ish devices.
|
||||
usb_related_present() {
|
||||
[ -n "$(ls "${USB_BYID}"/usb-* 2>/dev/null)" ] && return 0
|
||||
command -v lsusb &>/dev/null \
|
||||
&& lsusb 2>/dev/null | grep -qiE 'mass storage|card reader|flash disk|usb.*(disk|drive|storage)|reader|external'
|
||||
}
|
||||
|
||||
# ── Mount an unmounted USB stick (CLI-box case) ──────────────────
|
||||
# Returns 0 → caller re-scans (mounted now or the user will mount manually);
|
||||
# 1 → skipped/EOF.
|
||||
usb_mount_offer() {
|
||||
local -a devs=("$@")
|
||||
local i=0 dev="" label="" size="" model="" mp="" resp="" n=""
|
||||
local -a chosen=()
|
||||
|
||||
if [ ${#devs[@]} -gt 1 ]; then
|
||||
echo "Multiple unmounted USB storages found:"
|
||||
for i in "${!devs[@]}"; do
|
||||
IFS='|' read -r dev label size model <<< "${devs[$i]}"
|
||||
printf "%2d) %s (%s, %s)\n" "$((i + 1))" "$dev" "$size" "${model:-no label}"
|
||||
done
|
||||
read -rp "Use which one? [1-${#devs[@]}] (0 = skip): " resp || return 1
|
||||
if ! [[ "$resp" =~ ^[0-9]+$ ]] || (( resp < 1 || resp > ${#devs[@]} )); then
|
||||
return 1
|
||||
fi
|
||||
chosen=("${devs[$((resp - 1))]}")
|
||||
else
|
||||
chosen=("${devs[0]}")
|
||||
fi
|
||||
|
||||
IFS='|' read -r dev label size model <<< "${chosen[0]}"
|
||||
label="${label//\//_}"
|
||||
mp="${USB_MOUNT_BASE}/${label:-usb-$(basename "$dev")}"
|
||||
if [ -d "$mp" ] && mountpoint -q "$mp" 2>/dev/null; then
|
||||
n=2
|
||||
while [ -d "${mp}-${n}" ] && mountpoint -q "${mp}-${n}" 2>/dev/null; do
|
||||
n=$((n + 1))
|
||||
done
|
||||
mp="${mp}-${n}"
|
||||
fi
|
||||
|
||||
echo "Found USB storage not mounted: $dev ($size, ${model:-no label})"
|
||||
if confirm "Mount it at $mp (world-writable) so we can use it?" n; then
|
||||
run sudo mkdir -p "$mp"
|
||||
if ! run sudo mount -o umask=000 "$dev" "$mp"; then
|
||||
run sudo mount "$dev" "$mp" || true
|
||||
fi
|
||||
if [ "${DRY_RUN:-0}" -eq 1 ] || mountpoint -q "$mp" 2>/dev/null; then
|
||||
ok "Mounted $dev at $mp"
|
||||
return 0
|
||||
fi
|
||||
warn "Mount failed — do it manually:"
|
||||
log " sudo mkdir -p $mp && sudo mount $dev $mp"
|
||||
else
|
||||
warn "Mount it manually, then we'll continue:"
|
||||
log " sudo mkdir -p $mp"
|
||||
log " sudo mount $dev $mp"
|
||||
fi
|
||||
|
||||
read -rp "Press Enter once mounted (or 's' to skip): " resp || return 1
|
||||
case "$resp" in
|
||||
s|S) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Full pick flow ───────────────────────────────────────────────
|
||||
# Loops detect → mount-offer (unmounted sticks) → "plug one in" re-scan until a
|
||||
# mounted USB root is chosen or the user gives up. USB_ROOT is set on success.
|
||||
usb_pick_root() {
|
||||
local confirm_prefix="${1:-Use this storage for}"
|
||||
local subfolder="${2:-}"
|
||||
local giveup="${3:-skipping}"
|
||||
local pass=0 resp="" i=0 root=""
|
||||
local -a roots=()
|
||||
local offered=0
|
||||
|
||||
while :; do
|
||||
pass=$((pass + 1))
|
||||
roots=()
|
||||
|
||||
usb_detect
|
||||
roots=("${USB_MOUNTED[@]}")
|
||||
|
||||
if [ ${#roots[@]} -eq 0 ] && [ ${#USB_UNMOUNTED[@]} -gt 0 ]; then
|
||||
if [ "$offered" -ge 1 ]; then
|
||||
warn "USB stick still not mounted — ${giveup}"
|
||||
return 1
|
||||
fi
|
||||
offered=1
|
||||
usb_mount_offer "${USB_UNMOUNTED[@]}" || return 1
|
||||
continue
|
||||
fi
|
||||
|
||||
[ ${#roots[@]} -gt 0 ] && break
|
||||
|
||||
if [ "$pass" -ge 2 ]; then
|
||||
warn "Still no USB storage detected — ${giveup}"
|
||||
return 1
|
||||
fi
|
||||
warn "No USB storage detected"
|
||||
read -rp "Plug a USB drive in now and press Enter to re-check (or 's' to skip): " resp || return 1
|
||||
case "$resp" in
|
||||
s|S) return 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ ${#roots[@]} -eq 1 ]; then
|
||||
root="${roots[0]}"
|
||||
if ! confirm "${confirm_prefix} ${root%/}/${subfolder}?" n; then
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
echo "Multiple USB storages found:"
|
||||
for i in "${!roots[@]}"; do
|
||||
printf "%2d) %s\n" "$((i + 1))" "${roots[$i]}"
|
||||
done
|
||||
read -rp "Use which one? [1-${#roots[@]}] (0 = skip): " resp || return 1
|
||||
if ! [[ "$resp" =~ ^[0-9]+$ ]] || (( resp < 1 || resp > ${#roots[@]} )); then
|
||||
return 1
|
||||
fi
|
||||
root="${roots[$((resp - 1))]}"
|
||||
fi
|
||||
|
||||
USB_ROOT="$root"
|
||||
return 0
|
||||
}
|
||||
Reference in New Issue
Block a user