Spaces:
Sleeping
Sleeping
fix(backup): default backup source to state dir (#13)
Browse files- Dockerfile +1 -1
- README.md +2 -2
- openclaw_hf/backup.py +5 -1
- scripts/openclaw-entrypoint.sh +1 -1
- tests/test_backup.py +18 -2
- tests/test_runtime_user_defaults.py +2 -0
Dockerfile
CHANGED
|
@@ -92,7 +92,7 @@ ENV OPENCLAW_LLM_PROVIDER=thirdparty
|
|
| 92 |
ENV OPENCLAW_LLM_API=openai-completions
|
| 93 |
ENV OPENCLAW_BACKUP_ENABLED=true
|
| 94 |
ENV OPENCLAW_BACKUP_CRON="*/30 * * * *"
|
| 95 |
-
ENV OPENCLAW_BACKUP_SOURCE_DIR=/root
|
| 96 |
ENV OPENCLAW_SSHX_AUTO_START=false
|
| 97 |
ENV OPENCLAW_BACKUP_PRIVATE=true
|
| 98 |
ENV OPENCLAW_BACKUP_PATH_PREFIX=backups
|
|
|
|
| 92 |
ENV OPENCLAW_LLM_API=openai-completions
|
| 93 |
ENV OPENCLAW_BACKUP_ENABLED=true
|
| 94 |
ENV OPENCLAW_BACKUP_CRON="*/30 * * * *"
|
| 95 |
+
ENV OPENCLAW_BACKUP_SOURCE_DIR=/root/.openclaw
|
| 96 |
ENV OPENCLAW_SSHX_AUTO_START=false
|
| 97 |
ENV OPENCLAW_BACKUP_PRIVATE=true
|
| 98 |
ENV OPENCLAW_BACKUP_PATH_PREFIX=backups
|
README.md
CHANGED
|
@@ -66,7 +66,7 @@ In that case, you can still configure from inside the container (for example via
|
|
| 66 |
- `OPENCLAW_CONFIG_PATH` (default: `/root/.openclaw/openclaw.json`)
|
| 67 |
- `OPENCLAW_WORKSPACE_DIR` (default: `/root/.openclaw/workspace`)
|
| 68 |
- `OPENCLAW_BACKUP_CRON` (default: `*/30 * * * *`, backup every 30 minutes)
|
| 69 |
-
- `OPENCLAW_BACKUP_SOURCE_DIR` (default: `/root`, backup/restore base directory)
|
| 70 |
- `OPENCLAW_BACKUP_PATH_PREFIX` (default: `backups`)
|
| 71 |
- `OPENCLAW_BACKUP_LATEST_NAME` (default: `latest-backup.tar.gz`)
|
| 72 |
- `OPENCLAW_BACKUP_KEEP_COUNT` (default: `48`, keep newest N timestamped backup archives; older ones are auto-deleted)
|
|
@@ -291,7 +291,7 @@ For this project, if you need stable dashboard access without cold starts, use p
|
|
| 291 |
|
| 292 |
## Backup/Restore Flow
|
| 293 |
|
| 294 |
-
- Startup restore: on container startup, it tries to fetch `latest-backup.tar.gz` from the Dataset and restore it to `OPENCLAW_BACKUP_SOURCE_DIR` (default `/root`)
|
| 295 |
- Scheduled backup: cron runs based on `OPENCLAW_BACKUP_CRON`
|
| 296 |
- Shutdown backup: when the container receives a stop signal, one final backup is uploaded before exit
|
| 297 |
- Each backup upload includes:
|
|
|
|
| 66 |
- `OPENCLAW_CONFIG_PATH` (default: `/root/.openclaw/openclaw.json`)
|
| 67 |
- `OPENCLAW_WORKSPACE_DIR` (default: `/root/.openclaw/workspace`)
|
| 68 |
- `OPENCLAW_BACKUP_CRON` (default: `*/30 * * * *`, backup every 30 minutes)
|
| 69 |
+
- `OPENCLAW_BACKUP_SOURCE_DIR` (default: `/root/.openclaw`, backup/restore base directory)
|
| 70 |
- `OPENCLAW_BACKUP_PATH_PREFIX` (default: `backups`)
|
| 71 |
- `OPENCLAW_BACKUP_LATEST_NAME` (default: `latest-backup.tar.gz`)
|
| 72 |
- `OPENCLAW_BACKUP_KEEP_COUNT` (default: `48`, keep newest N timestamped backup archives; older ones are auto-deleted)
|
|
|
|
| 291 |
|
| 292 |
## Backup/Restore Flow
|
| 293 |
|
| 294 |
+
- Startup restore: on container startup, it tries to fetch `latest-backup.tar.gz` from the Dataset and restore it to `OPENCLAW_BACKUP_SOURCE_DIR` (default `/root/.openclaw`)
|
| 295 |
- Scheduled backup: cron runs based on `OPENCLAW_BACKUP_CRON`
|
| 296 |
- Shutdown backup: when the container receives a stop signal, one final backup is uploaded before exit
|
| 297 |
- Each backup upload includes:
|
openclaw_hf/backup.py
CHANGED
|
@@ -57,7 +57,11 @@ class BackupConfig:
|
|
| 57 |
raise ValueError("OPENCLAW_BACKUP_DATASET_REPO is not set")
|
| 58 |
|
| 59 |
state_dir = Path(os.getenv("OPENCLAW_STATE_DIR", "/root/.openclaw")).resolve()
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
work_dir = Path(os.getenv("OPENCLAW_BACKUP_WORK_DIR", "/tmp/openclaw-backup")).resolve()
|
| 62 |
repo_type = (os.getenv("OPENCLAW_BACKUP_REPO_TYPE") or "dataset").strip() or "dataset"
|
| 63 |
path_prefix = (os.getenv("OPENCLAW_BACKUP_PATH_PREFIX") or "backups").strip("/")
|
|
|
|
| 57 |
raise ValueError("OPENCLAW_BACKUP_DATASET_REPO is not set")
|
| 58 |
|
| 59 |
state_dir = Path(os.getenv("OPENCLAW_STATE_DIR", "/root/.openclaw")).resolve()
|
| 60 |
+
backup_source_raw = (os.getenv("OPENCLAW_BACKUP_SOURCE_DIR") or "").strip()
|
| 61 |
+
if backup_source_raw:
|
| 62 |
+
backup_source_dir = Path(backup_source_raw).resolve()
|
| 63 |
+
else:
|
| 64 |
+
backup_source_dir = state_dir
|
| 65 |
work_dir = Path(os.getenv("OPENCLAW_BACKUP_WORK_DIR", "/tmp/openclaw-backup")).resolve()
|
| 66 |
repo_type = (os.getenv("OPENCLAW_BACKUP_REPO_TYPE") or "dataset").strip() or "dataset"
|
| 67 |
path_prefix = (os.getenv("OPENCLAW_BACKUP_PATH_PREFIX") or "backups").strip("/")
|
scripts/openclaw-entrypoint.sh
CHANGED
|
@@ -18,7 +18,7 @@ OPENCLAW_LLM_BASE_URL_ENV="${OPENCLAW_LLM_BASE_URL_ENV:-OPENCLAW_LLM_BASE_URL}"
|
|
| 18 |
OPENCLAW_LLM_API_KEY_ENV="${OPENCLAW_LLM_API_KEY_ENV:-OPENCLAW_LLM_API_KEY}"
|
| 19 |
OPENCLAW_BACKUP_ENABLED="${OPENCLAW_BACKUP_ENABLED:-true}"
|
| 20 |
OPENCLAW_BACKUP_CRON="${OPENCLAW_BACKUP_CRON:-*/30 * * * *}"
|
| 21 |
-
OPENCLAW_BACKUP_SOURCE_DIR="${OPENCLAW_BACKUP_SOURCE_DIR:-
|
| 22 |
OPENCLAW_BACKUP_ENV_FILE_PATH="${OPENCLAW_BACKUP_ENV_FILE_PATH:-/etc/openclaw-backup.env}"
|
| 23 |
OPENCLAW_STDOUT_LOG_PATH="${OPENCLAW_STDOUT_LOG_PATH:-/var/log/openclaw/gateway.stdout.log}"
|
| 24 |
OPENCLAW_STDERR_LOG_PATH="${OPENCLAW_STDERR_LOG_PATH:-/var/log/openclaw/gateway.stderr.log}"
|
|
|
|
| 18 |
OPENCLAW_LLM_API_KEY_ENV="${OPENCLAW_LLM_API_KEY_ENV:-OPENCLAW_LLM_API_KEY}"
|
| 19 |
OPENCLAW_BACKUP_ENABLED="${OPENCLAW_BACKUP_ENABLED:-true}"
|
| 20 |
OPENCLAW_BACKUP_CRON="${OPENCLAW_BACKUP_CRON:-*/30 * * * *}"
|
| 21 |
+
OPENCLAW_BACKUP_SOURCE_DIR="${OPENCLAW_BACKUP_SOURCE_DIR:-${OPENCLAW_STATE_DIR}}"
|
| 22 |
OPENCLAW_BACKUP_ENV_FILE_PATH="${OPENCLAW_BACKUP_ENV_FILE_PATH:-/etc/openclaw-backup.env}"
|
| 23 |
OPENCLAW_STDOUT_LOG_PATH="${OPENCLAW_STDOUT_LOG_PATH:-/var/log/openclaw/gateway.stdout.log}"
|
| 24 |
OPENCLAW_STDERR_LOG_PATH="${OPENCLAW_STDERR_LOG_PATH:-/var/log/openclaw/gateway.stderr.log}"
|
tests/test_backup.py
CHANGED
|
@@ -30,7 +30,7 @@ class FakeApi:
|
|
| 30 |
|
| 31 |
|
| 32 |
class BackupTests(unittest.TestCase):
|
| 33 |
-
def
|
| 34 |
env = {
|
| 35 |
"OPENCLAW_BACKUP_DATASET_REPO": "user/demo-backup",
|
| 36 |
}
|
|
@@ -38,9 +38,25 @@ class BackupTests(unittest.TestCase):
|
|
| 38 |
config = BackupConfig.from_env()
|
| 39 |
|
| 40 |
self.assertEqual(config.state_dir, Path("/root/.openclaw").resolve())
|
| 41 |
-
self.assertEqual(config.backup_source_dir,
|
| 42 |
self.assertEqual(config.keep_count, 48)
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
def test_from_env_uses_explicit_backup_source_dir(self):
|
| 45 |
with tempfile.TemporaryDirectory() as tmp:
|
| 46 |
tmp_path = Path(tmp)
|
|
|
|
| 30 |
|
| 31 |
|
| 32 |
class BackupTests(unittest.TestCase):
|
| 33 |
+
def test_from_env_defaults_backup_source_to_state_dir(self):
|
| 34 |
env = {
|
| 35 |
"OPENCLAW_BACKUP_DATASET_REPO": "user/demo-backup",
|
| 36 |
}
|
|
|
|
| 38 |
config = BackupConfig.from_env()
|
| 39 |
|
| 40 |
self.assertEqual(config.state_dir, Path("/root/.openclaw").resolve())
|
| 41 |
+
self.assertEqual(config.backup_source_dir, config.state_dir)
|
| 42 |
self.assertEqual(config.keep_count, 48)
|
| 43 |
|
| 44 |
+
def test_from_env_defaults_backup_source_to_explicit_state_dir_when_unset(self):
|
| 45 |
+
with tempfile.TemporaryDirectory() as tmp:
|
| 46 |
+
tmp_path = Path(tmp)
|
| 47 |
+
state_dir = tmp_path / "state"
|
| 48 |
+
state_dir.mkdir(parents=True)
|
| 49 |
+
|
| 50 |
+
env = {
|
| 51 |
+
"OPENCLAW_BACKUP_DATASET_REPO": "user/demo-backup",
|
| 52 |
+
"OPENCLAW_STATE_DIR": str(state_dir),
|
| 53 |
+
}
|
| 54 |
+
with mock.patch.dict("os.environ", env, clear=True):
|
| 55 |
+
config = BackupConfig.from_env()
|
| 56 |
+
|
| 57 |
+
self.assertEqual(config.state_dir, state_dir.resolve())
|
| 58 |
+
self.assertEqual(config.backup_source_dir, state_dir.resolve())
|
| 59 |
+
|
| 60 |
def test_from_env_uses_explicit_backup_source_dir(self):
|
| 61 |
with tempfile.TemporaryDirectory() as tmp:
|
| 62 |
tmp_path = Path(tmp)
|
tests/test_runtime_user_defaults.py
CHANGED
|
@@ -24,6 +24,7 @@ class RuntimeUserDefaultsTests(unittest.TestCase):
|
|
| 24 |
self.assertIn("ENV OPENCLAW_HOME=/root", dockerfile)
|
| 25 |
self.assertIn("ENV OPENCLAW_USER=root", dockerfile)
|
| 26 |
self.assertIn("ENV OPENCLAW_GROUP=root", dockerfile)
|
|
|
|
| 27 |
self.assertIn('OPENCLAW_BIN="$(command -v openclaw || true)"', dockerfile)
|
| 28 |
self.assertIn('if [[ -z "$OPENCLAW_BIN" ]] && [[ -x /root/.npm-global/bin/openclaw ]]; then', dockerfile)
|
| 29 |
self.assertIn('elif [[ -z "$OPENCLAW_BIN" ]] && [[ -x /root/.local/bin/openclaw ]]; then', dockerfile)
|
|
@@ -42,6 +43,7 @@ class RuntimeUserDefaultsTests(unittest.TestCase):
|
|
| 42 |
self.assertIn('OPENCLAW_USER="${OPENCLAW_USER:-root}"', entrypoint)
|
| 43 |
self.assertIn('OPENCLAW_GROUP="${OPENCLAW_GROUP:-root}"', entrypoint)
|
| 44 |
self.assertIn('OPENCLAW_HOME="${OPENCLAW_HOME:-/root}"', entrypoint)
|
|
|
|
| 45 |
|
| 46 |
def test_backup_cron_uses_openclaw_user(self):
|
| 47 |
entrypoint = self._entrypoint_path().read_text(encoding="utf-8")
|
|
|
|
| 24 |
self.assertIn("ENV OPENCLAW_HOME=/root", dockerfile)
|
| 25 |
self.assertIn("ENV OPENCLAW_USER=root", dockerfile)
|
| 26 |
self.assertIn("ENV OPENCLAW_GROUP=root", dockerfile)
|
| 27 |
+
self.assertIn("ENV OPENCLAW_BACKUP_SOURCE_DIR=/root/.openclaw", dockerfile)
|
| 28 |
self.assertIn('OPENCLAW_BIN="$(command -v openclaw || true)"', dockerfile)
|
| 29 |
self.assertIn('if [[ -z "$OPENCLAW_BIN" ]] && [[ -x /root/.npm-global/bin/openclaw ]]; then', dockerfile)
|
| 30 |
self.assertIn('elif [[ -z "$OPENCLAW_BIN" ]] && [[ -x /root/.local/bin/openclaw ]]; then', dockerfile)
|
|
|
|
| 43 |
self.assertIn('OPENCLAW_USER="${OPENCLAW_USER:-root}"', entrypoint)
|
| 44 |
self.assertIn('OPENCLAW_GROUP="${OPENCLAW_GROUP:-root}"', entrypoint)
|
| 45 |
self.assertIn('OPENCLAW_HOME="${OPENCLAW_HOME:-/root}"', entrypoint)
|
| 46 |
+
self.assertIn('OPENCLAW_BACKUP_SOURCE_DIR="${OPENCLAW_BACKUP_SOURCE_DIR:-${OPENCLAW_STATE_DIR}}"', entrypoint)
|
| 47 |
|
| 48 |
def test_backup_cron_uses_openclaw_user(self):
|
| 49 |
entrypoint = self._entrypoint_path().read_text(encoding="utf-8")
|