跨機器設定同步
je_auto_control.utils.config_sync 讓同一位使用者的快捷鍵、觸發器、通訊錄與其他小型設定
在多台機器上保持一致。每位使用者在同步 server 上有一個 bucket;signaling server
(python -m je_auto_control.utils.remote_desktop.signaling_server,需要 [signaling] extra)
以 GET / PUT /config/{user_id} 提供它。
腳本需要的名稱都在套件門面上:import je_auto_control as ac 之後有
ac.config_sync_run 與另外三個入口、ac.run_sync、ac.ConfigSyncClient、
ac.ConfigBucket、ac.SyncOutbox、ac.SyncEntry、ac.SyncOperation、
ac.SyncAdapter 與五個具體 adapter(ac.ScriptSyncAdapter ……)、
ac.DirectoryAssetTransport、ac.HttpAssetTransport、ac.ConfigStore、ac.BlobStore、
各個錯誤(ac.ConfigSyncError、ac.ConfigSyncConflict、ac.FullResyncRequired、
ac.OperationMismatchError、ac.ConfigStoreError、ac.RevisionConflictError)、
blob 清理(ac.config_sync_collect_blobs、ac.collect_unreferenced_blobs、
ac.referenced_blob_digests)
以及 ac.CONFIG_SYNC_WIRE_VERSION``(模組內的 ``WIRE_VERSION,值為 2)。
其餘的仍可從 je_auto_control.utils.config_sync 匯入。
Server:持久化、檢查修訂版的 bucket
Bucket 由 je_auto_control.utils.config_sync.store.ConfigStore 存在一個 SQLite 檔案,
server 重啟後仍在。檔案位置依序取自 --config-db PATH、環境變數
AC_SIGNALING_CONFIG_DB、預設的 ~/.je_auto_control/config_sync.sqlite3;
在第一個 /config 請求時才開啟,不是啟動時。內嵌 app 時傳
create_app(config_store_path=...)。
每個 bucket 有 server 指派的 revision(第一次寫入為 1,之後每次 commit +1)。
寫入必須指明它所依據的 revision,比較與寫入在同一個 SQLite transaction 內完成:
from je_auto_control.utils.config_sync.store import ConfigStore, RevisionConflictError
store = ConfigStore("buckets.sqlite3")
revision = store.commit("alice", bucket, base_revision=0, operation_id="op-1") # -> 1
store.commit("alice", bucket, base_revision=0, operation_id="op-1") # -> 仍是 1
store.commit("alice", other, base_revision=0, operation_id="op-2") # RevisionConflictError
重複的 operation_id 會回傳第一次 commit 產生的 revision 且不寫入任何東西,
所以沒收到回覆的 client 直接重送同一個請求即可。每位使用者保留最近 256 個 operation id。
一個 operation id 只代表 一次 寫入。Store 會記下每個 id 寫入內容的 SHA-256
(base revision 加上去掉 revision 欄位的 bucket),同一個 id 帶著不同的 bucket 或
base revision 送來時丟出 OperationMismatchError(.operation_id、第一次寫入的
.revision)且不寫入 —— 以前會被當成已提交來回覆。沒有記 hash 的舊版所留下的 id
仍照舊方式回覆;既有資料庫在第一次使用時補上這個欄位。
傳輸格式(version 2)
請求 |
回覆 |
|---|---|
|
|
|
|
|
|
base_revision 為 0 表示「還沒有 bucket」。共享密鑰(X-Signaling-Secret)、
1 MiB body 上限(413,讀取 body 前檢查)、必須帶 Content-Length``(``411)與
1,024 位使用者上限(503)都不變。Bucket 現在在進入時驗證:section 必須是
entry id 對應到物件的 mapping。
早於 version 2 的 client 送出裸 bucket。面對預設模式的 server,它們的 push / sync
會以 ConfigSyncError: config sync PUT returned HTTP 428 失敗且不寫入任何東西;
fetch 照常運作(回覆欄位相同,多一個它們會忽略的 version)。要繼續服務它們,
以 --allow-blind-config-writes 啟動 server,並接受兩個這種 client 同時 push 仍可能互相覆寫。
version-2 client 面對舊 server 會被明確告知:回覆沒有 revision, push 丟出
ConfigSyncError(「predates revision-checked writes」)—— 請先升級 server。
Client
from je_auto_control.utils.config_sync import ConfigBucket, ConfigSyncClient, ConfigSyncConflict
client = ConfigSyncClient("https://sync.example", user_id="alice", secret="...")
merged, conflicts = client.sync(local_bucket) # fetch、merge、在所 fetch 的版本上 push
print(merged.revision) # server 提交的 revision
sync 以 fetch 到的 revision 作為 base_revision push;若期間有另一台機器 push,
它會重新 fetch 與 merge,最多 max_attempts``(預設 4)次,之後丟出 ``ConfigSyncConflict。
每次 sync 也會把 client 的裝置
(ConfigSyncClient(..., device_id=...),預設為本機儲存的 id)記在 bucket 的 peers 下,
表示它已合併所提交的 revision,並移除每台裝置都看過的 tombstone —— 和 push_operations
做的記帳相同。被群組退休的裝置呼叫 sync 同樣會得到 FullResyncRequired。
push(bucket) 以 bucket.revision 為基準,回傳已提交的 revision;落後時丟出
ConfigSyncConflict``(.revision`` 為 server 目前的 revision)而不是覆寫。
code 為 operation_mismatch 的 409 則丟出 OperationMismatchError;
它是 ConfigSyncError 但不是 ConfigSyncConflict,所以 sync 不會重新 fetch 重試。
還不認得這個 code 的 client 會把這個回覆當成一般的衝突。
因果合併:時鐘不決定勝負
帶裝置 id 寫入的 entry 是 versioned
(je_auto_control.utils.config_sync.versions.SyncEntry):它帶有 version vector
(每台裝置的變更有幾次已包含在內)、origin 裝置與 operation_id。
merge_entries(left, right) 回傳 MergeDecision:
知道 對方而做出的那一筆取代對方 —— 不回報任何東西;
分開進行、針對同一 key 的兩個變更 兩份都保留:
decision.conflict是SyncConflict(key, local, remote),``decision.entry`` 以siblings保存候選, 直到有人呼叫entry.resolved(value, origin)。與刪除並行的編輯同樣是衝突;不同 key 的變更不會衝突(
merge_collections)。
modified_at 只供顯示。無論各機器的時鐘怎麼說,決定都相同,而且每台機器算出的結果一致。
bucket.upsert("hotkeys", "hk1", {"combo": "ctrl+a"}) # versioned,由本機寫入
bucket.upsert("hotkeys", "hk1", {"combo": "ctrl+b"}, origin="laptop") # ……或指名裝置
bucket.remove("hotkeys", "hk1") # versioned tombstone
bucket.values("hotkeys") # 有效值;仍在衝突中的 entry 不列入
bucket.conflicts() # [(section, 帶 .siblings 的 SyncEntry), ...]
origin 預設為本機的裝置 id(default_device_id(),存在
~/.je_auto_control/config_sync_device_id),所以在 version vector 之前寫的程式 ——
bucket.upsert(section, id, value) 與 client.sync(bucket) —— 不必修改就是因果合併。
傳給 upsert 的值裡若有 last_modified,它會成為 entry 的顯示時間,不存進值裡。
對扁平 entry 呼叫 remove 會寫入 versioned tombstone,並取代所有扁平副本。
Warning
這改變了 upsert 儲存的內容。Entry 現在是
{"value": {...}, "vector": {...}, "origin": ..., "operation_id": ...,
"deleted": false, "last_modified": ...} 而不是值本身,所以請用
bucket.values(section)(兩種形狀都適用)讀值,不要用
bucket.sections[section][id]["field"]。要繼續寫扁平 entry,對 upsert / remove
傳 versioned=False;兩筆扁平 entry 仍以「較晚者勝」合併(回報於 ConflictRecord),
versioned entry 會取代同 id 的扁平副本。因果衝突的 ConflictRecord.unresolved 為 true,
此時沒有任何東西被丟棄。
既有 bucket 在第一次用這個版本同步之後。 本機沒碰過的扁平 entry 原封不動
(位元組相同,彼此之間仍是「較晚者勝」)。本機新增、編輯或移除的 entry 以 versioned 形式儲存,
vector 為 {"<裝置 id>": n}。peers 多出
{"<裝置 id>": {"acked_revision": <提交的 revision>, "last_seen": ..., "retired": false}}。
這個版本之前的 version-2 client 讀得懂全部內容:它本來就認得兩種 entry 形狀,
不認得的 peer 會忽略。Tombstone 只會 等 已列在 peers 下的裝置,還沒同步過的機器改由下面說明的
保留期涵蓋:刪除會在 bucket 裡留三十天,這樣的機器在這段時間內第一次同步時會遇到它。
請讓每台機器在刪除後的三十天內同步一次,或是在刪除之前就先同步過。
刪除與退休裝置
刪除是一個 tombstone。Versioned tombstone 絕不因時間而丟棄:collect_tombstones
只在 bucket 的 peers 記錄的每台有效裝置都已確認包含它的 revision 之後才移除。
ConfigSyncClient.push_operations 負責維護這些確認。
為 bucket 還沒列出的機器保留。 確認只涵蓋 peers 下的裝置。手上有那筆 entry、
但從沒同步過的機器不在其中;tombstone 一旦被移除,它的第一次同步就會把 entry 直接加回來,
沒有衝突也沒有回報。所以已被確認的 tombstone 還會從第一次帶著它的那次提交起再保留
TOMBSTONE_HOLD_S``(三十天)。在這段時間內加入的機器會遇到這筆刪除:它的 versioned 副本變成
**衝突**(刪除與副本並存,由人用 ``config_sync_resolve 決定),扁平副本則維持刪除並回報於
ConflictRecord。保留期過後,任何裝置的下一次提交會移除 tombstone,所以 bucket 不會無限成長;
比這更晚才第一次同步的機器仍然可能把 entry 帶回來。
第一次帶著 tombstone 的那次提交,會在 deleted_revision 旁邊蓋上 deleted_at``(該裝置的時鐘)。
時間只會是 *多留一陣子* 的理由:時鐘不準只會讓保留期變短或變長,不會影響對已列出裝置的等待。
``ConfigSyncClient(..., tombstone_hold_s=0) 可讓該 client 提交的 bucket 回到先前的規則;
collect_tombstones(entries, peers, now=..., hold_s=...) 只有在給了 now 時才套用保留期。
這個版本之前蓋章的 tombstone 沒有 deleted_at,和以前一樣在確認後就移除;那樣的版本也不會保留
已確認的 tombstone,所以保留期的效果取決於仍在提交的最舊 client。
裝置在第一次提交變更時加入 peers。bucket 還沒列出的裝置呼叫 push_operations([])
不會寫入任何東西,不論 bucket 有沒有 entry:只讀取 bucket 的裝置不會為了加入而提交 revision,
空帳號的第一次同步也不會。(先前對已有內容的 bucket 加入時會寫一個 revision,
讓之後的刪除等這台裝置。)
還沒被列出的裝置仍然得到什麼、得不到什麼。 裝置未被列出期間發生的刪除,透過保留中的
tombstone 送達:三十天內,它沒動過的副本會被移除,期間編輯過的副本會變成衝突。保留期過後
tombstone 就不在了。裝置沒動過的副本仍然會被移除 —— 它自己的基準記錄了那筆 entry 來自
bucket,所以 entry 不見就是別處做的刪除。不再保證的是編輯過的副本:一台只讀取過 bucket
的裝置,編輯了某筆 entry,並在該 entry 於別處被刪除超過 TOMBSTONE_HOLD_S 之後才第一次
送出這個編輯,會在沒有衝突的情況下把 entry 重新建立。有加入 revision 時,tombstone
會無限期等這台裝置,那個編輯會是衝突。補救方式和從未同步過的機器相同:讓每台機器至少每三十天
同步一次,或在離線之前先提交一次變更。
不會再回來的裝置以 client.retire_peer(device_id) 退休(或以
push_operations(..., max_offline_s=...) 自動退休,它比較 last_seen ——
這是唯一用到時鐘的地方,而且從不在兩個值之間做決定)。Tombstone 不再等待已退休的裝置,
所以它回來時 push 會被 FullResyncRequired 拒絕;它必須呼叫
client.full_resync(device_id=...),以回傳的 bucket 取代本機狀態並丟棄待送操作。
改用合併可能讓已刪除的 entry 復活。
離線 outbox
SyncOutbox(db_path, account=..., endpoint=...) 是尚未送達 server 的
SyncOperation(section, entry) 的 SQLite 佇列(預設檔案
~/.je_auto_control/config_sync_outbox.sqlite3;一個檔案可容納多個帳號與 endpoint,彼此隔離)。
from je_auto_control.utils.config_sync import SyncEntry, SyncOperation, SyncOutbox
outbox = SyncOutbox(account="alice", endpoint="https://sync.example")
outbox.enqueue(SyncOperation("hotkeys", SyncEntry.create("hk1", {"combo": "ctrl+a"}, "laptop")))
report = outbox.drain(
lambda batch: client.push_operations(batch, device_id="laptop"), cancel=stop_event)
# DrainReport(sent, pending, attempts, offline, cancelled, error)
操作以 operation id 入列(同一筆入列兩次不會重複)、重啟後仍在,並以相同 id 重送 ——
套用 server 已有的批次不會改變任何東西。送出失敗時退避 2 秒、4 秒、8 秒……最多 300 秒
(base_delay_s / max_delay_s),一次 drain 最多送 max_attempts 次(預設 5),
wait=False 會直接返回而不在退避期間睡眠,設定 cancel 即使在等待中途也會結束 drain。
佇列仍在退避期間時,drain 不送任何東西,回傳
DrainReport(backing_off=True, retry_in_s=<剩餘秒數>);``offline`` 同時為 true(佇列沒有送出去),
error 是造成等待的那次失敗。drain(..., force=True) 不管延遲直接送一次;
如果這次也失敗,就遵守它換來的(更長的)延遲。
Adapter:同步什麼、什麼留在本機
je_auto_control.utils.config_sync.adapters 把一個 bucket section 接到一個本機儲存。
SyncAdapter.snapshot() 回傳以上次合併狀態為基準的 {key: SyncEntry},
apply(entries) 把合併後的 entry 寫回並回傳 ApplyReport
(written / removed / skipped / conflicts / left_disabled)。
Section |
Adapter |
本機儲存 |
|---|---|---|
|
|
資料夾下的 |
|
|
一個 |
|
|
|
|
|
|
|
|
遠端桌面的 |
每個 adapter 都遵守三條規則:
秘密留在本機。 名稱顯示為秘密的欄位(
password、token、api_key……) 以{"$local": "secret"}送出;接收端保留自己的值。${secrets.NAME}參照原樣傳送。 含有明文秘密的腳本完全不同步,並列在withheld。機器路徑留在本機。
scripts_dir內的路徑以相對參照傳送,並對接收端自己的資料夾解析。 其他路徑變成{"$local": "path"};需要它而本機沒有對應值的項目回報在skipped—— 而且它在本機不存在 不會 被當成對其他機器的刪除。同步絕不啟用任何東西。
enabled不同步。送達的快捷鍵或觸發器以 停用 狀態建立 (HotkeyDaemon.bind(..., enabled=False)),既有的保留本機給它的狀態, 沒有任何 adapter 會啟動引擎或執行腳本。
複合觸發器。 AllOfTrigger / AnyOfTrigger / SequenceTrigger 以自己的欄位
加上一個 children 定義清單傳送,巢狀幾層就傳幾層:
{"type": "AllOfTrigger", "repeat": true, "cooldown_seconds": 30.0,
"script_path": {"$local": "path", "root": "scripts", "relative": "report.json"},
"children": [
{"type": "CronTrigger", "trigger_id": "at-nine", "cron": "0 9 * * *", "...": "..."},
{"type": "ImageAppearsTrigger", "trigger_id": "logo", "threshold": 0.9,
"image_path": {"$local": "path", "root": "scripts", "relative": "logo.png"}}]}
子觸發器保留它的 trigger_id,路徑和其他路徑一樣轉成可攜形式;它的 enabled、fired 與
script_path 在複合觸發器內沒有意義,不會送出。複合觸發器以 停用 狀態建立,子觸發器也是停用;
之後的更新會保留本機對 enabled 的選擇。定義會先完整建好才加入引擎,所以只要有一個子觸發器不對
(不認得的型別、無效的 cron、超過 64 個子觸發器、超過 8 層)整個複合觸發器就跳過,並回報在 skipped。
含有上述型別以外的子觸發器的複合觸發器留在本機,列在 withheld。註冊觸發器的 RBAC 身分
(owner)絕不送出。執行較舊版本的機器收到複合觸發器時會回報在 skipped
(「unknown trigger type」),bucket 裡的 entry 不受影響。
資產
64 KiB 以內的腳本連同它的 SHA-256 放在 entry 內傳送;內容與 hash 不符就不寫入。
更大的腳本與其他檔案(樣板圖)經由 AssetTransport 傳送:
from je_auto_control.utils.config_sync.assets import (
AssetManifest, DirectoryAssetTransport, publish_assets, sync_assets)
transport = DirectoryAssetTransport("//nas/autocontrol-assets") # 兩台機器都能存取的資料夾
manifest = AssetManifest.from_directory("scripts", ("*.png",))
publish_assets(manifest, transport) # 傳送端
result = sync_assets(AssetManifest(root=their_scripts, assets=manifest.assets), transport)
# AssetSyncResult(transferred, unchanged, failed, hashes, cancelled)
sync_assets 在寫入 之前 以 SHA-256 與大小檢查每個檔案、以原子方式取代目的檔、
檢查失敗時不動既有檔案,並拒絕會離開資料夾的路徑。隨附兩種 transport;
其他 blob 儲存請實作 fetch / store / has。
Transport |
適用情況 |
|---|---|
|
兩台機器都能存取同一個資料夾(共用磁碟、掛載的 bucket);
|
|
兩台機器只共用同步 server;
|
同時給 assets_dir 與 assets_server 是錯誤。
同步 server 上的 blob。 Signaling server 以「依內容定址、依帳號分開」的 blob 保存資產:
請求 |
回覆 |
|---|---|
|
|
|
|
|
|
|
|
|
|
規則和 /config 相同:每個請求都要帶共享密鑰(X-Signaling-Secret,否則 401);
帳號就是路徑中的那個,所以一個帳號無法讀取、列出或刪除另一個帳號的 blob;
大小上限在讀取 body 之前就以 Content-Length 檢查。除此之外每個帳號還有 總配額。
設定 |
預設 |
旗標 / |
|---|---|---|
單一 blob 上限 |
16 MiB |
|
每個帳號的總量 |
256 MiB |
|
blob 存放位置 |
config 資料庫路徑加上 |
|
持有 blob 的帳號數 |
1,024 |
– |
資料夾在第一次上傳時才建立,不是啟動時。儲存由
je_auto_control.utils.config_sync.blobs.BlobStore 負責(put / get / has /
delete / usage);每個帳號的 blob 放在以帳號 id 的 hash 命名的資料夾,blob 先寫到暫存檔再改名。
寫入時會持有鎖檔 <blob 資料夾>/store.lock``(和共用 JSON store 相同的 helper),
所以配額檢查與寫入對 **每一個** 共用該資料夾的 server 行程都是同一步,不只是單一行程內的執行緒。
鎖被另一個行程持有超過十秒時是 ``BlobStoreBusyError``(``503),不寫入任何東西;
行程當掉留下的鎖會在三十秒後被接手。
HttpAssetTransport 經由 je_auto_control.utils.http_client``(egress policy 適用),不跟隨轉址。
被拒絕的上傳會說明原因 —— 太大、配額用完、密鑰錯誤、或 server 還沒有 ``/blobs ——
publish_assets 會把它逐檔回報在 failed,不會中斷其他檔案。/blobs 路由是新增的:
/config 的傳輸格式仍是 version 2,不使用它們的 client 不受影響。
「太大」在上傳之前就決定。 Server 依宣告的長度拒絕過大的 PUT 並關閉連線;
還在送 body 的 client 可能先看到連線被重設而不是 413,失敗就被讀成連線錯誤。
所以 transport 會在第一次上傳前從清單讀出 max_blob_bytes
(transport.max_blob_bytes(),只問一次並記住),比它大的檔案由本機直接拒絕、不送出:
asset PUT <sha256>: the file is larger than the server accepts for one blob
(<size> bytes; the limit is <limit>)。
Client |
Server |
過大的檔案 |
|---|---|---|
這個版本 |
回報 |
在本機以上述訊息拒絕;不送出 body |
這個版本 |
沒有回報,或清單讀不到 |
照常送出;server 的 |
這個版本 |
完全沒有 |
|
較早的版本 |
任何 |
和以前一樣: |
清理。 Server 上沒有任何東西會自動移除 blob,所以被刪除或被取代的腳本會一直佔用配額。
config_sync_collect_blobs(server_url, user_id, keep=None, min_age_s=86400,
dry_run=False, **options) 刪除該帳號已經沒有任何東西引用的 blob,並回傳
{"deleted": [sha256...], "freed": bytes, "kept": n, "recent": [sha256...],
"failed": {sha256: 原因}, "dry_run": bool, "referenced": n}。會保留的有:
server 的 bucket、本機上次合併的狀態、以及本機還在 outbox 等待送出的變更中, 任何 entry 指名的 SHA-256(任何 section,包含仍在衝突中的每個候選;``referenced_blob_digests``);
keep裡的 digest(list 或一個逗號分隔的字串)—— 自己用publish_assets發佈、 沒有任何 bucket entry 引用的內容請列在這裡;存入不到
min_age_s的 blob(預設一天):機器會在提交引用它的 entry 之前先上傳檔案。 年齡用的是 server 的age_s,不會比較兩台機器的時鐘。
dry_run=True 不刪除任何東西,只列出會被刪的。連不到 server 是 ConfigSyncError,
不會刪除任何東西;刪不掉的 blob 列在 failed,其餘照常處理。某台機器還有檔案時被移除的 blob,
會在那台機器下次同步時重新上傳。面對還沒有 age_s 的 server,除非 min_age_s=0 否則什麼都不收。
collect_unreferenced_blobs(transport, referenced, min_age_s=..., dry_run=...)
是對你自己提供的 digest 集合做同樣的清理;它只接受 HttpAssetTransport,因為
DirectoryAssetTransport 背後的資料夾可能有其他使用者的檔案。
一個呼叫,三個介面
config_sync_run(server_url, user_id, **options) 執行整個循環 —— 把本機變更入列、
清空 outbox、合併、套用 —— 並回傳
{state, revision, pending, conflicts, applied, withheld, assets, error, retry_in_s},
state 為 synced / pending / conflict / offline / backing_off /
cancelled / resync_required 之一。連不到 server 不是例外:變更留在佇列,狀態為 offline。
狀態 |
意義 |
|---|---|
|
這次執行連過 server 而且失敗;``error`` 說明原因,``retry_in_s`` 是佇列自己再送一次的時間 |
|
這次執行 沒有 連 server:先前的失敗還在退避期間(2 秒起、加倍、最長 300 秒)。
現在 server 的狀況未知。 |
以前兩者都回報 offline,所以網路剛恢復時要求同步,看起來就像 server 還沒好。
傳 force=True 可以跳過一次延遲(GUI 的 立即同步 會這樣做),或傳 wait=True 睡到延遲結束。
config_sync_status 的 retry_in_s 是即時計算的,延遲結束後顯示 pending 而不是 backing_off。
選項:device_id``(預設:在 ``~/.je_auto_control/config_sync_device_id 建立一次的 id)、
secret``(預設 ``$AC_SIGNALING_SECRET)、sections``(見下)、``assets_server、scripts_dir、
locators_path、outbox_path、assets_dir、timeout_s、wait、force、
max_attempts。
一次同步涵蓋哪些 section。 由 resolve_sections(sections, scripts_dir=...,
locators_path=...) 決定,回報的 sections 會列出結果。
傳入 |
同步的 section |
|---|---|
什麼都不傳 |
|
|
|
|
以上四個再加 |
|
只有 |
|
正好這兩個 |
路徑是把它的 section 加到 預設的三個上,不是把同步縮小到它,所以只傳 scripts_dir
也會同步本機的快捷鍵、觸發器與通訊錄。這個預設是刻意的 —— 快捷鍵與觸發器需要那個資料夾
才能把腳本路徑變成可攜的參照 —— 所以維持不變;
想少同步一些就指名 sections``(或在 GUI 分頁取消勾選 section)。``sections 可以是 list 或一個逗號分隔的字串。
不認得的名稱、缺少路徑的 section、以及什麼都沒指名的選擇([]、",")都是
ConfigSyncError;同一個名稱寫兩次只同步一次。
Executor 指令 |
MCP 工具 |
作用 |
|---|---|---|
|
|
同步一次 |
|
|
已記錄的狀態;不連網、不建立任何東西 |
|
|
保留 |
|
|
被退休後採用 server 的狀態;待送變更會被捨棄並列出 |
|
|
刪除 server 上已沒有 entry 指名的 blob( |
五個都是 Script Builder Data 分類下的指令,而且形狀一致:
(server_url, user_id, ..., **options),options 即上面列的那些。
config_sync_status(server_url, user_id, outbox_path=None, **options) 只讀 outbox_path
(仍可用位置參數傳);其他選項會被接受,讓同一個 options dict 可以傳給每一個呼叫,
不認得的名稱和其他地方一樣是 ConfigSyncError。
[["AC_config_sync_run", {"server_url": "https://sync.example", "user_id": "alice",
"secret": "${secrets.sync}", "scripts_dir": "scripts"}]]
GUI
設定同步 分頁(分類 system)顯示狀態(退避期間會附上距離下次自動重試的秒數)、最後合併的 revision、待送變更數、
上次成功同步與最後的錯誤,並列出每個衝突及其候選。它的指令 —— 立即同步、取消同步、
重新整理同步狀態、保留所選候選、完整重新同步、刪除伺服器上未使用的 blob
(會先確認;以預設值執行 config_sync_collect_blobs)—— 在 Actions 選單。
同步在 worker 執行緒上執行;取消或關閉分頁都會釋放它。
輸入欄位有 server URL、使用者 id、共享密鑰、腳本資料夾、定位器儲存庫檔案與共享資產資料夾,另外還有:
要同步的區段 ——
SYNCABLE_SECTIONS的每個 section 一個核取方塊。全部勾選時分頁不傳sections,套用上面的預設。取消勾選任何一個,就把勾選的那些當成sections傳入; 勾選了但路徑是空的 section 會被略過而不是拒絕,若一個都不剩,分頁會說明並且不同步。 狀態會列出上次同步涵蓋的 section。把大型腳本存放在同步伺服器上 —— 傳
assets_server=True,並把它取代的共享資產資料夾欄位反灰。
除了密鑰之外,其餘都會記在 GUI 設定檔裡(WindowSettings.load_form / save_form,
表單 config_sync),下次開啟時還原。
資料夾鏡像與剪貼簿:不回送
FolderSyncEngine.note_received(remote_name, sha256=...) 把監看資料夾中的檔案標記為
來自對端;在內容於本機變更之前,引擎不會把它推回去。FolderSyncEngine.poll_once()
可隨時執行一次差異比對。
接收端會替你呼叫它。FileTransferReceiver``(WebRTC)與 ``FileReceiver``(TCP)把檔案先寫成
``.<name>.<id>.part,在改名到定位之前呼叫 file_sync.note_incoming(final_path, part_path):
每個資料夾包含該檔案、而且還存在的引擎都會記下內容的 SHA-256。所以收進鏡像資料夾的檔案不會被送回去,
呼叫端不需要接線;沒有引擎鏡像那個資料夾時,檔案連 hash 都不會算。
FolderSyncEngine.relative_name(path) 回報引擎是否涵蓋某個路徑(子資料夾需要 include_subdirs)。
什麼算是變更。 引擎記下每個檔案的修改時間與大小,兩者任一和紀錄不同就推送 ——
不再只看時間是否 較晚,所以被換回較舊時間的檔案也會推送。光靠時間分不出同一個時間刻度內的兩次寫入
(剛把收到的檔案記下之後緊接著的本機編輯,以前不會被送出),所以在檔案自己最後一次寫入後
RACY_WINDOW_S(2 秒)內被記錄的檔案,還會以 SHA-256 記下內容,在這段時間過去之前以內容比對。
較舊的檔案不會為了判斷是否變更而被讀取。
還在寫入的檔案。 變更過的檔案要連續兩次輪詢看到相同的大小與修改時間才會推送,
所以大檔案的複製會晚一次輪詢整個送出,而不是現在就送出截斷的前半段(一直在變的檔案,例如
寫入中的 log,要等它停下來才會推送)。傳 wait_until_stable=False 可回到先前「一看到就推」的行為。
名稱以 .part、.partial、.tmp 或 .crdownload 結尾的檔案
(IN_PROGRESS_SUFFIXES;可用 ignore_suffixes= 覆寫)永遠不鏡像 —— 包含接收端自己的 part 檔,
它們以前會在傳輸進行中被推送。先用這類名稱寫、寫完再改名的程式,會被完整地撿起來。
ClipboardEchoGuard``(``note_remote / note_sent / should_send / reset)
為剪貼簿內容做同樣的事。RemoteDesktopHost 為每個連線中的 viewer 各持有一個,
RemoteDesktopViewer 為它的 host 持有一個(每次連線時重設);收到的 CLIPBOARD 訊息會在套用之前先記下。
host.broadcast_clipboard_text(text, automatic=True) 與
viewer.send_clipboard_text(text, automatic=True)``(以及 ``_image 版本)是給自動轉送剪貼簿
變更 的程式用的:剛從那個對端送達、或已經送給它的內容不會再送一次 —— 以 viewer 為單位,
所以某個 viewer 送來的內容仍會送到其他 viewer。不帶 automatic``(使用者按「送出剪貼簿」)時一律送出並記下。
``broadcast_clipboard_* 回傳送達的 viewer 數;``send_clipboard_*`` 現在回傳是否有送出。
GUI 只在按鈕按下時送剪貼簿,所以目前沒有任何地方傳 automatic=True。