209 lines
6.4 KiB
Go
209 lines
6.4 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
|
)
|
|
|
|
func runPostArchiveCleanup(ctx context.Context, env *Env, manifestPath string, m *manifest.Manifest, executed []string) error {
|
|
if env == nil || env.Config == nil || env.Config.Pipeline == nil || m == nil {
|
|
return nil
|
|
}
|
|
|
|
spoolRequested := env.Config.Pipeline.Spool.DeleteAudioAfterArchive
|
|
workRequested := env.Config.Pipeline.Workspace.CleanupAfterArchive
|
|
if !spoolRequested && !workRequested {
|
|
return nil
|
|
}
|
|
|
|
sr := archiveStageRecordForCleanup(m, executed)
|
|
if sr == nil {
|
|
return nil
|
|
}
|
|
if sr.Metadata == nil {
|
|
sr.Metadata = map[string]any{}
|
|
}
|
|
sr.Metadata["spool_cleanup_requested"] = spoolRequested
|
|
sr.Metadata["workdir_cleanup_requested"] = workRequested
|
|
|
|
eligible, reason := archiveCleanupEligible(env.Config, sr)
|
|
if !eligible {
|
|
sr.Metadata["cleanup_skipped"] = true
|
|
sr.Metadata["cleanup_skipped_reason"] = reason
|
|
if err := env.ManifestStore.Save(ctx, manifestPath, m); err != nil {
|
|
return fmt.Errorf("save manifest cleanup skip metadata %q: %w", manifestPath, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
spoolDir := strings.TrimSpace(m.LocalSpoolDir)
|
|
if spoolDir == "" {
|
|
spoolDir = artifacts.SessionSpoolAudioDir(
|
|
env.Config.Pipeline.Spool.Root,
|
|
strings.TrimSpace(env.Config.Session.Campaign),
|
|
strings.TrimSpace(env.Config.Session.SessionID),
|
|
strings.TrimSpace(m.RunID),
|
|
)
|
|
}
|
|
workDir := strings.TrimSpace(m.LocalWorkDir)
|
|
if workDir == "" {
|
|
workDir = artifacts.SessionRunWorkDir(
|
|
env.Config.Pipeline.Workspace.Root,
|
|
strings.TrimSpace(env.Config.Session.Campaign),
|
|
strings.TrimSpace(env.Config.Session.SessionID),
|
|
strings.TrimSpace(m.RunID),
|
|
)
|
|
}
|
|
|
|
if spoolRequested {
|
|
if err := removeRunScopedDir(strings.TrimSpace(env.Config.Pipeline.Spool.Root), spoolDir, "pipeline.spool.delete_audio_after_archive"); err != nil {
|
|
sr.Metadata["cleanup_failed"] = true
|
|
sr.Metadata["cleanup_failed_policy"] = "pipeline.spool.delete_audio_after_archive"
|
|
sr.Metadata["cleanup_failed_path"] = spoolDir
|
|
_ = env.ManifestStore.Save(ctx, manifestPath, m)
|
|
return err
|
|
}
|
|
sr.Metadata["spool_cleanup_deleted"] = filepath.Clean(spoolDir)
|
|
}
|
|
|
|
if !workRequested {
|
|
sr.Metadata["cleanup_completed"] = true
|
|
sr.Metadata["cleanup_skipped"] = false
|
|
if err := env.ManifestStore.Save(ctx, manifestPath, m); err != nil {
|
|
return fmt.Errorf("save manifest cleanup metadata %q: %w", manifestPath, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if err := removeRunScopedDir(strings.TrimSpace(env.Config.Pipeline.Workspace.Root), workDir, "pipeline.workspace.cleanup_after_archive"); err != nil {
|
|
sr.Metadata["cleanup_failed"] = true
|
|
sr.Metadata["cleanup_failed_policy"] = "pipeline.workspace.cleanup_after_archive"
|
|
sr.Metadata["cleanup_failed_path"] = workDir
|
|
_ = env.ManifestStore.Save(ctx, manifestPath, m)
|
|
return err
|
|
}
|
|
|
|
sr.Metadata["workdir_cleanup_deleted"] = filepath.Clean(workDir)
|
|
sr.Metadata["cleanup_completed"] = true
|
|
sr.Metadata["cleanup_skipped"] = false
|
|
return nil
|
|
}
|
|
|
|
func archiveStageRecordForCleanup(m *manifest.Manifest, executed []string) *manifest.StageRecord {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
archiveRan := false
|
|
for _, name := range executed {
|
|
if name == "archive" {
|
|
archiveRan = true
|
|
break
|
|
}
|
|
}
|
|
if !archiveRan {
|
|
return nil
|
|
}
|
|
sr := m.Stages["archive"]
|
|
if sr == nil || sr.Status != manifest.StatusSucceeded {
|
|
return nil
|
|
}
|
|
return sr
|
|
}
|
|
|
|
func archiveCleanupEligible(cfg *config.Config, sr *manifest.StageRecord) (bool, string) {
|
|
if cfg == nil || cfg.Pipeline == nil || cfg.Pipeline.Archive == nil {
|
|
return false, "archive configuration is missing"
|
|
}
|
|
enabled := true
|
|
if cfg.Pipeline.Archive.Enabled != nil {
|
|
enabled = *cfg.Pipeline.Archive.Enabled
|
|
}
|
|
if !enabled {
|
|
return false, "archive.enabled is false"
|
|
}
|
|
uploadRun := true
|
|
if cfg.Pipeline.Archive.UploadRun != nil {
|
|
uploadRun = *cfg.Pipeline.Archive.UploadRun
|
|
}
|
|
if !uploadRun {
|
|
return false, "archive.upload_run is false"
|
|
}
|
|
if sr == nil || sr.Metadata == nil {
|
|
return false, "archive metadata is missing"
|
|
}
|
|
if skipped, _ := sr.Metadata["skipped"].(bool); skipped {
|
|
return false, "archive stage was skipped"
|
|
}
|
|
if uploaded, _ := sr.Metadata["uploaded"].(bool); !uploaded {
|
|
return false, "archive did not upload run record"
|
|
}
|
|
if pointer, _ := sr.Metadata["current_pointer_written"].(bool); !pointer {
|
|
return false, "archive did not write current pointer"
|
|
}
|
|
if strings.TrimSpace(asString(sr.Metadata["current_run_id_key"])) == "" {
|
|
return false, "archive current run pointer key is missing"
|
|
}
|
|
return true, ""
|
|
}
|
|
|
|
func removeRunScopedDir(root, target, policy string) error {
|
|
cleanRoot := strings.TrimSpace(root)
|
|
cleanTarget := strings.TrimSpace(target)
|
|
if cleanRoot == "" {
|
|
return fmt.Errorf("cleanup policy %s: root path is required", policy)
|
|
}
|
|
if cleanTarget == "" {
|
|
return fmt.Errorf("cleanup policy %s: target path is required", policy)
|
|
}
|
|
|
|
rootAbs, err := filepath.Abs(cleanRoot)
|
|
if err != nil {
|
|
return fmt.Errorf("cleanup policy %s: resolve root %q: %w", policy, cleanRoot, err)
|
|
}
|
|
targetAbs, err := filepath.Abs(cleanTarget)
|
|
if err != nil {
|
|
return fmt.Errorf("cleanup policy %s: resolve target %q: %w", policy, cleanTarget, err)
|
|
}
|
|
|
|
rel, err := filepath.Rel(rootAbs, targetAbs)
|
|
if err != nil {
|
|
return fmt.Errorf("cleanup policy %s: relative path from %q to %q: %w", policy, rootAbs, targetAbs, err)
|
|
}
|
|
if rel == "." {
|
|
return fmt.Errorf("cleanup policy %s: refusing to delete root directory %q", policy, rootAbs)
|
|
}
|
|
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
|
return fmt.Errorf("cleanup policy %s: refusing to delete path outside root: root=%q target=%q", policy, rootAbs, targetAbs)
|
|
}
|
|
|
|
info, err := os.Lstat(targetAbs)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("cleanup policy %s: stat target %q: %w", policy, targetAbs, err)
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 {
|
|
return fmt.Errorf("cleanup policy %s: refusing to delete symlink path %q", policy, targetAbs)
|
|
}
|
|
if !info.IsDir() {
|
|
return fmt.Errorf("cleanup policy %s: target %q is not a directory", policy, targetAbs)
|
|
}
|
|
if err := os.RemoveAll(targetAbs); err != nil {
|
|
return fmt.Errorf("cleanup policy %s: remove %q: %w", policy, targetAbs, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func asString(v any) string {
|
|
s, _ := v.(string)
|
|
return s
|
|
}
|