Validate downloads before installing workspace files

This commit is contained in:
2026-09-02 00:37:00 +00:00
parent ba858cc997
commit 39b2665d8b
13 changed files with 204 additions and 665 deletions

View File

@@ -5,8 +5,10 @@ import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
@@ -58,23 +60,23 @@ func hydratePreviousSessionArtifacts(
return nil, fmt.Errorf("create previous-session path directory for %q: %w", record.LocalPath, err)
}
if err := fileops.DownloadAndInstall(record.LocalPath, fileops.WorkspaceFileMode, func(destination io.Writer) error {
temporaryPath, err := writePreviousTemp(record.LocalPath, func(destination io.Writer) error {
if len(record.VerifiedContent) > 0 {
_, err := io.Copy(destination, bytes.NewReader(record.VerifiedContent))
return err
}
return storage.DownloadTo(ctx, env.ObjectStore, record.RemoteKey, destination)
}); err != nil {
})
if err != nil {
return nil, fmt.Errorf("write previous-session object %q: %w", record.RemoteKey, err)
}
defer func() { _ = os.Remove(temporaryPath) }()
checksum, err := validatePreviousTemp(record, temporaryPath)
if err != nil {
return nil, err
}
if record.Kind == preparePreviousInputKindArtifact {
if err := requireNonEmptyFile(record.LocalPath, "previous-session artifact "+record.RequirementName); err != nil {
return nil, err
}
}
checksum, err := env.ArtifactStore.Checksum(record.LocalPath)
if err != nil {
return nil, fmt.Errorf("checksum previous-session object %q: %w", record.RemoteKey, err)
if err := fileops.InstallSiblingTempFile(temporaryPath, record.LocalPath, fileops.WorkspaceFileMode); err != nil {
return nil, fmt.Errorf("install previous-session object %q: %w", record.RemoteKey, err)
}
result.Inputs = append(result.Inputs, manifest.InputRecord{
@@ -100,3 +102,55 @@ func hydratePreviousSessionArtifacts(
})
return result, nil
}
func writePreviousTemp(destination string, write func(io.Writer) error) (string, error) {
if strings.TrimSpace(destination) == "" {
return "", fmt.Errorf("previous-session destination path is required")
}
if write == nil {
return "", fmt.Errorf("previous-session temporary writer is required")
}
directory := filepath.Dir(destination)
if err := fileops.EnsureWorkspaceDirectory(directory); err != nil {
return "", fmt.Errorf("create previous-session path directory: %w", err)
}
temporary, err := os.CreateTemp(directory, "."+filepath.Base(destination)+".tmp-*")
if err != nil {
return "", fmt.Errorf("create sibling temporary file: %w", err)
}
temporaryPath := temporary.Name()
if err := write(temporary); err != nil {
_ = temporary.Close()
_ = os.Remove(temporaryPath)
return "", err
}
if err := temporary.Close(); err != nil {
_ = os.Remove(temporaryPath)
return "", fmt.Errorf("close sibling temporary file: %w", err)
}
return temporaryPath, nil
}
func validatePreviousTemp(record previouscache.Record, temporaryPath string) (string, error) {
info, err := os.Stat(temporaryPath)
if err != nil {
return "", fmt.Errorf("inspect previous-session object %q: %w", record.RemoteKey, err)
}
hasDeclaredSize := record.Size > 0 || strings.TrimSpace(record.SHA256) != "" || strings.TrimSpace(record.Generation) != ""
if hasDeclaredSize && info.Size() != record.Size {
return "", fmt.Errorf("previous-session object %q size mismatch: got %d, want %d", record.RemoteKey, info.Size(), record.Size)
}
if record.Kind == preparePreviousInputKindArtifact {
if err := requireNonEmptyFile(temporaryPath, "previous-session artifact "+record.RequirementName); err != nil {
return "", err
}
}
checksum, err := artifacts.SHA256File(temporaryPath)
if err != nil {
return "", fmt.Errorf("checksum previous-session object %q: %w", record.RemoteKey, err)
}
if expected := strings.TrimSpace(record.SHA256); expected != "" && !strings.EqualFold(checksum, expected) {
return "", fmt.Errorf("previous-session object %q checksum mismatch", record.RemoteKey)
}
return checksum, nil
}