Add verified-upload purge for uploader
This commit is contained in:
parent
561e222250
commit
666ec71b9c
8 changed files with 622 additions and 12 deletions
42
data/manifests/checkpoint_011_verified_purge.json
Normal file
42
data/manifests/checkpoint_011_verified_purge.json
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"active_checkpoint": "Checkpoint 11: Verified Upload Purge",
|
||||||
|
"artifacts": [
|
||||||
|
"scripts/upload_archive_rclone.sh",
|
||||||
|
"scripts/purge_uploaded_local_files.sh",
|
||||||
|
"deploy/k8s/base/cronjob-uploader.yaml",
|
||||||
|
"systemd/polymarket-orderbook-uploader.service",
|
||||||
|
"docs/GOOGLE_DRIVE_OFFLOAD.md",
|
||||||
|
"docs/KUBERNETES_DEPLOYMENT.md",
|
||||||
|
"docs/POLYMARKET_WEBSOCKET_RECORDER.md",
|
||||||
|
"docs/VPS_CUTOVER_RUNBOOK.md",
|
||||||
|
"data/manifests/upload_archive_purge_validation_sample.json",
|
||||||
|
"data/manifests/purge_uploaded_local_validation_sample.json",
|
||||||
|
"data/manifests/purge_uploaded_local_validation_summary.json",
|
||||||
|
"reports/checkpoints/checkpoint_011_verified_purge.md"
|
||||||
|
],
|
||||||
|
"gate_status": "READY_FOR_DEPLOY_NOT_LIVE",
|
||||||
|
"project_rules_added": [
|
||||||
|
"Previously verified local files may be purged in later cycles using a durable verified-upload index.",
|
||||||
|
"The verified-upload index itself is protected from purge.",
|
||||||
|
"Purge uses prior verified-upload evidence and local checksum matching before deletion."
|
||||||
|
],
|
||||||
|
"validation": {
|
||||||
|
"bash_syntax": "PASS",
|
||||||
|
"systemd_verify": "PASS",
|
||||||
|
"kustomize_render": "PASS",
|
||||||
|
"kubernetes_server_dry_run": "PASS",
|
||||||
|
"local_rclone_validation": {
|
||||||
|
"upload_gate_status": "PASS",
|
||||||
|
"upload_operation_status": "UPLOAD_VERIFIED",
|
||||||
|
"verified_index_status": "updated",
|
||||||
|
"purge_gate_status": "PASS",
|
||||||
|
"purge_operation_status": "PURGE_PASS",
|
||||||
|
"purge_deleted_count": 2,
|
||||||
|
"old_raw_exists_after_purge": false,
|
||||||
|
"old_manifest_exists_after_purge": false,
|
||||||
|
"recent_raw_exists_after_purge": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strongest_fake_progress_risk": "The change is validated locally and in Kubernetes server dry-run only; the live cluster still needs a new image build and canary deploy before periodic purge is actually running there.",
|
||||||
|
"next_smallest_step": "Commit and push the change, deploy the websocket canary image that also updates the uploader CronJob, then inspect the next upload and purge manifests plus PVC usage."
|
||||||
|
}
|
||||||
|
|
@ -38,9 +38,15 @@ spec:
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command:
|
command:
|
||||||
- /bin/bash
|
- /bin/bash
|
||||||
- /app/scripts/upload_archive_rclone.sh
|
- -lc
|
||||||
- --execute
|
- |
|
||||||
- --cleanup-after-verify
|
upload_exit=0
|
||||||
|
/app/scripts/upload_archive_rclone.sh --execute --cleanup-after-verify || upload_exit=$?
|
||||||
|
purge_exit=0
|
||||||
|
/app/scripts/purge_uploaded_local_files.sh --execute || purge_exit=$?
|
||||||
|
if [[ "${upload_exit}" -ne 0 || "${purge_exit}" -ne 0 ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
env:
|
env:
|
||||||
- name: ORDERBOOKS_DATA_DIR
|
- name: ORDERBOOKS_DATA_DIR
|
||||||
value: /var/lib/orderbooks
|
value: /var/lib/orderbooks
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,15 @@ must still pass.
|
||||||
Included:
|
Included:
|
||||||
|
|
||||||
- `scripts/upload_archive_rclone.sh`
|
- `scripts/upload_archive_rclone.sh`
|
||||||
|
- `scripts/purge_uploaded_local_files.sh`
|
||||||
- `systemd/polymarket-orderbook-uploader.service`
|
- `systemd/polymarket-orderbook-uploader.service`
|
||||||
- `systemd/polymarket-orderbook-uploader.timer`
|
- `systemd/polymarket-orderbook-uploader.timer`
|
||||||
- dry-run mode by default
|
- dry-run mode by default
|
||||||
- real upload only with `--execute`
|
- real upload only with `--execute`
|
||||||
- rclone verification with `rclone check`
|
- rclone verification with `rclone check`
|
||||||
- per-run upload manifests
|
- per-run upload manifests
|
||||||
- optional local cleanup only after successful verification
|
- verified-upload index tracking
|
||||||
|
- periodic local purge of previously verified files
|
||||||
|
|
||||||
Excluded:
|
Excluded:
|
||||||
|
|
||||||
|
|
@ -162,7 +164,7 @@ The upload gate is `PASS` only when the copy succeeds and verification succeeds.
|
||||||
|
|
||||||
Local files are kept by default, even after upload verification.
|
Local files are kept by default, even after upload verification.
|
||||||
|
|
||||||
Cleanup requires an explicit flag:
|
Immediate same-run cleanup requires an explicit flag:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
/opt/orderbooks/scripts/upload_archive_rclone.sh \
|
/opt/orderbooks/scripts/upload_archive_rclone.sh \
|
||||||
|
|
@ -176,6 +178,28 @@ Cleanup requires an explicit flag:
|
||||||
Cleanup deletes only files that were selected for upload, uploaded, verified, and
|
Cleanup deletes only files that were selected for upload, uploaded, verified, and
|
||||||
older than the retention window. The default retention window is 7 days.
|
older than the retention window. The default retention window is 7 days.
|
||||||
|
|
||||||
|
The uploader also maintains a durable verified-upload index at:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/var/lib/orderbooks/manifests/upload_verified_index.json
|
||||||
|
```
|
||||||
|
|
||||||
|
That index records files that have already passed `rclone copy` and
|
||||||
|
`rclone check`. The periodic purge step uses that index to delete previously
|
||||||
|
verified local files after the retention window, even when the current upload
|
||||||
|
run is not the one that first verified them.
|
||||||
|
|
||||||
|
Run the purge manually with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
/opt/orderbooks/scripts/purge_uploaded_local_files.sh \
|
||||||
|
--execute \
|
||||||
|
--data-dir /var/lib/orderbooks \
|
||||||
|
--retention-days 7
|
||||||
|
```
|
||||||
|
|
||||||
|
The periodic systemd/Kubernetes runtime runs upload and purge together.
|
||||||
|
|
||||||
## Upload Manifest
|
## Upload Manifest
|
||||||
|
|
||||||
Each run writes a manifest such as:
|
Each run writes a manifest such as:
|
||||||
|
|
@ -199,6 +223,22 @@ The manifest records:
|
||||||
- start/end time
|
- start/end time
|
||||||
- rclone copy/check exit codes
|
- rclone copy/check exit codes
|
||||||
- gate status
|
- gate status
|
||||||
|
- verified-upload index update summary
|
||||||
|
|
||||||
|
Each purge run writes a separate manifest such as:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/var/lib/orderbooks/manifests/purge_uploaded_local_YYYYMMDDTHHMMSSZ.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The purge manifest records:
|
||||||
|
|
||||||
|
- verified-index path and record count
|
||||||
|
- eligible files older than retention
|
||||||
|
- deleted local files
|
||||||
|
- skipped files such as checksum mismatches
|
||||||
|
- retention configuration
|
||||||
|
- gate and operation status
|
||||||
|
|
||||||
For this repository, the sample manifest path is:
|
For this repository, the sample manifest path is:
|
||||||
|
|
||||||
|
|
@ -247,6 +287,9 @@ Run one upload immediately:
|
||||||
sudo systemctl start polymarket-orderbook-uploader.service
|
sudo systemctl start polymarket-orderbook-uploader.service
|
||||||
```
|
```
|
||||||
|
|
||||||
|
That service now runs upload verification first and then runs the verified-file
|
||||||
|
purge step in the same timer cycle.
|
||||||
|
|
||||||
## Logs
|
## Logs
|
||||||
|
|
||||||
Use the systemd journal:
|
Use the systemd journal:
|
||||||
|
|
@ -290,5 +333,5 @@ PASS
|
||||||
|
|
||||||
- Long-run upload reliability.
|
- Long-run upload reliability.
|
||||||
- Interaction between hourly uploads and a 24h collector soak test.
|
- Interaction between hourly uploads and a 24h collector soak test.
|
||||||
- Retention cleanup after verified upload.
|
- Long-run purge behavior under repeated intermittent `rclone check` failures.
|
||||||
- Production readiness.
|
- Production readiness.
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ Prepare repository permissions and the Python virtualenv:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd /opt/orderbooks
|
cd /opt/orderbooks
|
||||||
sudo chmod +x scripts/run_polymarket_collector_cycle.sh scripts/upload_archive_rclone.sh scripts/vps_preflight_check.sh scripts/vps_runtime_smoke_check.sh
|
sudo chmod +x scripts/run_polymarket_collector_cycle.sh scripts/upload_archive_rclone.sh scripts/purge_uploaded_local_files.sh scripts/vps_preflight_check.sh scripts/vps_runtime_smoke_check.sh
|
||||||
sudo python3 -m venv .venv
|
sudo python3 -m venv .venv
|
||||||
sudo .venv/bin/python -m pip install --upgrade pip
|
sudo .venv/bin/python -m pip install --upgrade pip
|
||||||
sudo chown -R root:root /opt/orderbooks
|
sudo chown -R root:root /opt/orderbooks
|
||||||
|
|
@ -141,7 +141,10 @@ ORDERBOOKS_UPLOAD_MIN_AGE_SECONDS=600
|
||||||
```
|
```
|
||||||
|
|
||||||
The uploader verifies uploads with `rclone check`. Dry runs do not prove remote
|
The uploader verifies uploads with `rclone check`. Dry runs do not prove remote
|
||||||
write access.
|
write access. Successful uploads update
|
||||||
|
`/var/lib/orderbooks/manifests/upload_verified_index.json`, and the uploader
|
||||||
|
service also runs a purge step that deletes older previously verified local
|
||||||
|
files after the retention window.
|
||||||
|
|
||||||
## Run VPS Preflight
|
## Run VPS Preflight
|
||||||
|
|
||||||
|
|
|
||||||
89
reports/checkpoints/checkpoint_011_verified_purge.md
Normal file
89
reports/checkpoints/checkpoint_011_verified_purge.md
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
# Checkpoint 11: Verified Upload Purge
|
||||||
|
|
||||||
|
## Gate
|
||||||
|
|
||||||
|
`READY_FOR_DEPLOY_NOT_LIVE`
|
||||||
|
|
||||||
|
The purge implementation is validated locally and the Kubernetes apply set
|
||||||
|
passes server dry-run, but this change has not been built into a new cluster
|
||||||
|
image yet.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Add periodic local deletion of files that have already been uploaded and
|
||||||
|
verified on the remote, without relying only on the current upload run.
|
||||||
|
|
||||||
|
## What Changed
|
||||||
|
|
||||||
|
- `scripts/upload_archive_rclone.sh`
|
||||||
|
- writes/updates a durable verified-upload index at
|
||||||
|
`/var/lib/orderbooks/manifests/upload_verified_index.json`
|
||||||
|
- records verified-index update summary in each upload manifest
|
||||||
|
- `scripts/purge_uploaded_local_files.sh`
|
||||||
|
- reads the verified-upload index
|
||||||
|
- deletes only files older than retention with matching local SHA-256
|
||||||
|
- protects the verified-upload index itself
|
||||||
|
- writes a purge manifest under `/var/lib/orderbooks/manifests/`
|
||||||
|
- `deploy/k8s/base/cronjob-uploader.yaml`
|
||||||
|
- runs upload verification and purge in the same periodic CronJob cycle
|
||||||
|
- `systemd/polymarket-orderbook-uploader.service`
|
||||||
|
- runs upload verification and purge in the same periodic service execution
|
||||||
|
- docs updated:
|
||||||
|
- `docs/GOOGLE_DRIVE_OFFLOAD.md`
|
||||||
|
- `docs/KUBERNETES_DEPLOYMENT.md`
|
||||||
|
- `docs/POLYMARKET_WEBSOCKET_RECORDER.md`
|
||||||
|
- `docs/VPS_CUTOVER_RUNBOOK.md`
|
||||||
|
|
||||||
|
## Validation Evidence
|
||||||
|
|
||||||
|
Local validation used a temporary data directory and a local `rclone`
|
||||||
|
destination path, not Google Drive, to prove the full flow:
|
||||||
|
|
||||||
|
1. real `rclone copy`
|
||||||
|
2. real `rclone check`
|
||||||
|
3. verified-upload index update
|
||||||
|
4. purge of files older than retention
|
||||||
|
5. retention of a newer local file
|
||||||
|
|
||||||
|
Durable artifacts:
|
||||||
|
|
||||||
|
- `data/manifests/upload_archive_purge_validation_sample.json`
|
||||||
|
- `data/manifests/purge_uploaded_local_validation_sample.json`
|
||||||
|
- `data/manifests/purge_uploaded_local_validation_summary.json`
|
||||||
|
|
||||||
|
Observed result:
|
||||||
|
|
||||||
|
- upload gate: `PASS`
|
||||||
|
- upload operation: `UPLOAD_VERIFIED`
|
||||||
|
- verified index status: `updated`
|
||||||
|
- purge gate: `PASS`
|
||||||
|
- purge operation: `PURGE_PASS`
|
||||||
|
- deleted files: `2`
|
||||||
|
- retained newer file: `1`
|
||||||
|
|
||||||
|
Kubernetes validation:
|
||||||
|
|
||||||
|
- `kubectl kustomize deploy/k8s/base`
|
||||||
|
- `KUBECONFIG=../nuri/unrip3/.state/hetzner/kubeconfig.yaml kubectl apply -k deploy/k8s/base --dry-run=server`
|
||||||
|
|
||||||
|
Both passed.
|
||||||
|
|
||||||
|
## Live Runtime Context
|
||||||
|
|
||||||
|
Before this change, the live cluster was already deleting files older than the
|
||||||
|
3-day retention window, but only during successful upload runs. The live disk
|
||||||
|
shape still showed many retained recent files, especially manifests within the
|
||||||
|
retention window. This checkpoint adds a separate verified-file purge phase so
|
||||||
|
older already-verified files can be removed based on durable local evidence.
|
||||||
|
|
||||||
|
## Strongest Fake-Progress Risk
|
||||||
|
|
||||||
|
This is not deployed yet. The current cluster image still runs the previous
|
||||||
|
uploader behavior until a new image is built and the canary deploy is applied.
|
||||||
|
|
||||||
|
## Next Smallest Step
|
||||||
|
|
||||||
|
Commit and push this source change to Forgejo `main`, run
|
||||||
|
`scripts/deploy/deploy_ws_canary_kaniko.sh --git-ref <new-sha>`, and then check
|
||||||
|
the next `upload_archive_*.json`, `purge_uploaded_local_*.json`, and PVC usage
|
||||||
|
to confirm the live CronJob is purging as designed.
|
||||||
299
scripts/purge_uploaded_local_files.sh
Executable file
299
scripts/purge_uploaded_local_files.sh
Executable file
|
|
@ -0,0 +1,299 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
SCRIPT_NAME="orderbooks_verified_file_purger"
|
||||||
|
SCRIPT_VERSION="0.1.0"
|
||||||
|
|
||||||
|
MODE="dry-run"
|
||||||
|
DATA_DIR="${ORDERBOOKS_UPLOAD_DATA_DIR:-${ORDERBOOKS_DATA_DIR:-/var/lib/orderbooks}}"
|
||||||
|
MANIFEST_DIR="${ORDERBOOKS_UPLOAD_MANIFEST_DIR:-}"
|
||||||
|
MANIFEST_PATH="${ORDERBOOKS_PURGE_MANIFEST_PATH:-}"
|
||||||
|
VERIFIED_INDEX_PATH="${ORDERBOOKS_UPLOAD_VERIFIED_INDEX_PATH:-}"
|
||||||
|
RETENTION_DAYS="${ORDERBOOKS_UPLOAD_RETENTION_DAYS:-7}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: scripts/purge_uploaded_local_files.sh [options]
|
||||||
|
|
||||||
|
Deletes local files only when they have prior verified-upload evidence in the
|
||||||
|
verified-upload index and are older than the retention window.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--dry-run Plan purge only (default).
|
||||||
|
--execute Delete eligible local files.
|
||||||
|
--data-dir DIR Base data directory. Default: /var/lib/orderbooks.
|
||||||
|
--manifest-dir DIR Purge manifest output directory. Default: DATA_DIR/manifests.
|
||||||
|
--manifest-path PATH Exact purge manifest path.
|
||||||
|
--verified-index-path PATH Verified-upload index path. Default: MANIFEST_DIR/upload_verified_index.json.
|
||||||
|
--retention-days N Keep at least N days locally. Default: 7.
|
||||||
|
--help Show this help.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run)
|
||||||
|
MODE="dry-run"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--execute)
|
||||||
|
MODE="execute"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--data-dir)
|
||||||
|
DATA_DIR="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--manifest-dir)
|
||||||
|
MANIFEST_DIR="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--manifest-path)
|
||||||
|
MANIFEST_PATH="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--verified-index-path)
|
||||||
|
VERIFIED_INDEX_PATH="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--retention-days)
|
||||||
|
RETENTION_DAYS="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown argument: $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "${MANIFEST_DIR}" ]]; then
|
||||||
|
MANIFEST_DIR="${DATA_DIR%/}/manifests"
|
||||||
|
fi
|
||||||
|
if [[ -z "${VERIFIED_INDEX_PATH}" ]]; then
|
||||||
|
VERIFIED_INDEX_PATH="${MANIFEST_DIR%/}/upload_verified_index.json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
STARTED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
if [[ -z "${MANIFEST_PATH}" ]]; then
|
||||||
|
MANIFEST_PATH="${MANIFEST_DIR%/}/purge_uploaded_local_${RUN_ID}.json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "${MANIFEST_PATH}")"
|
||||||
|
|
||||||
|
export SCRIPT_NAME SCRIPT_VERSION MODE DATA_DIR MANIFEST_DIR MANIFEST_PATH VERIFIED_INDEX_PATH RETENTION_DAYS STARTED_AT
|
||||||
|
|
||||||
|
python3 - <<'PY'
|
||||||
|
import datetime as dt
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
script_name = os.environ["SCRIPT_NAME"]
|
||||||
|
script_version = os.environ["SCRIPT_VERSION"]
|
||||||
|
mode = os.environ["MODE"]
|
||||||
|
data_dir = Path(os.environ["DATA_DIR"]).resolve()
|
||||||
|
manifest_dir = Path(os.environ["MANIFEST_DIR"]).resolve()
|
||||||
|
manifest_path = Path(os.environ["MANIFEST_PATH"]).resolve()
|
||||||
|
verified_index_path = Path(os.environ["VERIFIED_INDEX_PATH"]).resolve()
|
||||||
|
retention_days = int(os.environ["RETENTION_DAYS"])
|
||||||
|
started_at = os.environ["STARTED_AT"]
|
||||||
|
now = dt.datetime.now(dt.UTC)
|
||||||
|
ended_at = now.replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||||
|
cutoff = now - dt.timedelta(days=retention_days)
|
||||||
|
|
||||||
|
|
||||||
|
def iso_z_from_ts(ts: float) -> str:
|
||||||
|
return dt.datetime.fromtimestamp(ts, dt.UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||||
|
|
||||||
|
|
||||||
|
def sha256_file(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as handle:
|
||||||
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def write_atomic_json(path: Path, payload: dict) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=str(path.parent), delete=False) as tmp:
|
||||||
|
json.dump(payload, tmp, indent=2, sort_keys=True)
|
||||||
|
tmp.write("\n")
|
||||||
|
tmp_path = Path(tmp.name)
|
||||||
|
os.replace(tmp_path, path)
|
||||||
|
|
||||||
|
|
||||||
|
manifest = {
|
||||||
|
"schema_name": "purge_uploaded_local_manifest",
|
||||||
|
"schema_version": 1,
|
||||||
|
"purger": {"name": script_name, "version": script_version},
|
||||||
|
"started_at_utc": started_at,
|
||||||
|
"ended_at_utc": ended_at,
|
||||||
|
"command_mode": mode,
|
||||||
|
"operation_status": None,
|
||||||
|
"gate_status": "PASS",
|
||||||
|
"config": {
|
||||||
|
"data_dir": str(data_dir),
|
||||||
|
"manifest_dir": str(manifest_dir),
|
||||||
|
"manifest_path": str(manifest_path),
|
||||||
|
"verified_index_path": str(verified_index_path),
|
||||||
|
"retention_days": retention_days,
|
||||||
|
},
|
||||||
|
"verified_index": {
|
||||||
|
"path": str(verified_index_path),
|
||||||
|
"exists": verified_index_path.exists(),
|
||||||
|
"record_count_before": 0,
|
||||||
|
"record_count_after": 0,
|
||||||
|
},
|
||||||
|
"candidate_files": [],
|
||||||
|
"deleted_local_files": [],
|
||||||
|
"skipped_files": [],
|
||||||
|
"counts": {
|
||||||
|
"eligible": 0,
|
||||||
|
"deleted": 0,
|
||||||
|
"within_retention": 0,
|
||||||
|
"already_absent": 0,
|
||||||
|
"protected": 0,
|
||||||
|
"sha256_mismatch": 0,
|
||||||
|
"invalid_records": 0,
|
||||||
|
},
|
||||||
|
"warnings": [],
|
||||||
|
"known_gaps": [
|
||||||
|
"Purge trusts prior verified-upload evidence in the local verified index and does not re-run rclone copy/check during deletion.",
|
||||||
|
"Protected local state files, including the verified-upload index itself, are not deleted by this script.",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
if not verified_index_path.exists():
|
||||||
|
manifest["operation_status"] = "NO_VERIFIED_INDEX"
|
||||||
|
write_atomic_json(manifest_path, manifest)
|
||||||
|
print(json.dumps({
|
||||||
|
"gate_status": manifest["gate_status"],
|
||||||
|
"operation_status": manifest["operation_status"],
|
||||||
|
"manifest_path": str(manifest_path),
|
||||||
|
"eligible_files": 0,
|
||||||
|
"deleted_files": 0,
|
||||||
|
}, indent=2, sort_keys=True))
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
index_doc = json.loads(verified_index_path.read_text(encoding="utf-8"))
|
||||||
|
except Exception as exc:
|
||||||
|
manifest["operation_status"] = "INDEX_READ_FAILED"
|
||||||
|
manifest["gate_status"] = "FAIL"
|
||||||
|
manifest["warnings"].append(f"failed to read verified-upload index: {exc}")
|
||||||
|
write_atomic_json(manifest_path, manifest)
|
||||||
|
print(json.dumps({
|
||||||
|
"gate_status": manifest["gate_status"],
|
||||||
|
"operation_status": manifest["operation_status"],
|
||||||
|
"manifest_path": str(manifest_path),
|
||||||
|
"eligible_files": 0,
|
||||||
|
"deleted_files": 0,
|
||||||
|
}, indent=2, sort_keys=True))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
records = index_doc.get("records", [])
|
||||||
|
manifest["verified_index"]["record_count_before"] = len(records)
|
||||||
|
protected_path = verified_index_path.resolve()
|
||||||
|
index_changed = False
|
||||||
|
deleted_at = ended_at
|
||||||
|
|
||||||
|
for record in records:
|
||||||
|
relative_path = record.get("relative_path")
|
||||||
|
sha256_expected = record.get("sha256")
|
||||||
|
if not relative_path or not sha256_expected:
|
||||||
|
manifest["counts"]["invalid_records"] += 1
|
||||||
|
manifest["skipped_files"].append({
|
||||||
|
"relative_path": relative_path,
|
||||||
|
"reason": "invalid_index_record",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
local_path = (data_dir / relative_path).resolve()
|
||||||
|
if local_path == protected_path:
|
||||||
|
manifest["counts"]["protected"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not local_path.exists():
|
||||||
|
manifest["counts"]["already_absent"] += 1
|
||||||
|
if mode == "execute" and record.get("local_deleted_at_utc") is None:
|
||||||
|
record["local_deleted_at_utc"] = deleted_at
|
||||||
|
index_changed = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
stat = local_path.stat()
|
||||||
|
mtime = dt.datetime.fromtimestamp(stat.st_mtime, dt.UTC)
|
||||||
|
if mtime >= cutoff:
|
||||||
|
manifest["counts"]["within_retention"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
sha256_actual = sha256_file(local_path)
|
||||||
|
if sha256_actual != sha256_expected:
|
||||||
|
manifest["counts"]["sha256_mismatch"] += 1
|
||||||
|
manifest["skipped_files"].append({
|
||||||
|
"relative_path": relative_path,
|
||||||
|
"local_path": str(local_path),
|
||||||
|
"kind": record.get("kind"),
|
||||||
|
"reason": "sha256_mismatch",
|
||||||
|
"expected_sha256": sha256_expected,
|
||||||
|
"actual_sha256": sha256_actual,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
candidate = {
|
||||||
|
"relative_path": relative_path,
|
||||||
|
"local_path": str(local_path),
|
||||||
|
"kind": record.get("kind"),
|
||||||
|
"bytes": stat.st_size,
|
||||||
|
"mtime_utc": iso_z_from_ts(stat.st_mtime),
|
||||||
|
"sha256": sha256_actual,
|
||||||
|
"first_verified_at_utc": record.get("first_verified_at_utc"),
|
||||||
|
"last_verified_at_utc": record.get("last_verified_at_utc"),
|
||||||
|
"last_verified_by_manifest": record.get("last_verified_by_manifest"),
|
||||||
|
}
|
||||||
|
manifest["candidate_files"].append(candidate)
|
||||||
|
manifest["counts"]["eligible"] += 1
|
||||||
|
|
||||||
|
if mode == "execute":
|
||||||
|
local_path.unlink()
|
||||||
|
record["local_deleted_at_utc"] = deleted_at
|
||||||
|
index_changed = True
|
||||||
|
manifest["deleted_local_files"].append({**candidate, "deleted_at_utc": deleted_at})
|
||||||
|
|
||||||
|
manifest["counts"]["deleted"] = len(manifest["deleted_local_files"])
|
||||||
|
manifest["verified_index"]["record_count_after"] = len(records)
|
||||||
|
|
||||||
|
if mode == "execute" and index_changed:
|
||||||
|
index_doc["updated_at_utc"] = ended_at
|
||||||
|
write_atomic_json(verified_index_path, index_doc)
|
||||||
|
|
||||||
|
if manifest["operation_status"] is None:
|
||||||
|
if manifest["counts"]["eligible"] == 0:
|
||||||
|
manifest["operation_status"] = "NO_ELIGIBLE_FILES"
|
||||||
|
elif mode == "dry-run":
|
||||||
|
manifest["operation_status"] = "DRY_RUN_PASS"
|
||||||
|
else:
|
||||||
|
manifest["operation_status"] = "PURGE_PASS"
|
||||||
|
|
||||||
|
write_atomic_json(manifest_path, manifest)
|
||||||
|
print(json.dumps({
|
||||||
|
"gate_status": manifest["gate_status"],
|
||||||
|
"operation_status": manifest["operation_status"],
|
||||||
|
"manifest_path": str(manifest_path),
|
||||||
|
"eligible_files": manifest["counts"]["eligible"],
|
||||||
|
"deleted_files": manifest["counts"]["deleted"],
|
||||||
|
}, indent=2, sort_keys=True))
|
||||||
|
|
||||||
|
if manifest["gate_status"] != "PASS":
|
||||||
|
sys.exit(1)
|
||||||
|
PY
|
||||||
|
|
@ -11,6 +11,7 @@ RAW_DIR="${ORDERBOOKS_UPLOAD_RAW_DIR:-}"
|
||||||
SOURCE_MANIFEST_DIR="${ORDERBOOKS_UPLOAD_SOURCE_MANIFEST_DIR:-}"
|
SOURCE_MANIFEST_DIR="${ORDERBOOKS_UPLOAD_SOURCE_MANIFEST_DIR:-}"
|
||||||
MANIFEST_DIR="${ORDERBOOKS_UPLOAD_MANIFEST_DIR:-}"
|
MANIFEST_DIR="${ORDERBOOKS_UPLOAD_MANIFEST_DIR:-}"
|
||||||
MANIFEST_PATH="${ORDERBOOKS_UPLOAD_MANIFEST_PATH:-}"
|
MANIFEST_PATH="${ORDERBOOKS_UPLOAD_MANIFEST_PATH:-}"
|
||||||
|
VERIFIED_INDEX_PATH="${ORDERBOOKS_UPLOAD_VERIFIED_INDEX_PATH:-}"
|
||||||
DEST="${ORDERBOOKS_RCLONE_DEST:-}"
|
DEST="${ORDERBOOKS_RCLONE_DEST:-}"
|
||||||
RCLONE_BIN="${ORDERBOOKS_RCLONE_BIN:-rclone}"
|
RCLONE_BIN="${ORDERBOOKS_RCLONE_BIN:-rclone}"
|
||||||
MIN_AGE_SECONDS="${ORDERBOOKS_UPLOAD_MIN_AGE_SECONDS:-600}"
|
MIN_AGE_SECONDS="${ORDERBOOKS_UPLOAD_MIN_AGE_SECONDS:-600}"
|
||||||
|
|
@ -34,6 +35,7 @@ Options:
|
||||||
--source-manifest-dir DIR Source collector manifest directory. Default: DATA_DIR/manifests.
|
--source-manifest-dir DIR Source collector manifest directory. Default: DATA_DIR/manifests.
|
||||||
--manifest-dir DIR Upload manifest output directory. Default: DATA_DIR/manifests.
|
--manifest-dir DIR Upload manifest output directory. Default: DATA_DIR/manifests.
|
||||||
--manifest-path PATH Exact upload manifest path.
|
--manifest-path PATH Exact upload manifest path.
|
||||||
|
--verified-index-path PATH Verified-upload index path. Default: MANIFEST_DIR/upload_verified_index.json.
|
||||||
--dest REMOTE:PATH rclone destination. Or set ORDERBOOKS_RCLONE_DEST.
|
--dest REMOTE:PATH rclone destination. Or set ORDERBOOKS_RCLONE_DEST.
|
||||||
--min-age-seconds N Skip files modified within N seconds. Default: 600.
|
--min-age-seconds N Skip files modified within N seconds. Default: 600.
|
||||||
--retention-days N Keep at least N days locally. Default: 7.
|
--retention-days N Keep at least N days locally. Default: 7.
|
||||||
|
|
@ -76,6 +78,10 @@ while [[ $# -gt 0 ]]; do
|
||||||
MANIFEST_PATH="$2"
|
MANIFEST_PATH="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--verified-index-path)
|
||||||
|
VERIFIED_INDEX_PATH="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
--dest)
|
--dest)
|
||||||
DEST="$2"
|
DEST="$2"
|
||||||
shift 2
|
shift 2
|
||||||
|
|
@ -113,6 +119,9 @@ fi
|
||||||
if [[ -z "${MANIFEST_DIR}" ]]; then
|
if [[ -z "${MANIFEST_DIR}" ]]; then
|
||||||
MANIFEST_DIR="${DATA_DIR%/}/manifests"
|
MANIFEST_DIR="${DATA_DIR%/}/manifests"
|
||||||
fi
|
fi
|
||||||
|
if [[ -z "${VERIFIED_INDEX_PATH}" ]]; then
|
||||||
|
VERIFIED_INDEX_PATH="${MANIFEST_DIR%/}/upload_verified_index.json"
|
||||||
|
fi
|
||||||
|
|
||||||
STARTED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
STARTED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)"
|
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
|
@ -127,6 +136,7 @@ PLAN_PATH="${TMPDIR}/plan.json"
|
||||||
RCLONE_COPY_LOG="${TMPDIR}/rclone_copy.log"
|
RCLONE_COPY_LOG="${TMPDIR}/rclone_copy.log"
|
||||||
RCLONE_CHECK_LOG="${TMPDIR}/rclone_check.log"
|
RCLONE_CHECK_LOG="${TMPDIR}/rclone_check.log"
|
||||||
CLEANUP_PATH="${TMPDIR}/cleanup.json"
|
CLEANUP_PATH="${TMPDIR}/cleanup.json"
|
||||||
|
INDEX_UPDATE_PATH="${TMPDIR}/verified_index_update.json"
|
||||||
STAGING_DIR="${TMPDIR}/stage"
|
STAGING_DIR="${TMPDIR}/stage"
|
||||||
|
|
||||||
mkdir -p "$(dirname "${MANIFEST_PATH}")" "${STAGING_DIR}"
|
mkdir -p "$(dirname "${MANIFEST_PATH}")" "${STAGING_DIR}"
|
||||||
|
|
@ -324,13 +334,127 @@ PY
|
||||||
|
|
||||||
ENDED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
ENDED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
|
||||||
|
if ! python3 - "$PLAN_PATH" "$VERIFIED_INDEX_PATH" "$INDEX_UPDATE_PATH" "$OPERATION_STATUS" "$ENDED_AT" "$MANIFEST_PATH" "$DATA_DIR" <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
plan_path = Path(sys.argv[1])
|
||||||
|
index_path = Path(sys.argv[2])
|
||||||
|
summary_path = Path(sys.argv[3])
|
||||||
|
operation_status = sys.argv[4]
|
||||||
|
ended_at = sys.argv[5]
|
||||||
|
manifest_path = Path(sys.argv[6])
|
||||||
|
data_dir = Path(sys.argv[7]).resolve()
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"path": str(index_path),
|
||||||
|
"status": "skipped",
|
||||||
|
"updated": False,
|
||||||
|
"record_count": 0,
|
||||||
|
"new_records": 0,
|
||||||
|
"updated_records": 0,
|
||||||
|
"reason": "upload_not_verified",
|
||||||
|
}
|
||||||
|
|
||||||
|
records_by_rel = {}
|
||||||
|
existing = {}
|
||||||
|
if index_path.exists():
|
||||||
|
existing = json.loads(index_path.read_text(encoding="utf-8"))
|
||||||
|
for record in existing.get("records", []):
|
||||||
|
rel = record.get("relative_path")
|
||||||
|
if rel:
|
||||||
|
records_by_rel[rel] = record
|
||||||
|
|
||||||
|
if operation_status == "UPLOAD_VERIFIED":
|
||||||
|
plan = json.loads(plan_path.read_text(encoding="utf-8"))
|
||||||
|
for item in plan.get("selected_files", []):
|
||||||
|
rel = item["relative_path"]
|
||||||
|
prev = records_by_rel.get(rel, {})
|
||||||
|
first_verified = prev.get("first_verified_at_utc") or ended_at
|
||||||
|
local_path = Path(item["local_path"]).resolve()
|
||||||
|
try:
|
||||||
|
relative_path = local_path.relative_to(data_dir).as_posix()
|
||||||
|
except ValueError:
|
||||||
|
relative_path = rel
|
||||||
|
changed = (
|
||||||
|
not prev
|
||||||
|
or prev.get("sha256") != item["sha256"]
|
||||||
|
or prev.get("bytes") != item["bytes"]
|
||||||
|
or prev.get("mtime_utc") != item["mtime_utc"]
|
||||||
|
or prev.get("local_deleted_at_utc") is not None
|
||||||
|
)
|
||||||
|
merged = {
|
||||||
|
"relative_path": relative_path,
|
||||||
|
"local_path": str(local_path),
|
||||||
|
"kind": item["kind"],
|
||||||
|
"bytes": item["bytes"],
|
||||||
|
"mtime_utc": item["mtime_utc"],
|
||||||
|
"sha256": item["sha256"],
|
||||||
|
"first_verified_at_utc": first_verified,
|
||||||
|
"last_verified_at_utc": ended_at,
|
||||||
|
"last_verified_by_manifest": str(manifest_path),
|
||||||
|
"local_deleted_at_utc": None,
|
||||||
|
}
|
||||||
|
records_by_rel[rel] = merged
|
||||||
|
if prev:
|
||||||
|
if changed:
|
||||||
|
summary["updated_records"] += 1
|
||||||
|
else:
|
||||||
|
summary["new_records"] += 1
|
||||||
|
|
||||||
|
index_doc = {
|
||||||
|
"schema_name": "verified_upload_index",
|
||||||
|
"schema_version": 1,
|
||||||
|
"updated_at_utc": ended_at,
|
||||||
|
"records": [records_by_rel[key] for key in sorted(records_by_rel)],
|
||||||
|
}
|
||||||
|
index_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=str(index_path.parent), delete=False) as tmp:
|
||||||
|
json.dump(index_doc, tmp, indent=2, sort_keys=True)
|
||||||
|
tmp.write("\n")
|
||||||
|
tmp_path = Path(tmp.name)
|
||||||
|
os.replace(tmp_path, index_path)
|
||||||
|
summary["status"] = "updated"
|
||||||
|
summary["updated"] = True
|
||||||
|
summary["record_count"] = len(index_doc["records"])
|
||||||
|
summary["reason"] = None
|
||||||
|
else:
|
||||||
|
summary["record_count"] = len(records_by_rel)
|
||||||
|
|
||||||
|
summary_path.write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
PY
|
||||||
|
then
|
||||||
|
python3 - "$INDEX_UPDATE_PATH" "$VERIFIED_INDEX_PATH" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
summary_path = Path(sys.argv[1])
|
||||||
|
index_path = Path(sys.argv[2])
|
||||||
|
summary = {
|
||||||
|
"path": str(index_path),
|
||||||
|
"status": "failed",
|
||||||
|
"updated": False,
|
||||||
|
"record_count": 0,
|
||||||
|
"new_records": 0,
|
||||||
|
"updated_records": 0,
|
||||||
|
"reason": "index_update_failed",
|
||||||
|
}
|
||||||
|
summary_path.write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
PY
|
||||||
|
fi
|
||||||
|
|
||||||
export SCRIPT_NAME SCRIPT_VERSION STARTED_AT ENDED_AT
|
export SCRIPT_NAME SCRIPT_VERSION STARTED_AT ENDED_AT
|
||||||
export MODE OPERATION_STATUS GATE_STATUS
|
export MODE OPERATION_STATUS GATE_STATUS
|
||||||
export RCLONE_BIN RCLONE_AVAILABLE RCLONE_VERSION DEST
|
export RCLONE_BIN RCLONE_AVAILABLE RCLONE_VERSION DEST
|
||||||
export COPY_ATTEMPTED CHECK_ATTEMPTED COPY_EXIT_CODE CHECK_EXIT_CODE
|
export COPY_ATTEMPTED CHECK_ATTEMPTED COPY_EXIT_CODE CHECK_EXIT_CODE
|
||||||
export DATA_DIR RAW_DIR SOURCE_MANIFEST_DIR MIN_AGE_SECONDS RETENTION_DAYS CLEANUP_AFTER_VERIFY
|
export DATA_DIR RAW_DIR SOURCE_MANIFEST_DIR MIN_AGE_SECONDS RETENTION_DAYS CLEANUP_AFTER_VERIFY
|
||||||
|
export VERIFIED_INDEX_PATH
|
||||||
|
|
||||||
python3 - "$PLAN_PATH" "$CLEANUP_PATH" "$MANIFEST_PATH" <<'PY'
|
python3 - "$PLAN_PATH" "$CLEANUP_PATH" "$INDEX_UPDATE_PATH" "$MANIFEST_PATH" <<'PY'
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -338,7 +462,8 @@ from pathlib import Path
|
||||||
|
|
||||||
plan = json.loads(Path(sys.argv[1]).read_text())
|
plan = json.loads(Path(sys.argv[1]).read_text())
|
||||||
cleanup = json.loads(Path(sys.argv[2]).read_text())
|
cleanup = json.loads(Path(sys.argv[2]).read_text())
|
||||||
manifest_path = Path(sys.argv[3])
|
index_update = json.loads(Path(sys.argv[3]).read_text())
|
||||||
|
manifest_path = Path(sys.argv[4])
|
||||||
|
|
||||||
mode = os.environ["MODE"]
|
mode = os.environ["MODE"]
|
||||||
operation_status = os.environ["OPERATION_STATUS"]
|
operation_status = os.environ["OPERATION_STATUS"]
|
||||||
|
|
@ -393,10 +518,12 @@ manifest = {
|
||||||
"raw_dir": os.environ["RAW_DIR"],
|
"raw_dir": os.environ["RAW_DIR"],
|
||||||
"source_manifest_dir": os.environ["SOURCE_MANIFEST_DIR"],
|
"source_manifest_dir": os.environ["SOURCE_MANIFEST_DIR"],
|
||||||
"manifest_path": str(manifest_path),
|
"manifest_path": str(manifest_path),
|
||||||
|
"verified_index_path": os.environ["VERIFIED_INDEX_PATH"],
|
||||||
"min_age_seconds": int(os.environ["MIN_AGE_SECONDS"]),
|
"min_age_seconds": int(os.environ["MIN_AGE_SECONDS"]),
|
||||||
"retention_days": int(os.environ["RETENTION_DAYS"]),
|
"retention_days": int(os.environ["RETENTION_DAYS"]),
|
||||||
"cleanup_after_verify": os.environ["CLEANUP_AFTER_VERIFY"] == "1",
|
"cleanup_after_verify": os.environ["CLEANUP_AFTER_VERIFY"] == "1",
|
||||||
},
|
},
|
||||||
|
"verified_index": index_update,
|
||||||
"planned_files": selected,
|
"planned_files": selected,
|
||||||
"attempted_files": attempted_files,
|
"attempted_files": attempted_files,
|
||||||
"dry_run_files": dry_run_files,
|
"dry_run_files": dry_run_files,
|
||||||
|
|
@ -432,6 +559,8 @@ if operation_status == "BLOCKED_DEST_MISSING":
|
||||||
manifest["warnings"].append("No rclone destination was configured; set --dest or ORDERBOOKS_RCLONE_DEST.")
|
manifest["warnings"].append("No rclone destination was configured; set --dest or ORDERBOOKS_RCLONE_DEST.")
|
||||||
if mode == "dry-run":
|
if mode == "dry-run":
|
||||||
manifest["warnings"].append("Dry-run mode does not perform a real upload; checkpoint real-upload gate remains blocked.")
|
manifest["warnings"].append("Dry-run mode does not perform a real upload; checkpoint real-upload gate remains blocked.")
|
||||||
|
if index_update.get("status") == "failed":
|
||||||
|
manifest["warnings"].append("Verified-upload index update failed; previously verified file purge may lag until a later successful update.")
|
||||||
|
|
||||||
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
manifest_path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
manifest_path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ Environment=ORDERBOOKS_UPLOAD_MIN_AGE_SECONDS=600
|
||||||
Environment=ORDERBOOKS_UPLOAD_RETENTION_DAYS=7
|
Environment=ORDERBOOKS_UPLOAD_RETENTION_DAYS=7
|
||||||
Environment=ORDERBOOKS_RCLONE_BIN=/usr/bin/rclone
|
Environment=ORDERBOOKS_RCLONE_BIN=/usr/bin/rclone
|
||||||
EnvironmentFile=-/etc/orderbooks/orderbook-uploader.env
|
EnvironmentFile=-/etc/orderbooks/orderbook-uploader.env
|
||||||
ExecStart=/bin/bash /opt/orderbooks/scripts/upload_archive_rclone.sh --execute
|
ExecStart=/bin/bash -lc 'upload_exit=0; /opt/orderbooks/scripts/upload_archive_rclone.sh --execute --cleanup-after-verify || upload_exit=$?; purge_exit=0; /opt/orderbooks/scripts/purge_uploaded_local_files.sh --execute || purge_exit=$?; if [ "$upload_exit" -ne 0 ] || [ "$purge_exit" -ne 0 ]; then exit 1; fi'
|
||||||
StandardOutput=journal
|
StandardOutput=journal
|
||||||
StandardError=journal
|
StandardError=journal
|
||||||
SyslogIdentifier=polymarket-orderbook-uploader
|
SyslogIdentifier=polymarket-orderbook-uploader
|
||||||
|
|
@ -26,4 +26,3 @@ ProtectSystem=strict
|
||||||
ProtectHome=true
|
ProtectHome=true
|
||||||
ReadWritePaths=/var/lib/orderbooks
|
ReadWritePaths=/var/lib/orderbooks
|
||||||
StateDirectory=orderbooks
|
StateDirectory=orderbooks
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue