Files
90CNServer/pvf/internal/version/repository.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

817 lines
22 KiB
Go

package version
import (
"crypto/rand"
"database/sql"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
_ "modernc.org/sqlite"
)
const (
repositoryFormat = 1
hashAlgorithm = "sha256"
mainRef = "main"
snapshotCacheMax = 4
)
var (
ErrRepositoryNotFound = errors.New("版本库不存在")
ErrRepositoryCorrupt = errors.New("版本库损坏")
)
// Repository is a local sidecar version database and immutable object store.
type Repository struct {
mu sync.Mutex
root string
db *sql.DB
objects *ObjectStore
meta Meta
baseSnapshot Snapshot
snapshotCache map[string]Snapshot
snapshotOrder []string
commitCache map[string]Commit
}
// SidecarPath returns the repository directory for an artifact path.
func SidecarPath(artifactPath string) string { return artifactPath + ".pvfine" }
// Open opens an existing sidecar repository.
func Open(root string) (*Repository, error) {
root = filepath.Clean(root)
if _, err := os.Stat(filepath.Join(root, "repo.db")); err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, ErrRepositoryNotFound
}
return nil, err
}
db, err := sql.Open("sqlite", filepath.Join(root, "repo.db"))
if err != nil {
return nil, err
}
repo := &Repository{
root: root,
db: db,
objects: newObjectStore(filepath.Join(root, "objects")),
snapshotCache: make(map[string]Snapshot),
commitCache: make(map[string]Commit),
}
if err := repo.configure(); err != nil {
_ = db.Close()
return nil, err
}
if err := repo.loadMeta(); err != nil {
_ = db.Close()
return nil, err
}
return repo, nil
}
// Init creates a repository with one immutable base snapshot and an initial
// commit. The caller must pass an unmodified archive snapshot.
func Init(root, baseFilePath, baseHash string, base Snapshot) (*Repository, error) {
root = filepath.Clean(root)
if _, err := os.Stat(filepath.Join(root, "repo.db")); err == nil {
return nil, fmt.Errorf("版本库已存在: %s", root)
} else if !errors.Is(err, os.ErrNotExist) {
return nil, err
}
if err := os.MkdirAll(filepath.Join(root, "objects"), 0o755); err != nil {
return nil, err
}
basePath := filepath.Join(root, "base.pvf")
if err := copyFileAtomic(baseFilePath, basePath); err != nil {
return nil, fmt.Errorf("保存版本基线失败: %w", err)
}
db, err := sql.Open("sqlite", filepath.Join(root, "repo.db"))
if err != nil {
_ = os.Remove(basePath)
return nil, err
}
repo := &Repository{
root: root,
db: db,
objects: newObjectStore(filepath.Join(root, "objects")),
snapshotCache: make(map[string]Snapshot),
commitCache: make(map[string]Commit),
}
if err := repo.configure(); err != nil {
_ = db.Close()
return nil, err
}
if err := repo.initialize(baseFilePath, baseHash, base); err != nil {
_ = db.Close()
return nil, err
}
return repo, nil
}
func (r *Repository) configure() error {
statements := []string{
"PRAGMA busy_timeout = 5000",
"PRAGMA foreign_keys = ON",
"PRAGMA journal_mode = WAL",
"PRAGMA synchronous = FULL",
`CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)`,
`CREATE TABLE IF NOT EXISTS refs (
name TEXT PRIMARY KEY,
commit_id TEXT NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS commits (
id TEXT PRIMARY KEY,
parent_id TEXT,
message TEXT NOT NULL,
created_at INTEGER NOT NULL,
change_count INTEGER NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS changes (
commit_id TEXT NOT NULL,
sequence INTEGER NOT NULL,
path TEXT NOT NULL,
display_path TEXT NOT NULL,
old_path TEXT NOT NULL DEFAULT '',
old_display_path TEXT NOT NULL DEFAULT '',
operation TEXT NOT NULL,
before_hash TEXT NOT NULL DEFAULT '',
after_hash TEXT NOT NULL DEFAULT '',
before_type INTEGER NOT NULL DEFAULT 0,
after_type INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (commit_id, sequence),
FOREIGN KEY (commit_id) REFERENCES commits(id) ON DELETE CASCADE
)`,
`CREATE INDEX IF NOT EXISTS changes_path_idx ON changes(path, commit_id)`,
`CREATE TABLE IF NOT EXISTS base_entries (
path TEXT PRIMARY KEY,
display_path TEXT NOT NULL,
data_type INTEGER NOT NULL,
hash TEXT NOT NULL
)`,
}
for _, statement := range statements {
if _, err := r.db.Exec(statement); err != nil {
return err
}
}
return nil
}
func (r *Repository) initialize(baseFilePath, baseHash string, base Snapshot) error {
rootID, err := newID()
if err != nil {
return err
}
now := time.Now().UTC().UnixNano()
tx, err := r.db.Begin()
if err != nil {
return err
}
rollback := true
defer func() {
if rollback {
_ = tx.Rollback()
}
}()
meta := map[string]string{
"format": fmt.Sprint(repositoryFormat),
"base_file": filepath.Base(baseFilePath),
"base_hash": baseHash,
"hash_algorithm": hashAlgorithm,
"artifact_hash": baseHash,
"artifact_tree_hash": TreeHash(base),
"artifact_commit_id": rootID,
}
for key, value := range meta {
if _, err := tx.Exec(`INSERT INTO meta(key, value) VALUES(?, ?)`, key, value); err != nil {
return err
}
}
if _, err := tx.Exec(
`INSERT INTO commits(id, parent_id, message, created_at, change_count) VALUES(?, NULL, ?, ?, 0)`,
rootID, "初始版本", now,
); err != nil {
return err
}
if _, err := tx.Exec(`INSERT INTO refs(name, commit_id) VALUES(?, ?)`, mainRef, rootID); err != nil {
return err
}
keys := make([]string, 0, len(base))
for key := range base {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
entry := base[key]
if _, err := tx.Exec(
`INSERT INTO base_entries(path, display_path, data_type, hash) VALUES(?, ?, ?, ?)`,
key, entry.Path, entry.DataType, entry.Hash,
); err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
rollback = false
r.baseSnapshot = CloneSnapshot(base)
r.snapshotCache = make(map[string]Snapshot)
r.snapshotOrder = nil
r.commitCache = map[string]Commit{
rootID: {ID: rootID, Message: "初始版本", CreatedAt: now},
}
return r.loadMeta()
}
func (r *Repository) loadMeta() error {
values := make(map[string]string)
rows, err := r.db.Query(`SELECT key, value FROM meta`)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var key, value string
if err := rows.Scan(&key, &value); err != nil {
return err
}
values[key] = value
}
if err := rows.Err(); err != nil {
return err
}
if values["format"] != "1" || values["hash_algorithm"] != hashAlgorithm || values["base_hash"] == "" {
return fmt.Errorf("%w:不支持的格式或元数据不完整", ErrRepositoryCorrupt)
}
if _, err := os.Stat(filepath.Join(r.root, "base.pvf")); err != nil {
return fmt.Errorf("%w:基线文件不存在", ErrRepositoryCorrupt)
}
r.meta = Meta{
Format: repositoryFormat,
BaseFile: values["base_file"],
BaseHash: values["base_hash"],
HashAlgorithm: values["hash_algorithm"],
Artifact: ArtifactState{
PVFHash: values["artifact_hash"],
TreeHash: values["artifact_tree_hash"],
CommitID: values["artifact_commit_id"],
},
}
return nil
}
// Close closes the SQLite handle. It is safe to call more than once.
func (r *Repository) Close() error {
if r == nil {
return nil
}
r.mu.Lock()
defer r.mu.Unlock()
if r.db == nil {
return nil
}
err := r.db.Close()
r.db = nil
r.baseSnapshot = nil
r.snapshotCache = nil
r.snapshotOrder = nil
r.commitCache = nil
return err
}
func (r *Repository) Root() string { return r.root }
func (r *Repository) BasePath() string { return filepath.Join(r.root, "base.pvf") }
func (r *Repository) Meta() Meta {
r.mu.Lock()
defer r.mu.Unlock()
return r.meta
}
func (r *Repository) Head() (Commit, error) {
r.mu.Lock()
defer r.mu.Unlock()
return r.headLocked()
}
// GetCommit returns a commit by ID and verifies that it exists in the
// repository's commit graph.
func (r *Repository) GetCommit(id string) (Commit, error) {
r.mu.Lock()
defer r.mu.Unlock()
if strings.TrimSpace(id) == "" {
return Commit{}, errors.New("提交 ID 不能为空")
}
return r.commitLocked(strings.TrimSpace(id))
}
func (r *Repository) headLocked() (Commit, error) {
var id string
if err := r.db.QueryRow(`SELECT commit_id FROM refs WHERE name = ?`, mainRef).Scan(&id); err != nil {
return Commit{}, err
}
return r.commitLocked(id)
}
func (r *Repository) commitLocked(id string) (Commit, error) {
if commit, ok := r.commitCache[id]; ok {
return commit, nil
}
var commit Commit
var parent sql.NullString
if err := r.db.QueryRow(
`SELECT id, parent_id, message, created_at, change_count FROM commits WHERE id = ?`, id,
).Scan(&commit.ID, &parent, &commit.Message, &commit.CreatedAt, &commit.ChangeCount); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return Commit{}, fmt.Errorf("%w:找不到提交 %s", ErrRepositoryCorrupt, id)
}
return Commit{}, err
}
if parent.Valid {
commit.ParentID = parent.String
}
if r.commitCache == nil {
r.commitCache = make(map[string]Commit)
}
r.commitCache[id] = commit
return commit, nil
}
// History walks the main branch from HEAD backwards. Cursor is an offset in
// that chain rather than a timestamp, which keeps pagination deterministic.
func (r *Repository) History(cursor, limit int) ([]Commit, int, error) {
r.mu.Lock()
defer r.mu.Unlock()
if cursor < 0 {
cursor = 0
}
if limit <= 0 || limit > 200 {
limit = 50
}
head, err := r.headLocked()
if err != nil {
return nil, -1, err
}
chain := make([]Commit, 0)
current := head
for {
chain = append(chain, current)
if current.ParentID == "" {
break
}
current, err = r.commitLocked(current.ParentID)
if err != nil {
return nil, -1, err
}
if len(chain) > 1_000_000 {
return nil, -1, fmt.Errorf("%w:提交链过长或存在循环", ErrRepositoryCorrupt)
}
}
if cursor >= len(chain) {
return []Commit{}, -1, nil
}
end := cursor + limit
if end > len(chain) {
end = len(chain)
}
result := make([]Commit, end-cursor)
copy(result, chain[cursor:end])
next := -1
if end < len(chain) {
next = end
}
return result, next, nil
}
// Changes returns one commit's compact path changes.
func (r *Repository) Changes(commitID string) ([]FileChange, error) {
r.mu.Lock()
defer r.mu.Unlock()
return r.changesLocked(commitID)
}
// ChangedPathsBetween returns the union of paths changed between two commits
// on the single main branch. It intentionally returns only identities, so a
// caller can materialize the target snapshot without scanning every file.
func (r *Repository) ChangedPathsBetween(fromID, toID string) ([]string, error) {
r.mu.Lock()
defer r.mu.Unlock()
fromID = strings.TrimSpace(fromID)
toID = strings.TrimSpace(toID)
if fromID == "" || toID == "" {
return nil, errors.New("起止提交 ID 不能为空")
}
if fromID == toID {
return []string{}, nil
}
fromAncestors := make(map[string]struct{})
for currentID := fromID; currentID != ""; {
if _, exists := fromAncestors[currentID]; exists {
return nil, fmt.Errorf("%w:提交链存在循环", ErrRepositoryCorrupt)
}
fromAncestors[currentID] = struct{}{}
commit, err := r.commitLocked(currentID)
if err != nil {
return nil, err
}
currentID = commit.ParentID
if len(fromAncestors) > 1_000_000 {
return nil, fmt.Errorf("%w:提交链过长", ErrRepositoryCorrupt)
}
}
pathSet := make(map[string]struct{})
addCommitChanges := func(commitID string) error {
changes, err := r.changesLocked(commitID)
if err != nil {
return err
}
for _, change := range changes {
pathSet[CanonicalPath(change.Path)] = struct{}{}
}
return nil
}
// Walk from the target backwards until the first common ancestor. This
// covers the target-only side of the interval.
targetOnly := make([]string, 0)
commonID := ""
for currentID := toID; currentID != ""; {
if _, common := fromAncestors[currentID]; common {
commonID = currentID
break
}
targetOnly = append(targetOnly, currentID)
commit, err := r.commitLocked(currentID)
if err != nil {
return nil, err
}
currentID = commit.ParentID
if len(targetOnly) > 1_000_000 {
return nil, fmt.Errorf("%w:提交链过长", ErrRepositoryCorrupt)
}
}
if commonID == "" {
return nil, fmt.Errorf("%w:提交不属于同一主分支", ErrRepositoryCorrupt)
}
for _, commitID := range targetOnly {
if err := addCommitChanges(commitID); err != nil {
return nil, err
}
}
// Walk from the source back to the common ancestor. These are the source
// commits that must be undone when moving to the target.
for currentID := fromID; currentID != commonID; {
if err := addCommitChanges(currentID); err != nil {
return nil, err
}
commit, err := r.commitLocked(currentID)
if err != nil {
return nil, err
}
currentID = commit.ParentID
if currentID == "" {
return nil, fmt.Errorf("%w:找不到共同祖先", ErrRepositoryCorrupt)
}
}
result := make([]string, 0, len(pathSet))
for path := range pathSet {
if path != "" {
result = append(result, path)
}
}
sort.Strings(result)
return result, nil
}
func (r *Repository) changesLocked(commitID string) ([]FileChange, error) {
rows, err := r.db.Query(`
SELECT path, display_path, old_path, old_display_path, operation,
before_hash, after_hash, before_type, after_type
FROM changes WHERE commit_id = ? ORDER BY sequence`, commitID)
if err != nil {
return nil, err
}
defer rows.Close()
result := make([]FileChange, 0)
for rows.Next() {
var change FileChange
if err := rows.Scan(
&change.Path, &change.DisplayPath, &change.OldPath, &change.OldDisplayPath,
&change.Operation, &change.BeforeHash, &change.AfterHash,
&change.BeforeType, &change.AfterType,
); err != nil {
return nil, err
}
result = append(result, change)
}
return result, rows.Err()
}
// Snapshot reconstructs a commit's logical tree from the base manifest and
// its parent deltas.
func (r *Repository) Snapshot(commitID string) (Snapshot, error) {
r.mu.Lock()
defer r.mu.Unlock()
if commitID == "" {
commit, err := r.headLocked()
if err != nil {
return nil, err
}
commitID = commit.ID
}
return r.snapshotLocked(commitID)
}
func (r *Repository) baseSnapshotLocked() (Snapshot, error) {
if r.baseSnapshot != nil {
return CloneSnapshot(r.baseSnapshot), nil
}
rows, err := r.db.Query(`SELECT path, display_path, data_type, hash FROM base_entries ORDER BY path`)
if err != nil {
return nil, err
}
defer rows.Close()
result := make(Snapshot)
for rows.Next() {
var key, path, hash string
var dataType int32
if err := rows.Scan(&key, &path, &dataType, &hash); err != nil {
return nil, err
}
result[key] = Entry{Path: path, DataType: dataType, Hash: hash}
}
if err := rows.Err(); err != nil {
return nil, err
}
r.baseSnapshot = CloneSnapshot(result)
return result, nil
}
// PutObject stores one after-state payload before its commit transaction.
func (r *Repository) PutObject(raw []byte) (string, error) {
r.mu.Lock()
defer r.mu.Unlock()
return r.objects.Put(raw)
}
func (r *Repository) ReadObject(hash string) ([]byte, error) {
r.mu.Lock()
defer r.mu.Unlock()
return r.objects.Read(hash)
}
// Commit appends a commit to main after validating its parent and before
// hashes. Object writes are immutable; an interrupted database transaction
// can leave harmless unreachable objects which can be collected later.
func (r *Repository) Commit(message string, changes []FileChange, objects map[string][]byte) (Commit, error) {
r.mu.Lock()
defer r.mu.Unlock()
message = strings.TrimSpace(message)
if message == "" {
return Commit{}, errors.New("提交说明不能为空")
}
if len(changes) == 0 {
return Commit{}, errors.New("没有可提交的变更")
}
head, err := r.headLocked()
if err != nil {
return Commit{}, err
}
headSnapshot, err := r.snapshotLocked(head.ID)
if err != nil {
return Commit{}, err
}
for _, change := range changes {
if err := validateChange(headSnapshot, change); err != nil {
return Commit{}, err
}
if change.AfterHash != "" {
raw, ok := objects[change.AfterHash]
if !ok {
existing, readErr := r.objects.Read(change.AfterHash)
if readErr != nil {
return Commit{}, fmt.Errorf("提交 %q 缺少对象 %s: %w", change.DisplayPath, change.AfterHash, readErr)
}
if HashBytes(existing) != change.AfterHash {
return Commit{}, fmt.Errorf("对象 hash 校验失败: %s", change.AfterHash)
}
continue
}
if HashBytes(raw) != change.AfterHash {
return Commit{}, fmt.Errorf("对象 hash 校验失败: %s", change.AfterHash)
}
if _, err := r.objects.Put(raw); err != nil {
return Commit{}, err
}
}
}
id, err := newID()
if err != nil {
return Commit{}, err
}
now := time.Now().UTC().UnixNano()
tx, err := r.db.Begin()
if err != nil {
return Commit{}, err
}
rollback := true
defer func() {
if rollback {
_ = tx.Rollback()
}
}()
if _, err := tx.Exec(
`INSERT INTO commits(id, parent_id, message, created_at, change_count) VALUES(?, ?, ?, ?, ?)`,
id, head.ID, message, now, len(changes),
); err != nil {
return Commit{}, err
}
for index, change := range changes {
if _, err := tx.Exec(`
INSERT INTO changes(
commit_id, sequence, path, display_path, old_path, old_display_path,
operation, before_hash, after_hash, before_type, after_type
) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
id, index, CanonicalPath(change.Path), DisplayPath(change.DisplayPath),
CanonicalPath(change.OldPath), DisplayPath(change.OldDisplayPath),
string(change.Operation), change.BeforeHash, change.AfterHash,
change.BeforeType, change.AfterType,
); err != nil {
return Commit{}, err
}
}
if _, err := tx.Exec(`UPDATE refs SET commit_id = ? WHERE name = ?`, id, mainRef); err != nil {
return Commit{}, err
}
if err := tx.Commit(); err != nil {
return Commit{}, err
}
rollback = false
commit := Commit{ID: id, ParentID: head.ID, Message: message, CreatedAt: now, ChangeCount: len(changes)}
if r.commitCache == nil {
r.commitCache = make(map[string]Commit)
}
r.commitCache[id] = commit
nextSnapshot := CloneSnapshot(headSnapshot)
for _, change := range changes {
key := CanonicalPath(change.Path)
if change.Operation == OperationDelete {
delete(nextSnapshot, key)
continue
}
nextSnapshot[key] = Entry{
Path: change.DisplayPath, DataType: change.AfterType, Hash: change.AfterHash,
}
}
if r.snapshotCache == nil {
r.snapshotCache = make(map[string]Snapshot)
}
r.cacheSnapshotLocked(id, nextSnapshot)
return commit, nil
}
func validateChange(head Snapshot, change FileChange) error {
key := CanonicalPath(change.Path)
entry, exists := head[key]
switch change.Operation {
case OperationAdd:
if exists {
return fmt.Errorf("新增文件已存在: %s", change.DisplayPath)
}
if change.AfterHash == "" {
return fmt.Errorf("新增文件缺少内容: %s", change.DisplayPath)
}
case OperationModify:
if !exists {
return fmt.Errorf("修改文件不存在: %s", change.DisplayPath)
}
if entry.Hash != change.BeforeHash || entry.DataType != change.BeforeType {
return fmt.Errorf("文件在提交前已变化: %s", change.DisplayPath)
}
if change.AfterHash == "" {
return fmt.Errorf("修改文件缺少新内容: %s", change.DisplayPath)
}
case OperationDelete:
if !exists {
return fmt.Errorf("删除文件不存在: %s", change.DisplayPath)
}
if entry.Hash != change.BeforeHash || entry.DataType != change.BeforeType {
return fmt.Errorf("文件在删除前已变化: %s", change.DisplayPath)
}
default:
return fmt.Errorf("V1 暂不支持版本操作: %s", change.Operation)
}
return nil
}
func (r *Repository) snapshotLocked(commitID string) (Snapshot, error) {
if cached, ok := r.snapshotCache[commitID]; ok {
return CloneSnapshot(cached), nil
}
base, err := r.baseSnapshotLocked()
if err != nil {
return nil, err
}
chain := make([]Commit, 0)
for currentID := commitID; currentID != ""; {
commit, err := r.commitLocked(currentID)
if err != nil {
return nil, err
}
chain = append(chain, commit)
currentID = commit.ParentID
if len(chain) > 1_000_000 {
return nil, fmt.Errorf("%w:提交链过长或存在循环", ErrRepositoryCorrupt)
}
}
for index := len(chain) - 1; index >= 0; index-- {
changes, err := r.changesLocked(chain[index].ID)
if err != nil {
return nil, err
}
for _, change := range changes {
key := CanonicalPath(change.Path)
if change.Operation == OperationDelete {
delete(base, key)
continue
}
base[key] = Entry{Path: change.DisplayPath, DataType: change.AfterType, Hash: change.AfterHash}
}
}
r.cacheSnapshotLocked(commitID, base)
return base, nil
}
func (r *Repository) cacheSnapshotLocked(commitID string, snapshot Snapshot) {
if r.snapshotCache == nil {
r.snapshotCache = make(map[string]Snapshot)
}
if _, exists := r.snapshotCache[commitID]; exists {
for index, cachedID := range r.snapshotOrder {
if cachedID == commitID {
r.snapshotOrder = append(r.snapshotOrder[:index], r.snapshotOrder[index+1:]...)
break
}
}
}
r.snapshotCache[commitID] = CloneSnapshot(snapshot)
r.snapshotOrder = append(r.snapshotOrder, commitID)
for len(r.snapshotOrder) > snapshotCacheMax {
oldest := r.snapshotOrder[0]
r.snapshotOrder = r.snapshotOrder[1:]
delete(r.snapshotCache, oldest)
}
}
// SetArtifactState records the last successful PVF save. It is intentionally
// separate from HEAD so save and commit remain independent operations.
func (r *Repository) SetArtifactState(state ArtifactState) error {
r.mu.Lock()
defer r.mu.Unlock()
values := map[string]string{
"artifact_hash": state.PVFHash,
"artifact_tree_hash": state.TreeHash,
"artifact_commit_id": state.CommitID,
}
tx, err := r.db.Begin()
if err != nil {
return err
}
rollback := true
defer func() {
if rollback {
_ = tx.Rollback()
}
}()
for key, value := range values {
if _, err := tx.Exec(`INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`, key, value); err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
rollback = false
r.meta.Artifact = state
return nil
}
func newID() (string, error) {
var raw [16]byte
if _, err := rand.Read(raw[:]); err != nil {
return "", err
}
return hex.EncodeToString(raw[:]), nil
}