Download internal/syncer/state.go from a3216/gcli2api: direct link, hf CLI and curl.
- Browser
- Download file 7.26 kB
-
https://huggingface.co/spaces/a3216/gcli2api/resolve/main/internal/syncer/state.go
- Command line
-
hf download hf://spaces/a3216/gcli2api/internal/syncer/state.go
-
curl -L -o state.go https://huggingface.co/spaces/a3216/gcli2api/resolve/main/internal/syncer/state.go
7.26 kB
| package syncer | |
| import ( | |
| "crypto/sha256" | |
| "encoding/hex" | |
| "fmt" | |
| "os" | |
| "path" | |
| "path/filepath" | |
| "sort" | |
| "strings" | |
| "time" | |
| ) | |
| // 分类常量:决定用哪套合并规则(凭证看 expiresAt,普通数据看 mtime)。 | |
| const ( | |
| catAuth = "auth" | |
| catData = "data" | |
| ) | |
| // FileState 是一个文件在某一端的可比较状态。 | |
| // | |
| // Content 只在需要时填充(本地端天然有;远端端按需下载)。比较优先用 | |
| // SHA256,避免为了判断"是否相同"而多跑一次下载。 | |
| type FileState struct { | |
| Path string `json:"path"` | |
| Size int64 `json:"size"` | |
| SHA256 string `json:"sha256"` | |
| Mtime time.Time `json:"mtime"` | |
| // Content 是文件原始字节,**绝不能被序列化出去**。 | |
| // | |
| // 它只用于本地合并与上传;而 Action 会被 `wb2api-sync -json` 整个打印出来, | |
| // 一旦让 Content 进 JSON,`status -json` 就会把 accessToken / refreshToken | |
| // 明文(base64)灌进终端、CI 日志或 issue 附件里——等于自己泄露凭证。 | |
| Content []byte `json:"-"` | |
| // 仅 auth 文件填充:这三项是"谁的凭证更新"的判据。 | |
| IsAuth bool `json:"is_auth,omitempty"` | |
| UID string `json:"uid,omitempty"` | |
| Nickname string `json:"nickname,omitempty"` | |
| ExpiresAt int64 `json:"expires_at,omitempty"` | |
| HasRefresh bool `json:"has_refresh,omitempty"` | |
| Realm string `json:"realm,omitempty"` | |
| } | |
| // hasContent 报告该状态是否已带内容。 | |
| func (f *FileState) hasContent() bool { return f.Content != nil } | |
| // scanLocal 扫描本地待同步文件,key 为桶内相对路径(如 auths/workbuddy-x.json)。 | |
| // | |
| // 只扫顶层 *.json: | |
| // - auths/ —— 与网关 auth.LoadDir 的口径一致(它也只解析一层); | |
| // - data/ —— 只取 IncludeData 白名单。 | |
| // | |
| // 子目录(如 data/request-logs/)一律不扫:请求归档是本机观测数据, | |
| // 互相搬运既无意义又会把桶撑大。 | |
| func (e *Engine) scanLocal() (map[string]*FileState, error) { | |
| out := map[string]*FileState{} | |
| if e.cfg.AuthDir != "" { | |
| files, err := readTopLevelJSON(e.cfg.AuthDir) | |
| if err != nil { | |
| return nil, err | |
| } | |
| for _, name := range files { | |
| fp := filepath.Join(e.cfg.AuthDir, name) | |
| st, err := statLocalFile(fp, "auths/"+name, catAuth) | |
| if err != nil { | |
| // 单个坏文件不阻断整体同步(与 auth.LoadDir 的容错口径一致)。 | |
| continue | |
| } | |
| out[st.Path] = st | |
| } | |
| } | |
| for _, name := range e.cfg.IncludeData { | |
| if e.cfg.DataDir == "" || name == "" || strings.ContainsAny(name, `/\`) { | |
| continue | |
| } | |
| fp := filepath.Join(e.cfg.DataDir, name) | |
| st, err := statLocalFile(fp, "data/"+name, catData) | |
| if err != nil { | |
| continue // 本地还没有这个文件是正常态(首跑) | |
| } | |
| out[st.Path] = st | |
| } | |
| return out, nil | |
| } | |
| // statLocalFile 读取并按 category 解析一个本地文件。 | |
| func statLocalFile(fp, relPath, category string) (*FileState, error) { | |
| raw, err := os.ReadFile(fp) | |
| if err != nil { | |
| return nil, err | |
| } | |
| info, err := os.Stat(fp) | |
| if err != nil { | |
| return nil, err | |
| } | |
| st := newFileState(relPath, raw, info.ModTime(), category) | |
| return st, nil | |
| } | |
| // newFileState 由内容 + mtime + 分类构造 FileState。 | |
| func newFileState(relPath string, raw []byte, mtime time.Time, category string) *FileState { | |
| sum := sha256.Sum256(raw) | |
| st := &FileState{ | |
| Path: relPath, | |
| Size: int64(len(raw)), | |
| SHA256: hex.EncodeToString(sum[:]), | |
| Mtime: mtime, | |
| Content: raw, | |
| } | |
| if category == catAuth { | |
| annotateAuth(st, raw) | |
| } | |
| return st | |
| } | |
| // annotateAuth 给 auth 文件标注比较维度(uid / expiresAt / refreshToken 有无)。 | |
| // | |
| // 用自带的容错解析而不是网关的 auth.Parse:启动器要做成独立 exe,不能和网关 | |
| // 内部包绑死(详见 cred.go 的说明)。解析不出来就退化成纯字节比较,不是错误。 | |
| func annotateAuth(st *FileState, raw []byte) { | |
| c, ok := parseCredential(raw) | |
| if !ok { | |
| return | |
| } | |
| st.IsAuth = true | |
| st.UID = c.UID | |
| st.Nickname = c.Nickname | |
| st.ExpiresAt = c.ExpiresAt | |
| st.HasRefresh = c.HasRefresh | |
| st.Realm = c.Realm | |
| } | |
| // readTopLevelJSON 返回 dir 下顶层的 *.json 文件名(升序,结果稳定)。 | |
| // dir 不存在返回空集而非错误——本地首次运行时 auths/ 还没建。 | |
| func readTopLevelJSON(dir string) ([]string, error) { | |
| entries, err := os.ReadDir(dir) | |
| if err != nil { | |
| if os.IsNotExist(err) { | |
| return nil, nil | |
| } | |
| return nil, err | |
| } | |
| var out []string | |
| for _, ent := range entries { | |
| if ent.IsDir() || !strings.HasSuffix(strings.ToLower(ent.Name()), ".json") { | |
| continue | |
| } | |
| out = append(out, ent.Name()) | |
| } | |
| sort.Strings(out) | |
| return out, nil | |
| } | |
| // --------------------------------------------------------------------------- | |
| // 写入 | |
| // --------------------------------------------------------------------------- | |
| // writeLocalAtomic 原子写入本地文件(tmp + rename),与网关自身的落盘约定一致。 | |
| // | |
| // 权限 0600:auths/ 里是明文 refreshToken,不能给组/其他用户读。 | |
| func writeLocalAtomic(fp string, content []byte, mtime time.Time) error { | |
| dir := filepath.Dir(fp) | |
| if err := os.MkdirAll(dir, 0o755); err != nil { | |
| return fmt.Errorf("创建目录 %s 失败: %w", dir, err) | |
| } | |
| tmp := fp + ".tmp" | |
| if err := os.WriteFile(tmp, content, 0o600); err != nil { | |
| return fmt.Errorf("写入 %s 失败: %w", tmp, err) | |
| } | |
| if err := os.Rename(tmp, fp); err != nil { | |
| _ = os.Remove(tmp) | |
| return fmt.Errorf("替换 %s 失败: %w", fp, err) | |
| } | |
| // 保留远端 mtime:下一轮比较靠它判断谁更新,丢了会导致对端反复重传。 | |
| if !mtime.IsZero() { | |
| _ = os.Chtimes(fp, mtime, mtime) | |
| } | |
| return nil | |
| } | |
| // localPath 把桶内相对路径映射为本地绝对路径。 | |
| func (e *Engine) localPath(relPath string) (string, error) { | |
| clean := path.Clean("/" + strings.ReplaceAll(relPath, `\`, "/")) | |
| switch { | |
| case strings.HasPrefix(clean, "/auths/"): | |
| name := path.Base(clean) | |
| if name == "" || name == "." || name == "/" { | |
| return "", fmt.Errorf("非法 auths 路径: %q", relPath) | |
| } | |
| return filepath.Join(e.cfg.AuthDir, name), nil | |
| case strings.HasPrefix(clean, "/data/"): | |
| name := path.Base(clean) | |
| if name == "" || name == "." || name == "/" { | |
| return "", fmt.Errorf("非法 data 路径: %q", relPath) | |
| } | |
| return filepath.Join(e.cfg.DataDir, name), nil | |
| default: | |
| return "", fmt.Errorf("同步路径不在白名单内: %q(只允许 auths/ 与 data/ 顶层 json)", relPath) | |
| } | |
| } | |
| // PathAllowed 报告一个桶内相对路径是否属于同步白名单。 | |
| // 面板接口与本地写入都过这一关,杜绝 ../ 穿越与任意路径写盘。 | |
| func PathAllowed(relPath string, includeData []string) bool { | |
| p := strings.ReplaceAll(relPath, `\`, "/") | |
| if strings.Contains(p, "..") { | |
| return false | |
| } | |
| clean := path.Clean("/" + p) | |
| if strings.HasPrefix(clean, "/auths/") { | |
| name := path.Base(clean) | |
| return name != "." && name != "/" && strings.HasSuffix(strings.ToLower(name), ".json") && | |
| path.Dir(clean) == "/auths" | |
| } | |
| if strings.HasPrefix(clean, "/data/") { | |
| if path.Dir(clean) != "/data" { | |
| return false | |
| } | |
| name := path.Base(clean) | |
| for _, allowed := range includeData { | |
| if name == allowed { | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| return false | |
| } | |