192 lines
6.7 KiB
Go
192 lines
6.7 KiB
Go
package upgrade
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"errors"
|
||
"fmt"
|
||
"hash"
|
||
"os"
|
||
"path/filepath"
|
||
"sort"
|
||
)
|
||
|
||
// SkillPathPublication records enough immutable identity to prove that a
|
||
// destination still belongs to the transaction that published it. The
|
||
// fingerprint is intentionally private so callers cannot forge records.
|
||
type SkillPathPublication struct {
|
||
Destination string
|
||
fingerprint [sha256.Size]byte
|
||
identity os.FileInfo
|
||
incarnation string
|
||
}
|
||
|
||
// PublishSkillPathNoReplace atomically publishes a staged path without ever
|
||
// replacing a destination created after the backup phase.
|
||
func PublishSkillPathNoReplace(staged, destination string) (SkillPathPublication, error) {
|
||
identity, err := skillPathLstat(staged)
|
||
if err != nil {
|
||
return SkillPathPublication{}, fmt.Errorf("读取待发布 Skill 身份失败 %s: %w", staged, err)
|
||
}
|
||
fingerprint, err := fingerprintSkillPath(staged)
|
||
if err != nil {
|
||
return SkillPathPublication{}, fmt.Errorf("计算待发布 Skill 身份失败 %s: %w", staged, err)
|
||
}
|
||
if err := skillPathRenameNoReplace(staged, destination); err != nil {
|
||
return SkillPathPublication{}, fmt.Errorf("目标必须不存在的 Skill 发布失败 %s: %w", destination, err)
|
||
}
|
||
publishedIdentity, err := skillPathLstat(destination)
|
||
if err != nil {
|
||
return SkillPathPublication{}, fmt.Errorf("确认已发布 Skill 身份失败 %s(对象保留): %w", destination, err)
|
||
}
|
||
// Force os.SameFile to resolve and cache any path-backed identity (notably
|
||
// on Windows) while the just-published object is known to occupy the path.
|
||
if !os.SameFile(identity, publishedIdentity) {
|
||
return SkillPathPublication{}, fmt.Errorf("确认已发布 Skill 身份失败 %s(对象保留): staging 身份已变化", destination)
|
||
}
|
||
_ = os.SameFile(publishedIdentity, publishedIdentity)
|
||
return SkillPathPublication{
|
||
Destination: destination,
|
||
fingerprint: fingerprint,
|
||
identity: publishedIdentity,
|
||
incarnation: skillPathFileIncarnation(publishedIdentity),
|
||
}, nil
|
||
}
|
||
|
||
// RollbackSkillPathPublications removes only objects that can still be proven
|
||
// to have been published by this transaction. Each live path is first claimed
|
||
// into a private sibling quarantine. A concurrent replacement is restored when
|
||
// possible, otherwise retained in quarantine and reported explicitly.
|
||
func RollbackSkillPathPublications(publications []SkillPathPublication) error {
|
||
var rollbackErr error
|
||
for i := len(publications) - 1; i >= 0; i-- {
|
||
if err := rollbackSkillPathPublication(publications[i]); err != nil {
|
||
rollbackErr = errors.Join(rollbackErr, err)
|
||
}
|
||
}
|
||
return rollbackErr
|
||
}
|
||
|
||
func rollbackSkillPathPublication(publication SkillPathPublication) (err error) {
|
||
destination := publication.Destination
|
||
quarantineRoot, err := skillPathMkdirTemp(filepath.Dir(destination), "."+filepath.Base(destination)+".rollback-")
|
||
if err != nil {
|
||
return fmt.Errorf("创建 Skill 回滚隔离目录失败 %s: %w", destination, err)
|
||
}
|
||
quarantine := filepath.Join(quarantineRoot, "payload")
|
||
cleanupRoot := func() error {
|
||
if cleanupErr := skillPathRemoveAll(quarantineRoot); cleanupErr != nil {
|
||
return fmt.Errorf("清理 Skill 回滚隔离目录失败 %s: %w", quarantineRoot, cleanupErr)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
liveIdentity, liveIdentityErr := skillPathLstat(destination)
|
||
if os.IsNotExist(liveIdentityErr) {
|
||
return cleanupRoot()
|
||
}
|
||
liveFingerprint, liveFingerprintErr := fingerprintSkillPath(destination)
|
||
liveVerificationErr := errors.Join(liveIdentityErr, liveFingerprintErr)
|
||
if liveVerificationErr != nil {
|
||
return errors.Join(
|
||
fmt.Errorf("拒绝删除无法验证的 Skill %s: %w", destination, liveVerificationErr),
|
||
cleanupRoot(),
|
||
)
|
||
}
|
||
if publication.identity == nil || !os.SameFile(publication.identity, liveIdentity) ||
|
||
publication.incarnation != skillPathFileIncarnation(liveIdentity) ||
|
||
liveFingerprint != publication.fingerprint {
|
||
return errors.Join(
|
||
fmt.Errorf("拒绝删除非本事务 Skill %s: 发布对象身份已变化", destination),
|
||
cleanupRoot(),
|
||
)
|
||
}
|
||
|
||
if err := skillPathRename(destination, quarantine); err != nil {
|
||
cleanupErr := cleanupRoot()
|
||
if os.IsNotExist(err) {
|
||
return cleanupErr
|
||
}
|
||
return errors.Join(fmt.Errorf("隔离待回滚 Skill 失败 %s: %w", destination, err), cleanupErr)
|
||
}
|
||
|
||
actualIdentity, identityErr := skillPathLstat(quarantine)
|
||
actual, fingerprintErr := fingerprintSkillPath(quarantine)
|
||
if identityErr == nil && fingerprintErr == nil &&
|
||
os.SameFile(publication.identity, actualIdentity) &&
|
||
actual == publication.fingerprint {
|
||
if removeErr := removePublishedSkillSource(quarantine); removeErr != nil {
|
||
return fmt.Errorf("移除事务发布的 Skill 失败 %s(隔离于 %s): %w", destination, quarantine, removeErr)
|
||
}
|
||
return cleanupRoot()
|
||
}
|
||
|
||
verificationErr := errors.Join(identityErr, fingerprintErr)
|
||
if verificationErr == nil {
|
||
verificationErr = errors.New("发布对象身份已变化")
|
||
}
|
||
if restoreErr := skillPathRenameNoReplace(quarantine, destination); restoreErr != nil {
|
||
return fmt.Errorf("拒绝删除非本事务 Skill %s: %v;并发对象保留于 %s,恢复原路径失败: %w", destination, verificationErr, quarantine, restoreErr)
|
||
}
|
||
return errors.Join(
|
||
fmt.Errorf("拒绝删除非本事务 Skill %s: %w", destination, verificationErr),
|
||
cleanupRoot(),
|
||
)
|
||
}
|
||
|
||
func fingerprintSkillPath(path string) ([sha256.Size]byte, error) {
|
||
h := sha256.New()
|
||
if err := fingerprintSkillPathInto(h, path); err != nil {
|
||
return [sha256.Size]byte{}, err
|
||
}
|
||
var result [sha256.Size]byte
|
||
copy(result[:], h.Sum(nil))
|
||
return result, nil
|
||
}
|
||
|
||
func fingerprintSkillPathInto(h hash.Hash, path string) error {
|
||
info, err := skillPathLstat(path)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
writeFingerprintField(h, info.Mode().Type().String())
|
||
writeFingerprintField(h, info.Mode().Perm().String())
|
||
switch {
|
||
case info.IsDir():
|
||
entries, err := skillPathReadDir(path)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() })
|
||
writeFingerprintField(h, fmt.Sprintf("%d", len(entries)))
|
||
for _, entry := range entries {
|
||
writeFingerprintField(h, entry.Name())
|
||
if err := fingerprintSkillPathInto(h, filepath.Join(path, entry.Name())); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
case info.Mode()&os.ModeSymlink != 0:
|
||
target, err := skillPathReadlink(path)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
writeFingerprintField(h, target)
|
||
return nil
|
||
case info.Mode().IsRegular():
|
||
digest, err := skillPathFileDigest(path)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
writeFingerprintField(h, fmt.Sprintf("%d", info.Size()))
|
||
_, err = h.Write(digest[:])
|
||
return err
|
||
default:
|
||
return fmt.Errorf("不支持识别特殊 Skill 路径 %s(mode=%s)", path, info.Mode())
|
||
}
|
||
}
|
||
|
||
func writeFingerprintField(h hash.Hash, value string) {
|
||
_, _ = fmt.Fprintf(h, "%d:", len(value))
|
||
_, _ = h.Write([]byte(value))
|
||
}
|