106 lines
2.9 KiB
Go
106 lines
2.9 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// RecoverDue discovers failure-only recording pairs after an Agent restart.
|
|
// Blocked/corrupt entries remain untouched and are returned as errors while
|
|
// other independent entries continue; none can produce a fabricated result.
|
|
func (r *RecordingRecovery) RecoverDue(ctx context.Context, grant RecoveryGrant, report RecordingResultReporter) (int, error) {
|
|
if strings.TrimSpace(r.Root) == "" {
|
|
return 0, errors.New("recording recovery root is required")
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
if err := os.MkdirAll(r.Root, 0700); err != nil {
|
|
return 0, fmt.Errorf("prepare recording recovery root: %w", err)
|
|
}
|
|
if err := checkRecoveryDirectory(r.Root); err != nil {
|
|
return 0, err
|
|
}
|
|
processed := 0
|
|
var blocked []error
|
|
addBlocked := func(dir string, cause error) {
|
|
rel, err := filepath.Rel(r.Root, dir)
|
|
if err != nil {
|
|
blocked = append(blocked, fmt.Errorf("recording recovery path unavailable: %w", cause))
|
|
return
|
|
}
|
|
sum := sha256.Sum256([]byte(filepath.ToSlash(rel)))
|
|
blocked = append(blocked, fmt.Errorf("recording recovery %s: %w", hex.EncodeToString(sum[:6]), cause))
|
|
}
|
|
walkErr := filepath.WalkDir(r.Root, func(path string, item fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if item.IsDir() {
|
|
return checkRecoveryDirectory(path)
|
|
}
|
|
dir := filepath.Dir(path)
|
|
switch item.Name() {
|
|
case "recording.wav":
|
|
if _, err := os.Stat(filepath.Join(dir, "info.json")); err != nil {
|
|
addBlocked(dir, fmt.Errorf("%w: original audio has no call information", ErrRecordingRecoveryIncomplete))
|
|
}
|
|
return nil
|
|
case "info.json":
|
|
default:
|
|
addBlocked(dir, fmt.Errorf("%w: unexpected recovery file", ErrRecordingRecoveryIncomplete))
|
|
return nil
|
|
}
|
|
rel, err := filepath.Rel(r.Root, dir)
|
|
if err != nil {
|
|
addBlocked(dir, err)
|
|
return nil
|
|
}
|
|
parts := strings.Split(filepath.ToSlash(rel), "/")
|
|
if len(parts) < 2 {
|
|
addBlocked(dir, ErrRecordingRecoveryIncomplete)
|
|
return nil
|
|
}
|
|
bucket, objectKey := parts[0], strings.Join(parts[1:], "/")
|
|
before, err := r.Load(bucket, objectKey)
|
|
if err != nil {
|
|
addBlocked(dir, err)
|
|
return nil
|
|
}
|
|
after, err := r.Retry(ctx, bucket, objectKey, grant, report)
|
|
if errors.Is(err, ErrRecordingNotDue) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
addBlocked(dir, err)
|
|
return nil
|
|
}
|
|
if before.State != "delivered" && after.State == "delivered" {
|
|
processed++
|
|
}
|
|
return nil
|
|
})
|
|
return processed, errors.Join(append(blocked, walkErr)...)
|
|
}
|
|
|
|
func checkRecoveryDirectory(path string) error {
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.IsDir() || info.Mode().Perm()&0077 != 0 {
|
|
return fmt.Errorf("recording recovery directory must be private (mode=%#o dir=%t)", info.Mode().Perm(), info.IsDir())
|
|
}
|
|
return nil
|
|
}
|