Skip to content

Commit 24d5e77

Browse files
authored
Fix cross-device link error when running in Docker with volumes (#66)
`fileblob` creates temp files in `os.TempDir()` (`/tmp`) by default, then uses `os.Rename` to move them to the final path. When the storage directory is on a different filesystem (e.g. a Docker volume mount at `/data`), the rename fails with "invalid cross-device link". Set `no_tmp_dir=true` on file:// bucket URLs so fileblob creates temp files next to the final destination instead. Fixes #65
1 parent 15c133f commit 24d5e77

3 files changed

Lines changed: 47 additions & 3 deletions

File tree

internal/server/server_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -989,10 +989,10 @@ func TestNewServer_StorageConnectivityCheck(t *testing.T) {
989989
// On Windows, OpenBucket normalises to file:///C:/path; on Unix the
990990
// absolute path already starts with /, so file:// + /path == file:///path.
991991
wantPrefix := "file://"
992-
wantSuffix := filepath.ToSlash(storagePath)
992+
wantPath := filepath.ToSlash(storagePath)
993993
got := srv.storage.URL()
994-
if !strings.HasPrefix(got, wantPrefix) || !strings.HasSuffix(got, wantSuffix) {
995-
t.Errorf("expected storage URL ending with %s, got %s", wantSuffix, got)
994+
if !strings.HasPrefix(got, wantPrefix) || !strings.Contains(got, wantPath) {
995+
t.Errorf("expected storage URL containing %s, got %s", wantPath, got)
996996
}
997997

998998
_ = srv.db.Close()

internal/storage/blob.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ func OpenBucket(ctx context.Context, urlStr string) (*Blob, error) {
7070
} else {
7171
urlStr = "file://" + urlPath
7272
}
73+
74+
// Create temp files next to the final path instead of in os.TempDir.
75+
// This avoids "invalid cross-device link" errors from os.Rename when
76+
// the bucket directory and os.TempDir are on different filesystems
77+
// (e.g. Docker volume mounts).
78+
urlStr += "?no_tmp_dir=true"
7379
}
7480

7581
bucket, err := blob.OpenBucket(ctx, urlStr)

internal/storage/blob_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,44 @@ func TestBlobOverwrite(t *testing.T) {
217217
}
218218
}
219219

220+
func TestOpenBucketSetsNoTmpDir(t *testing.T) {
221+
dir := t.TempDir()
222+
ctx := context.Background()
223+
224+
b, err := OpenBucket(ctx, fileURLFromPath(dir))
225+
if err != nil {
226+
t.Fatalf("OpenBucket failed: %v", err)
227+
}
228+
defer func() { _ = b.Close() }()
229+
230+
// fileblob uses os.TempDir() by default for temp files, then os.Rename to
231+
// the final path. This fails with "invalid cross-device link" when the bucket
232+
// dir and os.TempDir() are on different filesystems (e.g. Docker volumes).
233+
// OpenBucket must set no_tmp_dir=true so temp files are created next to the
234+
// final path instead.
235+
if !strings.Contains(b.URL(), "no_tmp_dir=true") {
236+
t.Errorf("URL should contain no_tmp_dir=true to avoid cross-device rename errors, got %q", b.URL())
237+
}
238+
239+
// Verify Store still works with the parameter set
240+
content := "cross-device test"
241+
_, _, err = b.Store(ctx, "test/cross-device.txt", strings.NewReader(content))
242+
if err != nil {
243+
t.Fatalf("Store failed with no_tmp_dir=true: %v", err)
244+
}
245+
246+
r, err := b.Open(ctx, "test/cross-device.txt")
247+
if err != nil {
248+
t.Fatalf("Open failed: %v", err)
249+
}
250+
defer func() { _ = r.Close() }()
251+
252+
data, _ := io.ReadAll(r)
253+
if string(data) != content {
254+
t.Errorf("content = %q, want %q", string(data), content)
255+
}
256+
}
257+
220258
func createTestBlob(t *testing.T) *Blob {
221259
t.Helper()
222260
dir := t.TempDir()

0 commit comments

Comments
 (0)