Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

379 lines
13 KiB
Go

package artifact
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
market "github.com/tutti-os/tutti/packages/connector/daemon/core"
)
const (
downloadCacheReceiptSchema = "tutti.connector.download-cache.v1"
downloadCacheArtifactFile = "artifact"
downloadCacheReceiptFile = "receipt.json"
)
type DownloadCacheConfig struct {
RootDir string
Fetcher Fetcher
MaxDownloadBytes int64
}
// CachedArtifact is a verified raw release archive. Slot is either current or
// candidate. Callers must not persist Path outside the owning machine.
type CachedArtifact struct {
SchemaVersion string `json:"schemaVersion"`
OperationID string `json:"operationId"`
ConnectorKey string `json:"connectorKey"`
ReleaseID string `json:"releaseId"`
ReleaseDigest string `json:"releaseDigest"`
ArtifactSHA string `json:"artifactSha256"`
SizeBytes int64 `json:"sizeBytes"`
MediaType string `json:"mediaType"`
ObjectVersion string `json:"objectVersion,omitempty"`
Slot string `json:"slot"`
Path string `json:"path"`
}
// DownloadCache retains only the last successfully installed raw archive plus
// one replaceable candidate per Connector. It intentionally does not provide a
// historical content-addressed blob store.
type DownloadCache struct {
rootDir string
fetcher Fetcher
maxDownloadBytes int64
mu sync.Mutex
connectorLanes map[string]*sync.Mutex
downloadSlots chan struct{}
}
func NewDownloadCache(config DownloadCacheConfig) (*DownloadCache, error) {
if !filepath.IsAbs(strings.TrimSpace(config.RootDir)) {
return nil, errors.New("connector download cache root must be absolute")
}
if config.Fetcher == nil {
return nil, errors.New("connector download cache fetcher is required")
}
limit := config.MaxDownloadBytes
if limit <= 0 {
limit = DefaultLimits().MaxDownloadBytes
}
return &DownloadCache{rootDir: filepath.Clean(config.RootDir), fetcher: config.Fetcher, maxDownloadBytes: limit,
connectorLanes: make(map[string]*sync.Mutex), downloadSlots: make(chan struct{}, 4)}, nil
}
func (cache *DownloadCache) PrepareCandidate(
ctx context.Context,
request market.PrepareArtifactRequest,
) (CachedArtifact, error) {
return cache.prepareCandidate(ctx, request, market.ValidateReleaseShape)
}
func (cache *DownloadCache) prepareCandidate(
ctx context.Context,
request market.PrepareArtifactRequest,
validate func(market.Release) error,
) (CachedArtifact, error) {
if cache == nil {
return CachedArtifact{}, errors.New("connector download cache is unavailable")
}
if !safeSegment(request.OperationID) {
return CachedArtifact{}, errors.New("connector artifact operation id is invalid")
}
if err := validate(request.Release); err != nil {
return CachedArtifact{}, err
}
releaseLane := cache.lockConnector(request.Release.ConnectorKey)
defer releaseLane()
select {
case cache.downloadSlots <- struct{}{}:
defer func() { <-cache.downloadSlots }()
case <-ctx.Done():
return CachedArtifact{}, ctx.Err()
}
if err := ctx.Err(); err != nil {
return CachedArtifact{}, err
}
root, err := cache.connectorRoot(request.Release.ConnectorKey)
if err != nil {
return CachedArtifact{}, err
}
if err := cache.recoverPromotion(root); err != nil {
return CachedArtifact{}, err
}
if current, ok := cache.readSlot(root, "current", request.OperationID, request.Release); ok {
return current, nil
}
if candidate, ok := cache.readSlot(root, "candidate", request.OperationID, request.Release); ok {
return candidate, nil
}
return cache.downloadCandidate(ctx, root, request)
}
func (cache *DownloadCache) PromoteCandidate(
ctx context.Context,
candidate CachedArtifact,
release market.Release,
) (CachedArtifact, error) {
if cache == nil {
return CachedArtifact{}, errors.New("connector download cache is unavailable")
}
releaseLane := cache.lockConnector(release.ConnectorKey)
defer releaseLane()
if err := ctx.Err(); err != nil {
return CachedArtifact{}, err
}
root, err := cache.connectorRoot(release.ConnectorKey)
if err != nil {
return CachedArtifact{}, err
}
if err := cache.recoverPromotion(root); err != nil {
return CachedArtifact{}, err
}
if current, ok := cache.readSlot(root, "current", candidate.OperationID, release); ok {
_ = os.RemoveAll(filepath.Join(root, "candidate"))
return current, nil
}
verified, ok := cache.readSlot(root, "candidate", candidate.OperationID, release)
if !ok || candidate.ReleaseDigest != verified.ReleaseDigest || candidate.ArtifactSHA != verified.ArtifactSHA {
return CachedArtifact{}, errors.New("connector artifact candidate is missing or mismatched")
}
currentPath := filepath.Join(root, "current")
candidatePath := filepath.Join(root, "candidate")
retiredPath := filepath.Join(root, ".retired")
if err := os.RemoveAll(retiredPath); err != nil {
return CachedArtifact{}, fmt.Errorf("reset retired connector artifact: %w", err)
}
if _, err := os.Stat(currentPath); err == nil {
if err := os.Rename(currentPath, retiredPath); err != nil {
return CachedArtifact{}, fmt.Errorf("retire current connector artifact: %w", err)
}
} else if !errors.Is(err, os.ErrNotExist) {
return CachedArtifact{}, err
}
if err := os.Rename(candidatePath, currentPath); err != nil {
_ = os.Rename(retiredPath, currentPath)
return CachedArtifact{}, fmt.Errorf("promote connector artifact candidate: %w", err)
}
if err := syncDirectory(root); err != nil {
return CachedArtifact{}, err
}
if err := os.RemoveAll(retiredPath); err != nil {
return CachedArtifact{}, fmt.Errorf("remove retired connector artifact: %w", err)
}
current, ok := cache.readSlot(root, "current", candidate.OperationID, release)
if !ok {
return CachedArtifact{}, errors.New("promoted connector artifact could not be verified")
}
return current, nil
}
func (cache *DownloadCache) RemoveConnector(ctx context.Context, connectorKey string) error {
if cache == nil {
return errors.New("connector download cache is unavailable")
}
releaseLane := cache.lockConnector(connectorKey)
defer releaseLane()
if err := ctx.Err(); err != nil {
return err
}
root, err := cache.connectorRoot(connectorKey)
if err != nil {
return err
}
return removeAllWithin(cache.rootDir, root)
}
func (cache *DownloadCache) lockConnector(connectorKey string) func() {
cache.mu.Lock()
lane := cache.connectorLanes[connectorKey]
if lane == nil {
lane = &sync.Mutex{}
cache.connectorLanes[connectorKey] = lane
}
cache.mu.Unlock()
lane.Lock()
return lane.Unlock
}
func (cache *DownloadCache) downloadCandidate(
ctx context.Context,
root string,
request market.PrepareArtifactRequest,
) (CachedArtifact, error) {
release := request.Release
staging := filepath.Join(root, ".candidate-staging")
if err := os.RemoveAll(staging); err != nil {
return CachedArtifact{}, fmt.Errorf("reset connector artifact candidate staging: %w", err)
}
if err := os.MkdirAll(staging, 0o700); err != nil {
return CachedArtifact{}, fmt.Errorf("create connector artifact candidate staging: %w", err)
}
defer os.RemoveAll(staging)
response, err := cache.fetcher.Fetch(ctx, FetchRequest{
OperationID: request.OperationID,
Scope: request.Scope,
Generation: request.Generation,
Release: release,
})
if err != nil {
return CachedArtifact{}, fmt.Errorf("fetch connector artifact: %w", err)
}
if response.Body == nil {
return CachedArtifact{}, errors.New("fetch connector artifact: response body is nil")
}
defer response.Body.Close()
if response.ContentLength > 0 && response.ContentLength != release.Artifact.SizeBytes {
return CachedArtifact{}, fmt.Errorf("connector artifact content length %d does not match declared size %d", response.ContentLength, release.Artifact.SizeBytes)
}
if !sameMediaType(response.MediaType, release.Artifact.MediaType) {
return CachedArtifact{}, fmt.Errorf("connector artifact media type %q does not match declared media type %q", response.MediaType, release.Artifact.MediaType)
}
limit := minPositive(cache.maxDownloadBytes, release.Artifact.SizeBytes)
artifactPath := filepath.Join(staging, downloadCacheArtifactFile)
file, err := os.OpenFile(artifactPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
if err != nil {
return CachedArtifact{}, fmt.Errorf("create connector artifact candidate: %w", err)
}
hash := sha256.New()
written, copyErr := io.Copy(io.MultiWriter(file, hash), io.LimitReader(response.Body, limit+1))
syncErr := file.Sync()
closeErr := file.Close()
if copyErr != nil {
return CachedArtifact{}, fmt.Errorf("download connector artifact: %w", copyErr)
}
if syncErr != nil {
return CachedArtifact{}, fmt.Errorf("sync connector artifact candidate: %w", syncErr)
}
if closeErr != nil {
return CachedArtifact{}, fmt.Errorf("close connector artifact candidate: %w", closeErr)
}
if written != release.Artifact.SizeBytes || written > limit {
return CachedArtifact{}, fmt.Errorf("connector artifact size %d does not match declared size %d", written, release.Artifact.SizeBytes)
}
if actual := hex.EncodeToString(hash.Sum(nil)); actual != release.Artifact.SHA256 {
return CachedArtifact{}, errors.New("connector artifact SHA-256 does not match release")
}
receipt := CachedArtifact{SchemaVersion: downloadCacheReceiptSchema, OperationID: request.OperationID,
ConnectorKey: release.ConnectorKey, ReleaseID: release.ReleaseID, ReleaseDigest: release.ReleaseDigest,
ArtifactSHA: release.Artifact.SHA256, SizeBytes: release.Artifact.SizeBytes,
MediaType: release.Artifact.MediaType, ObjectVersion: release.Artifact.ObjectVersion, Slot: "candidate"}
if err := writeDownloadCacheReceipt(staging, receipt); err != nil {
return CachedArtifact{}, err
}
candidatePath := filepath.Join(root, "candidate")
if err := os.RemoveAll(candidatePath); err != nil {
return CachedArtifact{}, fmt.Errorf("replace connector artifact candidate: %w", err)
}
if err := os.Rename(staging, candidatePath); err != nil {
return CachedArtifact{}, fmt.Errorf("publish connector artifact candidate: %w", err)
}
if err := syncDirectory(root); err != nil {
return CachedArtifact{}, err
}
result, ok := cache.readSlot(root, "candidate", request.OperationID, release)
if !ok {
return CachedArtifact{}, errors.New("downloaded connector artifact candidate could not be verified")
}
return result, nil
}
func (*DownloadCache) readSlot(
root string,
slot string,
operationID string,
release market.Release,
) (CachedArtifact, bool) {
directory := filepath.Join(root, slot)
data, err := os.ReadFile(filepath.Join(directory, downloadCacheReceiptFile))
if err != nil {
return CachedArtifact{}, false
}
var receipt CachedArtifact
if json.Unmarshal(data, &receipt) != nil || receipt.SchemaVersion != downloadCacheReceiptSchema ||
receipt.ConnectorKey != release.ConnectorKey || receipt.ReleaseID != release.ReleaseID ||
receipt.ReleaseDigest != release.ReleaseDigest || receipt.ArtifactSHA != release.Artifact.SHA256 ||
receipt.SizeBytes != release.Artifact.SizeBytes || receipt.MediaType != release.Artifact.MediaType ||
receipt.ObjectVersion != release.Artifact.ObjectVersion {
return CachedArtifact{}, false
}
artifactPath := filepath.Join(directory, downloadCacheArtifactFile)
if verifyArtifactFile(artifactPath, release.Artifact) != nil {
return CachedArtifact{}, false
}
receipt.OperationID = operationID
receipt.Slot = slot
receipt.Path = artifactPath
return receipt, true
}
func (*DownloadCache) recoverPromotion(root string) error {
if err := os.MkdirAll(root, 0o700); err != nil {
return err
}
current := filepath.Join(root, "current")
retired := filepath.Join(root, ".retired")
_, currentErr := os.Stat(current)
_, retiredErr := os.Stat(retired)
switch {
case errors.Is(currentErr, os.ErrNotExist) && retiredErr == nil:
if err := os.Rename(retired, current); err != nil {
return fmt.Errorf("recover current connector artifact: %w", err)
}
case currentErr == nil && retiredErr == nil:
if err := os.RemoveAll(retired); err != nil {
return fmt.Errorf("remove recovered connector artifact: %w", err)
}
case currentErr != nil && !errors.Is(currentErr, os.ErrNotExist):
return currentErr
case retiredErr != nil && !errors.Is(retiredErr, os.ErrNotExist):
return retiredErr
}
return nil
}
func (cache *DownloadCache) connectorRoot(connectorKey string) (string, error) {
if !safeSegment(connectorKey) {
return "", errors.New("connector cache key is invalid")
}
root := filepath.Join(cache.rootDir, connectorKey)
if err := ensureWithin(cache.rootDir, root); err != nil {
return "", err
}
return root, nil
}
func writeDownloadCacheReceipt(root string, receipt CachedArtifact) error {
receipt.Path = ""
data, err := json.Marshal(receipt)
if err != nil {
return err
}
path := filepath.Join(root, downloadCacheReceiptFile)
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
if err != nil {
return fmt.Errorf("create connector download cache receipt: %w", err)
}
if _, err := file.Write(data); err != nil {
_ = file.Close()
return err
}
if err := file.Sync(); err != nil {
_ = file.Close()
return err
}
if err := file.Close(); err != nil {
return err
}
return syncDirectory(root)
}