add virtual filesystem support

This commit is contained in:
AJ ONeal 2020-10-23 13:55:37 -06:00
parent 54ae170929
commit 657f7b4be2
4 changed files with 119 additions and 32 deletions

79
copy.go
View File

@ -2,7 +2,6 @@ package vfscopy
import ( import (
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
) )
@ -15,31 +14,39 @@ const (
) )
// Copy copies src to dest, doesn't matter if src is a directory or a file. // Copy copies src to dest, doesn't matter if src is a directory or a file.
func Copy(src, dest string, opt ...Options) error { func Copy(vfs FileSystem, src, dest string, opt ...Options) error {
info, err := os.Lstat(src) //info, err := fs.Lstat(src)
f, err := vfs.Open(src)
if err != nil { if err != nil {
return err return err
} }
return switchboard(src, dest, info, assure(opt...)) info, err := f.Stat()
if err != nil {
return err
}
return switchboard(vfs, src, dest, f, info, assure(opt...))
} }
// switchboard switches proper copy functions regarding file type, etc... // switchboard switches proper copy functions regarding file type, etc...
// If there would be anything else here, add a case to this switchboard. // If there would be anything else here, add a case to this switchboard.
func switchboard(src, dest string, info os.FileInfo, opt Options) error { func switchboard(
vfs FileSystem, src, dest string, f File, info os.FileInfo, opt Options,
) error {
switch { switch {
case info.Mode()&os.ModeSymlink != 0: //case info.Mode()&os.ModeSymlink != 0:
return onsymlink(src, dest, opt) // TODO
//return onsymlink(vfs, src, dest, opt)
case info.IsDir(): case info.IsDir():
return dcopy(src, dest, info, opt) return dcopy(vfs, src, dest, f, info, opt)
default: default:
return fcopy(src, dest, info, opt) return fcopy(vfs, src, dest, f, info, opt)
} }
} }
// copy decide if this src should be copied or not. // copy decide if this src should be copied or not.
// Because this "copy" could be called recursively, // Because this "copy" could be called recursively,
// "info" MUST be given here, NOT nil. // "info" MUST be given here, NOT nil.
func copy(src, dest string, info os.FileInfo, opt Options) error { func copy(vfs FileSystem, src, dest string, f File, info os.FileInfo, opt Options) error {
skip, err := opt.Skip(src) skip, err := opt.Skip(src)
if err != nil { if err != nil {
return err return err
@ -47,40 +54,40 @@ func copy(src, dest string, info os.FileInfo, opt Options) error {
if skip { if skip {
return nil return nil
} }
return switchboard(src, dest, info, opt) return switchboard(vfs, src, dest, f, info, opt)
} }
// fcopy is for just a file, // fcopy is for just a file,
// with considering existence of parent directory // with considering existence of parent directory
// and file permission. // and file permission.
func fcopy(src, dest string, info os.FileInfo, opt Options) (err error) { func fcopy(vfs FileSystem, src, dest string, f File, info os.FileInfo, opt Options) (err error) {
if err = os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil { if err = os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil {
return return
} }
f, err := os.Create(dest) df, err := os.Create(dest)
if err != nil { if err != nil {
return return
} }
defer fclose(f, &err) defer fclose(df, &err)
if err = os.Chmod(f.Name(), info.Mode()|opt.AddPermission); err != nil { if err = os.Chmod(df.Name(), info.Mode()|opt.AddPermission); err != nil {
return return
} }
s, err := os.Open(src) s, err := vfs.Open(src)
if err != nil { if err != nil {
return return
} }
defer fclose(s, &err) defer fclose(s, &err)
if _, err = io.Copy(f, s); err != nil { if _, err = io.Copy(df, s); err != nil {
return return
} }
if opt.Sync { if opt.Sync {
err = f.Sync() err = df.Sync()
} }
return return
@ -89,7 +96,7 @@ func fcopy(src, dest string, info os.FileInfo, opt Options) (err error) {
// dcopy is for a directory, // dcopy is for a directory,
// with scanning contents inside the directory // with scanning contents inside the directory
// and pass everything to "copy" recursively. // and pass everything to "copy" recursively.
func dcopy(srcdir, destdir string, info os.FileInfo, opt Options) (err error) { func dcopy(vfs FileSystem, srcdir, destdir string, d File, info os.FileInfo, opt Options) (err error) {
originalMode := info.Mode() originalMode := info.Mode()
@ -100,28 +107,34 @@ func dcopy(srcdir, destdir string, info os.FileInfo, opt Options) (err error) {
// Recover dir mode with original one. // Recover dir mode with original one.
defer chmod(destdir, originalMode|opt.AddPermission, &err) defer chmod(destdir, originalMode|opt.AddPermission, &err)
contents, err := ioutil.ReadDir(srcdir) fileInfos, err := d.Readdir(-1)
if err != nil { if err != nil {
return return
} }
for _, content := range contents { for _, newInfo := range fileInfos {
cs, cd := filepath.Join(srcdir, content.Name()), filepath.Join(destdir, content.Name()) cs, cd := filepath.Join(
srcdir, newInfo.Name()),
filepath.Join(destdir, newInfo.Name())
if err = copy(cs, cd, content, opt); err != nil { f, err := vfs.Open(cs)
if nil != err {
return err
}
if err := copy(vfs, cs, cd, f, newInfo, opt); err != nil {
// If any error, exit immediately // If any error, exit immediately
return return err
} }
} }
return return
} }
func onsymlink(src, dest string, opt Options) error { /*
func onsymlink(vfs FileSystem, src, dest string, opt Options) error {
switch opt.OnSymlink(src) { switch opt.OnSymlink(src) {
case Shallow: case Shallow:
return lcopy(src, dest) return lcopy(vfs, src, dest)
case Deep: case Deep:
orig, err := filepath.EvalSymlinks(src) orig, err := filepath.EvalSymlinks(src)
if err != nil { if err != nil {
@ -131,24 +144,28 @@ func onsymlink(src, dest string, opt Options) error {
if err != nil { if err != nil {
return err return err
} }
return copy(orig, dest, info, opt) return copy(vfs, orig, dest, info, opt)
case Skip: case Skip:
fallthrough fallthrough
default: default:
return nil // do nothing return nil // do nothing
} }
} }
*/
// lcopy is for a symlink, // lcopy is for a symlink,
// with just creating a new symlink by replicating src symlink. // with just creating a new symlink by replicating src symlink.
func lcopy(src, dest string) error { func lcopy(vfs FileSystem, src, dest string) error {
/*
// TODO
src, err := os.Readlink(src) src, err := os.Readlink(src)
if err != nil { if err != nil {
return err return err
} }
*/
// Create the directories on the path to the dest symlink. // Create the directories on the path to the dest symlink.
if err = os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil { if err := os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil {
return err return err
} }
@ -158,7 +175,7 @@ func lcopy(src, dest string) error {
// fclose ANYHOW closes file, // fclose ANYHOW closes file,
// with asiging error raised during Close, // with asiging error raised during Close,
// BUT respecting the error already reported. // BUT respecting the error already reported.
func fclose(f *os.File, reported *error) { func fclose(f File, reported *error) {
if err := f.Close(); *reported == nil { if err := f.Close(); *reported == nil {
*reported = err *reported = err
} }

View File

@ -11,7 +11,8 @@ func TestRecursiveCopy(t *testing.T) {
return Shallow return Shallow
}, },
} }
if err := Copy( "./fixtures/src/", "/tmp/dst/", opts,); nil != err { vfs := Dir("./fixtures/src/")
if err := Copy(vfs, ".", "/tmp/dst/", opts,); nil != err {
t.Fatalf("error: %v", err) t.Fatalf("error: %v", err)
} }
} }

1
fixtures/src/baz/baz.txt Normal file
View File

@ -0,0 +1 @@
Baz

68
vfs.go Normal file
View File

@ -0,0 +1,68 @@
package vfscopy
import (
"os"
"io"
"path/filepath"
"path"
"strings"
"errors"
)
// FileSystem is copied from http.FileSystem
type FileSystem interface {
Open(name string) (File, error)
}
// File is copied from http.File
type File interface {
io.Closer
io.Reader
io.Seeker
Readdir(count int) ([]os.FileInfo, error)
Stat() (os.FileInfo, error)
}
// Dir is an implementation of a Virtual FileSystem
type Dir string
// mapDirOpenError maps the provided non-nil error from opening name
// to a possibly better non-nil error. In particular, it turns OS-specific errors
// about opening files in non-directories into os.ErrNotExist. See Issue 18984.
func mapDirOpenError(originalErr error, name string) error {
if os.IsNotExist(originalErr) || os.IsPermission(originalErr) {
return originalErr
}
parts := strings.Split(name, string(filepath.Separator))
for i := range parts {
if parts[i] == "" {
continue
}
fi, err := os.Stat(strings.Join(parts[:i+1], string(filepath.Separator)))
if err != nil {
return originalErr
}
if !fi.IsDir() {
return os.ErrNotExist
}
}
return originalErr
}
// Open opens a file relative to a virtual filesystem
func (d Dir) Open(name string) (File, error) {
if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) {
return nil, errors.New("http: invalid character in file path")
}
dir := string(d)
if dir == "" {
dir = "."
}
fullName := filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name)))
f, err := os.Open(fullName)
if err != nil {
return nil, mapDirOpenError(err, fullName)
}
return f, nil
}