Added samples/roloopbackfs (#95)
parent
e0296dec95
commit
9677d03922
@ -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) |
||||
} |
||||
} |
@ -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) |
||||
} |
@ -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 |
||||
} |
@ -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)) |
||||
} |
Loading…
Reference in new issue