fdb7a67484
- collector upload 子命令=scan+把 added/modified 原稿傳 R2(CF REST API Bearer token,key=raw/<sha256hex>,對齊 collector-trigger.v1 的 r2_key) - 冪等:存在檢查命中=skipped_exists 不 PUT;CF API objects 端點不支援 HEAD(live 實測 405)→ 改 GET+Range: bytes=0-0 - 完整性:上傳前重算 hash 核對 key,不符=failed 不上傳 - 失敗語意:failed → exit 1,ingested_hash 不動=下輪自動重試; 回寫鉤子 Manifest.MarkIngested 留給 task 4 - 設定只走環境變數 CF_ACCOUNT_ID/CF_API_TOKEN/R2_BUCKET,不落 repo - go test 14/14 綠(httptest mock 對齊真 API:HEAD 405);live e2e 全通 (arcrun-rag-raw-demo:真上傳→重傳 no-op→下載 diff 一致+sha256==key) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
119 lines
3.6 KiB
Go
119 lines
3.6 KiB
Go
// manifest.go — collector 本機 manifest 讀寫(SDD ingest-hash-trigger design §2)。
|
||
// manifest 只由 collector 寫入,雲端不得回寫(design §6-4)。
|
||
package main
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
)
|
||
|
||
// ManifestEntry 是單一檔案在 manifest 裡的狀態。
|
||
// - mtime 只決定「要不要重算 hash」(fast-path),絕不作為內容變更判準。
|
||
// - ingested_hash 與 content_hash 分開存=可表達「已變更但尚未成功 ingest」;
|
||
// 上傳失敗不更新 ingested_hash,天然可重試。本階段(不接網路)永不寫 ingested_hash,
|
||
// 留給之後的上傳/webhook task 在成功後回寫。
|
||
type ManifestEntry struct {
|
||
ContentHash string `json:"content_hash"`
|
||
Size int64 `json:"size"`
|
||
Mtime int64 `json:"mtime"`
|
||
IngestedHash string `json:"ingested_hash,omitempty"`
|
||
IngestedAt int64 `json:"ingested_at,omitempty"`
|
||
}
|
||
|
||
// Manifest 對應一個被勾選的資料夾。
|
||
type Manifest struct {
|
||
FolderID string `json:"folder_id"`
|
||
Root string `json:"root"`
|
||
Entries map[string]*ManifestEntry `json:"entries"`
|
||
}
|
||
|
||
// newUUID 產生 RFC 4122 v4 UUID(純 stdlib)。
|
||
func newUUID() (string, error) {
|
||
var b [16]byte
|
||
if _, err := rand.Read(b[:]); err != nil {
|
||
return "", err
|
||
}
|
||
b[6] = (b[6] & 0x0f) | 0x40 // version 4
|
||
b[8] = (b[8] & 0x3f) | 0x80 // variant 10
|
||
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]), nil
|
||
}
|
||
|
||
// LoadManifest 讀 manifest 檔;不存在=新資料夾,生成 folder_id。
|
||
func LoadManifest(path, root string) (*Manifest, error) {
|
||
data, err := os.ReadFile(path)
|
||
if err != nil {
|
||
if errors.Is(err, os.ErrNotExist) {
|
||
id, uerr := newUUID()
|
||
if uerr != nil {
|
||
return nil, uerr
|
||
}
|
||
return &Manifest{FolderID: id, Root: root, Entries: map[string]*ManifestEntry{}}, nil
|
||
}
|
||
return nil, err
|
||
}
|
||
var m Manifest
|
||
if err := json.Unmarshal(data, &m); err != nil {
|
||
return nil, fmt.Errorf("manifest %s 解析失敗(不覆寫、直接報錯): %w", path, err)
|
||
}
|
||
if m.Entries == nil {
|
||
m.Entries = map[string]*ManifestEntry{}
|
||
}
|
||
if m.FolderID == "" {
|
||
id, uerr := newUUID()
|
||
if uerr != nil {
|
||
return nil, uerr
|
||
}
|
||
m.FolderID = id
|
||
}
|
||
if root != "" {
|
||
m.Root = root
|
||
}
|
||
return &m, nil
|
||
}
|
||
|
||
// MarkIngested 是「整條 ingest 鏈成功」後的回寫鉤子(design §2):把該路徑的
|
||
// ingested_hash 接上當時送出的 source_hash。**R2 上傳成功不呼叫它**——上傳只是鏈的
|
||
// 第一環,要等 task 4(named-webhook → ingest workflow)確認成功才回寫;在那之前
|
||
// 同檔每輪重發 added/modified=設計內重試,R2 端靠 HEAD no-op 天然冪等、零浪費。
|
||
// 回傳 false=路徑已不在 manifest(例如回報前檔案又被改名/刪除),呼叫端自行決定忽略或告警。
|
||
func (m *Manifest) MarkIngested(path, sourceHash string, at int64) bool {
|
||
e, ok := m.Entries[path]
|
||
if !ok {
|
||
return false
|
||
}
|
||
e.IngestedHash = sourceHash
|
||
e.IngestedAt = at
|
||
return true
|
||
}
|
||
|
||
// Save 原子寫入(temp + rename),避免掃描中斷留半個 JSON。
|
||
func (m *Manifest) Save(path string) error {
|
||
data, err := json.MarshalIndent(m, "", " ")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
dir := filepath.Dir(path)
|
||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||
return err
|
||
}
|
||
tmp, err := os.CreateTemp(dir, ".manifest-*.tmp")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
tmpName := tmp.Name()
|
||
if _, err := tmp.Write(append(data, '\n')); err != nil {
|
||
tmp.Close()
|
||
os.Remove(tmpName)
|
||
return err
|
||
}
|
||
if err := tmp.Close(); err != nil {
|
||
os.Remove(tmpName)
|
||
return err
|
||
}
|
||
return os.Rename(tmpName, path)
|
||
}
|