pix-cli 0.1.2__tar.gz → 0.1.3__tar.gz
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.
- {pix_cli-0.1.2 → pix_cli-0.1.3}/AGENTS.md +4 -4
- {pix_cli-0.1.2 → pix_cli-0.1.3}/PKG-INFO +20 -20
- {pix_cli-0.1.2 → pix_cli-0.1.3}/README.md +18 -18
- {pix_cli-0.1.2 → pix_cli-0.1.3}/README.zh-CN.md +18 -18
- {pix_cli-0.1.2 → pix_cli-0.1.3}/docs/architecture.md +21 -21
- {pix_cli-0.1.2 → pix_cli-0.1.3}/docs/decisions.md +17 -9
- {pix_cli-0.1.2 → pix_cli-0.1.3}/docs/development.md +9 -9
- {pix_cli-0.1.2 → pix_cli-0.1.3}/docs/roadmap.md +9 -9
- {pix_cli-0.1.2 → pix_cli-0.1.3}/pyproject.toml +2 -2
- {pix_cli-0.1.2 → pix_cli-0.1.3}/src/pix/__init__.py +1 -1
- {pix_cli-0.1.2 → pix_cli-0.1.3}/src/pix/cli.py +11 -11
- {pix_cli-0.1.2 → pix_cli-0.1.3}/src/pix/config.py +2 -2
- {pix_cli-0.1.2 → pix_cli-0.1.3}/src/pix/default_config.toml +1 -1
- {pix_cli-0.1.2 → pix_cli-0.1.3}/src/pix/sync.py +3 -3
- {pix_cli-0.1.2 → pix_cli-0.1.3}/tests/test_cli.py +18 -18
- {pix_cli-0.1.2 → pix_cli-0.1.3}/uv.lock +1 -1
- {pix_cli-0.1.2 → pix_cli-0.1.3}/.github/workflows/ci.yml +0 -0
- {pix_cli-0.1.2 → pix_cli-0.1.3}/.github/workflows/publish.yml +0 -0
- {pix_cli-0.1.2 → pix_cli-0.1.3}/.gitignore +0 -0
- {pix_cli-0.1.2 → pix_cli-0.1.3}/.python-version +0 -0
- {pix_cli-0.1.2 → pix_cli-0.1.3}/LICENSE +0 -0
- {pix_cli-0.1.2 → pix_cli-0.1.3}/src/pix/doctor.py +0 -0
- {pix_cli-0.1.2 → pix_cli-0.1.3}/src/pix/logger.py +0 -0
- {pix_cli-0.1.2 → pix_cli-0.1.3}/src/pix/py.typed +0 -0
- {pix_cli-0.1.2 → pix_cli-0.1.3}/src/pix/rclone.py +0 -0
- {pix_cli-0.1.2 → pix_cli-0.1.3}/src/pix/shrink.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AGENTS.md
|
|
2
2
|
|
|
3
|
-
`pix` 是批量图片压缩(AVIF)与 rclone 图床同步 CLI。已实现 `shrink` / `
|
|
3
|
+
`pix` 是批量图片压缩(AVIF)与 rclone 图床同步 CLI。已实现 `shrink` / `mirror` / `publish` / `doctor` / `config init`;行为规格见 `docs/architecture.md`。
|
|
4
4
|
|
|
5
5
|
## 文档
|
|
6
6
|
|
|
@@ -31,9 +31,9 @@ uv build
|
|
|
31
31
|
- `pyvips` 只在 `shrink` / `doctor` 内延迟导入。
|
|
32
32
|
- 不通过 shell 调用 `vips` 或 `rclone`。
|
|
33
33
|
- `-q` 是 quality,`--quiet` 是静默。
|
|
34
|
-
- 退出码 0/1/2,日志走 stderr;`
|
|
35
|
-
- `
|
|
36
|
-
- `--max-delete` 默认 10(删除百分比,rclone bisync 语义);`
|
|
34
|
+
- 退出码 0/1/2,日志走 stderr;`mirror` / `publish` 透传 rclone 退出码。
|
|
35
|
+
- `mirror` / `publish` 使用 `rclone bisync`;首次运行必须显式 `--resync`,不要自动加。它们是双向镜像,不是单向备份。
|
|
36
|
+
- `--max-delete` 默认 10(删除百分比,rclone bisync 语义);`mirror` / `publish` 源为空时默认拒绝,`--allow-empty` 加 `--resync` 才放行。
|
|
37
37
|
- 修改排除项后必须提示用户先 `--resync`;`--exclude` 是配置在前、命令行追加并去重。
|
|
38
38
|
- 增量状态固定在 `OUT/.pix/state.json`,每 200 个成功文件刷新并在 `finally` 落盘;`publish` 必须排除 `.pix/**`。
|
|
39
39
|
- `shrink` 必须拒绝目标冲突,`--prune` 必须拒绝空源,非 dry-run 必须持有 `OUT/.pix/lock`。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pix-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Batch AVIF shrink and image-host sync CLI
|
|
5
5
|
Project-URL: Homepage, https://github.com/uzvg/pix
|
|
6
6
|
Project-URL: Repository, https://github.com/uzvg/pix
|
|
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.14
|
|
23
23
|
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
|
|
24
|
-
Classifier: Topic :: System :: Archiving ::
|
|
24
|
+
Classifier: Topic :: System :: Archiving :: Mirroring
|
|
25
25
|
Requires-Python: >=3.11
|
|
26
26
|
Requires-Dist: pyvips[binary]>=3.2
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
@@ -41,7 +41,7 @@ Description-Content-Type: text/markdown
|
|
|
41
41
|
| Command | Purpose |
|
|
42
42
|
|---|---|
|
|
43
43
|
| `pix shrink` | Recursively compress images to AVIF, re-encoding only changed files |
|
|
44
|
-
| `pix
|
|
44
|
+
| `pix mirror` | Bidirectionally mirror an original photo directory to a cloud drive |
|
|
45
45
|
| `pix publish` | Bidirectionally mirror AVIF output to an image host; optionally emit public URLs |
|
|
46
46
|
| `pix doctor` | Check Python, libvips, rclone and configuration |
|
|
47
47
|
| `pix config init` | Write a commented default configuration file |
|
|
@@ -58,25 +58,25 @@ Description-Content-Type: text/markdown
|
|
|
58
58
|
photos/ ── shrink (pyvips) ──> out/*.avif + out/.pix/state.json
|
|
59
59
|
│
|
|
60
60
|
└── publish (rclone bisync) ──> r2:images
|
|
61
|
-
photos/ ──
|
|
61
|
+
photos/ ── mirror (rclone bisync) ────────────────────────────────> onedrive:photos
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
| Command | What actually runs | Who does the work |
|
|
65
65
|
|---|---|---|
|
|
66
66
|
| `shrink` | pyvips thumbnail + AVIF encode | pix |
|
|
67
|
-
| `
|
|
67
|
+
| `mirror` | `rclone bisync SRC REMOTE` | pix composes arguments, rclone executes |
|
|
68
68
|
| `publish` | `rclone bisync OUT REMOTE`, excluding `.pix/**` | pix + rclone |
|
|
69
69
|
| `doctor` / `config init` | Dependency checks / config scaffold | pix |
|
|
70
70
|
|
|
71
|
-
**Sync is rclone.** `
|
|
71
|
+
**Sync is rclone.** `mirror` and `publish` only compose `rclone bisync` arguments; rclone handles authentication, transfers and conflict handling. Configure remotes with `rclone config` before using them. `pix` never reads or writes `rclone.conf`.
|
|
72
72
|
|
|
73
|
-
`
|
|
73
|
+
`mirror` / `publish` are **bidirectional mirrors**, not one-way copies. Deletions propagate in both directions.
|
|
74
74
|
|
|
75
75
|
## Requirements
|
|
76
76
|
|
|
77
77
|
- Python 3.11+
|
|
78
78
|
- [uv](https://docs.astral.sh/uv/)
|
|
79
|
-
- rclone >= 1.66 for `
|
|
79
|
+
- rclone >= 1.66 for `mirror` / `publish`
|
|
80
80
|
- At least one remote already configured in rclone
|
|
81
81
|
|
|
82
82
|
`pyvips[binary]` bundles libvips, so no separate vips installation is required.
|
|
@@ -96,7 +96,7 @@ Linux installs `~/.local/bin/pix`; Windows installs `%USERPROFILE%\.local\bin\pi
|
|
|
96
96
|
|
|
97
97
|
## Configure rclone
|
|
98
98
|
|
|
99
|
-
Only needed for `
|
|
99
|
+
Only needed for `mirror` / `publish`:
|
|
100
100
|
|
|
101
101
|
```bash
|
|
102
102
|
rclone config # create a remote, for example onedrive or r2
|
|
@@ -129,12 +129,12 @@ Common keys:
|
|
|
129
129
|
| | `effort` | `5` | libvips effort, 0–9 |
|
|
130
130
|
| | `min_size` | `0` | Skip sources smaller than this; accepts `500K`, `1M`, `2G` |
|
|
131
131
|
| | `include` | `jpg`, `jpeg`, `png`, `webp` | Extensions to scan, without dots |
|
|
132
|
-
| `[
|
|
132
|
+
| `[mirror]` | `exclude` | `[]` | rclone exclude globs; command-line values are appended |
|
|
133
133
|
| | `max_delete` | `10` | Delete limit as a percentage; `-1` disables, `0` forbids |
|
|
134
134
|
| `[publish]` | `base_url` | `""` | Public URL prefix; must start with `http://` or `https://` |
|
|
135
135
|
| | `urls` | `false` | Emit a JSON URL manifest |
|
|
136
|
-
| | `exclude` | `[]` | Same as
|
|
137
|
-
| | `max_delete` | `10` | Same as
|
|
136
|
+
| | `exclude` | `[]` | Same as mirror |
|
|
137
|
+
| | `max_delete` | `10` | Same as mirror |
|
|
138
138
|
| `[rclone]` | `binary` | `rclone` | rclone executable name or path |
|
|
139
139
|
|
|
140
140
|
Precedence is **command line > pix config > built-in defaults**. `--exclude` is appended to config excludes rather than replacing them. `-q` means quality; silence is `--quiet` (no short form).
|
|
@@ -156,11 +156,11 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
|
|
|
156
156
|
|
|
157
157
|
The first run encodes everything; later runs skip unchanged files. State lives in `OUT/.pix/state.json`. Changing `width`, `quality` or `effort` triggers a full re-encode. `--dry-run` only prints the plan.
|
|
158
158
|
|
|
159
|
-
3.
|
|
159
|
+
3. Mirror originals:
|
|
160
160
|
|
|
161
161
|
```bash
|
|
162
|
-
pix
|
|
163
|
-
pix
|
|
162
|
+
pix mirror -s ~/Pictures -d onedrive:photos --dry-run
|
|
163
|
+
pix mirror -s ~/Pictures -d onedrive:photos --resync
|
|
164
164
|
```
|
|
165
165
|
|
|
166
166
|
`--resync` is required on the first run. Always preview with `--dry-run` first.
|
|
@@ -178,7 +178,7 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
|
|
|
178
178
|
|
|
179
179
|
```bash
|
|
180
180
|
pix shrink -s ~/Pictures -d ~/Pictures-avif
|
|
181
|
-
pix
|
|
181
|
+
pix mirror -s ~/Pictures -d onedrive:photos
|
|
182
182
|
pix publish -s ~/Pictures-avif -d r2:images --urls
|
|
183
183
|
```
|
|
184
184
|
|
|
@@ -191,8 +191,8 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
|
|
|
191
191
|
| Preview compression | `pix shrink -s SRC -d OUT --dry-run` |
|
|
192
192
|
| Re-encode everything | `pix shrink -s SRC -d OUT --force` |
|
|
193
193
|
| Remove outputs whose source is gone | `pix shrink -s SRC -d OUT --prune` |
|
|
194
|
-
| Preview a sync | `pix
|
|
195
|
-
| First run, or after changing excludes | `pix
|
|
194
|
+
| Preview a sync | `pix mirror -s SRC -d REMOTE --dry-run` |
|
|
195
|
+
| First run, or after changing excludes | `pix mirror -s SRC -d REMOTE --resync` |
|
|
196
196
|
| Emit a URL manifest | `pix publish -s OUT -d REMOTE --base-url https://img.example.com --urls` |
|
|
197
197
|
| Diagnose the environment | `pix doctor` |
|
|
198
198
|
| Generate a config file | `pix config init` |
|
|
@@ -204,7 +204,7 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
|
|
|
204
204
|
| Deletions on either side | Propagated in both directions; accidental deletion is irreversible | Preview with `--dry-run`; default `--max-delete 10` (percentage) |
|
|
205
205
|
| First sync | rclone has no baseline and errors | Add `--resync` explicitly |
|
|
206
206
|
| Excludes changed | The old baseline may treat excluded files as deletions | Run with `--resync` again after changing excludes |
|
|
207
|
-
| Empty source | `
|
|
207
|
+
| Empty source | `mirror` / `publish` refuse; `shrink --prune` also refuses | Use `--allow-empty` (plus `--resync` for sync) only when intentional |
|
|
208
208
|
| `a.jpg` and `a.jpeg` both exist | Both map to the same output | Detected before encoding; exits 2 |
|
|
209
209
|
| Two concurrent shrinks | They would write the same state and outputs | `OUT/.pix/lock` blocks the second process |
|
|
210
210
|
| State lost or corrupt | Warning, then a full rebuild | Only causes re-encoding; outputs stay valid |
|
|
@@ -214,7 +214,7 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
|
|
|
214
214
|
- Logs and progress go to stderr.
|
|
215
215
|
- stdout carries `doctor` reports, `--dry-run` plans and `--urls` JSON.
|
|
216
216
|
- Exit codes: `0` success, `1` runtime failure, `2` argument or configuration error.
|
|
217
|
-
- `
|
|
217
|
+
- `mirror` / `publish` return rclone's exit code unchanged.
|
|
218
218
|
|
|
219
219
|
URL manifest example:
|
|
220
220
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
| Command | Purpose |
|
|
14
14
|
|---|---|
|
|
15
15
|
| `pix shrink` | Recursively compress images to AVIF, re-encoding only changed files |
|
|
16
|
-
| `pix
|
|
16
|
+
| `pix mirror` | Bidirectionally mirror an original photo directory to a cloud drive |
|
|
17
17
|
| `pix publish` | Bidirectionally mirror AVIF output to an image host; optionally emit public URLs |
|
|
18
18
|
| `pix doctor` | Check Python, libvips, rclone and configuration |
|
|
19
19
|
| `pix config init` | Write a commented default configuration file |
|
|
@@ -30,25 +30,25 @@
|
|
|
30
30
|
photos/ ── shrink (pyvips) ──> out/*.avif + out/.pix/state.json
|
|
31
31
|
│
|
|
32
32
|
└── publish (rclone bisync) ──> r2:images
|
|
33
|
-
photos/ ──
|
|
33
|
+
photos/ ── mirror (rclone bisync) ────────────────────────────────> onedrive:photos
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
| Command | What actually runs | Who does the work |
|
|
37
37
|
|---|---|---|
|
|
38
38
|
| `shrink` | pyvips thumbnail + AVIF encode | pix |
|
|
39
|
-
| `
|
|
39
|
+
| `mirror` | `rclone bisync SRC REMOTE` | pix composes arguments, rclone executes |
|
|
40
40
|
| `publish` | `rclone bisync OUT REMOTE`, excluding `.pix/**` | pix + rclone |
|
|
41
41
|
| `doctor` / `config init` | Dependency checks / config scaffold | pix |
|
|
42
42
|
|
|
43
|
-
**Sync is rclone.** `
|
|
43
|
+
**Sync is rclone.** `mirror` and `publish` only compose `rclone bisync` arguments; rclone handles authentication, transfers and conflict handling. Configure remotes with `rclone config` before using them. `pix` never reads or writes `rclone.conf`.
|
|
44
44
|
|
|
45
|
-
`
|
|
45
|
+
`mirror` / `publish` are **bidirectional mirrors**, not one-way copies. Deletions propagate in both directions.
|
|
46
46
|
|
|
47
47
|
## Requirements
|
|
48
48
|
|
|
49
49
|
- Python 3.11+
|
|
50
50
|
- [uv](https://docs.astral.sh/uv/)
|
|
51
|
-
- rclone >= 1.66 for `
|
|
51
|
+
- rclone >= 1.66 for `mirror` / `publish`
|
|
52
52
|
- At least one remote already configured in rclone
|
|
53
53
|
|
|
54
54
|
`pyvips[binary]` bundles libvips, so no separate vips installation is required.
|
|
@@ -68,7 +68,7 @@ Linux installs `~/.local/bin/pix`; Windows installs `%USERPROFILE%\.local\bin\pi
|
|
|
68
68
|
|
|
69
69
|
## Configure rclone
|
|
70
70
|
|
|
71
|
-
Only needed for `
|
|
71
|
+
Only needed for `mirror` / `publish`:
|
|
72
72
|
|
|
73
73
|
```bash
|
|
74
74
|
rclone config # create a remote, for example onedrive or r2
|
|
@@ -101,12 +101,12 @@ Common keys:
|
|
|
101
101
|
| | `effort` | `5` | libvips effort, 0–9 |
|
|
102
102
|
| | `min_size` | `0` | Skip sources smaller than this; accepts `500K`, `1M`, `2G` |
|
|
103
103
|
| | `include` | `jpg`, `jpeg`, `png`, `webp` | Extensions to scan, without dots |
|
|
104
|
-
| `[
|
|
104
|
+
| `[mirror]` | `exclude` | `[]` | rclone exclude globs; command-line values are appended |
|
|
105
105
|
| | `max_delete` | `10` | Delete limit as a percentage; `-1` disables, `0` forbids |
|
|
106
106
|
| `[publish]` | `base_url` | `""` | Public URL prefix; must start with `http://` or `https://` |
|
|
107
107
|
| | `urls` | `false` | Emit a JSON URL manifest |
|
|
108
|
-
| | `exclude` | `[]` | Same as
|
|
109
|
-
| | `max_delete` | `10` | Same as
|
|
108
|
+
| | `exclude` | `[]` | Same as mirror |
|
|
109
|
+
| | `max_delete` | `10` | Same as mirror |
|
|
110
110
|
| `[rclone]` | `binary` | `rclone` | rclone executable name or path |
|
|
111
111
|
|
|
112
112
|
Precedence is **command line > pix config > built-in defaults**. `--exclude` is appended to config excludes rather than replacing them. `-q` means quality; silence is `--quiet` (no short form).
|
|
@@ -128,11 +128,11 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
|
|
|
128
128
|
|
|
129
129
|
The first run encodes everything; later runs skip unchanged files. State lives in `OUT/.pix/state.json`. Changing `width`, `quality` or `effort` triggers a full re-encode. `--dry-run` only prints the plan.
|
|
130
130
|
|
|
131
|
-
3.
|
|
131
|
+
3. Mirror originals:
|
|
132
132
|
|
|
133
133
|
```bash
|
|
134
|
-
pix
|
|
135
|
-
pix
|
|
134
|
+
pix mirror -s ~/Pictures -d onedrive:photos --dry-run
|
|
135
|
+
pix mirror -s ~/Pictures -d onedrive:photos --resync
|
|
136
136
|
```
|
|
137
137
|
|
|
138
138
|
`--resync` is required on the first run. Always preview with `--dry-run` first.
|
|
@@ -150,7 +150,7 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
|
|
|
150
150
|
|
|
151
151
|
```bash
|
|
152
152
|
pix shrink -s ~/Pictures -d ~/Pictures-avif
|
|
153
|
-
pix
|
|
153
|
+
pix mirror -s ~/Pictures -d onedrive:photos
|
|
154
154
|
pix publish -s ~/Pictures-avif -d r2:images --urls
|
|
155
155
|
```
|
|
156
156
|
|
|
@@ -163,8 +163,8 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
|
|
|
163
163
|
| Preview compression | `pix shrink -s SRC -d OUT --dry-run` |
|
|
164
164
|
| Re-encode everything | `pix shrink -s SRC -d OUT --force` |
|
|
165
165
|
| Remove outputs whose source is gone | `pix shrink -s SRC -d OUT --prune` |
|
|
166
|
-
| Preview a sync | `pix
|
|
167
|
-
| First run, or after changing excludes | `pix
|
|
166
|
+
| Preview a sync | `pix mirror -s SRC -d REMOTE --dry-run` |
|
|
167
|
+
| First run, or after changing excludes | `pix mirror -s SRC -d REMOTE --resync` |
|
|
168
168
|
| Emit a URL manifest | `pix publish -s OUT -d REMOTE --base-url https://img.example.com --urls` |
|
|
169
169
|
| Diagnose the environment | `pix doctor` |
|
|
170
170
|
| Generate a config file | `pix config init` |
|
|
@@ -176,7 +176,7 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
|
|
|
176
176
|
| Deletions on either side | Propagated in both directions; accidental deletion is irreversible | Preview with `--dry-run`; default `--max-delete 10` (percentage) |
|
|
177
177
|
| First sync | rclone has no baseline and errors | Add `--resync` explicitly |
|
|
178
178
|
| Excludes changed | The old baseline may treat excluded files as deletions | Run with `--resync` again after changing excludes |
|
|
179
|
-
| Empty source | `
|
|
179
|
+
| Empty source | `mirror` / `publish` refuse; `shrink --prune` also refuses | Use `--allow-empty` (plus `--resync` for sync) only when intentional |
|
|
180
180
|
| `a.jpg` and `a.jpeg` both exist | Both map to the same output | Detected before encoding; exits 2 |
|
|
181
181
|
| Two concurrent shrinks | They would write the same state and outputs | `OUT/.pix/lock` blocks the second process |
|
|
182
182
|
| State lost or corrupt | Warning, then a full rebuild | Only causes re-encoding; outputs stay valid |
|
|
@@ -186,7 +186,7 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
|
|
|
186
186
|
- Logs and progress go to stderr.
|
|
187
187
|
- stdout carries `doctor` reports, `--dry-run` plans and `--urls` JSON.
|
|
188
188
|
- Exit codes: `0` success, `1` runtime failure, `2` argument or configuration error.
|
|
189
|
-
- `
|
|
189
|
+
- `mirror` / `publish` return rclone's exit code unchanged.
|
|
190
190
|
|
|
191
191
|
URL manifest example:
|
|
192
192
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
| 命令 | 作用 |
|
|
14
14
|
|---|---|
|
|
15
15
|
| `pix shrink` | 递归压缩图片为 AVIF,只重新编码变化的文件 |
|
|
16
|
-
| `pix
|
|
16
|
+
| `pix mirror` | 把原图目录与云盘做双向镜像 |
|
|
17
17
|
| `pix publish` | 把 AVIF 输出目录与图床做双向镜像,可选输出公开 URL |
|
|
18
18
|
| `pix doctor` | 检查 Python、libvips、rclone 与配置 |
|
|
19
19
|
| `pix config init` | 生成带注释的默认配置文件 |
|
|
@@ -30,25 +30,25 @@
|
|
|
30
30
|
photos/ ── shrink (pyvips) ──> out/*.avif + out/.pix/state.json
|
|
31
31
|
│
|
|
32
32
|
└── publish (rclone bisync) ──> r2:images
|
|
33
|
-
photos/ ──
|
|
33
|
+
photos/ ── mirror (rclone bisync) ────────────────────────────────> onedrive:photos
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
| 命令 | 实际执行的内容 | 谁负责 |
|
|
37
37
|
|---|---|---|
|
|
38
38
|
| `shrink` | pyvips 缩放并编码 AVIF | pix |
|
|
39
|
-
| `
|
|
39
|
+
| `mirror` | `rclone bisync SRC REMOTE` | pix 拼参数,rclone 执行 |
|
|
40
40
|
| `publish` | `rclone bisync OUT REMOTE`,排除 `.pix/**` | pix + rclone |
|
|
41
41
|
| `doctor` / `config init` | 依赖检查 / 配置脚手架 | pix |
|
|
42
42
|
|
|
43
|
-
**同步由 rclone 执行。** `
|
|
43
|
+
**同步由 rclone 执行。** `mirror` / `publish` 只负责拼装 `rclone bisync` 参数;认证、传输和冲突处理都由 rclone 完成。使用前请先用 `rclone config` 配好 remote。`pix` 不读取也不写入 `rclone.conf`。
|
|
44
44
|
|
|
45
|
-
`
|
|
45
|
+
`mirror` / `publish` 是**双向镜像**,不是单向备份。删除会双向传播。
|
|
46
46
|
|
|
47
47
|
## 前置条件
|
|
48
48
|
|
|
49
49
|
- Python 3.11+
|
|
50
50
|
- [uv](https://docs.astral.sh/uv/)
|
|
51
|
-
- `
|
|
51
|
+
- `mirror` / `publish` 需要 rclone >= 1.66
|
|
52
52
|
- 已在 rclone 中配置好至少一个 remote
|
|
53
53
|
|
|
54
54
|
`pyvips[binary]` 自带 libvips,不需要单独安装 vips。
|
|
@@ -68,7 +68,7 @@ Linux 安装到 `~/.local/bin/pix`,Windows 为 `%USERPROFILE%\.local\bin\pix.e
|
|
|
68
68
|
|
|
69
69
|
## 配置 rclone
|
|
70
70
|
|
|
71
|
-
仅 `
|
|
71
|
+
仅 `mirror` / `publish` 需要:
|
|
72
72
|
|
|
73
73
|
```bash
|
|
74
74
|
rclone config # 新建 remote,例如 onedrive 或 r2
|
|
@@ -101,12 +101,12 @@ pix config init
|
|
|
101
101
|
| | `effort` | `5` | libvips effort,0–9 |
|
|
102
102
|
| | `min_size` | `0` | 小于该值的源文件跳过;支持 `500K`、`1M`、`2G` |
|
|
103
103
|
| | `include` | `jpg`、`jpeg`、`png`、`webp` | 扫描扩展名,不带点 |
|
|
104
|
-
| `[
|
|
104
|
+
| `[mirror]` | `exclude` | `[]` | rclone 排除 glob;命令行项会追加 |
|
|
105
105
|
| | `max_delete` | `10` | 删除百分比上限;`-1` 不限制,`0` 禁止删除 |
|
|
106
106
|
| `[publish]` | `base_url` | `""` | 公开 URL 前缀,必须以 `http://` 或 `https://` 开头 |
|
|
107
107
|
| | `urls` | `false` | 是否输出 JSON URL 清单 |
|
|
108
|
-
| | `exclude` | `[]` | 同 `
|
|
109
|
-
| | `max_delete` | `10` | 同 `
|
|
108
|
+
| | `exclude` | `[]` | 同 `mirror` |
|
|
109
|
+
| | `max_delete` | `10` | 同 `mirror` |
|
|
110
110
|
| `[rclone]` | `binary` | `rclone` | rclone 可执行文件名或路径 |
|
|
111
111
|
|
|
112
112
|
优先级为**命令行 > pix 配置 > 内置默认**。`--exclude` 是追加而非覆盖配置项。`-q` 表示 quality,静默模式用 `--quiet`(无短选项)。
|
|
@@ -128,11 +128,11 @@ pix config init
|
|
|
128
128
|
|
|
129
129
|
首次全量编码,之后跳过未变化的文件。状态保存在 `OUT/.pix/state.json`。修改 `width`、`quality` 或 `effort` 会触发全量重编码。`--dry-run` 只打印计划。
|
|
130
130
|
|
|
131
|
-
3.
|
|
131
|
+
3. 镜像原图:
|
|
132
132
|
|
|
133
133
|
```bash
|
|
134
|
-
pix
|
|
135
|
-
pix
|
|
134
|
+
pix mirror -s ~/Pictures -d onedrive:photos --dry-run
|
|
135
|
+
pix mirror -s ~/Pictures -d onedrive:photos --resync
|
|
136
136
|
```
|
|
137
137
|
|
|
138
138
|
首次运行必须显式加 `--resync`。建议先跑 `--dry-run` 预览。
|
|
@@ -150,7 +150,7 @@ pix config init
|
|
|
150
150
|
|
|
151
151
|
```bash
|
|
152
152
|
pix shrink -s ~/Pictures -d ~/Pictures-avif
|
|
153
|
-
pix
|
|
153
|
+
pix mirror -s ~/Pictures -d onedrive:photos
|
|
154
154
|
pix publish -s ~/Pictures-avif -d r2:images --urls
|
|
155
155
|
```
|
|
156
156
|
|
|
@@ -163,8 +163,8 @@ pix config init
|
|
|
163
163
|
| 预览压缩 | `pix shrink -s SRC -d OUT --dry-run` |
|
|
164
164
|
| 强制全部重压 | `pix shrink -s SRC -d OUT --force` |
|
|
165
165
|
| 删除源已消失的输出 | `pix shrink -s SRC -d OUT --prune` |
|
|
166
|
-
| 预览同步 | `pix
|
|
167
|
-
| 首次运行或修改排除项后 | `pix
|
|
166
|
+
| 预览同步 | `pix mirror -s SRC -d REMOTE --dry-run` |
|
|
167
|
+
| 首次运行或修改排除项后 | `pix mirror -s SRC -d REMOTE --resync` |
|
|
168
168
|
| 输出 URL 清单 | `pix publish -s OUT -d REMOTE --base-url https://img.example.com --urls` |
|
|
169
169
|
| 诊断环境 | `pix doctor` |
|
|
170
170
|
| 生成配置文件 | `pix config init` |
|
|
@@ -176,7 +176,7 @@ pix config init
|
|
|
176
176
|
| 任意一侧删除文件 | 删除双向传播,误删不可逆 | 先 `--dry-run`;默认 `--max-delete 10`(百分比) |
|
|
177
177
|
| 首次同步 | rclone 没有基线,直接报错 | 显式加 `--resync` |
|
|
178
178
|
| 修改排除项 | 旧基线可能把被排除的文件当成删除传播 | 修改后必须重新 `--resync` |
|
|
179
|
-
| 源目录为空 | `
|
|
179
|
+
| 源目录为空 | `mirror` / `publish` 默认拒绝;`shrink --prune` 同样拒绝 | 仅在确认要清空时用 `--allow-empty`(同步还需 `--resync`) |
|
|
180
180
|
| `a.jpg` 与 `a.jpeg` 同时存在 | 映射到同一输出 | 编码前检测并以 2 退出 |
|
|
181
181
|
| 同时运行两个 shrink | 会写同一份状态和输出 | `OUT/.pix/lock` 阻止第二个进程 |
|
|
182
182
|
| 状态文件丢失或损坏 | 告警后全量重建 | 只会重新编码,不会产生错误结果 |
|
|
@@ -186,7 +186,7 @@ pix config init
|
|
|
186
186
|
- 日志和进度输出到 stderr。
|
|
187
187
|
- stdout 用于 `doctor` 报告、`--dry-run` 计划和 `--urls` JSON。
|
|
188
188
|
- 退出码:`0` 成功,`1` 运行失败,`2` 参数或配置错误。
|
|
189
|
-
- `
|
|
189
|
+
- `mirror` / `publish` 原样返回 rclone 的退出码。
|
|
190
190
|
|
|
191
191
|
URL 清单示例:
|
|
192
192
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
`pix` 是独立的图床流水线 CLI:
|
|
8
8
|
|
|
9
9
|
- 把本地图片目录增量压缩为 AVIF。
|
|
10
|
-
-
|
|
10
|
+
- 把原始图片目录双向镜像到 OneDrive 等 rclone 兼容云盘。
|
|
11
11
|
- 把压缩结果双向同步到 Cloudflare R2 等 rclone 兼容图床。
|
|
12
12
|
|
|
13
13
|
`pix` 只处理用户显式传入的本地路径和 rclone 远端,不感知项目类型或版本控制状态。所有路径和远端都由命令行或用户级配置提供。
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
第一版做:
|
|
25
25
|
|
|
26
26
|
- `pix shrink`:递归扫描图片,按宽度等比缩放并输出 AVIF,带增量状态。
|
|
27
|
-
- `pix
|
|
27
|
+
- `pix mirror`:把原始图片目录与 rclone 远端双向同步。
|
|
28
28
|
- `pix publish`:把压缩目录与图床远端双向同步,可选输出公开 URL 清单。
|
|
29
29
|
- `pix doctor`:检查运行依赖、rclone bisync 能力和配置。
|
|
30
30
|
- `pix config init`:把带注释的默认配置写到默认路径,供用户修改。
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
- Python `>=3.11`。
|
|
46
46
|
- 仅标准库 + `pyvips[binary]`。
|
|
47
|
-
- 运行时依赖外部 `rclone >= 1.66`(bisync 稳定版),由 `
|
|
47
|
+
- 运行时依赖外部 `rclone >= 1.66`(bisync 稳定版),由 `mirror` / `publish` 调用。
|
|
48
48
|
|
|
49
49
|
```text
|
|
50
50
|
src/pix/
|
|
@@ -56,7 +56,7 @@ src/pix/
|
|
|
56
56
|
├── logger.py # stderr 日志
|
|
57
57
|
├── shrink.py # shrink 子命令
|
|
58
58
|
├── rclone.py # rclone 探测、版本检查与执行
|
|
59
|
-
├── sync.py #
|
|
59
|
+
├── sync.py # mirror / publish 子命令
|
|
60
60
|
└── doctor.py # doctor 子命令
|
|
61
61
|
tests/
|
|
62
62
|
└── test_cli.py
|
|
@@ -77,7 +77,7 @@ tests/
|
|
|
77
77
|
| `config` | `parse_size(value: str \| int) -> int` | 尺寸字符串转字节 |
|
|
78
78
|
| `config` | `init_config(args) -> int` | `config init` 子命令:写入默认配置 |
|
|
79
79
|
| `shrink` | `shrink(args, settings, log) -> int` | `shrink` 子命令 |
|
|
80
|
-
| `sync` | `
|
|
80
|
+
| `sync` | `mirror(args, settings, log) -> int` | `mirror` 子命令 |
|
|
81
81
|
| `sync` | `publish(args, settings, log) -> int` | `publish` 子命令 |
|
|
82
82
|
| `doctor` | `doctor(args, settings, log) -> int` | `doctor` 子命令 |
|
|
83
83
|
| `rclone` | `run_rclone(cmd: list[str]) -> int` | 调用 rclone 并返回其退出码 |
|
|
@@ -95,13 +95,13 @@ pix.rclone ──> subprocess
|
|
|
95
95
|
## 3. CLI 契约
|
|
96
96
|
|
|
97
97
|
```bash
|
|
98
|
-
pix [--config PATH] [--version] {shrink,
|
|
98
|
+
pix [--config PATH] [--version] {shrink,mirror,publish,doctor,config}
|
|
99
99
|
|
|
100
100
|
pix shrink -s SRC -d OUT
|
|
101
101
|
[-w 1920] [-q 85] [-e 5] [-m 0]
|
|
102
102
|
[--force] [--prune] [--allow-empty] [--dry-run] [-v|--quiet]
|
|
103
103
|
|
|
104
|
-
pix
|
|
104
|
+
pix mirror -s SRC -d REMOTE
|
|
105
105
|
[--exclude GLOB ...] [--max-delete PERCENT] [--allow-empty] [--resync]
|
|
106
106
|
[--dry-run] [-v|--quiet]
|
|
107
107
|
|
|
@@ -120,15 +120,15 @@ pix config init [--force]
|
|
|
120
120
|
- `--config` 属于主 parser,写在子命令之前。
|
|
121
121
|
- `--version` 输出 `pix <version>` 后以 0 退出。
|
|
122
122
|
- `-s` 为源,`-d` 为目标。
|
|
123
|
-
- `shrink` 的 `-d` 是本地目录;`
|
|
123
|
+
- `shrink` 的 `-d` 是本地目录;`mirror` / `publish` 的 `-d` 是 rclone 路径,可以是 remote,也可以是本地目录。
|
|
124
124
|
- `-q` 只表示 quality;安静模式使用 `--quiet`,不提供短选项。
|
|
125
125
|
- `-m/--min-size` 表示“小于该大小的源文件跳过”,`0` 表示不限制;支持 `500K` / `1M` / `2G`,按 1024 进制。
|
|
126
126
|
- `-v` 与 `--quiet` 互斥。
|
|
127
|
-
- `--dry-run` 不产生本地写入;`
|
|
127
|
+
- `--dry-run` 不产生本地写入;`mirror` / `publish` 中原样传给 rclone。与 `shrink --prune` 同用时只列出待删除项。
|
|
128
128
|
- `--exclude` 可重复。配置中的排除项在前,命令行排除项追加在后,保留顺序并去重。
|
|
129
|
-
- `--max-delete` 只用于 `
|
|
130
|
-
- `--allow-empty` 只用于 `shrink --prune`、`
|
|
131
|
-
- `--resync` 只用于 `
|
|
129
|
+
- `--max-delete` 只用于 `mirror` / `publish`,按删除百分比限制(rclone bisync 语义,不是数量),默认 `10`(即 10%);`0` 禁止删除,`-1` 不限制,最大 `100`。超过比例时 rclone 拒绝运行。
|
|
130
|
+
- `--allow-empty` 只用于 `shrink --prune`、`mirror` 和 `publish`:允许源为空时继续执行破坏性操作。默认拒绝;`mirror` / `publish` 还必须同时带 `--resync`。
|
|
131
|
+
- `--resync` 只用于 `mirror` / `publish`,原样传给 rclone。
|
|
132
132
|
- `--force` 只用于 `shrink`。
|
|
133
133
|
- `--prune` 只用于 `shrink`,默认关闭。
|
|
134
134
|
- `--base-url` / `--urls` / `--no-urls` 只用于 `publish`;`--no-urls` 可临时关闭配置中的 `[publish].urls = true`。
|
|
@@ -160,7 +160,7 @@ effort = 5
|
|
|
160
160
|
min_size = 0
|
|
161
161
|
include = ["jpg", "jpeg", "png", "webp"]
|
|
162
162
|
|
|
163
|
-
[
|
|
163
|
+
[mirror]
|
|
164
164
|
exclude = []
|
|
165
165
|
max_delete = 10
|
|
166
166
|
|
|
@@ -266,9 +266,9 @@ binary = "rclone"
|
|
|
266
266
|
- 删除失败记录错误,最后返回 1。
|
|
267
267
|
- 默认关闭,避免误删。
|
|
268
268
|
|
|
269
|
-
## 6.
|
|
269
|
+
## 6. mirror 行为
|
|
270
270
|
|
|
271
|
-
`
|
|
271
|
+
`mirror` 把原始图片目录与远端做 **rclone bisync 双向同步**。它是双向镜像,不是单向备份。
|
|
272
272
|
|
|
273
273
|
命令拼装:
|
|
274
274
|
|
|
@@ -287,7 +287,7 @@ cmd.append("--progress" if verbose else "--quiet")
|
|
|
287
287
|
- 首次运行必须显式加 `--resync`。`pix` 不自动添加,避免两边都有内容时悄悄覆盖一侧。
|
|
288
288
|
- 不加 `--resync` 且 bisync 基线不存在时,rclone 会报错,`pix` 原样返回其退出码。
|
|
289
289
|
- `-v` 映射 `--progress`,默认映射 `--quiet`。
|
|
290
|
-
- `--exclude` 按配置在前、命令行在后合并,逐条传给 rclone;`pix`
|
|
290
|
+
- `--exclude` 按配置在前、命令行在后合并,逐条传给 rclone;`pix` 不内置任何镜像排除规则。
|
|
291
291
|
- **修改排除项后必须先 `--resync`**:rclone 要求过滤器变更后重建基线,否则被排除的文件可能被当作删除传播。先跑 `--dry-run`。
|
|
292
292
|
- `--max-delete` 默认 `10`,是删除百分比;一次运行要删除的比例超过 10% 时 rclone 会拒绝。确认要大量删除时显式调高,或传 `-1` 取消限制。
|
|
293
293
|
- 双向同步意味着删除会传播:本地删除会同步到远端,远端删除也会同步到本地。首次操作建议先 `--dry-run`。
|
|
@@ -298,11 +298,11 @@ cmd.append("--progress" if verbose else "--quiet")
|
|
|
298
298
|
|
|
299
299
|
## 7. publish 行为
|
|
300
300
|
|
|
301
|
-
`publish` 把 shrink 输出目录与图床远端做 **rclone bisync 双向同步**,参数与 `
|
|
301
|
+
`publish` 把 shrink 输出目录与图床远端做 **rclone bisync 双向同步**,参数与 `mirror` 相同,另加:
|
|
302
302
|
|
|
303
303
|
- `-s` 必须是已存在的本地目录,且至少包含一个 `.avif`。源为空时默认退出 2;只有显式 `--allow-empty` 且带 `--resync` 时才允许。
|
|
304
304
|
- 固定追加 `--exclude .pix/**`,防止状态文件被同步到图床。配置排除项随后,命令行排除项最后,去重。
|
|
305
|
-
- `--max-delete` 与 `
|
|
305
|
+
- `--max-delete` 与 `mirror` 相同,默认 `10`,按删除百分比计。
|
|
306
306
|
- `--base-url` 优先取命令行,其次取 `[publish].base_url`。值必须以 `http://` 或 `https://` 开头,否则退出 2。
|
|
307
307
|
- `--urls` / `--no-urls` 优先取命令行,其次取 `[publish].urls`,默认关闭;`--no-urls` 可临时关闭配置中的 `true`。
|
|
308
308
|
- 开启 URL 清单但没有可用的 `base_url` 时,在调用 rclone 前以退出码 2 拒绝。
|
|
@@ -318,7 +318,7 @@ cmd.append("--progress" if verbose else "--quiet")
|
|
|
318
318
|
- 生成清单时递归收集 OUT 下所有 `.avif`(大小写不敏感),路径段用 `urllib.parse.quote(..., safe="/")` 编码。
|
|
319
319
|
- `base_url` 末尾的 `/` 在拼接时去掉。
|
|
320
320
|
- `--dry-run` 不生成 URL 清单。
|
|
321
|
-
- 修改排除项后必须先 `--resync`,规则同 `
|
|
321
|
+
- 修改排除项后必须先 `--resync`,规则同 `mirror`。
|
|
322
322
|
- 双向同步同样会传播删除,首次操作建议先 `--dry-run`。
|
|
323
323
|
|
|
324
324
|
## 8. doctor
|
|
@@ -337,7 +337,7 @@ config /home/uzvg/.config/pix/config.toml (ok)
|
|
|
337
337
|
|
|
338
338
|
- 缺少 pyvips/libvips:打印安装提示(文案见 [development.md](development.md#7-缺少-libvips-时)),退出 1。
|
|
339
339
|
- 缺少 rclone:提示官方下载地址或 `winget search rclone`,退出 1。
|
|
340
|
-
- rclone 版本低于 1.66:提示升级,退出 1。`
|
|
340
|
+
- rclone 版本低于 1.66:提示升级,退出 1。`mirror` / `publish` 启动时也执行同样的版本检查,不依赖用户先跑 `doctor`。
|
|
341
341
|
- 配置文件解析失败:指出文件和错误位置,退出 2。
|
|
342
342
|
- 配置文件不存在:显示 `missing (using defaults)`,属于正常状态,退出 0。
|
|
343
343
|
- 多个问题同时存在时,配置错误(2)优先于依赖缺失(1)。
|
|
@@ -349,7 +349,7 @@ config /home/uzvg/.config/pix/config.toml (ok)
|
|
|
349
349
|
- `--quiet`:只输出错误。
|
|
350
350
|
- `doctor` 报告输出到 stdout;`--dry-run` 的计划文件列表和 `--urls` 的 JSON 输出到 stdout。
|
|
351
351
|
- 退出码:`0` 成功,`1` 运行失败,`2` 参数/配置错误。
|
|
352
|
-
- 例外:`
|
|
352
|
+
- 例外:`mirror` / `publish` 直接返回 rclone 的退出码,不归一化;调用方按非零即失败处理。
|
|
353
353
|
- 不引入 colorama、rich、click;仅标准库 + pyvips。
|
|
354
354
|
|
|
355
355
|
## 10. 跨平台约定
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
**决策**:`import pyvips` 只出现在 `shrink` / `doctor` 内部。
|
|
40
40
|
|
|
41
|
-
**原因**:`
|
|
41
|
+
**原因**:`mirror` / `publish` 不需要图像后端;缺少 libvips 时同步仍应可用。导入失败只在真正需要图像能力时提示。
|
|
42
42
|
|
|
43
43
|
## D7. 配置优先级:命令行 > 配置文件 > 内置默认
|
|
44
44
|
|
|
@@ -66,15 +66,15 @@
|
|
|
66
66
|
|
|
67
67
|
## D11. 使用 rclone bisync 双向同步
|
|
68
68
|
|
|
69
|
-
**决策**:`
|
|
69
|
+
**决策**:`mirror` / `publish` 都调用 `rclone bisync`,不使用单向 `copy` 或 `sync`。
|
|
70
70
|
|
|
71
71
|
**原因**:两边都可能新增、修改或删除文件,单向同步会让另一侧的改动被覆盖或永远无法回流。
|
|
72
72
|
|
|
73
|
-
**影响**:`
|
|
73
|
+
**影响**:`mirror` / `publish` 是双向镜像,不是单向备份;删除会双向传播,误删不可逆。用 `--dry-run` 预览、`--resync` 显式重建基线,并默认 `--max-delete 10`(rclone bisync 的删除百分比,`0` 禁止删除,`-1` 不限制)来控制风险;`pix` 不自动 resync。修改排除项后也必须先 `--resync`。
|
|
74
74
|
|
|
75
75
|
## D12. 不内置 rclone 排除规则
|
|
76
76
|
|
|
77
|
-
**决策**:`pix`
|
|
77
|
+
**决策**:`pix` 不硬编码镜像排除项;排除全部由 `--exclude` 传入。配置排除项在前,命令行排除项追加在后并去重。唯一例外是 `publish` 固定排除 `.pix/**`。
|
|
78
78
|
|
|
79
79
|
**原因**:排除项属于调用方的同步策略。`.pix/**` 是 `pix` 自己的状态文件,不属于用户内容,必须排除,否则会被传到图床。合并而不是覆盖,避免加一条规则时丢掉另一条。
|
|
80
80
|
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
|
|
87
87
|
## D14. `subprocess.run(list)`,退出码透传
|
|
88
88
|
|
|
89
|
-
**决策**:不经过 shell,`
|
|
89
|
+
**决策**:不经过 shell,`mirror` / `publish` 直接返回 rclone 的退出码。
|
|
90
90
|
|
|
91
91
|
**原因**:避免 shell 引号和 Windows 路径转义问题;透传退出码便于脚本判断 bisync 的具体失败原因。这是全局 `0/1/2` 约定的明确例外。
|
|
92
92
|
|
|
@@ -128,11 +128,11 @@
|
|
|
128
128
|
|
|
129
129
|
**影响**:首次运行没有状态,会全量编码一次;状态丢失只会导致重编码,不会产生错误结果。
|
|
130
130
|
|
|
131
|
-
## D21.
|
|
131
|
+
## D21. mirror 与 publish 分离
|
|
132
132
|
|
|
133
133
|
**决策**:两个子命令,而不是一个带模式开关的 `sync`。
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
**原因**:镜像原图和发布压缩图的目标、源、默认排除不同。分开后每个命令只有一个明确意图,也不需要 `--delete` 这类容易误用的开关。
|
|
136
136
|
|
|
137
137
|
## D22. URL 清单默认关闭
|
|
138
138
|
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
|
|
149
149
|
## D24. 破坏性操作先拒绝空输入与目标冲突
|
|
150
150
|
|
|
151
|
-
**决策**:`shrink --prune` 扫描到 0 个源图片但状态仍有条目时拒绝删除;`
|
|
151
|
+
**决策**:`shrink --prune` 扫描到 0 个源图片但状态仍有条目时拒绝删除;`mirror` / `publish` 源为空时要求显式 `--allow-empty` 且同时带 `--resync`;两个源文件映射到同一输出时退出 2。
|
|
152
152
|
|
|
153
153
|
**原因**:空目录最常见的来源是挂载失败、路径写错或上游失败,而不是用户真的想删除全部。目标冲突(如 `a.jpg` 与 `a.jpeg`)会让后一个编码静默覆盖前一个。这些情况都应在写入或删除前拒绝。
|
|
154
154
|
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
|
|
161
161
|
## D26. rclone 版本在命令入口检查
|
|
162
162
|
|
|
163
|
-
**决策**:`
|
|
163
|
+
**决策**:`mirror` / `publish` 启动时执行 `rclone version` 并要求 `>= 1.66`;`doctor` 保留同样的检查作为诊断。
|
|
164
164
|
|
|
165
165
|
**原因**:旧版 rclone 的 bisync 参数或行为不同,只靠 `doctor` 会让直接运行同步命令的用户遇到晦涩错误。版本检查放在真正调用 rclone 之前,失败信息才可操作。
|
|
166
166
|
|
|
@@ -175,3 +175,11 @@
|
|
|
175
175
|
**决策**:随包分发的 `src/pix/default_config.toml` 是默认值的唯一来源:`load_settings` 通过 `load_defaults()` 解析它,`pix config init` 直接复制它(保留注释)。目标文件已存在时拒绝覆盖并退出 2,`--force` 才覆盖;该命令在 `load_settings` 之前分派。
|
|
176
176
|
|
|
177
177
|
**原因**:Python 3.11 标准库只有 `tomllib`,没有 TOML 写入器;引入 `tomli-w` 违反“仅标准库 + pyvips”的约束。独立文件可以直接带注释,用户打开就能改,编辑器也能按 TOML 高亮;默认值只在这一处维护,避免代码常量与模板漂移。拒绝覆盖保护用户已经修改过的配置,误覆盖的代价远高于多敲一个 `--force`。提前分派让 `--config` 指向新路径时也能创建工作,而不是被“config file not found”挡住。
|
|
178
|
+
|
|
179
|
+
## D29. `backup` 改名为 `mirror`
|
|
180
|
+
|
|
181
|
+
**决策**:原 `backup` 子命令改名为 `mirror`,配置节 `[backup]` 同步改为 `[mirror]`,`publish` 保持不变;不保留旧名别名。
|
|
182
|
+
|
|
183
|
+
**原因**:`backup` 暗示单向、只增、可回滚,但实现是 `rclone bisync` 双向镜像,删除会双向传播且不可逆,命令名本身就在误导用户。`mirror` 准确描述机制。`publish` 描述的是目的,不隐含“远端只增”,取消发布时删除传播符合直觉,因此保留。处于测试阶段且未发布,改名成本最低。
|
|
184
|
+
|
|
185
|
+
**影响**:破坏性改名,不提供旧命令名或旧配置节兼容。
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
- [uv](https://docs.astral.sh/uv/)。
|
|
8
8
|
- Python `>=3.11`。仓库的 `.python-version` 和本地虚拟环境是 3.14,但代码必须兼容 3.11,不能使用 3.12+ 语法。
|
|
9
9
|
- 图像后端 `pyvips[binary]` 由依赖自动安装,不需要单独安装 libvips。
|
|
10
|
-
- `
|
|
10
|
+
- `mirror` / `publish` 需要外部 `rclone >= 1.66`;单元测试不需要安装真实 rclone。
|
|
11
11
|
|
|
12
12
|
## 2. 常用命令
|
|
13
13
|
|
|
@@ -55,16 +55,16 @@ editable 安装后修改源码立即生效。升级依赖后可用 `uv tool inst
|
|
|
55
55
|
| 原子写入 | 编码失败时临时文件被清理 |
|
|
56
56
|
| 并发锁 | 第二个 `shrink` 遇到 `OUT/.pix/lock` 时返回 1 |
|
|
57
57
|
| `min_size` 类型 | 配置写整数 `0` 或字符串 `"500K"` 均可解析 |
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
58
|
+
| `mirror` 命令拼装 | `bisync`、`--exclude`、`--max-delete`、`--resync`、`--dry-run`、`-v` / `--quiet` |
|
|
59
|
+
| `mirror` / `publish` 空源 | 默认退出 2;`--allow-empty` 加 `--resync` 才放行 |
|
|
60
60
|
| `publish` 命令拼装 | 固定带 `--exclude .pix/**`,配置排除项随后,命令行追加 |
|
|
61
61
|
| `publish` 无 AVIF | 源目录没有 `.avif` 时退出 2,不调用 rclone |
|
|
62
62
|
| `publish --urls` | 输出排序后的 JSON;路径段正确编码 |
|
|
63
63
|
| `--urls` / `--no-urls` | 配置 `urls = true` 时 `--no-urls` 能单次关闭 |
|
|
64
64
|
| `--urls` 缺少 base_url | 退出 2,且不调用 rclone |
|
|
65
65
|
| `base_url` 协议 | 非 `http(s)://` 时退出 2 |
|
|
66
|
-
| 退出码透传 | mock 返回非零时 `
|
|
67
|
-
| rclone 缺失/过旧 | `
|
|
66
|
+
| 退出码透传 | mock 返回非零时 `mirror` / `publish` 原样返回 |
|
|
67
|
+
| rclone 缺失/过旧 | `mirror` / `publish` 启动时检查版本,失败返回 1 |
|
|
68
68
|
| 真实 shrink | 生成小 PNG,运行 `pix shrink`,断言输出存在且宽度正确 |
|
|
69
69
|
|
|
70
70
|
mtime 测试用 `os.utime` 控制时间,避免依赖真实文件系统精度。状态测试直接读写 `.pix/state.json`,不要依赖真实 rclone。
|
|
@@ -111,7 +111,7 @@ vips thumbnail src 'out.avif[Q=85,strip,effort=5]' 1920 --size down --linear
|
|
|
111
111
|
- `thumbnail(..., size="down", linear=True)` 的默认旋转行为与 CLI 一致。
|
|
112
112
|
- libvips 8.18.7 没有 `avifsave` action;AVIF 通过 `heifsave`(`.avif` 后缀自动选择)保存。
|
|
113
113
|
- `effort` 是有效参数。
|
|
114
|
-
- `import pyvips` 延迟到 `shrink` / `doctor` 内部,保证 `
|
|
114
|
+
- `import pyvips` 延迟到 `shrink` / `doctor` 内部,保证 `mirror` / `publish` 在图像后端缺失时仍可用。
|
|
115
115
|
|
|
116
116
|
复验方法:对同一张真实图片分别用上述两种方式生成 AVIF,比较文件大小和 MD5/内容哈希。
|
|
117
117
|
|
|
@@ -136,11 +136,11 @@ pyvips/libvips 不可用。
|
|
|
136
136
|
|
|
137
137
|
## 8. rclone 与 bisync
|
|
138
138
|
|
|
139
|
-
- 需要 `rclone >= 1.66`。`
|
|
140
|
-
- 首次运行 `pix
|
|
139
|
+
- 需要 `rclone >= 1.66`。`mirror` / `publish` 启动时解析 `rclone version` 并检查;`doctor` 保留同样的检查。
|
|
140
|
+
- 首次运行 `pix mirror` / `pix publish` 必须加 `--resync`,否则 rclone 会因缺少基线报错。
|
|
141
141
|
- 修改 `--exclude` 或配置排除项后,必须重新带 `--resync` 运行,否则被排除的文件可能被当作删除传播。
|
|
142
142
|
- `--max-delete` 默认 `10`,是 rclone bisync 的删除百分比(不是数量);`0` 禁止删除,`-1` 不限制。超过比例时 rclone 拒绝执行,这是默认保护,不要静默改成 `-1`。
|
|
143
|
-
- `
|
|
143
|
+
- `mirror` / `publish` 是双向镜像,不是单向备份,删除会双向传播。手动验证时先用 `--dry-run`,再在临时目录里测试。
|
|
144
144
|
- 源目录为空时默认拒绝;确实要清空远端时才用 `--allow-empty`(`publish` 还需 `--resync`)。
|
|
145
145
|
- bisync 基线在 `~/.cache/rclone/bisync/`,由 rclone 管理;排查问题时可以用 `rclone bisync --help` 查看当前版本的参数。
|
|
146
146
|
- 单元测试只断言 argv,不访问网络,也不需要真实 remote。
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
截至 2026-11-04:
|
|
8
8
|
|
|
9
|
-
- 四个子命令 `shrink` / `
|
|
9
|
+
- 四个子命令 `shrink` / `mirror` / `publish` / `doctor` 已实现,`uv run pytest` 通过。
|
|
10
10
|
- `pix config init` 可生成带注释的默认配置;`--force` 覆盖已有文件。
|
|
11
11
|
- 真实 rclone 的本地目录端到端(首次 `--resync`、二次增量、双向删除)已验证;`pix shrink` 与 `vips thumbnail` 的 AVIF 字节一致性已验证。真实 R2 与 Windows 原生库仍待手工验证。
|
|
12
12
|
- 文档已按新定位重写,并完成 2026-11-04 审计修复。
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
## M0. 文档重写(完成)
|
|
16
16
|
|
|
17
17
|
- [x] 移除旧项目的全部残留内容。
|
|
18
|
-
- [x] 定义 `shrink` / `
|
|
18
|
+
- [x] 定义 `shrink` / `mirror` / `publish` / `doctor` 四个子命令。
|
|
19
19
|
- [x] 定义 `.pix/state.json` 增量状态。
|
|
20
20
|
- [x] 定义 bisync 双向同步与 `--resync` 规则。
|
|
21
21
|
- [x] 更新 README、AGENTS.md、pyproject 描述。
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
按 2026-11-04 审计逐项修正规格与脚手架:
|
|
26
26
|
|
|
27
27
|
- [x] `--prune` 空源保护与 `--allow-empty`。
|
|
28
|
-
- [x] `
|
|
28
|
+
- [x] `mirror` 明确为双向镜像,新增 `--max-delete`(默认 10%,rclone bisync 的删除百分比)。
|
|
29
29
|
- [x] 目标路径冲突检测(`a.jpg` / `a.jpeg` 等)。
|
|
30
30
|
- [x] 状态每 200 个文件刷新,`finally` 落盘;`out_mtime_ns` 退出跳过条件。
|
|
31
31
|
- [x] `publish` 源校验:必须是含 `.avif` 的目录。
|
|
@@ -36,13 +36,13 @@
|
|
|
36
36
|
- [x] `min_size` 接受整数或尺寸字符串。
|
|
37
37
|
- [x] 修改排除项后必须 `--resync`。
|
|
38
38
|
- [x] 删除模板死代码;拆分规则改为按职责拆分。
|
|
39
|
-
- [x] rclone 版本在 `
|
|
39
|
+
- [x] rclone 版本在 `mirror` / `publish` 入口检查。
|
|
40
40
|
- [x] `shrink` 单实例锁 `OUT/.pix/lock`。
|
|
41
41
|
- [x] `base_url` 必须带 `http(s)://`。
|
|
42
42
|
|
|
43
43
|
## M1. CLI 骨架
|
|
44
44
|
|
|
45
|
-
- [x] argparse 主 parser + `shrink` / `
|
|
45
|
+
- [x] argparse 主 parser + `shrink` / `mirror` / `publish` / `doctor` 子命令。
|
|
46
46
|
- [x] 全局 `--config`、`--version`。
|
|
47
47
|
- [x] 配置加载与三层合并,argparse 默认值使用哨兵。
|
|
48
48
|
- [x] 日志级别:默认、`-v`、`--quiet`。
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
- 改参数后全量重编码;改单个源文件只重编码该文件。
|
|
75
75
|
- 中断或编码失败不会留下半截 AVIF。
|
|
76
76
|
|
|
77
|
-
## M3.
|
|
77
|
+
## M3. mirror
|
|
78
78
|
|
|
79
79
|
- [x] rclone 路径解析与本地源目录校验;空源默认拒绝,`--allow-empty` 加 `--resync` 才放行。
|
|
80
80
|
- [x] bisync 命令拼装:`--exclude`(合并去重)、`--max-delete`(默认 10%,百分比)、`--resync`、`--dry-run`、`-v` / `--quiet`。
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
|
|
94
94
|
- [x] bisync 命令固定追加 `--exclude .pix/**`,配置排除项随后,命令行追加。
|
|
95
95
|
- [x] `publish` 源必须是含至少一个 `.avif` 的目录;空源需 `--allow-empty` 加 `--resync`。
|
|
96
|
-
- [x] `--max-delete` 默认 10%,与 `
|
|
96
|
+
- [x] `--max-delete` 默认 10%,与 `mirror` 一致。
|
|
97
97
|
- [x] `--base-url` / `[publish].base_url` 合并,协议必须是 `http(s)://`。
|
|
98
98
|
- [x] `--urls` / `--no-urls` 合并与 JSON 输出,路径段编码正确。
|
|
99
99
|
- [x] 缺少 base_url 时退出 2,且在调用 rclone 前拒绝。
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
|
|
123
123
|
## Backlog(第一版不做)
|
|
124
124
|
|
|
125
|
-
- 一键流水线 `pix run`(shrink →
|
|
125
|
+
- 一键流水线 `pix run`(shrink → mirror → publish)。
|
|
126
126
|
- 自动 `--resync`。
|
|
127
127
|
- 远端可达性探测。
|
|
128
128
|
- 图片裁剪(crop)。
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
| 风险 | 处理 |
|
|
136
136
|
|---|---|
|
|
137
137
|
| bisync 双向删除不可逆 | 默认 `--max-delete 10`(10% 删除比例);显式 `--resync`;先 `--dry-run` |
|
|
138
|
-
| 空目录/挂载失败被当成“删除全部” | `--prune` 空源拒绝;`
|
|
138
|
+
| 空目录/挂载失败被当成“删除全部” | `--prune` 空源拒绝;`mirror` / `publish` 空源需 `--allow-empty` |
|
|
139
139
|
| `a.jpg` 与 `a.jpeg` 争用同一输出 | 编码前冲突检测,退出 2 |
|
|
140
140
|
| 修改排除项后未 resync 导致误删 | 文档与错误提示要求先 `--resync` |
|
|
141
141
|
| `pyvips[binary]` 与系统 libvips 版本不一致导致 AVIF 差异 | 已在 8.18.7 验证字节一致;升级 libvips 后重跑抽样对比 |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pix-cli"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.3"
|
|
4
4
|
description = "Batch AVIF shrink and image-host sync CLI"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -26,7 +26,7 @@ classifiers = [
|
|
|
26
26
|
"Programming Language :: Python :: 3.13",
|
|
27
27
|
"Programming Language :: Python :: 3.14",
|
|
28
28
|
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
|
|
29
|
-
"Topic :: System :: Archiving ::
|
|
29
|
+
"Topic :: System :: Archiving :: Mirroring",
|
|
30
30
|
]
|
|
31
31
|
requires-python = ">=3.11"
|
|
32
32
|
dependencies = [
|
|
@@ -11,7 +11,7 @@ from .config import ConfigError, init_config, load_settings
|
|
|
11
11
|
from .doctor import doctor
|
|
12
12
|
from .logger import Logger
|
|
13
13
|
from .shrink import shrink
|
|
14
|
-
from .sync import
|
|
14
|
+
from .sync import mirror, publish
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def _add_logging_args(parser: argparse.ArgumentParser) -> None:
|
|
@@ -39,21 +39,21 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
39
39
|
shrink.add_argument("--dry-run", action="store_true")
|
|
40
40
|
_add_logging_args(shrink)
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
mirror = subparsers.add_parser("mirror", help="bidirectionally mirror originals")
|
|
43
|
+
mirror.add_argument("-s", "--source", type=Path, required=True)
|
|
44
|
+
mirror.add_argument("-d", "--dest", required=True)
|
|
45
|
+
mirror.add_argument("--exclude", action="append", default=None, metavar="GLOB")
|
|
46
|
+
mirror.add_argument(
|
|
47
47
|
"--max-delete",
|
|
48
48
|
type=int,
|
|
49
49
|
default=None,
|
|
50
50
|
metavar="PERCENT",
|
|
51
51
|
help="abort if more than PERCENT%% of files are deleted (default: 10)",
|
|
52
52
|
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
_add_logging_args(
|
|
53
|
+
mirror.add_argument("--allow-empty", action="store_true")
|
|
54
|
+
mirror.add_argument("--resync", action="store_true")
|
|
55
|
+
mirror.add_argument("--dry-run", action="store_true")
|
|
56
|
+
_add_logging_args(mirror)
|
|
57
57
|
|
|
58
58
|
publish = subparsers.add_parser("publish", help="bidirectionally mirror compressed images")
|
|
59
59
|
publish.add_argument("-s", "--source", type=Path, required=True)
|
|
@@ -105,7 +105,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
105
105
|
)
|
|
106
106
|
handlers = {
|
|
107
107
|
"shrink": shrink,
|
|
108
|
-
"
|
|
108
|
+
"mirror": mirror,
|
|
109
109
|
"publish": publish,
|
|
110
110
|
"doctor": doctor,
|
|
111
111
|
}
|
|
@@ -108,7 +108,7 @@ def _require_excludes(name: str, value: Any) -> list[str]:
|
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
def validate_settings(settings: dict[str, Any]) -> None:
|
|
111
|
-
for section in ("shrink", "
|
|
111
|
+
for section in ("shrink", "mirror", "publish", "rclone"):
|
|
112
112
|
if not isinstance(settings.get(section), dict):
|
|
113
113
|
raise ConfigError(f"[{section}] must be a table")
|
|
114
114
|
|
|
@@ -127,7 +127,7 @@ def validate_settings(settings: dict[str, Any]) -> None:
|
|
|
127
127
|
):
|
|
128
128
|
raise ConfigError("shrink.include must be a list of extensions without dots")
|
|
129
129
|
|
|
130
|
-
for section in ("
|
|
130
|
+
for section in ("mirror", "publish"):
|
|
131
131
|
_require_excludes(f"{section}.exclude", settings[section]["exclude"])
|
|
132
132
|
require_int(f"{section}.max_delete", settings[section]["max_delete"], -1, 100)
|
|
133
133
|
|
|
@@ -13,7 +13,7 @@ min_size = 0
|
|
|
13
13
|
# Extensions to scan, written without dots.
|
|
14
14
|
include = ["jpg", "jpeg", "png", "webp"]
|
|
15
15
|
|
|
16
|
-
[
|
|
16
|
+
[mirror]
|
|
17
17
|
# rclone exclude globs; command-line --exclude is appended after these.
|
|
18
18
|
exclude = []
|
|
19
19
|
# Maximum percent of files rclone may delete (-1 disables, 0 forbids).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""mirror and publish subcommands."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -13,8 +13,8 @@ from .logger import Logger
|
|
|
13
13
|
from .rclone import check_rclone, has_files, merge_excludes, run_rclone
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def
|
|
17
|
-
config = settings["
|
|
16
|
+
def mirror(args: argparse.Namespace, settings: dict[str, Any], log: Logger) -> int:
|
|
17
|
+
config = settings["mirror"]
|
|
18
18
|
source = args.source
|
|
19
19
|
if not source.is_dir():
|
|
20
20
|
log.error(f"source is not a directory: {source}")
|
|
@@ -407,11 +407,11 @@ def make_source(tmp_path: Path) -> Path:
|
|
|
407
407
|
return source
|
|
408
408
|
|
|
409
409
|
|
|
410
|
-
def
|
|
410
|
+
def test_mirror_builds_bisync_command(tmp_path: Path, monkeypatch):
|
|
411
411
|
source = make_source(tmp_path)
|
|
412
412
|
config = tmp_path / "config.toml"
|
|
413
413
|
config.write_text(
|
|
414
|
-
'[
|
|
414
|
+
'[mirror]\nexclude = ["cfg", "dup"]\nmax_delete = 9\n', encoding="utf-8"
|
|
415
415
|
)
|
|
416
416
|
calls = fake_rclone(monkeypatch)
|
|
417
417
|
|
|
@@ -419,7 +419,7 @@ def test_backup_builds_bisync_command(tmp_path: Path, monkeypatch):
|
|
|
419
419
|
[
|
|
420
420
|
"--config",
|
|
421
421
|
str(config),
|
|
422
|
-
"
|
|
422
|
+
"mirror",
|
|
423
423
|
"-s",
|
|
424
424
|
str(source),
|
|
425
425
|
"-d",
|
|
@@ -450,39 +450,39 @@ def test_backup_builds_bisync_command(tmp_path: Path, monkeypatch):
|
|
|
450
450
|
]
|
|
451
451
|
|
|
452
452
|
|
|
453
|
-
def
|
|
453
|
+
def test_mirror_defaults_to_quiet_and_max_delete_ten(tmp_path: Path, monkeypatch):
|
|
454
454
|
source = make_source(tmp_path)
|
|
455
455
|
calls = fake_rclone(monkeypatch)
|
|
456
456
|
|
|
457
|
-
assert cli.main(["
|
|
457
|
+
assert cli.main(["mirror", "-s", str(source), "-d", "remote:photos"]) == 0
|
|
458
458
|
|
|
459
459
|
assert calls[1][-3:] == ["--max-delete", "10", "--quiet"]
|
|
460
460
|
|
|
461
461
|
|
|
462
|
-
def
|
|
462
|
+
def test_mirror_empty_source_refuses(tmp_path: Path, monkeypatch, capsys):
|
|
463
463
|
source = tmp_path / "src"
|
|
464
464
|
source.mkdir()
|
|
465
465
|
calls = fake_rclone(monkeypatch)
|
|
466
466
|
|
|
467
|
-
assert cli.main(["
|
|
467
|
+
assert cli.main(["mirror", "-s", str(source), "-d", "remote:photos"]) == 2
|
|
468
468
|
|
|
469
469
|
assert calls == []
|
|
470
470
|
assert "source is empty" in capsys.readouterr().err
|
|
471
471
|
|
|
472
472
|
|
|
473
|
-
def
|
|
473
|
+
def test_mirror_allow_empty_requires_resync(tmp_path: Path, monkeypatch):
|
|
474
474
|
source = tmp_path / "src"
|
|
475
475
|
source.mkdir()
|
|
476
476
|
calls = fake_rclone(monkeypatch)
|
|
477
477
|
|
|
478
478
|
assert cli.main(
|
|
479
|
-
["
|
|
479
|
+
["mirror", "-s", str(source), "-d", "remote:photos", "--allow-empty"]
|
|
480
480
|
) == 2
|
|
481
481
|
assert calls == []
|
|
482
482
|
|
|
483
483
|
assert cli.main(
|
|
484
484
|
[
|
|
485
|
-
"
|
|
485
|
+
"mirror",
|
|
486
486
|
"-s",
|
|
487
487
|
str(source),
|
|
488
488
|
"-d",
|
|
@@ -494,38 +494,38 @@ def test_backup_allow_empty_requires_resync(tmp_path: Path, monkeypatch):
|
|
|
494
494
|
assert "--resync" in calls[1]
|
|
495
495
|
|
|
496
496
|
|
|
497
|
-
def
|
|
497
|
+
def test_mirror_dry_run_and_passthrough_exit_code(tmp_path: Path, monkeypatch):
|
|
498
498
|
source = make_source(tmp_path)
|
|
499
499
|
calls = fake_rclone(monkeypatch, bisync_code=7)
|
|
500
500
|
|
|
501
501
|
assert cli.main(
|
|
502
|
-
["
|
|
502
|
+
["mirror", "-s", str(source), "-d", "remote:photos", "--dry-run", "--quiet"]
|
|
503
503
|
) == 7
|
|
504
504
|
|
|
505
505
|
assert calls[1][-2:] == ["--dry-run", "--quiet"]
|
|
506
506
|
|
|
507
507
|
|
|
508
|
-
def
|
|
508
|
+
def test_mirror_rejects_old_or_missing_rclone(tmp_path: Path, monkeypatch, capsys):
|
|
509
509
|
source = make_source(tmp_path)
|
|
510
510
|
|
|
511
511
|
fake_rclone(monkeypatch, version="rclone v1.65.2\n")
|
|
512
|
-
assert cli.main(["
|
|
512
|
+
assert cli.main(["mirror", "-s", str(source), "-d", "remote:photos"]) == 1
|
|
513
513
|
assert "too old" in capsys.readouterr().err
|
|
514
514
|
|
|
515
515
|
fake_rclone(monkeypatch, version=FileNotFoundError())
|
|
516
|
-
assert cli.main(["
|
|
516
|
+
assert cli.main(["mirror", "-s", str(source), "-d", "remote:photos"]) == 1
|
|
517
517
|
assert "not found" in capsys.readouterr().err
|
|
518
518
|
|
|
519
519
|
|
|
520
|
-
def
|
|
520
|
+
def test_mirror_rejects_invalid_max_delete(tmp_path: Path, monkeypatch):
|
|
521
521
|
source = make_source(tmp_path)
|
|
522
522
|
calls = fake_rclone(monkeypatch)
|
|
523
523
|
|
|
524
524
|
assert cli.main(
|
|
525
|
-
["
|
|
525
|
+
["mirror", "-s", str(source), "-d", "remote:photos", "--max-delete", "-2"]
|
|
526
526
|
) == 2
|
|
527
527
|
assert cli.main(
|
|
528
|
-
["
|
|
528
|
+
["mirror", "-s", str(source), "-d", "remote:photos", "--max-delete", "101"]
|
|
529
529
|
) == 2
|
|
530
530
|
assert calls == []
|
|
531
531
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|