skillwiki 0.9.63 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-5TBIMLTZ.js +343 -0
- package/dist/chunk-C5OLZRRM.js +357 -0
- package/dist/{chunk-TUFQZ5K4.js → chunk-DR7KFHNH.js} +792 -1983
- package/dist/chunk-IZABIE44.js +647 -0
- package/dist/chunk-S5ABQCXQ.js +580 -0
- package/dist/cli.js +1540 -550
- package/dist/index-projection-ERFX76U5.js +10 -0
- package/dist/managed-write-preflight-SILEUQEV.js +11 -0
- package/dist/skillwiki-mcp.js +4 -1
- package/dist/vault-sync/scripts/lib/conflict-markers.sh +69 -0
- package/dist/vault-sync/scripts/lib/delete-intent.sh +74 -0
- package/dist/vault-sync/scripts/lib/fleet.sh +103 -0
- package/dist/vault-sync/scripts/lib/git-case.sh +71 -0
- package/dist/vault-sync/scripts/lib/git-materialization.sh +264 -0
- package/dist/vault-sync/scripts/lib/git-operation-journal.sh +469 -0
- package/dist/vault-sync/scripts/lib/git-rebase-state.sh +180 -0
- package/dist/vault-sync/scripts/lib/lockfile.sh +70 -0
- package/dist/vault-sync/scripts/lib/managed-write-lock.sh +80 -0
- package/dist/vault-sync/scripts/lib/platform.sh +184 -0
- package/dist/vault-sync/scripts/lib/runtime-manifest.sh +223 -0
- package/dist/vault-sync/scripts/wiki-fetch-notify.sh +207 -0
- package/dist/vault-sync/scripts/wiki-fuse-refresh.sh +405 -0
- package/dist/vault-sync/scripts/wiki-pull-with-auto-resolve.sh +631 -0
- package/dist/vault-sync/scripts/wiki-push.sh +364 -0
- package/dist/vault-sync/scripts/wiki-snapshot.sh +587 -0
- package/package.json +2 -2
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- package/skills/README.md +13 -0
- package/skills/package.json +1 -1
- package/skills/proj-work/SKILL.md +3 -0
- package/skills/skills/proj-work/SKILL.md +3 -0
- package/skills/skills/using-skillwiki/SKILL.md +13 -0
- package/skills/skills/wiki-crystallize/SKILL.md +3 -0
- package/skills/using-skillwiki/SKILL.md +13 -0
- package/skills/wiki-crystallize/SKILL.md +3 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# wiki-push.sh — one-way push from ~/wiki to SeaweedFS S3 via rclone copy.
|
|
3
|
+
#
|
|
4
|
+
# Push-only (rclone copy, NOT sync) — NEVER deletes files on the remote.
|
|
5
|
+
# Pull side is handled separately by:
|
|
6
|
+
# - Obsidian Remotely Save (option 5: Incremental Pull And Delete), or
|
|
7
|
+
# - wiki-fetch-notify.sh (opt-in WIKI_FETCH_PULL_ON_DELTA=1 performs
|
|
8
|
+
# git pull --rebase on positive delta), or
|
|
9
|
+
# - manual `skillwiki sync` / `wiki-sync` skill.
|
|
10
|
+
#
|
|
11
|
+
# This script is the SOLE macOS → S3 transport. It does NOT touch git —
|
|
12
|
+
# single-writer-git is enforced: only sg01's wiki-snapshot.sh pushes to
|
|
13
|
+
# GitHub (concepts/vault-write-authority-model.md D2, D3, D6). macOS
|
|
14
|
+
# consumes sg01 Snapshot commits via the pull mechanisms above.
|
|
15
|
+
#
|
|
16
|
+
# Lockfile prevents overlapping runs if a push takes longer than the launchd interval.
|
|
17
|
+
# Safe by default: no flag = real run (no dry-run mode — meant to be unattended).
|
|
18
|
+
|
|
19
|
+
set -u
|
|
20
|
+
|
|
21
|
+
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]:-$0}" )" && pwd )"
|
|
22
|
+
. "$SCRIPT_DIR/lib/platform.sh"
|
|
23
|
+
. "$SCRIPT_DIR/lib/lockfile.sh"
|
|
24
|
+
. "$SCRIPT_DIR/lib/git-case.sh"
|
|
25
|
+
. "$SCRIPT_DIR/lib/conflict-markers.sh"
|
|
26
|
+
# Optional: tombstone helpers (present in package installs after 2026-07-14)
|
|
27
|
+
if [ -f "$SCRIPT_DIR/lib/delete-intent.sh" ]; then
|
|
28
|
+
# shellcheck source=lib/delete-intent.sh
|
|
29
|
+
. "$SCRIPT_DIR/lib/delete-intent.sh"
|
|
30
|
+
fi
|
|
31
|
+
platform_detect_os
|
|
32
|
+
|
|
33
|
+
WIKI_DIR="${WIKI_DIR:-$HOME/wiki}"
|
|
34
|
+
REMOTE="${WIKI_REMOTE:-seaweed-wiki:cloud/wiki}"
|
|
35
|
+
FILTERS="${WIKI_PUSH_FILTERS:-$(platform_rclone_config_dir)/wiki-push-filters.txt}"
|
|
36
|
+
LOCK_FILE="$(platform_cache_dir)/wiki-push.lock"
|
|
37
|
+
LOG_FILE="$(platform_log_dir)/wiki-push.log"
|
|
38
|
+
LOG_MAX_SIZE=1048576 # 1 MB
|
|
39
|
+
LOG_KEEP=5
|
|
40
|
+
|
|
41
|
+
mkdir -p "$(dirname "$LOCK_FILE")" "$(dirname "$LOG_FILE")"
|
|
42
|
+
|
|
43
|
+
log() { printf '%s %s\n' "$(date -u +%FT%TZ)" "$*" >>"$LOG_FILE"; }
|
|
44
|
+
|
|
45
|
+
conflict_marker_guard() {
|
|
46
|
+
local findings
|
|
47
|
+
findings="$(mktemp)" || { log "FAIL could not create conflict-marker scan temp file"; return 1; }
|
|
48
|
+
if ! vault_sync_scan_conflict_markers "$WIKI_DIR" "$findings"; then
|
|
49
|
+
log "FAIL conflict marker blocks present; refusing S3 push"
|
|
50
|
+
vault_sync_log_conflict_marker_findings "$findings" "$LOG_FILE"
|
|
51
|
+
rm -f "$findings"
|
|
52
|
+
return 1
|
|
53
|
+
fi
|
|
54
|
+
rm -f "$findings"
|
|
55
|
+
return 0
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Parse WIKI_PUSH_MAX_REMOTE_DELETES into global _VS_MAX_REMOTE_DELETES (echo for callers).
|
|
59
|
+
remote_prune_resolve_max_deletes() {
|
|
60
|
+
local max_remote_deletes="${WIKI_PUSH_MAX_REMOTE_DELETES:-10}"
|
|
61
|
+
case "$max_remote_deletes" in
|
|
62
|
+
''|*[!0-9]*)
|
|
63
|
+
log "FAIL remote prune invalid WIKI_PUSH_MAX_REMOTE_DELETES=$max_remote_deletes"
|
|
64
|
+
return 1
|
|
65
|
+
;;
|
|
66
|
+
esac
|
|
67
|
+
if [ "$max_remote_deletes" -lt 1 ]; then
|
|
68
|
+
log "FAIL remote prune invalid WIKI_PUSH_MAX_REMOTE_DELETES=$max_remote_deletes"
|
|
69
|
+
return 1
|
|
70
|
+
fi
|
|
71
|
+
printf '%s\n' "$max_remote_deletes"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
remote_prune_archived_source_paths() {
|
|
75
|
+
local max_remote_deletes
|
|
76
|
+
max_remote_deletes="$(remote_prune_resolve_max_deletes)" || return 1
|
|
77
|
+
|
|
78
|
+
local tmp_dir archive_pairs remote_paths plan_file
|
|
79
|
+
tmp_dir="$(mktemp -d)"
|
|
80
|
+
archive_pairs="$tmp_dir/archive-pairs.tsv"
|
|
81
|
+
remote_paths="$tmp_dir/remote-paths.txt"
|
|
82
|
+
plan_file="$tmp_dir/plan.txt"
|
|
83
|
+
|
|
84
|
+
if [ ! -d "$WIKI_DIR/_archive" ]; then
|
|
85
|
+
rm -rf "$tmp_dir"
|
|
86
|
+
return 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
(
|
|
90
|
+
cd "$WIKI_DIR" || exit 1
|
|
91
|
+
find _archive -type f | while IFS= read -r archive_path; do
|
|
92
|
+
local source_path="${archive_path#_archive/}"
|
|
93
|
+
[ "$source_path" = "$archive_path" ] && continue
|
|
94
|
+
[ -e "$source_path" ] && continue
|
|
95
|
+
printf '%s\t%s\n' "$source_path" "$archive_path"
|
|
96
|
+
done | LC_ALL=C sort -u
|
|
97
|
+
) > "$archive_pairs" || {
|
|
98
|
+
log "FAIL remote prune could not enumerate _archive pairs"
|
|
99
|
+
rm -rf "$tmp_dir"
|
|
100
|
+
return 1
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if [ ! -s "$archive_pairs" ]; then
|
|
104
|
+
rm -rf "$tmp_dir"
|
|
105
|
+
return 0
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
if ! rclone lsf "$REMOTE" --recursive --files-only 2>>"$LOG_FILE" | LC_ALL=C sort -u > "$remote_paths"; then
|
|
109
|
+
log "FAIL remote prune could not list remote paths"
|
|
110
|
+
rm -rf "$tmp_dir"
|
|
111
|
+
return 1
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
while IFS="$(printf '\t')" read -r source_path archive_path; do
|
|
115
|
+
[ -z "$source_path" ] && continue
|
|
116
|
+
if grep -Fxq -- "$source_path" "$remote_paths" && grep -Fxq -- "$archive_path" "$remote_paths"; then
|
|
117
|
+
printf '%s\n' "$source_path"
|
|
118
|
+
fi
|
|
119
|
+
done < "$archive_pairs" | LC_ALL=C sort -u > "$plan_file"
|
|
120
|
+
|
|
121
|
+
local planned_count
|
|
122
|
+
planned_count="$(wc -l < "$plan_file" | tr -d ' ')"
|
|
123
|
+
if [ "$planned_count" = "0" ]; then
|
|
124
|
+
rm -rf "$tmp_dir"
|
|
125
|
+
return 0
|
|
126
|
+
fi
|
|
127
|
+
if [ "$planned_count" -gt "$max_remote_deletes" ]; then
|
|
128
|
+
log "FAIL remote prune cap exceeded: $planned_count > $max_remote_deletes"
|
|
129
|
+
sed 's/^/remote prune skipped: /' "$plan_file" >> "$LOG_FILE"
|
|
130
|
+
rm -rf "$tmp_dir"
|
|
131
|
+
return 1
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
local rel_path remote_path
|
|
135
|
+
while IFS= read -r rel_path; do
|
|
136
|
+
[ -z "$rel_path" ] && continue
|
|
137
|
+
remote_path="${REMOTE%/}/$rel_path"
|
|
138
|
+
if rclone deletefile "$remote_path" >>"$LOG_FILE" 2>&1; then
|
|
139
|
+
log "OK remote pruned archived source $remote_path"
|
|
140
|
+
else
|
|
141
|
+
log "FAIL remote prune deletefile failed for $remote_path"
|
|
142
|
+
rm -rf "$tmp_dir"
|
|
143
|
+
return 1
|
|
144
|
+
fi
|
|
145
|
+
done < "$plan_file"
|
|
146
|
+
|
|
147
|
+
rm -rf "$tmp_dir"
|
|
148
|
+
return 0
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# Prune S3 objects listed in active meta/delete-intents tombstones.
|
|
152
|
+
# Combined with archive prune under the same WIKI_PUSH_MAX_REMOTE_DELETES budget
|
|
153
|
+
# only for this function's own plan (archive prune uses its own budget per call).
|
|
154
|
+
remote_prune_tombstoned_paths() {
|
|
155
|
+
if ! command -v delete_intent_list_active_paths >/dev/null 2>&1; then
|
|
156
|
+
return 0
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
local max_remote_deletes
|
|
160
|
+
max_remote_deletes="$(remote_prune_resolve_max_deletes)" || return 1
|
|
161
|
+
|
|
162
|
+
local tmp_dir remote_paths plan_file intent_paths
|
|
163
|
+
tmp_dir="$(mktemp -d)"
|
|
164
|
+
remote_paths="$tmp_dir/remote-paths.txt"
|
|
165
|
+
plan_file="$tmp_dir/plan.txt"
|
|
166
|
+
intent_paths="$tmp_dir/intents.txt"
|
|
167
|
+
|
|
168
|
+
delete_intent_list_active_paths "$WIKI_DIR" > "$intent_paths" || true
|
|
169
|
+
if [ ! -s "$intent_paths" ]; then
|
|
170
|
+
rm -rf "$tmp_dir"
|
|
171
|
+
return 0
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
if ! rclone lsf "$REMOTE" --recursive --files-only 2>>"$LOG_FILE" | LC_ALL=C sort -u > "$remote_paths"; then
|
|
175
|
+
log "FAIL tombstone prune could not list remote paths"
|
|
176
|
+
rm -rf "$tmp_dir"
|
|
177
|
+
return 1
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
: > "$plan_file"
|
|
181
|
+
while IFS= read -r rel_path; do
|
|
182
|
+
[ -z "$rel_path" ] && continue
|
|
183
|
+
if grep -Fxq -- "$rel_path" "$remote_paths"; then
|
|
184
|
+
printf '%s\n' "$rel_path" >> "$plan_file"
|
|
185
|
+
fi
|
|
186
|
+
done < "$intent_paths"
|
|
187
|
+
|
|
188
|
+
local planned_count
|
|
189
|
+
planned_count="$(wc -l < "$plan_file" | tr -d ' ')"
|
|
190
|
+
if [ "$planned_count" = "0" ]; then
|
|
191
|
+
rm -rf "$tmp_dir"
|
|
192
|
+
return 0
|
|
193
|
+
fi
|
|
194
|
+
if [ "$planned_count" -gt "$max_remote_deletes" ]; then
|
|
195
|
+
log "FAIL tombstone prune cap exceeded: $planned_count > $max_remote_deletes"
|
|
196
|
+
sed 's/^/tombstone prune skipped: /' "$plan_file" >> "$LOG_FILE"
|
|
197
|
+
rm -rf "$tmp_dir"
|
|
198
|
+
return 1
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
local rel_path remote_path
|
|
202
|
+
while IFS= read -r rel_path; do
|
|
203
|
+
[ -z "$rel_path" ] && continue
|
|
204
|
+
remote_path="${REMOTE%/}/$rel_path"
|
|
205
|
+
if rclone deletefile "$remote_path" >>"$LOG_FILE" 2>&1; then
|
|
206
|
+
log "OK remote pruned tombstone $remote_path"
|
|
207
|
+
else
|
|
208
|
+
log "FAIL tombstone prune deletefile failed for $remote_path"
|
|
209
|
+
rm -rf "$tmp_dir"
|
|
210
|
+
return 1
|
|
211
|
+
fi
|
|
212
|
+
done < "$plan_file"
|
|
213
|
+
|
|
214
|
+
rm -rf "$tmp_dir"
|
|
215
|
+
return 0
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# Rotate log if oversized.
|
|
219
|
+
if [ -f "$LOG_FILE" ] && [ "$(platform_stat_size "$LOG_FILE")" -gt "$LOG_MAX_SIZE" ]; then
|
|
220
|
+
for i in $(seq $((LOG_KEEP - 1)) -1 1); do
|
|
221
|
+
[ -f "$LOG_FILE.$i" ] && mv "$LOG_FILE.$i" "$LOG_FILE.$((i + 1))"
|
|
222
|
+
done
|
|
223
|
+
mv "$LOG_FILE" "$LOG_FILE.1"
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
# Guard: wiki dir present.
|
|
227
|
+
if [ ! -d "$WIKI_DIR" ]; then
|
|
228
|
+
log "ERROR: WIKI_DIR not found at $WIKI_DIR"
|
|
229
|
+
exit 0
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
# Guard: filter file present (it's not strictly required, but missing it would push .git/).
|
|
233
|
+
if [ ! -f "$FILTERS" ]; then
|
|
234
|
+
log "ERROR: filter file missing at $FILTERS — refusing to push without exclusions"
|
|
235
|
+
exit 0
|
|
236
|
+
fi
|
|
237
|
+
# Projection-authority transport: non-authority leaves must not push root aggregates.
|
|
238
|
+
if ! grep -qxF -- '- /index.md' "$FILTERS" || ! grep -qxF -- '- /log.md' "$FILTERS"; then
|
|
239
|
+
log "ERROR: filter file missing root aggregate exclusions (- /index.md and - /log.md)"
|
|
240
|
+
exit 1
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
# Acquire lockfile (non-blocking).
|
|
244
|
+
LOCK_RC=0
|
|
245
|
+
lockfile_acquire "$LOCK_FILE" 600 || LOCK_RC=$?
|
|
246
|
+
if [ "$LOCK_RC" -eq 1 ]; then
|
|
247
|
+
log "skip: previous run still in flight"
|
|
248
|
+
exit 0
|
|
249
|
+
elif [ "$LOCK_RC" -eq 2 ]; then
|
|
250
|
+
log "stale lock reclaimed"
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
START=$(date +%s)
|
|
254
|
+
|
|
255
|
+
if ! CASE_CONFLICTS=$(cd "$WIKI_DIR" && git_case_conflicts); then
|
|
256
|
+
log "ERROR: case-only path collision detected — refusing rclone push"
|
|
257
|
+
printf '%s\n' "$CASE_CONFLICTS" >>"$LOG_FILE"
|
|
258
|
+
exit 0
|
|
259
|
+
fi
|
|
260
|
+
|
|
261
|
+
# Fix Windows-hostile markdown paths before publishing to S3.
|
|
262
|
+
# The skillwiki CLI owns the rename and citation-rewire semantics; this script
|
|
263
|
+
# only gates the unattended push pipeline.
|
|
264
|
+
if command -v skillwiki >/dev/null 2>&1; then
|
|
265
|
+
PATH_FIX_OUTPUT=$(skillwiki lint "$WIKI_DIR" --only path_too_long --fix 2>&1)
|
|
266
|
+
PATH_FIX_RC=$?
|
|
267
|
+
printf '%s\n' "$PATH_FIX_OUTPUT" >>"$LOG_FILE"
|
|
268
|
+
if [ "$PATH_FIX_RC" -ne 0 ]; then
|
|
269
|
+
log "ERROR: path_too_long fix failed exit=$PATH_FIX_RC — refusing rclone push"
|
|
270
|
+
exit 0
|
|
271
|
+
fi
|
|
272
|
+
else
|
|
273
|
+
log "ERROR: skillwiki CLI not found — refusing push because path_too_long guard cannot run"
|
|
274
|
+
exit 0
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
if ! conflict_marker_guard; then
|
|
278
|
+
exit 1
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
# Lint delta gate: block S3 publication only on newly introduced lint errors.
|
|
282
|
+
# Inherited debt is logged. Malformed/missing delta evidence fails closed.
|
|
283
|
+
if command -v skillwiki >/dev/null 2>&1; then
|
|
284
|
+
DELTA_OUT=$(skillwiki sync lint-delta "$WIKI_DIR" --base-ref origin/main 2>&1) || true
|
|
285
|
+
printf '%s
|
|
286
|
+
' "$DELTA_OUT" >>"$LOG_FILE"
|
|
287
|
+
eval "$(printf '%s\n' "$DELTA_OUT" | python3 -c '
|
|
288
|
+
import json,sys
|
|
289
|
+
try:
|
|
290
|
+
d=json.load(sys.stdin)
|
|
291
|
+
data=d.get("data") or {}
|
|
292
|
+
print("DELTA_FULL=%s" % int(data.get("full_errors", -1)))
|
|
293
|
+
print("DELTA_BASE=%s" % int(data.get("base_errors", -1)))
|
|
294
|
+
print("DELTA_NEW=%s" % int(data.get("new_errors", -1)))
|
|
295
|
+
print("DELTA_RESOLVED=%s" % int(data.get("resolved_errors", -1)))
|
|
296
|
+
except Exception:
|
|
297
|
+
print("DELTA_FULL=-1")
|
|
298
|
+
print("DELTA_BASE=-1")
|
|
299
|
+
print("DELTA_NEW=-1")
|
|
300
|
+
print("DELTA_RESOLVED=-1")
|
|
301
|
+
' 2>/dev/null)" || { DELTA_FULL=-1; DELTA_BASE=-1; DELTA_NEW=-1; DELTA_RESOLVED=-1; }
|
|
302
|
+
if [ "${DELTA_NEW:--1}" = "-1" ] || [ "${DELTA_FULL:--1}" = "-1" ]; then
|
|
303
|
+
log "FAIL lint-delta evidence missing or malformed — refusing S3 push"
|
|
304
|
+
exit 1
|
|
305
|
+
fi
|
|
306
|
+
log "LINT-DELTA full=$DELTA_FULL base=$DELTA_BASE new=$DELTA_NEW resolved=$DELTA_RESOLVED"
|
|
307
|
+
if [ "$DELTA_NEW" -gt 0 ]; then
|
|
308
|
+
log "FAIL lint-delta new_errors=$DELTA_NEW blocks S3 push (full=$DELTA_FULL)"
|
|
309
|
+
exit 1
|
|
310
|
+
fi
|
|
311
|
+
if [ "$DELTA_FULL" -gt 0 ]; then
|
|
312
|
+
log "WARN inherited lint debt full_errors=$DELTA_FULL (new_errors=0) — push allowed"
|
|
313
|
+
fi
|
|
314
|
+
else
|
|
315
|
+
log "FAIL skillwiki CLI not found — lint-delta gate cannot run; refusing S3 push"
|
|
316
|
+
exit 1
|
|
317
|
+
fi
|
|
318
|
+
|
|
319
|
+
# rclone copy (NOT sync) → never bulk-deletes on remote.
|
|
320
|
+
# After a successful copy, prune only those stale live paths whose matching
|
|
321
|
+
# _archive/<path> object is now present on the remote and whose live path is
|
|
322
|
+
# absent locally. This keeps archive moves from leaving stale live note paths
|
|
323
|
+
# on S3 without reintroducing git pushes on macOS.
|
|
324
|
+
# --update : only newer source files overwrite remote (mod-time + size based)
|
|
325
|
+
# --filter-from : reuse the bisync filter list
|
|
326
|
+
# --transfers 4 : modest parallelism for small files
|
|
327
|
+
# --checkers 8 : default-ish
|
|
328
|
+
# --low-level-retries 10 --retries 3 : tolerate transient network blips
|
|
329
|
+
OUTPUT=$(rclone copy "$WIKI_DIR" "$REMOTE" \
|
|
330
|
+
--filter-from "$FILTERS" \
|
|
331
|
+
--update \
|
|
332
|
+
--transfers 4 \
|
|
333
|
+
--checkers 8 \
|
|
334
|
+
--low-level-retries 10 \
|
|
335
|
+
--retries 3 \
|
|
336
|
+
--stats-one-line \
|
|
337
|
+
--stats 60s 2>&1)
|
|
338
|
+
RC=$?
|
|
339
|
+
|
|
340
|
+
DURATION=$(( $(date +%s) - START ))
|
|
341
|
+
|
|
342
|
+
if [ "$RC" -eq 0 ]; then
|
|
343
|
+
# rclone with --stats-one-line emits a "Transferred: N / N, 100%, ..." line on completion.
|
|
344
|
+
STATS_LINE=$(printf '%s\n' "$OUTPUT" | grep -E '^Transferred:.*[0-9]+ B' | tail -1)
|
|
345
|
+
if [ -n "$STATS_LINE" ]; then
|
|
346
|
+
log "OK push duration=${DURATION}s | $STATS_LINE"
|
|
347
|
+
else
|
|
348
|
+
log "OK push (no changes) duration=${DURATION}s"
|
|
349
|
+
fi
|
|
350
|
+
else
|
|
351
|
+
log "FAIL rclone exit=$RC duration=${DURATION}s"
|
|
352
|
+
printf '%s\n' "$OUTPUT" >>"$LOG_FILE"
|
|
353
|
+
fi
|
|
354
|
+
|
|
355
|
+
if [ "$RC" -eq 0 ]; then
|
|
356
|
+
if ! remote_prune_archived_source_paths; then
|
|
357
|
+
log "FAIL remote prune failed after rclone copy"
|
|
358
|
+
fi
|
|
359
|
+
if ! remote_prune_tombstoned_paths; then
|
|
360
|
+
log "FAIL tombstone prune failed after rclone copy"
|
|
361
|
+
fi
|
|
362
|
+
fi
|
|
363
|
+
|
|
364
|
+
exit 0
|