Skip to content

Commit 54a8470

Browse files
committed
vfsutil: add CopyRecursive
1 parent c43b380 commit 54a8470

File tree

3 files changed

+146
-0
lines changed

3 files changed

+146
-0
lines changed

pkg/util/vfsutil/BUILD.bazel

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
2+
3+
go_library(
4+
name = "vfsutil",
5+
srcs = ["copy.go"],
6+
importpath = "github.com/cockroachdb/cockroach/pkg/util/vfsutil",
7+
visibility = ["//visibility:public"],
8+
deps = ["@com_github_cockroachdb_pebble//vfs"],
9+
)
10+
11+
go_test(
12+
name = "vfsutil_test",
13+
srcs = ["copy_test.go"],
14+
deps = [
15+
":vfsutil",
16+
"@com_github_cockroachdb_pebble//vfs",
17+
"@com_github_stretchr_testify//require",
18+
],
19+
)

pkg/util/vfsutil/copy.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright 2025 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the CockroachDB Software License
4+
// included in the /LICENSE file.
5+
6+
package vfsutil
7+
8+
import (
9+
"io"
10+
11+
"github.com/cockroachdb/pebble/vfs"
12+
)
13+
14+
// CopyRecursive recursively copies all files and directories from srcPath on
15+
// srcFS to dstPath on dstFS. If the destination path does not exist, it is
16+
// created.
17+
// Files that already exist in the destination path will be replaced.
18+
func CopyRecursive(srcFS, dstFS vfs.FS, srcPath, dstPath string) error {
19+
srcInfo, err := srcFS.Stat(srcPath)
20+
if err != nil {
21+
return err
22+
}
23+
24+
if srcInfo.IsDir() {
25+
if err := dstFS.MkdirAll(dstPath, srcInfo.Mode()); err != nil {
26+
return err
27+
}
28+
29+
entries, err := srcFS.List(srcPath)
30+
if err != nil {
31+
return err
32+
}
33+
34+
for _, entry := range entries {
35+
srcEntryPath := srcFS.PathJoin(srcPath, entry)
36+
dstEntryPath := dstFS.PathJoin(dstPath, entry)
37+
if err := CopyRecursive(srcFS, dstFS, srcEntryPath, dstEntryPath); err != nil {
38+
return err
39+
}
40+
}
41+
return nil
42+
}
43+
44+
// Copy regular file
45+
return copyFile(srcFS, dstFS, srcPath, dstPath)
46+
}
47+
48+
func copyFile(srcFS, dstFS vfs.FS, srcPath, dstPath string) error {
49+
// Open source file
50+
src, err := srcFS.Open(srcPath)
51+
if err != nil {
52+
return err
53+
}
54+
defer src.Close()
55+
56+
// Create destination file
57+
dst, err := dstFS.Create(dstPath, vfs.WriteCategoryUnspecified)
58+
if err != nil {
59+
return err
60+
}
61+
defer dst.Close()
62+
63+
// Copy file contents
64+
if _, err := io.Copy(dst, src); err != nil {
65+
return err
66+
}
67+
68+
// Sync to ensure data is written
69+
return dst.Sync()
70+
}

pkg/util/vfsutil/copy_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2025 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the CockroachDB Software License
4+
// included in the /LICENSE file.
5+
6+
package vfsutil_test
7+
8+
import (
9+
"testing"
10+
11+
"github.com/cockroachdb/cockroach/pkg/util/vfsutil"
12+
"github.com/cockroachdb/pebble/vfs"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
func TestCopyRecursive(t *testing.T) {
17+
// Create source memFS with test data
18+
srcFS := vfs.NewMem()
19+
20+
// Create a directory structure
21+
require.NoError(t, srcFS.MkdirAll("testdir/subdir", 0755))
22+
23+
// Create some test files
24+
f1, err := srcFS.Create("testdir/file1.txt", vfs.WriteCategoryUnspecified)
25+
require.NoError(t, err)
26+
_, err = f1.Write([]byte("hello world"))
27+
require.NoError(t, err)
28+
require.NoError(t, f1.Close())
29+
30+
f2, err := srcFS.Create("testdir/subdir/file2.txt", vfs.WriteCategoryUnspecified)
31+
require.NoError(t, err)
32+
_, err = f2.Write([]byte("nested file"))
33+
require.NoError(t, err)
34+
require.NoError(t, f2.Close())
35+
36+
// Create destination FS (real filesystem for this example)
37+
dstFS := vfs.Default
38+
39+
// Copy from memFS to real filesystem in a temp directory
40+
tempDir := t.TempDir()
41+
require.NoError(t, vfsutil.CopyRecursive(srcFS, dstFS, "testdir", dstFS.PathJoin(tempDir, "copied")))
42+
43+
// Verify the copy worked
44+
files, err := dstFS.List(dstFS.PathJoin(tempDir, "copied"))
45+
require.NoError(t, err)
46+
require.Contains(t, files, "file1.txt")
47+
require.Contains(t, files, "subdir")
48+
49+
// Verify nested file
50+
content, err := dstFS.Open(dstFS.PathJoin(tempDir, "copied", "subdir", "file2.txt"))
51+
require.NoError(t, err)
52+
defer content.Close()
53+
buf := make([]byte, 100)
54+
n, err := content.Read(buf)
55+
require.NoError(t, err)
56+
require.Equal(t, "nested file", string(buf[:n]))
57+
}

0 commit comments

Comments
 (0)