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