Add campaign-aware workspace path foundation
This commit is contained in:
@@ -30,13 +30,18 @@ func NewLocalStore(workspaceRoot string) *LocalStore {
|
||||
return &LocalStore{WorkspaceRoot: workspaceRoot}
|
||||
}
|
||||
|
||||
// SessionPaths resolves canonical paths for a session workdir.
|
||||
// SessionPaths resolves legacy paths for a session workdir.
|
||||
func (s *LocalStore) SessionPaths(sessionID string) SessionPaths {
|
||||
return buildSessionPaths(s.WorkspaceRoot, sessionID)
|
||||
return buildLegacySessionPaths(s.WorkspaceRoot, sessionID)
|
||||
}
|
||||
|
||||
// EnsureLayout creates and verifies the canonical session workdir directory layout.
|
||||
func (s *LocalStore) EnsureLayout(sessionID string) (SessionPaths, error) {
|
||||
// SessionPathsFor resolves canonical campaign-aware paths for a session workdir.
|
||||
func (s *LocalStore) SessionPathsFor(campaign, sessionID string) SessionPaths {
|
||||
return buildSessionPaths(s.WorkspaceRoot, campaign, sessionID)
|
||||
}
|
||||
|
||||
// ResolveSessionPathsFor resolves the active session path with legacy compatibility.
|
||||
func (s *LocalStore) ResolveSessionPathsFor(campaign, sessionID string) (SessionPaths, error) {
|
||||
if strings.TrimSpace(s.WorkspaceRoot) == "" {
|
||||
return SessionPaths{}, fmt.Errorf("workspace root is required")
|
||||
}
|
||||
@@ -44,7 +49,62 @@ func (s *LocalStore) EnsureLayout(sessionID string) (SessionPaths, error) {
|
||||
return SessionPaths{}, fmt.Errorf("sessionID is required")
|
||||
}
|
||||
|
||||
paths := s.SessionPaths(sessionID)
|
||||
campaign = strings.TrimSpace(campaign)
|
||||
if campaign == "" {
|
||||
return s.SessionPaths(sessionID), nil
|
||||
}
|
||||
|
||||
canonical := s.SessionPathsFor(campaign, sessionID)
|
||||
legacy := s.SessionPaths(sessionID)
|
||||
canonicalExists, err := dirExists(canonical.Root)
|
||||
if err != nil {
|
||||
return SessionPaths{}, fmt.Errorf("check canonical session root %q: %w", canonical.Root, err)
|
||||
}
|
||||
legacyExists, err := dirExists(legacy.Root)
|
||||
if err != nil {
|
||||
return SessionPaths{}, fmt.Errorf("check legacy session root %q: %w", legacy.Root, err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case canonicalExists && legacyExists:
|
||||
return SessionPaths{}, fmt.Errorf(
|
||||
"ambiguous session workspace roots for campaign %q session %q: canonical=%q legacy=%q",
|
||||
campaign,
|
||||
sessionID,
|
||||
canonical.Root,
|
||||
legacy.Root,
|
||||
)
|
||||
case canonicalExists:
|
||||
return canonical, nil
|
||||
case legacyExists:
|
||||
return legacy, nil
|
||||
default:
|
||||
return canonical, nil
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureLayout creates and verifies the canonical session workdir directory layout.
|
||||
func (s *LocalStore) EnsureLayout(sessionID string) (SessionPaths, error) {
|
||||
return s.ensureLayout(s.SessionPaths(sessionID))
|
||||
}
|
||||
|
||||
// EnsureLayoutFor creates and verifies campaign-aware session layout, with controlled legacy compatibility.
|
||||
func (s *LocalStore) EnsureLayoutFor(campaign, sessionID string) (SessionPaths, error) {
|
||||
paths, err := s.ResolveSessionPathsFor(campaign, sessionID)
|
||||
if err != nil {
|
||||
return SessionPaths{}, err
|
||||
}
|
||||
return s.ensureLayout(paths)
|
||||
}
|
||||
|
||||
func (s *LocalStore) ensureLayout(paths SessionPaths) (SessionPaths, error) {
|
||||
if strings.TrimSpace(s.WorkspaceRoot) == "" {
|
||||
return SessionPaths{}, fmt.Errorf("workspace root is required")
|
||||
}
|
||||
if strings.TrimSpace(paths.SessionID) == "" {
|
||||
return SessionPaths{}, fmt.Errorf("sessionID is required")
|
||||
}
|
||||
|
||||
dirs := []string{
|
||||
paths.Root,
|
||||
paths.InputsDir,
|
||||
@@ -53,8 +113,11 @@ func (s *LocalStore) EnsureLayout(sessionID string) (SessionPaths, error) {
|
||||
paths.TranscriptsRawDir,
|
||||
paths.TranscriptsTrimmedDir,
|
||||
paths.ArtifactsDir,
|
||||
paths.ReportsDir,
|
||||
paths.ConfigDir,
|
||||
paths.LogsDir,
|
||||
paths.CurrentDir,
|
||||
paths.RunsDir,
|
||||
}
|
||||
|
||||
for _, dir := range dirs {
|
||||
@@ -72,7 +135,19 @@ func (s *LocalStore) CopyInput(sessionID, srcPath, destRelativePath string) (Ref
|
||||
if err != nil {
|
||||
return Ref{}, err
|
||||
}
|
||||
return s.copyInputWithPaths(paths, sessionID, srcPath, destRelativePath)
|
||||
}
|
||||
|
||||
// CopyInputFor copies an input file into the campaign-aware session workdir under destRelativePath.
|
||||
func (s *LocalStore) CopyInputFor(campaign, sessionID, srcPath, destRelativePath string) (Ref, error) {
|
||||
paths, err := s.EnsureLayoutFor(campaign, sessionID)
|
||||
if err != nil {
|
||||
return Ref{}, err
|
||||
}
|
||||
return s.copyInputWithPaths(paths, sessionID, srcPath, destRelativePath)
|
||||
}
|
||||
|
||||
func (s *LocalStore) copyInputWithPaths(paths SessionPaths, sessionID, srcPath, destRelativePath string) (Ref, error) {
|
||||
destAbs, err := resolveInRoot(paths.Root, destRelativePath)
|
||||
if err != nil {
|
||||
return Ref{}, fmt.Errorf("copy input: %w", err)
|
||||
@@ -179,7 +254,19 @@ func (s *LocalStore) AcquireSessionLock(sessionID string) (*LockHandle, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.acquireSessionLockForPaths(paths)
|
||||
}
|
||||
|
||||
// AcquireSessionLockFor acquires an exclusive lock file for a campaign/session workdir.
|
||||
func (s *LocalStore) AcquireSessionLockFor(campaign, sessionID string) (*LockHandle, error) {
|
||||
paths, err := s.EnsureLayoutFor(campaign, sessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.acquireSessionLockForPaths(paths)
|
||||
}
|
||||
|
||||
func (s *LocalStore) acquireSessionLockForPaths(paths SessionPaths) (*LockHandle, error) {
|
||||
f, err := os.OpenFile(paths.LockPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrExist) {
|
||||
@@ -203,6 +290,17 @@ func (s *LocalStore) AcquireSessionLock(sessionID string) (*LockHandle, error) {
|
||||
return &LockHandle{path: paths.LockPath, file: f}, nil
|
||||
}
|
||||
|
||||
func dirExists(path string) (bool, error) {
|
||||
info, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return info.IsDir(), nil
|
||||
}
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
// ReleaseSessionLock releases a previously acquired session lock.
|
||||
func (s *LocalStore) ReleaseSessionLock(lock *LockHandle) error {
|
||||
if lock == nil {
|
||||
|
||||
Reference in New Issue
Block a user