moonraker-bosun 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +16 -0
  3. data/LICENSE +21 -0
  4. data/README.ja.md +467 -0
  5. data/README.md +124 -0
  6. data/docs/design.md +358 -0
  7. data/docs/moonraker-api.md +108 -0
  8. data/exe/bosun +7 -0
  9. data/lib/moonraker/bosun/cli.rb +64 -0
  10. data/lib/moonraker/bosun/client.rb +147 -0
  11. data/lib/moonraker/bosun/commands/add.rb +38 -0
  12. data/lib/moonraker/bosun/commands/audit.rb +48 -0
  13. data/lib/moonraker/bosun/commands/backup.rb +135 -0
  14. data/lib/moonraker/bosun/commands/base.rb +61 -0
  15. data/lib/moonraker/bosun/commands/diff.rb +29 -0
  16. data/lib/moonraker/bosun/commands/export.rb +103 -0
  17. data/lib/moonraker/bosun/commands/fetching.rb +76 -0
  18. data/lib/moonraker/bosun/commands/forget.rb +26 -0
  19. data/lib/moonraker/bosun/commands/get.rb +57 -0
  20. data/lib/moonraker/bosun/commands/ls.rb +43 -0
  21. data/lib/moonraker/bosun/commands/pull.rb +52 -0
  22. data/lib/moonraker/bosun/commands/push.rb +136 -0
  23. data/lib/moonraker/bosun/commands/removing.rb +38 -0
  24. data/lib/moonraker/bosun/commands/rm.rb +105 -0
  25. data/lib/moonraker/bosun/commands/roots.rb +31 -0
  26. data/lib/moonraker/bosun/commands/status.rb +22 -0
  27. data/lib/moonraker/bosun/commands/status_report.rb +75 -0
  28. data/lib/moonraker/bosun/config.rb +31 -0
  29. data/lib/moonraker/bosun/context.rb +26 -0
  30. data/lib/moonraker/bosun/error.rb +8 -0
  31. data/lib/moonraker/bosun/file_rules.rb +37 -0
  32. data/lib/moonraker/bosun/git.rb +139 -0
  33. data/lib/moonraker/bosun/host_tracking.rb +214 -0
  34. data/lib/moonraker/bosun/messages/catalog.rb +837 -0
  35. data/lib/moonraker/bosun/messages.rb +42 -0
  36. data/lib/moonraker/bosun/push_plan.rb +89 -0
  37. data/lib/moonraker/bosun/sync_status.rb +130 -0
  38. data/lib/moonraker/bosun/version.rb +7 -0
  39. data/lib/moonraker/bosun.rb +31 -0
  40. metadata +86 -0
data/docs/design.md ADDED
@@ -0,0 +1,358 @@
1
+ # トラッキングの設計
2
+
3
+ ホスト側の設定ファイルのトラッキング状態を、gitでどのように記録・運用するかを定める。
4
+
5
+ ## 基本方針
6
+
7
+ * ホストから取得した内容だけを積み重ねる`host-tracking`ブランチを用意し、これ自体をトラッキングの記録とする
8
+ * いわゆるvendor branchと同じ考え方
9
+ * マニフェストファイルなど、トラッキング用の別のデータは持たない
10
+ * ローカルでの編集は`main`ブランチで行い、`host-tracking`を`main`にマージして取り込む
11
+ * 削除は明示的な操作をしない限り行わず、警告と手順の案内にとどめる
12
+
13
+ ```
14
+ host-tracking: H1 ── H2 ── H3 ─────── H4 ← ホストから取得した内容
15
+ \ \ \ \
16
+ main: ── M1 ─ M2 ─ L1 ─ M3 ─ L2 ─ M4 ← ローカルでの編集(L)とhost-trackingのマージ(M)
17
+ ```
18
+
19
+ ## ブランチ
20
+
21
+ | ブランチ | 内容 |
22
+ | --- | --- |
23
+ | host-tracking | ホストから取得した内容のみ。ツリーには`config/`と`backup/`だけを置く |
24
+ | main | 作業用のブランチ。ツールのコードやドキュメントもここに置く |
25
+
26
+ * `host-tracking`の先頭のツリーが、トラッキング中のファイルの一覧と、最後に取得した内容を表す
27
+ * トラッキング中のファイル: `git ls-tree -r --name-only host-tracking -- config/`
28
+ * 最後に取得した内容: `host-tracking`の先頭にある各ファイルのblob
29
+ * `host-tracking`の履歴が、ホストのスナップショットの列になる
30
+ * `host-tracking`ブランチも`main`と同様にリモートリポジトリにpushする
31
+ * リモートへのpushはツールでは行わず、gitで行う(`git push origin main host-tracking`)
32
+
33
+ ### 不変条件
34
+
35
+ 以下は常に守る。`audit`で検査する。
36
+
37
+ 1. `host-tracking`の祖先に`host-tracking`以外のコミットを含めない
38
+ * `main`を`host-tracking`にマージしない
39
+ * `host-tracking`へのコミットの親は、`host-tracking`の先頭のコミットのみとする
40
+ * 理由: `main`のコミットが`host-tracking`の祖先に入ると、`host-tracking`を`main`にマージしたときに、`host-tracking`のツリーにない`lib/`などが「削除された」と判断されて消えてしまう
41
+ 2. `host-tracking`のツリーには`config/`と`backup/`以外を置かない
42
+ 3. `host-tracking`には、ホストから実際に取得した内容だけをコミットする
43
+ * `push`の後も、アップロードした内容ではなく、取得し直した内容をコミットする
44
+ 4. `host-tracking`へのコミットは、作業ツリーとindexを使わずに作成する
45
+ * ローカルでの変更と混ざらないようにするため
46
+
47
+ ## host-trackingへのコミット
48
+
49
+ ### 作成方法
50
+
51
+ 一時的なindexファイルを使い、plumbingコマンドで作成する。
52
+
53
+ 1. `GIT_INDEX_FILE`に一時ファイルを指定し、`git read-tree host-tracking`で`host-tracking`の先頭のツリーを読み込む
54
+ 2. 取得したファイルを`git hash-object -w --no-filters`でblobにし、`git update-index --index-info`で置き換える
55
+ * `--no-filters`により、`.gitattributes`などによる改行コードの変換を行わず、ホストのバイト列をそのまま記録する
56
+ * 削除する場合は`git update-index --force-remove`
57
+ 3. `git write-tree`でツリーを作成する
58
+ 4. `git commit-tree -p host-tracking`でコミットを作成する
59
+ 5. `git update-ref refs/heads/host-tracking <新しいコミット> <元のコミット>`で`host-tracking`を更新する
60
+ * 元のコミットを指定して、途中で`host-tracking`が変わっていた場合には失敗させる
61
+
62
+ `host-tracking`が存在しない場合(初回)は、1を省略し、4で親を指定しない(orphan)。
63
+ 5では元のコミットに空文字列を指定して、`host-tracking`が存在しないことを確認する。
64
+
65
+ 3で作成したツリーが`host-tracking`の先頭と同じ場合は、コミットしない。
66
+ この場合、`Host-File`の`modified`は更新されない(「HとTの比較方法」を参照)。
67
+
68
+ ### コミットメッセージ
69
+
70
+ 機械的に読み取れるよう、形式を固定する。
71
+
72
+ ```
73
+ pull: 2 files
74
+
75
+ Host-Op: pull
76
+ Host-File: config/printer.cfg modified=1728620000.123 size=4321
77
+ Host-File: config/moonraker.conf modified=1728610000.456 size=1234
78
+ ```
79
+
80
+ * 1行目は`<操作>: <件数> files`(1件の場合は`file`)
81
+ * `Host-Op`は`add`、`pull`、`push`、`backup`、`forget`、`rm`のいずれか
82
+ * `Host-File`は、そのコミットで取得・削除したファイルごとに1行
83
+ * `modified`と`size`は、Moonrakerのファイル一覧から得たホスト側の値
84
+ * ホスト側の時刻を使うので、ローカルとホストの時計のずれの影響を受けない
85
+ * 削除の場合は`deleted`とする
86
+ * `Host-DB`は、Moonraker DBのバックアップのファイルごとに1行(`Host-DB: backup/moonraker-db/mainsail.json`)
87
+ * DBには更新日時やサイズがないので、パスだけを記録する
88
+
89
+ ## mainへのマージ
90
+
91
+ * `host-tracking`へのコミットの後、`host-tracking`を現在のブランチにマージする
92
+ * 現在のブランチが`host-tracking`の場合はエラーにする
93
+ * `host-tracking`は常に`main`に含まれないコミットを持つので、fast-forwardにはならない
94
+ * 不変条件が崩れていた場合に備えて、`--no-ff`を必ず付ける(fast-forwardすると、現在のブランチのツリーが`config/`だけになる)
95
+ * 共通の祖先がない場合(初回)は、`--allow-unrelated-histories`を付ける
96
+ * マージコミットのメッセージは`Merge host-tracking (<操作>: <変更したパス>, ...)`とする
97
+ * 変更したパスがない場合(未マージのコミットだけを取り込む場合)は`Merge host-tracking (<操作>)`
98
+ * gitに記録するデータなので、表示の言語によらず英語にする
99
+ * ホストからの取得の前に、次の場合は中止する
100
+ * detached HEADの場合、マージの途中の場合
101
+ * ステージ済みの変更がある場合(`git merge`は、indexに変更があると対象のファイルと関係がなくても中止するため)
102
+ * 次のファイルに、作業ツリーの変更やgit管理外のファイルがある場合
103
+ * 今回取得するファイル
104
+ * `host-tracking`にあって、まだ現在のブランチに取り込んでいない変更のファイル
105
+ * マージで衝突した場合は、マージを中断した状態で終了し、gitの通常の手順で解決するよう案内する
106
+ * `host-tracking`へのコミットは完了しているので、取得し直す必要はない
107
+ * 衝突以外の理由でマージできなかった場合も、`host-tracking`へのコミットは残し、`git merge --no-ff host-tracking`を案内する
108
+
109
+ ## 状態の判定
110
+
111
+ ファイルごとに、次の3つを比べて状態を判定する。
112
+
113
+ * H: ホストの現在の内容
114
+ * T: `host-tracking`の先頭の内容(最後に取得した内容)
115
+ * L: 現在のブランチの`HEAD`の内容
116
+
117
+ Tがある(トラッキング中の)ファイル:
118
+
119
+ | H | L | 状態 | 案内 |
120
+ | --- | --- | --- | --- |
121
+ | Tと一致 | Tと一致 | 同期済み | なし |
122
+ | Tと不一致 | Tと一致 | ホストで変更 | `pull` |
123
+ | Tと一致 | Tと不一致 | ローカルで変更 | `push` |
124
+ | Tと不一致 | Tと不一致 | 両方で変更 | `pull`でマージしてから`push` |
125
+ | なし | あり | ホストで削除 | トラッキングをやめるなら`forget` |
126
+ | なし | なし | 両方で削除 | トラッキングをやめるなら`forget` |
127
+ | `rw`でない | - | ホストで書き込めなくなった | Web UIやSSHで確認する。トラッキングをやめるなら`forget` |
128
+ | Tと一致 | なし | ローカルで削除 | ホストからも削除するなら`rm`、ローカルに戻すなら`git restore --source=host-tracking --staged --worktree -- config/<file>`でコミット |
129
+ | Tと不一致 | なし | ホストで変更、ローカルで削除 | `pull`で取り込んでから判断する |
130
+
131
+ Tがない(トラッキングしていない)ファイル:
132
+
133
+ | H | L | 状態 | 案内 |
134
+ | --- | --- | --- | --- |
135
+ | なし | あり | ローカルで追加 | `push`でホストに作成 |
136
+ | あり | あり | 両方にある | `add`で取り込んでマージする |
137
+ | あり(除外しないファイル) | なし | トラッキングしていない | トラッキングするなら`add` |
138
+
139
+ * Lは`HEAD`の内容なので、未コミットの変更は判定に含めない
140
+ * `status`では、`config/`の未コミットの変更を別に表示する
141
+ * `host-tracking`に、まだ現在のブランチに取り込んでいないコミットがある場合、TとLの比較は正しくない
142
+ * `status`では警告し、`git merge --no-ff host-tracking`を案内する
143
+ * ホスト側での名前の変更は、「ホストで削除」と「トラッキングしていない」の組として現れる
144
+ * 内容が同じもの(Tのblobと、トラッキングしていないファイルのハッシュが一致するもの)を組にして、`add <新しい名前>`と`forget <古い名前>`を案内する
145
+ * サイズが同じトラッキングしていないファイルだけを取得して比べる
146
+
147
+ ### HとTの比較方法
148
+
149
+ * 簡易(`status`)
150
+ * ファイル一覧の`modified`・`size`と、そのファイルを最後に記録した`Host-File`の値を比べる
151
+ * どちらも一致すれば、一致とする(取得しない)
152
+ * `size`が違えば、不一致とする(取得しない)
153
+ * それ以外(`modified`だけが違う、記録がないなど)は、取得して厳密に比べる
154
+ * 内容が同じまま更新日時だけが変わったファイルは、一致と判定される
155
+ * そのファイルは、内容が変わって次にコミットされるまで、`status`のたびに取得する
156
+ * 更新日時とサイズが同じまま内容が変わった場合は検知できない(`audit`で検知する)
157
+ * 厳密(`audit`、`push`、`rm`)
158
+ * ホストからファイルを取得し、`git hash-object --stdin`で求めたハッシュとTのblobのハッシュを比べる
159
+
160
+ ## 対象のファイル
161
+
162
+ Moonraker APIの動作は @docs/moonraker-api.md を参照。
163
+
164
+ ### 書き込みの方針
165
+
166
+ `bosun`がホストに書き込む(アップロード・削除する)のは、Moonrakerが`permissions`を`rw`と報告するファイルだけとする。
167
+
168
+ * ファイルの種類(シンボリックリンクかどうかなど)ではなく、`permissions`だけで判断する
169
+ * 現在の環境では、シンボリックリンク(`mainsail.cfg`、`timelapse.cfg`)が`rw`でないファイルに該当する
170
+ * シンボリックリンクへのアップロードはリンク先を書き換えてしまうが、この方針で防がれる
171
+ * `rw`でないファイルを更新・削除する必要があるときは、`bosun`を使わず、Web UIやSSHで行う
172
+ * `push`と`rm`では、書き込む直前の一覧で`rw`であることを確認する
173
+ * ローカルで追加したファイルを`push`する場合、ホストに同名のファイルがあれば(`rw`かどうかにかかわらず)中止する
174
+
175
+ ### 一覧とトラッキングの対象
176
+
177
+ `config`の次のファイルは、`ls`では表示せず(`-a`で表示)、`add`ではエラーにする。
178
+
179
+ * `permissions`が`rw`でないファイル
180
+ * 書き込めないファイルをトラッキングしても、`push`できないため
181
+ * ホスト側で作られるバックアップファイル
182
+ * `SAVE_CONFIG`によるもの: `<名前>-YYYYMMDD_HHMMSS.cfg`
183
+ * Moonrakerによるもの: `.<名前>.bkp`
184
+
185
+ `get`では、これらも取得できる(シンボリックリンクはリンク先の内容になる)。
186
+
187
+ ## 印刷中の判定
188
+
189
+ * `print_stats.state`が`printing`または`paused`の場合は印刷中とする
190
+ * `print_stats`が空の場合(Klipperが`ready`でない場合)は、印刷中ではないとする
191
+ * Moonrakerに接続できない場合は、判定できないので中止する
192
+
193
+ ## コマンド
194
+
195
+ | コマンド | git | 処理 |
196
+ | --- | --- | --- |
197
+ | `roots` | なし | ホストのRootディレクトリの一覧を表示する |
198
+ | `ls` | なし | ホストのファイル一覧を表示する |
199
+ | `get` | なし | ホストのファイルをカレントディレクトリに取得する |
200
+ | `add` | `host-tracking`へコミット、マージ | 指定したファイルを取得してトラッキングを始める |
201
+ | `pull` | `host-tracking`へコミット、マージ | トラッキング中のファイルを一括で取得する |
202
+ | `status` | 読み取りのみ | 状態を簡易な方法で判定して表示する |
203
+ | `audit` | 読み取りのみ | 状態を厳密な方法で判定し、不変条件も検査する |
204
+ | `diff` | 読み取りのみ | `push`で反映する内容を表示する(`git diff host-tracking HEAD -- config/`) |
205
+ | `push` | `host-tracking`へコミット、マージ | ローカルでの変更をホストに反映する |
206
+ | `forget` | `host-tracking`へコミット、マージ | トラッキングをやめる。ホストのファイルは削除しない |
207
+ | `rm` | `host-tracking`へコミット、マージ | ホストのファイルを削除し、トラッキングをやめる |
208
+ | `backup` | `host-tracking`へコミット、マージ | Moonraker DBの内容を`backup/moonraker-db/`に取得する |
209
+ | `export` | 読み取りのみ | ある時点のスナップショットをディレクトリに書き出す |
210
+
211
+ ### pull
212
+
213
+ * トラッキング中のファイル(`host-tracking`の先頭のツリーにあるファイル)をすべて取得し、`host-tracking`へコミットしてマージする
214
+ * 内容が変わったファイルがなければコミットしない
215
+ * コミットする場合は、取得したすべてのファイルの`Host-File`を記録する(内容が変わっていないファイルの更新日時も記録し直される)
216
+ * `host-tracking`に未マージのコミットがあれば、変更がなくてもマージする
217
+ * 次のファイルは取得せず、マージの前に警告して手順を案内する
218
+ * ホストで削除されたファイル(`host-tracking`からは除かない)
219
+ * ホストで`rw`でなくなったファイル
220
+ * トラッキングしていないファイル(除外しないもの)
221
+
222
+ ### push
223
+
224
+ 1. `add`や`pull`と同じく、作業ツリーとindexの状態を確認する(「mainへのマージ」を参照)
225
+ * `config/`のすべてのファイルについて、未コミットの変更やgit管理外のファイルがないこと
226
+ * `host-tracking`に未マージのコミットがないこと
227
+ 2. 印刷中でないことを確認する(「印刷中の判定」を参照)
228
+ 3. 反映の対象を決める
229
+ * Lにあり、Tと異なるファイルを対象にする
230
+ * Tにもあれば更新、Tになければ作成
231
+ * 次のファイルは対象にせず、状態と手順を案内する
232
+ * ローカルで削除したファイル(`rm`などを案内する)
233
+ * ホストだけで変更・削除されたファイル(`pull`などを案内する)
234
+ 4. 対象のファイルについて、反映できるかを確認する
235
+ * 共通: Lのモードが`100644`(通常のファイル)であること
236
+ * シンボリックリンク(`120000`)をそのまま反映すると、リンク先のパスの文字列がファイルの内容になる
237
+ * `host-tracking`にはホストの内容を通常のファイルとして記録するので、実行可能ファイル(`100755`)などを反映すると、マージでモードが衝突する
238
+ * 更新: ホストにあり、`rw`で、HとTが厳密な方法で一致すること
239
+ * 作成: ホストに同名のファイルがない(`rw`かどうかにかかわらず)こと、ホスト側のバックアップファイルと同じ形式の名前でないこと
240
+ * 1件でも反映できないファイルがあれば、何も反映せずに中止し、理由と手順を案内する(終了コードは1)
241
+ 5. 書き込む直前にファイル一覧を取得し直し、4の後にホストで変わっていないことを確認する
242
+ * 更新: `rw`のままで、`modified`と`size`が4で確認したときと同じこと
243
+ * 作成: まだホストにないこと
244
+ * 1件でも変わっていれば、何も反映せずに中止する
245
+ 6. ファイルをアップロードする
246
+ * Moonrakerの`checksum`(SHA-256)を付け、内容が壊れた場合は書き込ませない
247
+ 7. アップロードしたファイルを取得し直して、`host-tracking`へコミットする
248
+ * 取得した内容が反映した内容と違う場合は、警告して取得した内容を記録する(不変条件3)
249
+ 8. `host-tracking`を現在のブランチにマージする
250
+ 9. 設定を有効にするための再起動(RESTARTなど)は行わず、案内だけを表示する
251
+
252
+ アップロードの途中で失敗した場合(Ctrl-Cなどのシグナルによる中断を含む)は、それまでにアップロードしたファイルだけを取得し直して`host-tracking`にコミット・マージしてから、エラーで終了する。
253
+ 反映していないファイルは、もう一度`push`すれば反映できる。
254
+
255
+ `--dry-run`(`-n`)では4までを行い、反映する内容を表示する。
256
+
257
+ 5の確認から6のアップロードまでの間にホストで起きた変更は防げないが、印刷中でないことを確認しているので許容する(「未決事項」を参照)。
258
+
259
+ ### diff
260
+
261
+ * `git diff host-tracking HEAD -- config/`を表示する
262
+ * 最後に取得した内容との差分であり、ホストの現在の内容との差分ではない
263
+ * 未コミットの変更は含めない
264
+ * ファイルを指定すると、そのファイルに限る。`--stat`で概要だけを表示する
265
+
266
+ ### forget
267
+
268
+ * 指定したファイルを`host-tracking`のツリーから除いてコミットし、マージする
269
+ * マージによって、現在のブランチからも削除される
270
+ * コミットメッセージには`Host-File: <パス> deleted`を記録する
271
+ * ホスト側のファイルは削除しない
272
+ * ホストには接続しないので、ホストがオフラインでも実行できる
273
+ * 次の場合は中止する
274
+ * トラッキングしていないファイルを指定した
275
+ * `add`と同じ作業ツリーの確認(「mainへのマージ」を参照)で問題がある
276
+ * ローカル(`HEAD`)で変更されている(マージで変更と削除が衝突するため)
277
+ * ローカルで削除したファイルは指定できる
278
+
279
+ ### rm
280
+
281
+ * `forget`と同じ確認に加えて、次のことを確認してから、ホストのファイルを削除する
282
+ * 印刷中でないこと
283
+ * ホストにあり、`rw`で、HとTが厳密な方法で一致すること(最後に取得した後の変更を失わないようにするため)
284
+ * 1件でも問題があれば、何も削除せずに中止する
285
+ * 削除する直前にファイル一覧を取得し直し、確認の後にホストで変わっていないことを確認する(`push`と同じ)
286
+ * 削除した後、`host-tracking`のツリーから除いてコミットし、マージする
287
+ * 削除の途中で失敗した場合(Ctrl-Cなどのシグナルによる中断を含む)は、削除できたファイルだけを記録してから、エラーで終了する
288
+ * `--dry-run`(`-n`)では確認までを行い、削除するファイルを表示する
289
+
290
+ ### backup
291
+
292
+ * Moonraker DBのnamespaceごとに、内容を`backup/moonraker-db/<namespace>.json`に取得し、`host-tracking`へコミットしてマージする
293
+ * キーを辞書順に並べた、整形したJSONで保存する(Moonrakerが返すキーの順が変わっても差分が出ないようにする)
294
+ * 内容が変わらなければコミットしない
295
+ * `config/`と同じく、トラッキングの考え方で扱う
296
+ * バックアップしているnamespaceは、`host-tracking`の`backup/moonraker-db/`にあるファイルの一覧で表す
297
+ * `bosun backup <namespace>...`: 指定したnamespaceを取得する(バックアップを始める)
298
+ * `bosun backup`: バックアップしているnamespaceをすべて取得し直す
299
+ * まだ何もバックアップしていない場合は、`mainsail`、`fluidd`、`mobileraker`、`maintenance`、`webcams`、`timelapse`のうち、ホストにあるものを取得する
300
+ * これらのうち、まだバックアップしていないものがホストにあれば案内する
301
+ * ホストになくなったnamespaceは取得せず、警告する(バックアップをやめる操作はまだない)
302
+ * `bosun backup --status`: namespaceごとに、バックアップしているかなどの状態を表示する
303
+ * `moonraker`、`update_manager`などは、Moonrakerが内部で管理する情報で、頻繁に変わるため推奨しない
304
+ * ホストへの書き戻し(復元)は行わない
305
+ * `status`と`audit`の同期の状態の判定には含めない
306
+
307
+ ### audit
308
+
309
+ `status`の内容に加えて、次のことを行う。
310
+
311
+ * トラッキング中のすべてのファイルについて、HとTを厳密な方法で比べる
312
+ * 不変条件を、`host-tracking`のすべてのコミットについて検査する
313
+ * マージコミット(親が2つ以上)でないこと
314
+ * `Host-Op`があり、既知の操作であること(`bosun`以外の経路で作られたコミットがないこと)
315
+ * ツリーに`config/`と`backup/`以外のファイルがないこと
316
+ * `Host-File`に記録したファイルがツリーにあり、`size`がblobのサイズと一致すること
317
+ * `Host-File`に削除(`deleted`)と記録したファイルが、ツリーにないこと
318
+ * `Host-DB`に記録したファイルがツリーにあること
319
+ * 状態ごとに、解消するための手順を案内する
320
+ * 次のいずれかがあれば、終了コードを1にする
321
+ * 不変条件の違反
322
+ * 同期済みでないファイル(トラッキングしていないファイルを除く)、名前の変更の可能性
323
+ * `host-tracking`の未マージのコミット
324
+ * 未コミットの変更は、表示するが問題としない
325
+
326
+ ## スナップショットの参照
327
+
328
+ `host-tracking`の各コミットのツリーが、その時点で最後に取得した内容の集まり(スナップショット)になる。
329
+
330
+ * `add`は指定したファイルだけを取得するので、ツリーは「その時点でホストにあったすべてのファイル」ではない
331
+ * ローカルでの変更(`main`のコミット)は含まれない
332
+
333
+ ### export
334
+
335
+ `bosun export`で、ある時点のツリーをディレクトリに書き出す。ホストにも作業ツリーにも書き込まない。
336
+
337
+ * 時点の指定(省略時は`host-tracking`の先頭)
338
+ * `--at <日時>`: その日時より前で最も新しい`host-tracking`のコミット(`git rev-list -1 --before=<日時> host-tracking`)
339
+ * gitは解釈できない日時を黙って現在の日時として扱うことがあるので、Rubyの`Time.parse`で解釈してからISO 8601でgitに渡す
340
+ * `--rev <コミット>`: そのコミットと`host-tracking`の共通の祖先(`git merge-base`)
341
+ * `host-tracking`は一直線の履歴なので、`main`のコミットを指定すると、そのコミットの時点で取り込んでいた最新の取得の時点になる
342
+ * `host-tracking`のコミットを指定すると、そのコミットになる
343
+ * 書き出す先(`-o`)
344
+ * 省略時は、カレントディレクトリの`export-<日時>-<コミットの短縮名>/`(`.gitignore`で除外している)
345
+ * すでにある場合は中止する
346
+ * ファイルを指定すると、そのファイルだけを書き出す(Root `config`からのパスか、リポジトリ上のパス)
347
+
348
+ gitで直接参照することもできる。
349
+
350
+ * 履歴: `git log host-tracking --stat`
351
+ * ある時点のファイル: `git show host-tracking~3:config/printer.cfg`
352
+ * 名前を付けて残したい時点にはタグを打つ
353
+
354
+ ## 未決事項
355
+
356
+ * `push`で、確認してからアップロードするまでの間にホスト側で変更が起きる場合
357
+ * 防げないが、印刷中でないことを確認しているので、許容する
358
+ * ホストで削除されたファイルを、ローカルの内容からホストに戻す操作
@@ -0,0 +1,108 @@
1
+ # Moonraker APIの調査結果
2
+
3
+ `bosun`の実装に必要なMoonraker APIの動作を、実機で確認した結果をまとめる。
4
+
5
+ * 確認日: 2026-10-11
6
+ * 確認した環境: Moonraker v0.11.0-3-g9e676eb(API 1.5.0)、Klipper v0.13.0-787-ge2b99a915
7
+ * プリンタの電源は入っていない状態(Klipperは`error`状態)で確認した
8
+
9
+ ## Root
10
+
11
+ `GET /server/files/roots`
12
+
13
+ | Root | ホスト側のディレクトリ | 権限 |
14
+ | --- | --- | --- |
15
+ | config | ~/printer_data/config | rw |
16
+ | logs | ~/printer_data/logs | r |
17
+ | gcodes | ~/printer_data/gcodes | rw |
18
+ | config_examples | ~/klipper/config | r |
19
+ | docs | ~/klipper/docs | r |
20
+
21
+ ## ファイルの一覧
22
+
23
+ `GET /server/files/list?root=config`
24
+
25
+ ```json
26
+ {"result": [
27
+ {"path": "printer.cfg", "modified": 1791695466.5512717, "size": 2767, "permissions": "rw"},
28
+ {"path": "mainsail.cfg", "modified": 1778097506.7109838, "size": 16493, "permissions": "r"}
29
+ ]}
30
+ ```
31
+
32
+ * `path`はRootからの相対パス。サブディレクトリ内のファイルも含めて再帰的に返る
33
+ * `modified`は秒単位の浮動小数点数で、マイクロ秒程度の精度がある(ホスト側の`st_mtime`)
34
+ * `GET /server/files/directory?path=config&extended=true`でも同じ情報が得られる(`filename`、ディレクトリ単位)
35
+
36
+ ### permissions
37
+
38
+ Moonrakerの`file_manager.py`(`get_path_info`)の実装と実機の結果から、次のように決まる。
39
+
40
+ * `rw`: 通常のファイル
41
+ * `r`: 次のいずれか
42
+ * 書き込みできないRoot(`logs`、`config_examples`など)のファイル
43
+ * **シンボリックリンク**(`mainsail.cfg`、`timelapse.cfg`)
44
+ * Moonrakerの予約パス(読み取り可)の下にあるファイル
45
+ * `""`: `.git`の下にあるファイル、または読み取りも禁止された予約パスの下にあるファイル
46
+
47
+ シンボリックリンクかどうかを直接示す項目はない。`bosun`では種類を判別せず、`permissions`が`rw`かどうかだけで判断する(design.mdの「対象のファイル」を参照)。
48
+ `modified`と`size`はリンク先のファイルの値になる。
49
+
50
+ ## ファイルのダウンロード
51
+
52
+ `GET /server/files/{root}/{path}`
53
+
54
+ * 本文がファイルの内容。`Last-Modified`ヘッダが付く
55
+ * シンボリックリンクは、リンク先の内容が返る
56
+ * 存在しない場合は`404`
57
+
58
+ ## ファイルのアップロード
59
+
60
+ `POST /server/files/upload`(multipart/form-data)
61
+
62
+ * フィールド: `root`(省略時は`gcodes`)、`file`、`path`(任意、サブディレクトリ)、`checksum`(任意、SHA-256の16進文字列)
63
+ * 同名のファイルがあれば**確認なしに上書きする**
64
+ * 新規作成と上書きのどちらでも`"action": "create_file"`が返る
65
+ * そのため、新しいファイルを作るときに同名のファイルがないことは、`bosun`の側で確認する必要がある
66
+ * `checksum`が一致しない場合は`422`
67
+ * 応答の`item`に、アップロード後の`modified`、`size`、`permissions`が含まれる
68
+ * **シンボリックリンクと同名のファイルをアップロードすると、リンク先のファイルを書き換える**(実装で確認、実機では未実施)
69
+ * `bosun`は`rw`でないファイルに書き込まないので、`mainsail.cfg`などへのアップロードは起きない
70
+
71
+ ## ファイルの削除
72
+
73
+ `DELETE /server/files/{root}/{path}`
74
+
75
+ * 成功すると`"action": "delete_file"`が返る
76
+ * 存在しない場合は`400`(`404`ではない)
77
+
78
+ ## ホスト側で作られるバックアップファイル
79
+
80
+ | 作成元 | 名前 | 例 |
81
+ | --- | --- | --- |
82
+ | Klipperの`SAVE_CONFIG` | `<名前>-YYYYMMDD_HHMMSS.cfg` | `printer-20261011_141106.cfg` |
83
+ | Moonrakerの設定の書き換え | `.<名前>.bkp` | `.moonraker.conf.bkp` |
84
+
85
+ * Klipper: `klippy/configfile.py`の`time.strftime("-%Y%m%d_%H%M%S")`
86
+ * Moonraker: `confighelper.py`の`f".{cfg.name}.bkp"`
87
+
88
+ ## 印刷状態
89
+
90
+ `GET /printer/objects/query?print_stats`
91
+
92
+ * 通常は`result.status.print_stats.state`が`standby`、`printing`、`paused`、`complete`、`cancelled`、`error`のいずれか
93
+ * Klipperが`ready`でない場合(プリンタの電源が入っていないなど)は、`print_stats`が空のオブジェクトになる
94
+ * この場合、印刷は行われていない
95
+ * Klipperの状態は`GET /printer/info`の`result.state`(`ready`、`error`、`shutdown`、`startup`)で得られる
96
+
97
+ ## データベース
98
+
99
+ `GET /server/database/list`、`GET /server/database/item?namespace=<名前>`
100
+
101
+ * namespace: `maintenance`、`moonraker`、`mainsail`、`update_manager`、`gcode_metadata`、`announcements`
102
+ * Mainsailの設定は`mainsail`にある(`dashboard`、`general`、`gcodeViewer`、`view`など)
103
+ * `moonraker`、`update_manager`、`gcode_metadata`、`announcements`は、Moonrakerが内部で管理する情報が中心
104
+
105
+ ## エラー
106
+
107
+ * エラーの場合は`{"error": {"code": ..., "message": ..., "traceback": ...}}`が返る
108
+ * `traceback`はMoonraker内部のPythonのトレースバックなので、ユーザには`code`と`message`だけを示す
data/exe/bosun ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
+ require "moonraker/bosun"
6
+
7
+ exit Moonraker::Bosun::CLI.new.run(ARGV)
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moonraker
4
+ module Bosun
5
+ class CLI
6
+ COMMANDS = {
7
+ "roots" => Commands::Roots,
8
+ "ls" => Commands::Ls,
9
+ "get" => Commands::Get,
10
+ "add" => Commands::Add,
11
+ "pull" => Commands::Pull,
12
+ "status" => Commands::Status,
13
+ "audit" => Commands::Audit,
14
+ "diff" => Commands::Diff,
15
+ "push" => Commands::Push,
16
+ "forget" => Commands::Forget,
17
+ "rm" => Commands::Rm,
18
+ "backup" => Commands::Backup,
19
+ "export" => Commands::Export
20
+ }.freeze
21
+
22
+ def initialize(out: $stdout, err: $stderr, env: ENV, cwd: nil, client: nil)
23
+ @context = Context.new(out: out, err: err, env: env, cwd: cwd, client: client)
24
+ end
25
+
26
+ # メッセージの言語は、環境変数(LC_ALL、LC_MESSAGES、LANG)で決める
27
+ def run(argv)
28
+ Messages.with_locale(Messages.locale_from_env(@context.env)) { dispatch(argv) }
29
+ end
30
+
31
+ private
32
+
33
+ def dispatch(argv)
34
+ name, *args = argv
35
+ case name
36
+ when nil, "help", "-h", "--help"
37
+ @context.out.print usage
38
+ 0
39
+ else
40
+ command = COMMANDS[name]
41
+ unless command
42
+ @context.err.puts "bosun: #{Messages.t(:"cli.unknown_command", name: name)}"
43
+ @context.err.print usage
44
+ return 1
45
+ end
46
+ command.new(@context).run(args)
47
+ end
48
+ rescue Error, SystemCallError => e
49
+ # ファイルの書き込みの失敗(Errno::EACCESなど)も、スタックトレースではなくメッセージだけを表示する
50
+ @context.err.puts "bosun: #{e.message}"
51
+ 1
52
+ rescue Interrupt
53
+ @context.err.puts "bosun: #{Messages.t(:"cli.interrupted")}"
54
+ 130
55
+ end
56
+
57
+ def usage
58
+ width = COMMANDS.keys.map(&:size).max
59
+ lines = COMMANDS.map { |name, command| format(" %-*s %s", width, name, command.summary) }
60
+ Messages.t(:"cli.usage", commands: lines.join("\n"))
61
+ end
62
+ end
63
+ end
64
+ end