239 lines
11 KiB
Go
239 lines
11 KiB
Go
package artifacts
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
|
)
|
|
|
|
// SessionPaths contains canonical local paths for one session work directory.
|
|
type SessionPaths struct {
|
|
WorkspaceRoot string
|
|
CampaignID string
|
|
SessionID string
|
|
Root string
|
|
InputsDir string
|
|
AudioDir string
|
|
TranscriptsDir string
|
|
TranscriptsRawDir string
|
|
TranscriptsTrimmedDir string
|
|
ArtifactsDir string
|
|
ReportsDir string
|
|
ConfigDir string
|
|
LogsDir string
|
|
CurrentDir string
|
|
RunsDir string
|
|
PreviousDir string
|
|
PreviousManifestPath string
|
|
PreviousArtifactsDir string
|
|
ManifestPath string
|
|
LockPath string
|
|
}
|
|
|
|
// SessionWorkDirForCampaign returns the canonical campaign-aware work directory for one session.
|
|
func SessionWorkDirForCampaign(rootDir, campaign, sessionID string) string {
|
|
return filepath.Join(rootDir, config.PathWorkDirSegment, campaign, sessionID)
|
|
}
|
|
|
|
// SessionManifestPathForCampaign returns the canonical session manifest path.
|
|
func SessionManifestPathForCampaign(rootDir, campaign, sessionID string) string {
|
|
return filepath.Join(SessionWorkDirForCampaign(rootDir, campaign, sessionID), config.PathManifestFile)
|
|
}
|
|
|
|
// SessionRunsDirForCampaign returns the canonical runs directory for one session.
|
|
func SessionRunsDirForCampaign(rootDir, campaign, sessionID string) string {
|
|
return filepath.Join(SessionWorkDirForCampaign(rootDir, campaign, sessionID), config.PathRunsDirSegment)
|
|
}
|
|
|
|
// SessionPreviousDirForCampaign returns the canonical previous-session state directory.
|
|
func SessionPreviousDirForCampaign(rootDir, campaign, sessionID string) string {
|
|
return filepath.Join(SessionWorkDirForCampaign(rootDir, campaign, sessionID), config.PathPreviousDirSegment)
|
|
}
|
|
|
|
// SessionPreviousManifestPathForCampaign returns the canonical previous-session manifest cache path.
|
|
func SessionPreviousManifestPathForCampaign(rootDir, campaign, sessionID string) string {
|
|
return filepath.Join(SessionPreviousDirForCampaign(rootDir, campaign, sessionID), config.PathManifestFile)
|
|
}
|
|
|
|
// SessionPreviousArtifactsDirForCampaign returns the canonical previous-session artifact cache directory.
|
|
func SessionPreviousArtifactsDirForCampaign(rootDir, campaign, sessionID string) string {
|
|
return filepath.Join(SessionPreviousDirForCampaign(rootDir, campaign, sessionID), config.PathArtifactsDirSegment)
|
|
}
|
|
|
|
// SessionPreviousArtifactPathForCampaign returns a path under previous/artifacts for one artifact.
|
|
func SessionPreviousArtifactPathForCampaign(rootDir, campaign, sessionID, artifactRelativePath string) string {
|
|
return filepath.Join(
|
|
SessionPreviousArtifactsDirForCampaign(rootDir, campaign, sessionID),
|
|
filepath.FromSlash(previousArtifactCacheRelativePath(artifactRelativePath)),
|
|
)
|
|
}
|
|
|
|
// SessionRunRootForCampaign returns the canonical run root under runs/{run_id}.
|
|
func SessionRunRootForCampaign(rootDir, campaign, sessionID, runID string) string {
|
|
return filepath.Join(SessionRunsDirForCampaign(rootDir, campaign, sessionID), runID)
|
|
}
|
|
|
|
// SessionRunManifestPathForCampaign returns the canonical run manifest path under runs/{run_id}/manifest.json.
|
|
func SessionRunManifestPathForCampaign(rootDir, campaign, sessionID, runID string) string {
|
|
return filepath.Join(SessionRunRootForCampaign(rootDir, campaign, sessionID, runID), config.PathManifestFile)
|
|
}
|
|
|
|
// SessionRunStageDirForCampaign returns the canonical stage directory under runs/{run_id}/{stage}.
|
|
func SessionRunStageDirForCampaign(rootDir, campaign, sessionID, runID, stageName string) string {
|
|
return filepath.Join(SessionRunRootForCampaign(rootDir, campaign, sessionID, runID), stageName)
|
|
}
|
|
|
|
// SessionRunExtractDirForCampaign returns the invocation-local extraction directory.
|
|
func SessionRunExtractDirForCampaign(rootDir, campaign, sessionID, runID string) string {
|
|
return SessionRunStageDirForCampaign(rootDir, campaign, sessionID, runID, "extract")
|
|
}
|
|
|
|
// SessionRunNotariusReceiptPathForCampaign returns the invocation-local receipt path.
|
|
func SessionRunNotariusReceiptPathForCampaign(rootDir, campaign, sessionID, runID string) string {
|
|
return filepath.Join(SessionRunExtractDirForCampaign(rootDir, campaign, sessionID, runID), "notarius.receipt.json")
|
|
}
|
|
|
|
// SessionRunNotariusLogPathForCampaign returns the invocation-local stderr log path.
|
|
func SessionRunNotariusLogPathForCampaign(rootDir, campaign, sessionID, runID string) string {
|
|
return filepath.Join(SessionRunExtractDirForCampaign(rootDir, campaign, sessionID, runID), "notarius.stderr.log")
|
|
}
|
|
|
|
// SessionRunNotariusOutputRootForCampaign returns the invocation-local Notarius output root.
|
|
func SessionRunNotariusOutputRootForCampaign(rootDir, campaign, sessionID, runID string) string {
|
|
return filepath.Join(SessionRunExtractDirForCampaign(rootDir, campaign, sessionID, runID), "notarius-output")
|
|
}
|
|
|
|
// SessionNotariusBundleDirForCampaign returns one immutable durable bundle destination.
|
|
func SessionNotariusBundleDirForCampaign(rootDir, campaign, sessionID, runID string) string {
|
|
return filepath.Join(
|
|
SessionWorkDirForCampaign(rootDir, campaign, sessionID),
|
|
config.PathArtifactsDirSegment,
|
|
"notarius",
|
|
runID,
|
|
)
|
|
}
|
|
|
|
// SessionSpoolAudioDir returns the campaign/session/run scoped local spool audio path.
|
|
func SessionSpoolAudioDir(spoolRoot, campaign, sessionID, runID string) string {
|
|
return filepath.Join(spoolRoot, campaign, sessionID, runID, config.PathAudioDirSegment)
|
|
}
|
|
|
|
// SessionSpoolDir returns the campaign/session scoped local spool root.
|
|
func SessionSpoolDir(spoolRoot, campaign, sessionID string) string {
|
|
return filepath.Join(spoolRoot, campaign, sessionID)
|
|
}
|
|
|
|
// SessionSpoolRestoreAudioDir returns the local spool audio path for restore downloads.
|
|
func SessionSpoolRestoreAudioDir(spoolRoot, campaign, sessionID string) string {
|
|
return filepath.Join(SessionSpoolDir(spoolRoot, campaign, sessionID), "restore", config.PathAudioDirSegment)
|
|
}
|
|
|
|
// S3AudioCachePath returns the durable local cache path for one S3 audio object.
|
|
func S3AudioCachePath(cacheRoot, bucket, key string) (string, error) {
|
|
root := filepath.Clean(strings.TrimSpace(cacheRoot))
|
|
if root == "." || root == "" {
|
|
return "", fmt.Errorf("cache root is required")
|
|
}
|
|
bucket = strings.Trim(strings.TrimSpace(bucket), "/")
|
|
if bucket == "" || bucket == "." || bucket == ".." || strings.Contains(bucket, "/") || strings.Contains(bucket, `\`) {
|
|
return "", fmt.Errorf("bucket is required and must be a single path segment")
|
|
}
|
|
rawKey := strings.ReplaceAll(strings.TrimSpace(key), `\`, "/")
|
|
if strings.HasPrefix(rawKey, "/") {
|
|
return "", fmt.Errorf("s3 key must not be absolute")
|
|
}
|
|
cleanKey := cleanCacheS3Key(rawKey)
|
|
if cleanKey == "" {
|
|
return "", fmt.Errorf("s3 key is required")
|
|
}
|
|
if cleanKey == ".." || strings.HasPrefix(cleanKey, "../") || strings.HasPrefix(cleanKey, "/") {
|
|
return "", fmt.Errorf("s3 key must not escape cache root")
|
|
}
|
|
return filepath.Join(root, "s3", bucket, filepath.FromSlash(cleanKey)), nil
|
|
}
|
|
|
|
// S3AudioCacheNamespaceDir returns the durable local cache namespace for all
|
|
// Narratio S3 audio objects under one bucket/root prefix.
|
|
func S3AudioCacheNamespaceDir(cacheRoot, bucket, rootPrefix string) (string, error) {
|
|
sentinelKey := path.Join(cleanS3PathPart(rootPrefix), config.S3CampaignsSegment, ".narratio-cache-sentinel")
|
|
sentinelPath, err := S3AudioCachePath(cacheRoot, bucket, sentinelKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Dir(sentinelPath), nil
|
|
}
|
|
|
|
func cleanCacheS3Key(key string) string {
|
|
normalized := strings.ReplaceAll(strings.TrimSpace(key), `\`, "/")
|
|
normalized = strings.Trim(normalized, "/")
|
|
if normalized == "" {
|
|
return ""
|
|
}
|
|
return path.Clean(normalized)
|
|
}
|
|
|
|
// SessionPreviousDir returns the previous-session state directory for already-resolved session paths.
|
|
func SessionPreviousDir(paths SessionPaths) string {
|
|
return paths.PreviousDir
|
|
}
|
|
|
|
// SessionPreviousManifestPath returns the previous-session manifest path for already-resolved session paths.
|
|
func SessionPreviousManifestPath(paths SessionPaths) string {
|
|
return paths.PreviousManifestPath
|
|
}
|
|
|
|
// SessionPreviousArtifactsDir returns the previous-session artifact directory for already-resolved session paths.
|
|
func SessionPreviousArtifactsDir(paths SessionPaths) string {
|
|
return paths.PreviousArtifactsDir
|
|
}
|
|
|
|
// SessionPreviousArtifactPath returns a path under previous/artifacts for already-resolved session paths.
|
|
func SessionPreviousArtifactPath(paths SessionPaths, artifactRelativePath string) string {
|
|
return filepath.Join(paths.PreviousArtifactsDir, filepath.FromSlash(previousArtifactCacheRelativePath(artifactRelativePath)))
|
|
}
|
|
|
|
func previousArtifactCacheRelativePath(artifactRelativePath string) string {
|
|
rel := filepath.ToSlash(filepath.Clean(filepath.FromSlash(strings.TrimSpace(artifactRelativePath))))
|
|
if rel == "." {
|
|
return ""
|
|
}
|
|
const artifactsPrefix = "artifacts/"
|
|
if strings.HasPrefix(rel, artifactsPrefix) && len(rel) > len(artifactsPrefix) {
|
|
return strings.TrimPrefix(rel, artifactsPrefix)
|
|
}
|
|
return rel
|
|
}
|
|
|
|
func buildSessionPaths(workspaceRoot, campaign, sessionID string) SessionPaths {
|
|
root := SessionWorkDirForCampaign(workspaceRoot, campaign, sessionID)
|
|
return buildSessionPathsFromRoot(workspaceRoot, campaign, sessionID, root)
|
|
}
|
|
|
|
func buildSessionPathsFromRoot(workspaceRoot, campaign, sessionID, root string) SessionPaths {
|
|
return SessionPaths{
|
|
WorkspaceRoot: workspaceRoot,
|
|
CampaignID: campaign,
|
|
SessionID: sessionID,
|
|
Root: root,
|
|
InputsDir: filepath.Join(root, config.PathInputsDirSegment),
|
|
AudioDir: filepath.Join(root, config.PathAudioDirSegment),
|
|
TranscriptsDir: filepath.Join(root, config.PathTranscriptsSegment),
|
|
TranscriptsRawDir: filepath.Join(root, filepath.FromSlash(config.PathTranscriptsRaw)),
|
|
TranscriptsTrimmedDir: filepath.Join(root, filepath.FromSlash(config.PathTranscriptsTrimmed)),
|
|
ArtifactsDir: filepath.Join(root, config.PathArtifactsDirSegment),
|
|
ReportsDir: filepath.Join(root, config.PathReportsDirSegment),
|
|
ConfigDir: filepath.Join(root, config.PathConfigDirSegment),
|
|
LogsDir: filepath.Join(root, config.PathLogsDirSegment),
|
|
CurrentDir: filepath.Join(root, config.PathCurrentDirSegment),
|
|
RunsDir: filepath.Join(root, config.PathRunsDirSegment),
|
|
PreviousDir: filepath.Join(root, config.PathPreviousDirSegment),
|
|
PreviousManifestPath: filepath.Join(root, config.PathPreviousDirSegment, config.PathManifestFile),
|
|
PreviousArtifactsDir: filepath.Join(root, config.PathPreviousDirSegment, config.PathArtifactsDirSegment),
|
|
ManifestPath: filepath.Join(root, config.PathManifestFile),
|
|
LockPath: filepath.Join(root, config.PathLockFile),
|
|
}
|
|
}
|