Files
code-utils/cloudbind.go
2026-08-14 07:52:01 +08:00

198 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
// cloudbind.go 实现“项目身份与机器路径分离”的同步支撑:
// - machineID本机稳定标识哈希后缓存云端行名 paths:<machineID>
// - machinePathsDoc / applyMachinePaths只推拉本机的 {项目名:路径} 行
// - 云端有身份但本机没路径的项目进入待绑定清单,由 Dashboard 引导用户选目录绑定。
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"os"
"strings"
)
// machineID 返回本机稳定标识(原始 ID 哈希后取 16 个十六进制字符),缓存于 meta。
func (a *App) machineID() string {
if a.store == nil {
return "unknown"
}
if v := a.store.Meta("machine_id"); v != "" {
return v
}
raw := rawMachineID()
if raw == "" {
raw, _ = os.Hostname()
}
if raw == "" {
raw = "unknown-machine"
}
sum := sha256.Sum256([]byte(strings.ToLower(strings.TrimSpace(raw))))
id := hex.EncodeToString(sum[:8])
_ = a.store.SetMeta("machine_id", id)
return id
}
// machinePathsDoc 生成本机路径文档 {项目名: 本机路径}map 序列化按键排序,内容稳定可比较)。
func (a *App) machinePathsDoc() (string, error) {
rows, e := a.store.db.Query(`SELECT name,path FROM projects ORDER BY name`)
if e != nil {
return "", e
}
defer rows.Close()
m := map[string]string{}
for rows.Next() {
var name, path string
if e := rows.Scan(&name, &path); e != nil {
return "", e
}
if name != "" && path != "" {
m[name] = path
}
}
b, e := json.Marshal(m)
return string(b), e
}
// applyMachinePaths 套用本机路径文档(换库/重装同机场景):
// 本地没有该名项目且路径非空 → 直接入库;已有同名项目但路径不同 → 以远端为准更新。
func (a *App) applyMachinePaths(doc string) error {
var m map[string]string
if json.Unmarshal([]byte(doc), &m) != nil {
return nil // 远端数据异常时忽略,不阻断同步
}
for name, path := range m {
name, path = strings.TrimSpace(name), strings.TrimSpace(path)
if name == "" || path == "" {
continue
}
var pid int64
var cur string
e := a.store.db.QueryRow(`SELECT id,path FROM projects WHERE name=? LIMIT 1`, name).Scan(&pid, &cur)
switch {
case e == sql.ErrNoRows:
now := nowRFC()
// path 全局唯一同路径已有别名项目时不强插IGNORE
if _, e := a.store.db.Exec(`INSERT OR IGNORE INTO projects(name,path,description,group_id,created_at,updated_at) VALUES(?,?,'',1,?,?)`,
name, path, now, now); e != nil {
return e
}
case e != nil:
return e
case cur != path:
if _, e := a.store.db.Exec(`UPDATE projects SET path=?,updated_at=? WHERE id=?
AND NOT EXISTS(SELECT 1 FROM projects p2 WHERE p2.path=? AND p2.id!=?)`,
path, nowRFC(), pid, path, pid); e != nil {
return e
}
}
}
return nil
}
// rebuildCloudPending 以最近一次云端身份文档为基准,重算仍未落地本机的项目清单(幂等)。
func (a *App) rebuildCloudPending(entries []projectDocEntry) {
pending := []projectDocEntry{}
for _, it := range entries {
name := strings.TrimSpace(it.Name)
if name == "" {
continue
}
var pid int64
if a.store.db.QueryRow(`SELECT id FROM projects WHERE name=? LIMIT 1`, name).Scan(&pid) == sql.ErrNoRows {
it.Path = "" // 待绑定清单不携带其它机器的路径
pending = append(pending, it)
}
}
b, _ := json.Marshal(pending)
_ = a.store.SetMeta("cloud_projects_pending", string(b))
}
// ListCloudPendingProjects 返回云端存在但本机尚未绑定路径的项目。
func (a *App) ListCloudPendingProjects() ([]projectDocEntry, error) {
if e := a.ready(); e != nil {
return nil, e
}
out := []projectDocEntry{}
raw := a.store.Meta("cloud_projects_pending")
if raw == "" {
return out, nil
}
var list []projectDocEntry
if json.Unmarshal([]byte(raw), &list) != nil {
return out, nil
}
for _, it := range list {
// 本地可能在同步间隙手动建了同名项目,再过滤一遍
var pid int64
if a.store.db.QueryRow(`SELECT id FROM projects WHERE name=? LIMIT 1`, it.Name).Scan(&pid) == sql.ErrNoRows {
out = append(out, it)
}
}
return out, nil
}
// BindCloudProject 把云端项目绑定到本机目录:带身份信息入库、移出待绑定清单,并触发一轮推送。
func (a *App) BindCloudProject(name, path string) (Project, error) {
if e := a.ready(); e != nil {
return Project{}, e
}
name, path = strings.TrimSpace(name), strings.TrimSpace(path)
if name == "" {
return Project{}, errors.New("NAME_REQUIRED")
}
if fi, e := os.Stat(path); e != nil || !fi.IsDir() {
return Project{}, errors.New("DIR_NOT_FOUND")
}
entry := projectDocEntry{Name: name}
if raw := a.store.Meta("cloud_projects_pending"); raw != "" {
var list []projectDocEntry
if json.Unmarshal([]byte(raw), &list) == nil {
rest := make([]projectDocEntry, 0, len(list))
for _, it := range list {
if strings.TrimSpace(it.Name) == name {
entry = it
continue
}
rest = append(rest, it)
}
b, _ := json.Marshal(rest)
_ = a.store.SetMeta("cloud_projects_pending", string(b))
}
}
gid, e := a.ensureGroupID(entry.Group)
if e != nil {
return Project{}, e
}
var pid int64
now := nowRFC()
switch e := a.store.db.QueryRow(`SELECT id FROM projects WHERE path=?`, path).Scan(&pid); {
case e == sql.ErrNoRows:
r, e2 := a.store.db.Exec(`INSERT INTO projects(name,path,description,group_id,created_at,updated_at) VALUES(?,?,?,?,?,?)`,
name, path, entry.Description, gid, now, now)
if e2 != nil {
return Project{}, e2
}
pid, _ = r.LastInsertId()
case e != nil:
return Project{}, e
default:
// 该目录已是本地项目:改名并入云端身份,避免出现两个同路径项目。
if _, e := a.store.db.Exec(`UPDATE projects SET name=?,description=?,group_id=?,updated_at=? WHERE id=?`,
name, entry.Description, gid, now, pid); e != nil {
return Project{}, e
}
}
if entry.Favorite {
_, _ = a.store.db.Exec(`INSERT OR IGNORE INTO favorites(project_id,created_at) VALUES(?,?)`, pid, now)
}
a.store.Log("info", "同步", "云端项目已绑定本机目录", name+" → "+path)
if a.syncUserID() > 0 {
go a.syncOnce(true)
}
return a.store.GetProject(pid)
}