Skip to content

Commit d9be609

Browse files
panjf2000gopherbot
authored andcommitted
os: implement CopyFS
Fixes #62484 Change-Id: I5d8950dedf86af48f42a641940b34e62aa2cddcb Reviewed-on: https://go-review.googlesource.com/c/go/+/558995 Auto-Submit: Ian Lance Taylor <iant@google.com> Reviewed-by: Ian Lance Taylor <iant@google.com> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1 parent 8a0fbd7 commit d9be609

File tree

4 files changed

+316
-23
lines changed

4 files changed

+316
-23
lines changed

api/next/62484.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pkg os, func CopyFS(string, fs.FS) error #62484
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The [`CopyFS`](/os#CopyFS) function copies an [`io/fs.FS`](/io/fs#FS)
2+
into the local filesystem.

src/os/dir.go

+60
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
package os
66

77
import (
8+
"internal/safefilepath"
9+
"io"
810
"io/fs"
911
"sort"
1012
)
@@ -123,3 +125,61 @@ func ReadDir(name string) ([]DirEntry, error) {
123125
sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() })
124126
return dirs, err
125127
}
128+
129+
// CopyFS copies the file system fsys into the directory dir,
130+
// creating dir if necessary.
131+
//
132+
// Newly created directories and files have their default modes
133+
// where any bits from the file in fsys that are not part of the
134+
// standard read, write, and execute permissions will be zeroed
135+
// out, and standard read and write permissions are set for owner,
136+
// group, and others while retaining any existing execute bits from
137+
// the file in fsys.
138+
//
139+
// Symbolic links in fsys are not supported, a *PathError with Err set
140+
// to ErrInvalid is returned on symlink.
141+
//
142+
// Copying stops at and returns the first error encountered.
143+
func CopyFS(dir string, fsys fs.FS) error {
144+
return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
145+
if err != nil {
146+
return err
147+
}
148+
149+
fpath, err := safefilepath.FromFS(path)
150+
if err != nil {
151+
return err
152+
}
153+
newPath := joinPath(dir, fpath)
154+
if d.IsDir() {
155+
return MkdirAll(newPath, 0777)
156+
}
157+
158+
// TODO(panjf2000): handle symlinks with the help of fs.ReadLinkFS
159+
// once https://go.dev/issue/49580 is done.
160+
// we also need safefilepath.IsLocal from https://go.dev/cl/564295.
161+
if !d.Type().IsRegular() {
162+
return &PathError{Op: "CopyFS", Path: path, Err: ErrInvalid}
163+
}
164+
165+
r, err := fsys.Open(path)
166+
if err != nil {
167+
return err
168+
}
169+
defer r.Close()
170+
info, err := r.Stat()
171+
if err != nil {
172+
return err
173+
}
174+
w, err := OpenFile(newPath, O_CREATE|O_TRUNC|O_WRONLY, 0666|info.Mode()&0777)
175+
if err != nil {
176+
return err
177+
}
178+
179+
if _, err := io.Copy(w, r); err != nil {
180+
w.Close()
181+
return &PathError{Op: "Copy", Path: newPath, Err: err}
182+
}
183+
return w.Close()
184+
})
185+
}

src/os/os_test.go

+253-23
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package os_test
66

77
import (
8+
"bytes"
89
"errors"
910
"flag"
1011
"fmt"
@@ -3030,35 +3031,44 @@ func TestOpenFileKeepsPermissions(t *testing.T) {
30303031
}
30313032
}
30323033

3033-
func TestDirFS(t *testing.T) {
3034-
t.Parallel()
3034+
func forceMFTUpdateOnWindows(t *testing.T, path string) {
3035+
t.Helper()
3036+
3037+
if runtime.GOOS != "windows" {
3038+
return
3039+
}
30353040

30363041
// On Windows, we force the MFT to update by reading the actual metadata from GetFileInformationByHandle and then
30373042
// explicitly setting that. Otherwise it might get out of sync with FindFirstFile. See golang.org/issues/42637.
3038-
if runtime.GOOS == "windows" {
3039-
if err := filepath.WalkDir("./testdata/dirfs", func(path string, d fs.DirEntry, err error) error {
3040-
if err != nil {
3041-
t.Fatal(err)
3042-
}
3043-
info, err := d.Info()
3044-
if err != nil {
3045-
t.Fatal(err)
3046-
}
3047-
stat, err := Stat(path) // This uses GetFileInformationByHandle internally.
3048-
if err != nil {
3049-
t.Fatal(err)
3050-
}
3051-
if stat.ModTime() == info.ModTime() {
3052-
return nil
3053-
}
3054-
if err := Chtimes(path, stat.ModTime(), stat.ModTime()); err != nil {
3055-
t.Log(err) // We only log, not die, in case the test directory is not writable.
3056-
}
3057-
return nil
3058-
}); err != nil {
3043+
if err := filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error {
3044+
if err != nil {
3045+
t.Fatal(err)
3046+
}
3047+
info, err := d.Info()
3048+
if err != nil {
30593049
t.Fatal(err)
30603050
}
3051+
stat, err := Stat(path) // This uses GetFileInformationByHandle internally.
3052+
if err != nil {
3053+
t.Fatal(err)
3054+
}
3055+
if stat.ModTime() == info.ModTime() {
3056+
return nil
3057+
}
3058+
if err := Chtimes(path, stat.ModTime(), stat.ModTime()); err != nil {
3059+
t.Log(err) // We only log, not die, in case the test directory is not writable.
3060+
}
3061+
return nil
3062+
}); err != nil {
3063+
t.Fatal(err)
30613064
}
3065+
}
3066+
3067+
func TestDirFS(t *testing.T) {
3068+
t.Parallel()
3069+
3070+
forceMFTUpdateOnWindows(t, "./testdata/dirfs")
3071+
30623072
fsys := DirFS("./testdata/dirfs")
30633073
if err := fstest.TestFS(fsys, "a", "b", "dir/x"); err != nil {
30643074
t.Fatal(err)
@@ -3358,3 +3368,223 @@ func TestRandomLen(t *testing.T) {
33583368
}
33593369
}
33603370
}
3371+
3372+
func TestCopyFS(t *testing.T) {
3373+
t.Parallel()
3374+
3375+
// Test with disk filesystem.
3376+
forceMFTUpdateOnWindows(t, "./testdata/dirfs")
3377+
fsys := DirFS("./testdata/dirfs")
3378+
tmpDir := t.TempDir()
3379+
if err := CopyFS(tmpDir, fsys); err != nil {
3380+
t.Fatal("CopyFS:", err)
3381+
}
3382+
forceMFTUpdateOnWindows(t, tmpDir)
3383+
tmpFsys := DirFS(tmpDir)
3384+
if err := fstest.TestFS(tmpFsys, "a", "b", "dir/x"); err != nil {
3385+
t.Fatal("TestFS:", err)
3386+
}
3387+
if err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
3388+
if d.IsDir() {
3389+
return nil
3390+
}
3391+
3392+
data, err := fs.ReadFile(fsys, path)
3393+
if err != nil {
3394+
return err
3395+
}
3396+
newData, err := fs.ReadFile(tmpFsys, path)
3397+
if err != nil {
3398+
return err
3399+
}
3400+
if !bytes.Equal(data, newData) {
3401+
return errors.New("file " + path + " contents differ")
3402+
}
3403+
return nil
3404+
}); err != nil {
3405+
t.Fatal("comparing two directories:", err)
3406+
}
3407+
3408+
// Test with memory filesystem.
3409+
fsys = fstest.MapFS{
3410+
"william": {Data: []byte("Shakespeare\n")},
3411+
"carl": {Data: []byte("Gauss\n")},
3412+
"daVinci": {Data: []byte("Leonardo\n")},
3413+
"einstein": {Data: []byte("Albert\n")},
3414+
"dir/newton": {Data: []byte("Sir Isaac\n")},
3415+
}
3416+
tmpDir = t.TempDir()
3417+
if err := CopyFS(tmpDir, fsys); err != nil {
3418+
t.Fatal("CopyFS:", err)
3419+
}
3420+
forceMFTUpdateOnWindows(t, tmpDir)
3421+
tmpFsys = DirFS(tmpDir)
3422+
if err := fstest.TestFS(tmpFsys, "william", "carl", "daVinci", "einstein", "dir/newton"); err != nil {
3423+
t.Fatal("TestFS:", err)
3424+
}
3425+
if err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
3426+
if d.IsDir() {
3427+
return nil
3428+
}
3429+
3430+
data, err := fs.ReadFile(fsys, path)
3431+
if err != nil {
3432+
return err
3433+
}
3434+
newData, err := fs.ReadFile(tmpFsys, path)
3435+
if err != nil {
3436+
return err
3437+
}
3438+
if !bytes.Equal(data, newData) {
3439+
return errors.New("file " + path + " contents differ")
3440+
}
3441+
return nil
3442+
}); err != nil {
3443+
t.Fatal("comparing two directories:", err)
3444+
}
3445+
}
3446+
3447+
func TestCopyFSWithSymlinks(t *testing.T) {
3448+
// Test it with absolute and relative symlinks that point inside and outside the tree.
3449+
testenv.MustHaveSymlink(t)
3450+
3451+
// Create a directory and file outside.
3452+
tmpDir := t.TempDir()
3453+
outsideDir, err := MkdirTemp(tmpDir, "copyfs")
3454+
if err != nil {
3455+
t.Fatalf("MkdirTemp: %v", err)
3456+
}
3457+
outsideFile := filepath.Join(outsideDir, "file.out.txt")
3458+
3459+
if err := WriteFile(outsideFile, []byte("Testing CopyFS outside"), 0644); err != nil {
3460+
t.Fatalf("WriteFile: %v", err)
3461+
}
3462+
3463+
// Create a directory and file inside.
3464+
testDataDir, err := filepath.Abs("./testdata/")
3465+
if err != nil {
3466+
t.Fatalf("filepath.Abs: %v", err)
3467+
}
3468+
insideDir := filepath.Join(testDataDir, "copyfs")
3469+
if err := Mkdir(insideDir, 0755); err != nil {
3470+
t.Fatalf("Mkdir: %v", err)
3471+
}
3472+
defer RemoveAll(insideDir)
3473+
insideFile := filepath.Join(insideDir, "file.in.txt")
3474+
if err := WriteFile(insideFile, []byte("Testing CopyFS inside"), 0644); err != nil {
3475+
t.Fatalf("WriteFile: %v", err)
3476+
}
3477+
3478+
// Create directories for symlinks.
3479+
linkInDir := filepath.Join(insideDir, "in_symlinks")
3480+
if err := Mkdir(linkInDir, 0755); err != nil {
3481+
t.Fatalf("Mkdir: %v", err)
3482+
}
3483+
linkOutDir := filepath.Join(insideDir, "out_symlinks")
3484+
if err := Mkdir(linkOutDir, 0755); err != nil {
3485+
t.Fatalf("Mkdir: %v", err)
3486+
}
3487+
3488+
// First, we create the absolute symlink pointing outside.
3489+
outLinkFile := filepath.Join(linkOutDir, "file.abs.out.link")
3490+
if err := Symlink(outsideFile, outLinkFile); err != nil {
3491+
t.Fatalf("Symlink: %v", err)
3492+
}
3493+
3494+
// Then, we create the relative symlink pointing outside.
3495+
relOutsideFile, err := filepath.Rel(filepath.Join(linkOutDir, "."), outsideFile)
3496+
if err != nil {
3497+
t.Fatalf("filepath.Rel: %v", err)
3498+
}
3499+
relOutLinkFile := filepath.Join(linkOutDir, "file.rel.out.link")
3500+
if err := Symlink(relOutsideFile, relOutLinkFile); err != nil {
3501+
t.Fatalf("Symlink: %v", err)
3502+
}
3503+
3504+
// Last, we create the relative symlink pointing inside.
3505+
relInsideFile, err := filepath.Rel(filepath.Join(linkInDir, "."), insideFile)
3506+
if err != nil {
3507+
t.Fatalf("filepath.Rel: %v", err)
3508+
}
3509+
relInLinkFile := filepath.Join(linkInDir, "file.rel.in.link")
3510+
if err := Symlink(relInsideFile, relInLinkFile); err != nil {
3511+
t.Fatalf("Symlink: %v", err)
3512+
}
3513+
3514+
// Copy the directory tree and verify.
3515+
forceMFTUpdateOnWindows(t, insideDir)
3516+
fsys := DirFS(insideDir)
3517+
tmpDupDir, err := MkdirTemp(tmpDir, "copyfs_dup")
3518+
if err != nil {
3519+
t.Fatalf("MkdirTemp: %v", err)
3520+
}
3521+
3522+
// TODO(panjf2000): symlinks are currently not supported, and a specific error
3523+
// will be returned. Verify that error and skip the subsequent test,
3524+
// revisit this once #49580 is closed.
3525+
if err := CopyFS(tmpDupDir, fsys); !errors.Is(err, ErrInvalid) {
3526+
t.Fatalf("got %v, want ErrInvalid", err)
3527+
}
3528+
t.Skip("skip the subsequent test and wait for #49580")
3529+
3530+
forceMFTUpdateOnWindows(t, tmpDupDir)
3531+
tmpFsys := DirFS(tmpDupDir)
3532+
if err := fstest.TestFS(tmpFsys, "file.in.txt", "out_symlinks/file.abs.out.link", "out_symlinks/file.rel.out.link", "in_symlinks/file.rel.in.link"); err != nil {
3533+
t.Fatal("TestFS:", err)
3534+
}
3535+
if err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
3536+
if d.IsDir() {
3537+
return nil
3538+
}
3539+
3540+
fi, err := d.Info()
3541+
if err != nil {
3542+
return err
3543+
}
3544+
if filepath.Ext(path) == ".link" {
3545+
if fi.Mode()&ModeSymlink == 0 {
3546+
return errors.New("original file " + path + " should be a symlink")
3547+
}
3548+
tmpfi, err := fs.Stat(tmpFsys, path)
3549+
if err != nil {
3550+
return err
3551+
}
3552+
if tmpfi.Mode()&ModeSymlink != 0 {
3553+
return errors.New("copied file " + path + " should not be a symlink")
3554+
}
3555+
}
3556+
3557+
data, err := fs.ReadFile(fsys, path)
3558+
if err != nil {
3559+
return err
3560+
}
3561+
newData, err := fs.ReadFile(tmpFsys, path)
3562+
if err != nil {
3563+
return err
3564+
}
3565+
if !bytes.Equal(data, newData) {
3566+
return errors.New("file " + path + " contents differ")
3567+
}
3568+
3569+
var target string
3570+
switch fileName := filepath.Base(path); fileName {
3571+
case "file.abs.out.link", "file.rel.out.link":
3572+
target = outsideFile
3573+
case "file.rel.in.link":
3574+
target = insideFile
3575+
}
3576+
if len(target) > 0 {
3577+
targetData, err := ReadFile(target)
3578+
if err != nil {
3579+
return err
3580+
}
3581+
if !bytes.Equal(targetData, newData) {
3582+
return errors.New("file " + path + " contents differ from target")
3583+
}
3584+
}
3585+
3586+
return nil
3587+
}); err != nil {
3588+
t.Fatal("comparing two directories:", err)
3589+
}
3590+
}

0 commit comments

Comments
 (0)