From 6cd9860681d391e9b1ffc556cf102f1c8417eca2 Mon Sep 17 00:00:00 2001 From: Viktor Varland Date: Tue, 30 Sep 2025 12:39:49 +0200 Subject: [PATCH] fix: avoid bind mount read errors --- internal/server/queue.go | 34 +++++++++++++++++++++----- internal/server/queue_test.go | 46 +++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/internal/server/queue.go b/internal/server/queue.go index 14766dd..38b5e39 100644 --- a/internal/server/queue.go +++ b/internal/server/queue.go @@ -9,9 +9,15 @@ import ( "path/filepath" "sync" "sync/atomic" + "syscall" "time" ) +var ( + osWriteFile = os.WriteFile + osRename = os.Rename +) + type VideoRequest struct { URL string `json:"url"` OutDir string `json:"out_dir"` @@ -169,12 +175,7 @@ func (q *Queue) persistLocked() error { return err } - tmp := q.persist + ".tmp" - if err := os.WriteFile(tmp, data, 0o644); err != nil { - return err - } - - return os.Rename(tmp, q.persist) + return writeFileAtomic(q.persist, data) } var idCounter uint64 @@ -183,3 +184,24 @@ func generateID() string { count := atomic.AddUint64(&idCounter, 1) return fmt.Sprintf("%d-%d", time.Now().UTC().UnixNano(), count) } + +func writeFileAtomic(path string, data []byte) error { + tmp := path + ".tmp" + if err := osWriteFile(tmp, data, 0o644); err != nil { + return err + } + + if err := osRename(tmp, path); err != nil { + if errors.Is(err, syscall.EBUSY) { + if writeErr := osWriteFile(path, data, 0o644); writeErr != nil { + _ = os.Remove(tmp) + return writeErr + } + return os.Remove(tmp) + } + _ = os.Remove(tmp) + return err + } + + return nil +} diff --git a/internal/server/queue_test.go b/internal/server/queue_test.go index 7c7533b..1e4efb6 100644 --- a/internal/server/queue_test.go +++ b/internal/server/queue_test.go @@ -4,6 +4,7 @@ import ( "context" "os" "path/filepath" + "syscall" "testing" "time" ) @@ -90,3 +91,48 @@ func TestQueuePersistence(t *testing.T) { t.Fatalf("expected queue file removed after draining, got %v", err) } } + +func TestWriteFileAtomicFallback(t *testing.T) { + // Simulate EBUSY by replacing os.Rename temporarily. + origRename := osRename + origWriteFile := osWriteFile + defer func() { + osRename = origRename + osWriteFile = origWriteFile + }() + + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "queue.json") + odyssey := filepath.Join(tmpDir, "queue.json.tmp") + + osWriteFile = func(name string, data []byte, perm os.FileMode) error { + if name == path { + if err := os.WriteFile(name, data, perm); err != nil { + return err + } + return nil + } + return os.WriteFile(name, data, perm) + } + + osRename = func(oldpath, newpath string) error { + return syscall.EBUSY + } + + if err := writeFileAtomic(path, []byte("{}")); err != nil { + t.Fatalf("writeFileAtomic: %v", err) + } + + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read path: %v", err) + } + + if string(data) != "{}" { + t.Fatalf("expected file contents '{}', got %q", data) + } + + if _, err := os.Stat(odyssey); !os.IsNotExist(err) { + t.Fatalf("expected tmp removed, got %v", err) + } +}