Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,36 @@ htmlcov
build
dist
*.egg-info
scratch
*.session
*.session-shm
*.session-wal
*.session-journal
*.db
*.db-shm
*.db-wal
*.db-journal
*.sqlite
*.sqlite-shm
*.sqlite-wal
*.sqlite-journal
*.sqlite3
*.sqlite3-shm
*.sqlite3-wal
*.sqlite3-journal
config.json
config.local.json
.env
.env.local
DB_TARGETS.txt
*_state.json
.tg_msg_manager.lock
delete_log.txt
*.log
DB_EXPORTS
PUBLIC_GROUPS
PRIVAT_DIALOGS
exports
LOGS
logs
TG_MSG_MANAGER
152 changes: 140 additions & 12 deletions deploy/local/tg-state-sync.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
#!/bin/sh
set -eu

SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
DEFAULT_LOCAL_STATE_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)

TGMM_SSH_HOST="${TGMM_SSH_HOST:-vps}"
TGMM_LOCAL_STATE_DIR="${TGMM_LOCAL_STATE_DIR:-$HOME/TG_MSG_MANAGER}"
TGMM_LOCAL_STATE_DIR="${TGMM_LOCAL_STATE_DIR:-$DEFAULT_LOCAL_STATE_DIR}"
TGMM_REMOTE_STATE_DIR="${TGMM_REMOTE_STATE_DIR:-/opt/tg-msg-manager/state}"

usage() {
Expand All @@ -15,26 +18,151 @@ usage() {
}

run_rsync() {
rsync -az --partial --info=progress2 "$@"
rsync -azr --partial --info=progress2 "$@"
}

remote_shell_quote() {
printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"
}

warn_if_locked() {
if [ -e "$TGMM_LOCAL_STATE_DIR/.tg_msg_manager.lock" ]; then
printf '%s\n' "WARNING: local .tg_msg_manager.lock exists; stop tg-msg-manager before push. Lock file will not be synced." >&2
fi
}

append_existing_path() {
files_from=$1
relative_path=$2

if [ -e "$TGMM_LOCAL_STATE_DIR/$relative_path" ]; then
printf '%s\0' "$relative_path" >> "$files_from"
fi
}

append_root_matches() {
files_from=$1
pattern=$2

find "$TGMM_LOCAL_STATE_DIR" -maxdepth 1 -name "$pattern" -exec sh -c '
files_from=$1
shift
for path do
relative_path=${path##*/}
printf "%s\0" "$relative_path" >> "$files_from"
done
' sh "$files_from" {} +
}

build_push_file_list() {
files_from=$1

append_existing_path "$files_from" config.json
append_existing_path "$files_from" config.local.json
append_existing_path "$files_from" DB_TARGETS.txt
append_existing_path "$files_from" delete_log.txt
append_existing_path "$files_from" export_state.json
append_existing_path "$files_from" deep_export_state.json
append_existing_path "$files_from" deep_json_export_state.json
append_existing_path "$files_from" json_export_state.json
append_existing_path "$files_from" pm_export_state.json

append_root_matches "$files_from" "*.log"
append_root_matches "$files_from" "*.session"
append_root_matches "$files_from" "*.session-shm"
append_root_matches "$files_from" "*.session-wal"
append_root_matches "$files_from" "*.session-journal"
append_root_matches "$files_from" "*.db"
append_root_matches "$files_from" "*.db-shm"
append_root_matches "$files_from" "*.db-wal"
append_root_matches "$files_from" "*.db-journal"
append_root_matches "$files_from" "*.sqlite"
append_root_matches "$files_from" "*.sqlite-shm"
append_root_matches "$files_from" "*.sqlite-wal"
append_root_matches "$files_from" "*.sqlite-journal"
append_root_matches "$files_from" "*.sqlite3"
append_root_matches "$files_from" "*.sqlite3-shm"
append_root_matches "$files_from" "*.sqlite3-wal"
append_root_matches "$files_from" "*.sqlite3-journal"

append_existing_path "$files_from" DB_EXPORTS/
append_existing_path "$files_from" PUBLIC_GROUPS/
append_existing_path "$files_from" PRIVAT_DIALOGS/
append_existing_path "$files_from" exports/
append_existing_path "$files_from" LOGS/
}

push_state() {
mode=$1
mkdir -p "$TGMM_LOCAL_STATE_DIR"
files_from=$(mktemp)
trap 'rm -f "$files_from"' EXIT HUP INT TERM

warn_if_locked
build_push_file_list "$files_from"

if [ "$mode" = "--dry-run" ]; then
run_rsync --dry-run "$TGMM_LOCAL_STATE_DIR/" "$TGMM_SSH_HOST:$TGMM_REMOTE_STATE_DIR/"
run_rsync --dry-run --from0 --files-from="$files_from" "$TGMM_LOCAL_STATE_DIR/" "$TGMM_SSH_HOST:$TGMM_REMOTE_STATE_DIR/"
elif [ "$mode" = "--apply" ]; then
run_rsync "$TGMM_LOCAL_STATE_DIR/" "$TGMM_SSH_HOST:$TGMM_REMOTE_STATE_DIR/"
quoted_remote_dir=$(remote_shell_quote "$TGMM_REMOTE_STATE_DIR")
ssh "$TGMM_SSH_HOST" "mkdir -p $quoted_remote_dir"
run_rsync --from0 --files-from="$files_from" "$TGMM_LOCAL_STATE_DIR/" "$TGMM_SSH_HOST:$TGMM_REMOTE_STATE_DIR/"
else
usage
exit 2
fi
}

pull_dir() {
remote_path_exists() {
relative_path=$1
quoted_remote_path=$(remote_shell_quote "$TGMM_REMOTE_STATE_DIR/$relative_path")
ssh "$TGMM_SSH_HOST" "test -e $quoted_remote_path"
}

remote_root_logs_exist() {
quoted_remote_dir=$(remote_shell_quote "$TGMM_REMOTE_STATE_DIR")
ssh "$TGMM_SSH_HOST" "set -- $quoted_remote_dir/*.log; [ -e \"\$1\" ]"
}

pull_optional_dir() {
name=$1
if remote_path_exists "$name"; then
mkdir -p "$TGMM_LOCAL_STATE_DIR/$name"
run_rsync "$TGMM_SSH_HOST:$TGMM_REMOTE_STATE_DIR/$name/" "$TGMM_LOCAL_STATE_DIR/$name/"
else
printf '%s\n' "skip missing remote path: $name" >&2
fi
}

pull_optional_file() {
name=$1
mkdir -p "$TGMM_LOCAL_STATE_DIR/$name"
run_rsync "$TGMM_SSH_HOST:$TGMM_REMOTE_STATE_DIR/$name/" "$TGMM_LOCAL_STATE_DIR/$name/"
if remote_path_exists "$name"; then
mkdir -p "$TGMM_LOCAL_STATE_DIR"
run_rsync "$TGMM_SSH_HOST:$TGMM_REMOTE_STATE_DIR/$name" "$TGMM_LOCAL_STATE_DIR/"
else
printf '%s\n' "skip missing remote path: $name" >&2
fi
}

pull_root_logs() {
if remote_root_logs_exist; then
mkdir -p "$TGMM_LOCAL_STATE_DIR"
run_rsync "$TGMM_SSH_HOST:$TGMM_REMOTE_STATE_DIR/"*.log "$TGMM_LOCAL_STATE_DIR/"
else
printf '%s\n' "skip missing remote path: *.log" >&2
fi
}

pull_exports() {
pull_optional_dir exports
pull_optional_dir DB_EXPORTS
pull_optional_dir PUBLIC_GROUPS
pull_optional_dir PRIVAT_DIALOGS
}

pull_logs() {
pull_optional_dir LOGS
pull_optional_file delete_log.txt
pull_root_logs
}

if [ "$#" -lt 2 ]; then
Expand All @@ -47,14 +175,14 @@ case "$1:$2" in
push_state "$2"
;;
pull:exports)
pull_dir exports
pull_exports
;;
pull:logs)
pull_dir logs
pull_logs
;;
pull:all)
pull_dir exports
pull_dir logs
pull_exports
pull_logs
;;
*)
usage
Expand Down
23 changes: 21 additions & 2 deletions deploy/vps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,11 @@ tgd

На локальной машине остановите локальный запуск приложения перед push, чтобы не копировать состояние во время записи.

По умолчанию local source — корень текущего checkout, из которого запускается helper, например `/Users/maczone/dev/TG_CLEANER`. Это удобно, если runtime-файлы лежат рядом с исходным кодом, но helper синхронизирует только allowlist runtime state, а не исходный код, `.git`, `docs`, `tests`, `deploy`, virtualenv или build artifacts.

```bash
cd /Users/maczone/dev/TG_CLEANER
export TGMM_SSH_HOST=vps
export TGMM_LOCAL_STATE_DIR="$HOME/TG_MSG_MANAGER"
export TGMM_REMOTE_STATE_DIR=/opt/tg-msg-manager/state

./deploy/local/tg-state-sync.sh push --dry-run
Expand All @@ -75,4 +77,21 @@ export TGMM_REMOTE_STATE_DIR=/opt/tg-msg-manager/state
./deploy/local/tg-state-sync.sh pull all
```

Sync helper использует `rsync` over SSH, `--partial` и `--info=progress2`. Он не использует `--delete`.
Если state находится не в checkout root, задайте `TGMM_LOCAL_STATE_DIR` вручную перед запуском.

`push` передаёт только:

- `config.json`, `config.local.json`, `DB_TARGETS.txt`, `delete_log.txt`;
- root-level `*.log`;
- root-level Telegram `*.session` вместе с `*.session-shm`, `*.session-wal`, `*.session-journal`;
- root-level SQLite `*.db`, `*.sqlite`, `*.sqlite3` вместе с WAL/SHM/journal sidecars;
- `*_state.json` из поддерживаемых runtime state-файлов;
- директории `DB_EXPORTS/`, `PUBLIC_GROUPS/`, `PRIVAT_DIALOGS/`, `exports/`, `LOGS/`.

`.tg_msg_manager.lock` не синхронизируется. Если lock существует, helper выводит предупреждение; остановите локальное приложение и повторите push.

`pull exports` забирает только export directories: `exports/`, `DB_EXPORTS/`, `PUBLIC_GROUPS/`, `PRIVAT_DIALOGS/`. `pull logs` забирает `LOGS/`, `delete_log.txt` и root-level `*.log`. Pull не загружает обратно `config.json`, `config.local.json`, Telegram sessions или SQLite databases.

Sync helper использует `rsync` over SSH, archive mode, compression, `--partial` и `--info=progress2`. Он не использует `--delete`.

Не коммитьте `config.json`, `config.local.json`, Telegram sessions, SQLite databases, exports или logs; не прикладывайте их к PR/issues и не включайте в Docker image.
2 changes: 2 additions & 0 deletions docs/stages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Stage 7A.1 pytest authority and CI gate alignment is recorded in [`reports/STAGE
Stage 7A.2 fixture contract snapshot gate is recorded in [`reports/STAGE_7A_2_FIXTURE_CONTRACT_SNAPSHOT_GATE_REPORT.md`](reports/STAGE_7A_2_FIXTURE_CONTRACT_SNAPSHOT_GATE_REPORT.md).
Stage 7A.3 coverage and typecheck gate precheck is recorded in [`reports/STAGE_7A_3_COVERAGE_AND_TYPECHECK_GATE_PRECHECK_REPORT.md`](reports/STAGE_7A_3_COVERAGE_AND_TYPECHECK_GATE_PRECHECK_REPORT.md).
Stage 7B.0 VPS Docker Compose deployment wrapper is recorded in [`reports/STAGE_7B_0_VPS_DOCKER_COMPOSE_DEPLOYMENT_REPORT.md`](reports/STAGE_7B_0_VPS_DOCKER_COMPOSE_DEPLOYMENT_REPORT.md).
Stage 7B.1 VPS state sync safety correction is recorded in [`reports/STAGE_7B_1_VPS_STATE_SYNC_SAFETY_CORRECTION_REPORT.md`](reports/STAGE_7B_1_VPS_STATE_SYNC_SAFETY_CORRECTION_REPORT.md).
Stage 6B.0 direct export failure exit code is recorded in [`reports/STAGE_6B_0_DIRECT_EXPORT_FAILURE_EXIT_CODE_REPORT.md`](reports/STAGE_6B_0_DIRECT_EXPORT_FAILURE_EXIT_CODE_REPORT.md).
Stage 6B.1 direct PM archive failure exit code is recorded in [`reports/STAGE_6B_1_DIRECT_PM_ARCHIVE_FAILURE_EXIT_CODE_REPORT.md`](reports/STAGE_6B_1_DIRECT_PM_ARCHIVE_FAILURE_EXIT_CODE_REPORT.md).
Stage 6C.0 SQLite writer flush failure diagnosis is recorded in [`reports/STAGE_6C_0_SQLITE_WRITER_FLUSH_FAILURE_DIAGNOSIS_REPORT.md`](reports/STAGE_6C_0_SQLITE_WRITER_FLUSH_FAILURE_DIAGNOSIS_REPORT.md).
Expand Down Expand Up @@ -205,6 +206,7 @@ Completed stage files under [`completed/`](completed/) are historical instructio

Current completed prompt groups:

- Stage 7B.1 VPS state sync safety correction task prompt.
- Stage 7B.0 VPS Docker Compose deployment wrapper task prompt.
- Stage 7A.3 coverage and typecheck gate precheck task prompt.
- Stage 7A.2 fixture contract snapshot gate task prompt.
Expand Down
Loading
Loading