diff --git a/samples/mount_roloopbackfs/mount.go b/samples/mount_roloopbackfs/mount.go new file mode 100644 index 0000000..065ecbc --- /dev/null +++ b/samples/mount_roloopbackfs/mount.go @@ -0,0 +1,74 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "flag" + "log" + "os" + + "github.com/jacobsa/fuse" + "github.com/jacobsa/fuse/samples/roloopbackfs" +) + +var fPhysicalPath = flag.String("path", "", "Physical path to loopback.") +var fMountPoint = flag.String("mount_point", "", "Path to mount point.") + +var fDebug = flag.Bool("debug", false, "Enable debug logging.") + +func main() { + flag.Parse() + + debugLogger := log.New(os.Stdout, "fuse: ", 0) + errorLogger := log.New(os.Stderr, "fuse: ", 0) + + if *fPhysicalPath == "" { + log.Fatalf("You must set --path.") + } + + if *fMountPoint == "" { + log.Fatalf("You must set --mount_point.") + } + + err := os.MkdirAll(*fMountPoint, 0777) + if err != nil { + log.Fatalf("Failed to create mount point at '%v'", *fMountPoint) + } + + server, err := roloopbackfs.NewReadonlyLoopbackServer(*fPhysicalPath, errorLogger) + if err != nil { + log.Fatalf("makeFS: %v", err) + } + + cfg := &fuse.MountConfig{ + ReadOnly: true, + ErrorLogger: errorLogger, + } + + if *fDebug { + cfg.DebugLogger = debugLogger + } + + mfs, err := fuse.Mount(*fMountPoint, server, cfg) + if err != nil { + log.Fatalf("Mount: %v", err) + } + + // Wait for it to be unmounted. + if err = mfs.Join(context.Background()); err != nil { + log.Fatalf("Join: %v", err) + } +} diff --git a/samples/roloopbackfs/inode.go b/samples/roloopbackfs/inode.go new file mode 100644 index 0000000..88d2392 --- /dev/null +++ b/samples/roloopbackfs/inode.go @@ -0,0 +1,151 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package roloopbackfs + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sync" + "sync/atomic" + "time" + + "github.com/jacobsa/fuse/fuseops" + "github.com/jacobsa/fuse/fuseutil" +) + +var ( + uid = uint32(os.Getuid()) + gid = uint32(os.Getgid()) + allocatedInodeId uint64 = fuseops.RootInodeID +) + +func nextInodeID() (next fuseops.InodeID) { + nextInodeId := atomic.AddUint64(&allocatedInodeId, 1) + return fuseops.InodeID(nextInodeId) +} + +type Inode interface { + Id() fuseops.InodeID + Path() string + String() string + Attributes() (*fuseops.InodeAttributes, error) + ListChildren(inodes *sync.Map) ([]*fuseutil.Dirent, error) + Contents() ([]byte, error) +} + +func getOrCreateInode(inodes *sync.Map, parentId fuseops.InodeID, name string) (Inode, error) { + parent, found := inodes.Load(parentId) + if !found { + return nil, nil + } + parentPath := parent.(Inode).Path() + entries, err := ioutil.ReadDir(parentPath) + if err != nil { + return nil, err + } + for _, entry := range entries { + if entry.Name() == name { + inodeEntry := &inodeEntry{ + id: nextInodeID(), + path: filepath.Join(parentPath, name), + } + storedEntry, _ := inodes.LoadOrStore(inodeEntry.id, inodeEntry) + return storedEntry.(Inode), nil + } + } + return nil, nil +} + +type inodeEntry struct { + id fuseops.InodeID + path string +} + +var _ Inode = &inodeEntry{} + +func NewInode(path string) (Inode, error) { + return &inodeEntry{ + id: nextInodeID(), + path: path, + }, nil +} + +func (in *inodeEntry) Id() fuseops.InodeID { + return in.id +} + +func (in *inodeEntry) Path() string { + return in.path +} + +func (in *inodeEntry) String() string { + return fmt.Sprintf("%v::%v", in.id, in.path) +} + +func (in *inodeEntry) Attributes() (*fuseops.InodeAttributes, error) { + fileInfo, err := os.Stat(in.path) + if err != nil { + return &fuseops.InodeAttributes{}, err + } + return &fuseops.InodeAttributes{ + Size: uint64(fileInfo.Size()), + Nlink: 1, + Mode: fileInfo.Mode(), + Atime: fileInfo.ModTime(), + Mtime: fileInfo.ModTime(), + Ctime: time.Now(), + Crtime: time.Now(), + Uid: uid, + Gid: gid, + }, nil +} + +func (in *inodeEntry) ListChildren(inodes *sync.Map) ([]*fuseutil.Dirent, error) { + children, err := ioutil.ReadDir(in.path) + if err != nil { + return nil, err + } + dirents := make([]*fuseutil.Dirent, len(children)) + for i, child := range children { + + childInode, err := getOrCreateInode(inodes, in.id, child.Name()) + if err != nil || childInode == nil { + return nil, nil + } + + var childType fuseutil.DirentType + if child.IsDir() { + childType = fuseutil.DT_Directory + } else if child.Mode()&os.ModeSymlink != 0 { + childType = fuseutil.DT_Link + } else { + childType = fuseutil.DT_File + } + + dirents[i] = &fuseutil.Dirent{ + Offset: fuseops.DirOffset(i + 1), + Inode: childInode.Id(), + Name: child.Name(), + Type: childType, + } + } + return dirents, nil +} + +func (in *inodeEntry) Contents() ([]byte, error) { + return ioutil.ReadFile(in.path) +} diff --git a/samples/roloopbackfs/roloopbackfs.go b/samples/roloopbackfs/roloopbackfs.go new file mode 100644 index 0000000..bd2e9da --- /dev/null +++ b/samples/roloopbackfs/roloopbackfs.go @@ -0,0 +1,202 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package roloopbackfs + +import ( + "golang.org/x/net/context" + "log" + "os" + "sync" + + "github.com/jacobsa/fuse" + "github.com/jacobsa/fuse/fuseops" + "github.com/jacobsa/fuse/fuseutil" +) + +type readonlyLoopbackFs struct { + fuseutil.NotImplementedFileSystem + loopbackPath string + inodes *sync.Map + logger *log.Logger +} + +var _ fuseutil.FileSystem = &readonlyLoopbackFs{} + +// Create a file system that mirrors an existing physical path, in a readonly mode + +func NewReadonlyLoopbackServer(loopbackPath string, logger *log.Logger) (server fuse.Server, err error) { + + if _, err = os.Stat(loopbackPath); err != nil { + return nil, err + } + + inodes := &sync.Map{} + root := &inodeEntry{ + id: fuseops.RootInodeID, + path: loopbackPath, + } + inodes.Store(root.Id(), root) + server = fuseutil.NewFileSystemServer(&readonlyLoopbackFs{ + loopbackPath: loopbackPath, + inodes: inodes, + logger: logger, + }) + return +} + +func (fs *readonlyLoopbackFs) StatFS( + ctx context.Context, + op *fuseops.StatFSOp) error { + return nil +} + +func (fs *readonlyLoopbackFs) LookUpInode( + ctx context.Context, + op *fuseops.LookUpInodeOp) error { + entry, err := getOrCreateInode(fs.inodes, op.Parent, op.Name) + if err != nil { + fs.logger.Printf("fs.LookUpInode for '%v' on '%v': %v", entry, op.Name, err) + return fuse.EIO + } + if entry == nil { + return fuse.ENOENT + } + outputEntry := &op.Entry + outputEntry.Child = entry.Id() + attributes, err := entry.Attributes() + if err != nil { + fs.logger.Printf("fs.LookUpInode.Attributes for '%v' on '%v': %v", entry, op.Name, err) + return fuse.EIO + } + outputEntry.Attributes = *attributes + return nil +} + +func (fs *readonlyLoopbackFs) GetInodeAttributes( + ctx context.Context, + op *fuseops.GetInodeAttributesOp) error { + var entry, found = fs.inodes.Load(op.Inode) + if !found { + return fuse.ENOENT + } + attributes, err := entry.(Inode).Attributes() + if err != nil { + fs.logger.Printf("fs.GetInodeAttributes for '%v': %v", entry, err) + return fuse.EIO + } + op.Attributes = *attributes + return nil +} + +func (fs *readonlyLoopbackFs) OpenDir( + ctx context.Context, + op *fuseops.OpenDirOp) error { + // Allow opening any directory. + return nil +} + +func (fs *readonlyLoopbackFs) ReadDir( + ctx context.Context, + op *fuseops.ReadDirOp) error { + var entry, found = fs.inodes.Load(op.Inode) + if !found { + return fuse.ENOENT + } + children, err := entry.(Inode).ListChildren(fs.inodes) + if err != nil { + fs.logger.Printf("fs.ReadDir for '%v': %v", entry, err) + return fuse.EIO + } + + if op.Offset > fuseops.DirOffset(len(children)) { + return fuse.EIO + } + + children = children[op.Offset:] + + for _, child := range children { + bytesWritten := fuseutil.WriteDirent(op.Dst[op.BytesRead:], *child) + if bytesWritten == 0 { + break + } + op.BytesRead += bytesWritten + } + return nil +} + +func (fs *readonlyLoopbackFs) OpenFile( + ctx context.Context, + op *fuseops.OpenFileOp) error { + // Allow opening any file. + return nil +} + +func (fs *readonlyLoopbackFs) ReadFile( + ctx context.Context, + op *fuseops.ReadFileOp) error { + var entry, found = fs.inodes.Load(op.Inode) + if !found { + return fuse.ENOENT + } + contents, err := entry.(Inode).Contents() + if err != nil { + fs.logger.Printf("fs.ReadFile for '%v': %v", entry, err) + return fuse.EIO + } + + if op.Offset > int64(len(contents)) { + return fuse.EIO + } + + contents = contents[op.Offset:] + op.BytesRead = copy(op.Dst, contents) + return nil +} + +func (fs *readonlyLoopbackFs) ReleaseDirHandle( + ctx context.Context, + op *fuseops.ReleaseDirHandleOp) error { + return nil +} + +func (fs *readonlyLoopbackFs) GetXattr( + ctx context.Context, + op *fuseops.GetXattrOp) error { + return nil +} + +func (fs *readonlyLoopbackFs) ListXattr( + ctx context.Context, + op *fuseops.ListXattrOp) error { + return nil +} + +func (fs *readonlyLoopbackFs) ForgetInode( + ctx context.Context, + op *fuseops.ForgetInodeOp) error { + return nil +} + +func (fs *readonlyLoopbackFs) ReleaseFileHandle( + ctx context.Context, + op *fuseops.ReleaseFileHandleOp) error { + return nil +} + +func (fs *readonlyLoopbackFs) FlushFile( + ctx context.Context, + op *fuseops.FlushFileOp) error { + return nil +} diff --git a/samples/roloopbackfs/roloopbackfs_test.go b/samples/roloopbackfs/roloopbackfs_test.go new file mode 100644 index 0000000..3110417 --- /dev/null +++ b/samples/roloopbackfs/roloopbackfs_test.go @@ -0,0 +1,178 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package roloopbackfs_test + +import ( + "fmt" + "io/ioutil" + "log" + "math/rand" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/jacobsa/fuse/samples" + "github.com/jacobsa/fuse/samples/roloopbackfs" + . "github.com/jacobsa/ogletest" +) + +var ( + letters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") +) + +func TestReadonlyLoopbackFS(t *testing.T) { RunTests(t) } + +type ReadonlyLoopbackFSTest struct { + samples.SampleTest + physicalPath string +} + +func init() { + RegisterTestSuite(&ReadonlyLoopbackFSTest{}) + rand.Seed(time.Now().UnixNano()) +} + +func (t *ReadonlyLoopbackFSTest) SetUp(ti *TestInfo) { + var err error + + t.physicalPath, err = ioutil.TempDir("", "") + if err != nil { + panic(err) + } + + err = os.MkdirAll(t.physicalPath, 0777) + if err != nil { + panic(err) + } + + t.fillPhysicalFS() + + t.Server, err = roloopbackfs.NewReadonlyLoopbackServer( + t.physicalPath, + log.New(os.Stdout, "", 0), + ) + AssertEq(nil, err) + t.SampleTest.SetUp(ti) +} + +func (t *ReadonlyLoopbackFSTest) TearDown() { + t.SampleTest.TearDown() + err := os.RemoveAll(t.physicalPath) + if err != nil { + panic(err) + } +} + +func createDirectories(parentPath string, namePrefix string, count int, onDir func(dirPath string)) { + var err error + for i := 0; i < count; i++ { + dirName := fmt.Sprintf("%v_%v", namePrefix, i+1) + dirPath := filepath.Join(parentPath, dirName) + err = os.Mkdir(dirPath, 0777) + if err != nil { + panic(err) + } + if onDir != nil { + onDir(dirPath) + } + } +} + +func randomString(n int) []byte { + bytes := make([]byte, n) + for i := range bytes { + bytes[i] = letters[rand.Intn(len(letters))] + } + return bytes +} + +func (t *ReadonlyLoopbackFSTest) fillPhysicalFS() { + var err error + createDirectories(t.physicalPath, "top_dir", 10, func(dirPath string) { + fileName := fmt.Sprintf("secondary_file.txt") + contents := randomString(17) + err = ioutil.WriteFile(filepath.Join(dirPath, fileName), contents, 0777) + if err != nil { + panic(err) + } + createDirectories(dirPath, "secondary_dir", 5, func(dirPath string) { + for i := 0; i < 3; i++ { + fileName := fmt.Sprintf("file_%v.txt", i+1) + contents := randomString(i * 10) + err = ioutil.WriteFile(filepath.Join(dirPath, fileName), contents, 0777) + if err != nil { + panic(err) + } + } + }) + }) +} + +func (t *ReadonlyLoopbackFSTest) ListDirUsingWalk() { + countedFiles, countedDirs := 0, 0 + err := filepath.Walk(t.Dir, func(path string, info os.FileInfo, err error) error { + AssertNe(nil, info) + if info.IsDir() { + countedDirs++ + } else { + if strings.Contains(path, "file_1.txt") { + AssertEq(0, info.Size()) + } else { + AssertTrue(info.Size() > 0) + } + countedFiles++ + } + return nil + }) + AssertEq(nil, err) + AssertEq(1+10+10*5, countedDirs) + AssertEq(10+10*5*3, countedFiles) +} + +func (t *ReadonlyLoopbackFSTest) ListDirUsingDirectQuery() { + infos, err := ioutil.ReadDir(filepath.Join(t.Dir, "top_dir_3")) + AssertEq(nil, err) + AssertEq(1+5, len(infos)) + for i := 0; i < 5; i++ { + AssertEq(fmt.Sprintf("secondary_dir_%v", i+1), infos[i].Name()) + AssertTrue(infos[i].IsDir()) + } + AssertEq("secondary_file.txt", infos[5].Name()) + AssertFalse(infos[5].IsDir()) + + infos, err = ioutil.ReadDir(filepath.Join(t.Dir, "top_dir_4", "secondary_dir_1")) + AssertEq(nil, err) + AssertEq(3, len(infos)) + for i := 0; i < 3; i++ { + AssertEq(fmt.Sprintf("file_%v.txt", i+1), infos[i].Name()) + AssertFalse(infos[i].IsDir()) + } +} + +func (t *ReadonlyLoopbackFSTest) ReadFile() { + bytes, err := ioutil.ReadFile(filepath.Join(t.Dir, "top_dir_1", "secondary_file.txt")) + AssertEq(nil, err) + AssertEq(17, len(bytes)) + + bytes, err = ioutil.ReadFile(filepath.Join(t.Dir, "top_dir_1", "secondary_dir_3", "file_1.txt")) + AssertEq(nil, err) + AssertEq(0, len(bytes)) + + bytes, err = ioutil.ReadFile(filepath.Join(t.Dir, "top_dir_1", "secondary_dir_3", "file_3.txt")) + AssertEq(nil, err) + AssertEq(20, len(bytes)) +}