Centralize path-safe root joins and atomic file operations

This commit is contained in:
2026-05-23 15:45:31 +00:00
parent 98649f4d81
commit 094b0d2532
11 changed files with 466 additions and 189 deletions

View File

@@ -8,6 +8,7 @@ import (
"sort"
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
"gitea.maximumdirect.net/eric/narratio/internal/fileops"
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
"gitea.maximumdirect.net/eric/narratio/internal/previouscache"
)
@@ -54,8 +55,35 @@ func hydratePreviousSessionArtifacts(
if err := os.MkdirAll(filepath.Dir(record.LocalPath), 0o755); err != nil {
return nil, fmt.Errorf("create previous-session path directory for %q: %w", record.LocalPath, err)
}
if err := env.ObjectStore.Download(ctx, record.RemoteKey, record.LocalPath); err != nil {
return nil, fmt.Errorf("download previous-session object %q to %q: %w", record.RemoteKey, record.LocalPath, err)
base := filepath.Base(record.LocalPath)
tmp, err := os.CreateTemp(filepath.Dir(record.LocalPath), "."+base+".prepare-previous-*.tmp")
if err != nil {
return nil, fmt.Errorf("create previous-session temp file for %q: %w", record.LocalPath, err)
}
tmpPath := tmp.Name()
if err := tmp.Close(); err != nil {
_ = os.Remove(tmpPath)
return nil, fmt.Errorf("close previous-session temp file for %q: %w", record.LocalPath, err)
}
if err := func() error {
removeTmp := true
defer func() {
if removeTmp {
_ = os.Remove(tmpPath)
}
}()
if err := env.ObjectStore.Download(ctx, record.RemoteKey, tmpPath); err != nil {
return fmt.Errorf("download previous-session object %q to temp file: %w", record.RemoteKey, err)
}
if err := fileops.InstallDownloadedTempFile(tmpPath, record.LocalPath, 0o644); err != nil {
return fmt.Errorf("install previous-session object %q at %q: %w", record.RemoteKey, record.LocalPath, err)
}
removeTmp = false
return nil
}(); err != nil {
return nil, err
}
if record.Kind == preparePreviousInputKindArtifact {
if err := requireNonEmptyFile(record.LocalPath, "previous-session artifact "+record.RequirementName); err != nil {

View File

@@ -9,6 +9,7 @@ import (
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
"gitea.maximumdirect.net/eric/narratio/internal/config"
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
)
type runStageLayout struct {
@@ -92,18 +93,17 @@ func runLocalPathForCanonical(layout runStageLayout, sessionPaths artifacts.Sess
if cleanCanonical == "" {
return "", fmt.Errorf("canonical path is required")
}
rel, err := filepath.Rel(filepath.Clean(sessionPaths.Root), cleanCanonical)
rel, err := pathsafe.SlashRelativeFromRoot(sessionPaths.Root, cleanCanonical)
if err != nil {
return "", fmt.Errorf("derive session-relative path for %q: %w", cleanCanonical, err)
}
rel = filepath.Clean(rel)
if rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("canonical path %q is outside session root %q", cleanCanonical, sessionPaths.Root)
}
if rel == config.PathPreviousDirSegment || strings.HasPrefix(rel, config.PathPreviousDirSegment+string(filepath.Separator)) {
if rel == config.PathPreviousDirSegment || strings.HasPrefix(rel, config.PathPreviousDirSegment+"/") {
return cleanCanonical, nil
}
localPath := filepath.Join(layout.OutputsDir, rel)
localPath, err := pathsafe.JoinSlashRelativeUnderRoot(layout.OutputsDir, rel)
if err != nil {
return "", fmt.Errorf("resolve run-local output path for %q: %w", cleanCanonical, err)
}
if err := os.MkdirAll(filepath.Dir(localPath), 0o755); err != nil {
return "", fmt.Errorf("create run-local output parent for %q: %w", localPath, err)
}