diff --git a/main.go b/main.go index 6d0696f..30db94e 100644 --- a/main.go +++ b/main.go @@ -62,6 +62,17 @@ func main() { app.go2rtc = go2rtc } + // Wait for the recordings storage before starting any writers. On a slow + // boot (e.g. fsck after a power cut) the mount can appear tens of seconds + // late; this avoids the recorders failing against an unmounted, root-owned + // mountpoint. If it times out we still start — live view does not need + // storage, and the recorder retries on its own. + if st := waitForStorage(cfg.Storage.RecordingsPath, 120*time.Second); st.OK { + log.Printf("startup: storage ready — %s", cfg.Storage.RecordingsPath) + } else { + log.Printf("startup: storage NOT ready (%s) — starting anyway; recording will retry", st.Message) + } + // Start snapshot engine for all enabled cameras. snaps := NewSnapshotEngine(cfg.Storage) snaps.StartAll(cfg.Cameras) diff --git a/recorder.go b/recorder.go index b954b75..8a6b4e4 100644 --- a/recorder.go +++ b/recorder.go @@ -107,10 +107,6 @@ func (rm *RecorderManager) startRecorder(cam CameraConfig) { } outDir := filepath.Join(rm.config.RecordingsPath, camDirName) - if err := os.MkdirAll(outDir, 0755); err != nil { - log.Printf("recorder: %s — mkdir %s failed: %v", cam.ID, outDir, err) - return - } for { select { @@ -120,6 +116,20 @@ func (rm *RecorderManager) startRecorder(cam CameraConfig) { default: } + // Ensure the output directory exists. Storage may not be mounted yet + // (e.g. a slow boot after a power cut) or may have vanished mid-run — + // retry instead of exiting, so recording resumes automatically once + // storage returns and the mount covers the directory again. + if err := os.MkdirAll(outDir, 0755); err != nil { + log.Printf("recorder: %s — storage not ready at %s: %v — retrying in 15s", cam.ID, outDir, err) + select { + case <-proc.done: + return + case <-time.After(15 * time.Second): + } + continue + } + // Generate filename: rec_{YYYY-MM-DD-HH-MM-SS}.mp4 now := time.Now() timeStr := now.Format("2006-01-02-15-04-05") diff --git a/storage.go b/storage.go index 9a6021a..d43c8d0 100644 --- a/storage.go +++ b/storage.go @@ -174,3 +174,29 @@ func isMountPoint(path string) bool { } return cs.Dev != ps.Dev } + +// waitForStorage blocks until the recordings path exists and is writable, or +// until the timeout elapses. It is meant to run before the writers start: on a +// slow boot (e.g. an fsck on the recordings drive after a power cut) the mount +// can appear tens of seconds late, and without this the recorders would stumble +// against an unmounted, root-owned mountpoint. Returns the final status; if the +// timeout expires the caller may still proceed (live view does not need storage, +// and the recorder retries on its own). +func waitForStorage(path string, timeout time.Duration) StorageStatus { + deadline := time.Now().Add(timeout) + warned := false + for { + st := checkStorage(path, false) + if st.OK { + return st + } + if !warned { + log.Printf("startup: waiting for recordings storage at %s (%s)", path, st.Message) + warned = true + } + if !time.Now().Before(deadline) { + return st + } + time.Sleep(2 * time.Second) + } +}