Imported without modification from github.com/jacobsa/gcsfuse/fuseutil.

At commit 90c8d87fe8701d2335671eb01cbc1d70f655c87f.

I'm splitting this out because it's large and more generally useful.
geesefs-0-30-9
Aaron Jacobs 2015-02-27 08:54:16 +11:00
parent b6dc0c88f1
commit 915afb6308
8 changed files with 695 additions and 0 deletions

27
debug.go Normal file
View File

@ -0,0 +1,27 @@
// Copyright 2015 Google Inc. All Rights Reserved.
// Author: jacobsa@google.com (Aaron Jacobs)
package fuseutil
import (
"flag"
"io"
"io/ioutil"
"log"
"os"
)
var fEnableDebug = flag.Bool(
"fuseutil.debug",
false,
"Write FUSE debugging messages to stderr.")
// Create a logger based on command-line flag settings.
func getLogger() *log.Logger {
var writer io.Writer = ioutil.Discard
if *fEnableDebug {
writer = os.Stderr
}
return log.New(writer, "fuseutil: ", log.LstdFlags)
}

12
errors.go Normal file
View File

@ -0,0 +1,12 @@
// Copyright 2015 Google Inc. All Rights Reserved.
// Author: jacobsa@google.com (Aaron Jacobs)
package fuseutil
import "bazil.org/fuse"
const (
// Errors corresponding to kernel error numbers. These may be treated
// specially when returned by a FileSystem method.
ENOSYS = fuse.ENOSYS
)

199
file_system.go Normal file
View File

@ -0,0 +1,199 @@
// Copyright 2015 Google Inc. All Rights Reserved.
// Author: jacobsa@google.com (Aaron Jacobs)
package fuseutil
import (
"time"
"bazil.org/fuse"
"golang.org/x/net/context"
)
// An interface that must be implemented by file systems to be mounted with
// FUSE. See also the comments on request and response structs.
//
// Not all methods need to have interesting implementations. Embed a field of
// type NotImplementedFileSystem to inherit defaults that return ENOSYS to the
// kernel.
//
// Must be safe for concurrent access via all methods.
type FileSystem interface {
// Open a file or directory identified by an inode ID. The kernel calls this
// method when setting up a struct file for a particular inode, usually in
// response to an open(2) call from a user-space process. This may have side
// effects, depending on the flags passed.
Open(
ctx context.Context,
req *OpenRequest) (*OpenResponse, error)
// Look up a child by name within a parent directory. The kernel calls this
// when resolving user paths to dentry structs, which are then cached.
Lookup(
ctx context.Context,
req *LookupRequest) (*LookupResponse, error)
// Forget an inode ID previously issued (e.g. by Lookup). The kernel calls
// this when removing an inode from its internal caches.
//
// The kernel guarantees that the node ID will not be used in further calls
// to the file system (unless it is reissued by the file system).
Forget(
ctx context.Context,
req *ForgetRequest) (*ForgetResponse, error)
}
////////////////////////////////////////////////////////////////////////
// Simple types
////////////////////////////////////////////////////////////////////////
// A 64-bit number used to uniquely identify a file or directory in the file
// system. File systems may mint inode IDs with any value except for
// RootInodeID.
//
// This corresponds to struct inode::i_no in the VFS layer.
// (Cf. http://goo.gl/tvYyQt)
type InodeID uint64
// A distinguished inode ID that identifies the root of the file system, e.g.
// in a request to Open or Lookup. Unlike all other inode IDs, which are minted
// by the file system, the FUSE VFS layer may send a request for this ID
// without the file system ever having referenced it in a previous response.
const RootInodeID InodeID = InodeID(fuse.RootID)
// A generation number for an inode. Irrelevant for file systems that won't be
// exported over NFS. For those that will and that reuse inode IDs when they
// become free, the generation number must change when an ID is reused.
//
// This corresponds to struct inode::i_generation in the VFS layer.
// (Cf. http://goo.gl/tvYyQt)
//
// Some related reading:
//
// http://fuse.sourceforge.net/doxygen/structfuse__entry__param.html
// http://stackoverflow.com/q/11071996/1505451
// http://goo.gl/CqvwyX
// http://julipedia.meroh.net/2005/09/nfs-file-handles.html
// http://goo.gl/wvo3MB
//
type GenerationNumber uint64
// Attributes for a file or directory inode. Corresponds to struct inode (cf.
// http://goo.gl/tvYyQt).
type InodeAttributes struct {
// The size of the file in bytes.
Size uint64
}
////////////////////////////////////////////////////////////////////////
// Requests and responses
////////////////////////////////////////////////////////////////////////
type OpenRequest struct {
// The ID of the inode to be opened.
Inode InodeID
// Mode and options flags.
Flags fuse.OpenFlags
}
// Currently nothing interesting here. The file system should perform any
// checking and side effects necessary as part of FileSystem.Open, and return
// an error if appropriate.
type OpenResponse struct {
}
type LookupRequest struct {
// The ID of the directory inode to which the child belongs.
Parent InodeID
// The name of the child of interest, relative to the parent. For example, in
// this directory structure:
//
// foo/
// bar/
// baz
//
// the file system may receive a request to look up the child named "bar" for
// the parent foo/.
Name string
}
type LookupResponse struct {
// The ID of the child inode. The file system must ensure that the returned
// inode ID remains valid until a later call to Forget.
Child InodeID
// A generation number for this incarnation of the inode with the given ID.
// See comments on type GenerationNumber for more.
Generation GenerationNumber
// Current ttributes for the child inode.
Attributes InodeAttributes
// The FUSE VFS layer in the kernel maintains a cache of file attributes,
// used whenever up to date information about size, mode, etc. is needed.
//
// For example, this is the abridged call chain for fstat(2):
//
// * (http://goo.gl/tKBH1p) fstat calls vfs_fstat.
// * (http://goo.gl/3HeITq) vfs_fstat eventuall calls vfs_getattr_nosec.
// * (http://goo.gl/DccFQr) vfs_getattr_nosec calls i_op->getattr.
// * (http://goo.gl/dpKkst) fuse_getattr calls fuse_update_attributes.
// * (http://goo.gl/yNlqPw) fuse_update_attributes uses the values in the
// struct inode if allowed, otherwise calling out to the user-space code.
//
// In addition to obvious cases like fstat, this is also used in more subtle
// cases like updating size information before seeking (http://goo.gl/2nnMFa)
// or reading (http://goo.gl/FQSWs8).
//
// Most 'real' file systems do not set inode_operations::getattr, and
// therefore vfs_getattr_nosec calls generic_fillattr which simply grabs the
// information from the inode struct. This makes sense because these file
// systems cannot spontaneously change; all modifications go through the
// kernel which can update the inode struct as appropriate.
//
// In contrast, a FUSE file system may have spontaneous changes, so it calls
// out to user space to fetch attributes. However this is expensive, so the
// FUSE layer in the kernel caches the attributes if requested.
//
// This field controls when the attributes returned in this response and
// stashed in the struct inode should be re-queried. Leave at the zero value
// to disable caching.
//
// More reading:
// http://stackoverflow.com/q/21540315/1505451
AttributesExpiration time.Time
// The time until which the kernel may maintain an entry for this name to
// inode mapping in its dentry cache. After this time, it will revalidate the
// dentry.
//
// As in the discussion of attribute caching above, unlike real file systems,
// FUSE file systems may spontaneously change their name -> inode mapping.
// Therefore the FUSE VFS layer uses dentry_operations::d_revalidate
// (http://goo.gl/dVea0h) to intercept lookups and revalidate by calling the
// user-space Lookup method. However the latter may be slow, so it caches the
// entries until the time defined by this field.
//
// Example code walk:
//
// * (http://goo.gl/M2G3tO) lookup_dcache calls d_revalidate if enabled.
// * (http://goo.gl/ef0Elu) fuse_dentry_revalidate just uses the dentry's
// inode if fuse_dentry_time(entry) hasn't passed. Otherwise it sends a
// lookup request.
//
// Leave at the zero value to disable caching.
EntryExpiration time.Time
}
type ForgetRequest struct {
// The inode to be forgotten. The kernel guarantees that the node ID will not
// be used in further calls to the file system (unless it is reissued by the
// file system).
ID InodeID
}
type ForgetResponse struct {
}

131
mounted_file_system.go Normal file
View File

@ -0,0 +1,131 @@
// Copyright 2015 Google Inc. All Rights Reserved.
// Author: jacobsa@google.com (Aaron Jacobs)
package fuseutil
import (
"errors"
"bazil.org/fuse"
"golang.org/x/net/context"
)
// A struct representing the status of a mount operation, with methods for
// waiting on the mount to complete, waiting for unmounting, and causing
// unmounting.
type MountedFileSystem struct {
dir string
// The result to return from WaitForReady. Not valid until the channel is
// closed.
readyStatus error
readyStatusAvailable chan struct{}
// The result to return from Join. Not valid until the channel is closed.
joinStatus error
joinStatusAvailable chan struct{}
}
// Return the directory on which the file system is mounted (or where we
// attempted to mount it.)
func (mfs *MountedFileSystem) Dir() string {
return mfs.dir
}
// Wait until the mount point is ready to be used. After a successful return
// from this function, the contents of the mounted file system should be
// visible in the directory supplied to NewMountPoint. May be called multiple
// times.
func (mfs *MountedFileSystem) WaitForReady(ctx context.Context) error {
select {
case <-mfs.readyStatusAvailable:
return mfs.readyStatus
case <-ctx.Done():
return ctx.Err()
}
}
// Block until a mounted file system has been unmounted. The return value will
// be non-nil if anything unexpected happened while serving. May be called
// multiple times. Must not be called unless WaitForReady has returned nil.
func (mfs *MountedFileSystem) Join(ctx context.Context) error {
select {
case <-mfs.joinStatusAvailable:
return mfs.joinStatus
case <-ctx.Done():
return ctx.Err()
}
}
// Attempt to unmount the file system. Use Join to wait for it to actually be
// unmounted. You must first call WaitForReady to ensure there is no race with
// mounting.
func (mfs *MountedFileSystem) Unmount() error {
return fuse.Unmount(mfs.dir)
}
// Runs in the background.
func (mfs *MountedFileSystem) mountAndServe(
server *server,
options []fuse.MountOption) {
logger := getLogger()
// Open a FUSE connection.
logger.Println("Opening a FUSE connection.")
c, err := fuse.Mount(mfs.dir, options...)
if err != nil {
mfs.readyStatus = errors.New("fuse.Mount: " + err.Error())
close(mfs.readyStatusAvailable)
return
}
defer c.Close()
// Start a goroutine that will notify the MountedFileSystem object when the
// connection says it is ready (or it fails to become ready).
go func() {
logger.Println("Waiting for the FUSE connection to be ready.")
<-c.Ready
logger.Println("The FUSE connection is ready.")
mfs.readyStatus = c.MountError
close(mfs.readyStatusAvailable)
}()
// Serve the connection using the file system object.
logger.Println("Serving the FUSE connection.")
if err := server.Serve(c); err != nil {
mfs.joinStatus = errors.New("Serve: " + err.Error())
close(mfs.joinStatusAvailable)
return
}
// Signal that everything is okay.
close(mfs.joinStatusAvailable)
}
// Attempt to mount the supplied file system on the given directory.
// mfs.WaitForReady() must be called to find out whether the mount was
// successful.
func Mount(
dir string,
fs FileSystem,
options ...fuse.MountOption) (mfs *MountedFileSystem, err error) {
// Create a server object.
server, err := newServer(fs)
if err != nil {
return
}
// Initialize the struct.
mfs = &MountedFileSystem{
dir: dir,
readyStatusAvailable: make(chan struct{}),
joinStatusAvailable: make(chan struct{}),
}
// Mount in the background.
go mfs.mountAndServe(server, options)
return
}

View File

@ -0,0 +1,31 @@
// Copyright 2015 Google Inc. All Rights Reserved.
// Author: jacobsa@google.com (Aaron Jacobs)
package fuseutil
import "golang.org/x/net/context"
// Embed this within your file system type to inherit default implementations
// of all methods that return ENOSYS.
type NotImplementedFileSystem struct {
}
var _ FileSystem = &NotImplementedFileSystem{}
func (fs *NotImplementedFileSystem) Open(
ctx context.Context,
req *OpenRequest) (*OpenResponse, error) {
return nil, ENOSYS
}
func (fs *NotImplementedFileSystem) Lookup(
ctx context.Context,
req *LookupRequest) (*LookupResponse, error) {
return nil, ENOSYS
}
func (fs *NotImplementedFileSystem) Forget(
ctx context.Context,
req *ForgetRequest) (*ForgetResponse, error) {
return nil, ENOSYS
}

37
samples/hello_fs.go Normal file
View File

@ -0,0 +1,37 @@
// Copyright 2015 Google Inc. All Rights Reserved.
// Author: jacobsa@google.com (Aaron Jacobs)
package samples
import (
"github.com/jacobsa/gcsfuse/fuseutil"
"github.com/jacobsa/gcsfuse/timeutil"
"golang.org/x/net/context"
)
// A file system with a fixed structure that looks like this:
//
// hello
// dir/
// world
//
// Each file contains the string "Hello, world!".
type HelloFS struct {
fuseutil.NotImplementedFileSystem
Clock timeutil.Clock
}
var _ fuseutil.FileSystem = &HelloFS{}
func (fs *HelloFS) Open(
ctx context.Context,
req *fuseutil.OpenRequest) (resp *fuseutil.OpenResponse, err error) {
// We always allow opening the root directory.
if req.Inode == fuseutil.RootInodeID {
return
}
// TODO(jacobsa): Handle others.
err = fuseutil.ENOSYS
return
}

149
samples/hello_fs_test.go Normal file
View File

@ -0,0 +1,149 @@
// Copyright 2015 Google Inc. All Rights Reserved.
// Author: jacobsa@google.com (Aaron Jacobs)
package samples_test
import (
"io/ioutil"
"log"
"os"
"strings"
"testing"
"time"
"github.com/jacobsa/gcsfuse/fuseutil"
"github.com/jacobsa/gcsfuse/fuseutil/samples"
"github.com/jacobsa/gcsfuse/timeutil"
. "github.com/jacobsa/ogletest"
"golang.org/x/net/context"
)
func TestHelloFS(t *testing.T) { RunTests(t) }
////////////////////////////////////////////////////////////////////////
// Boilerplate
////////////////////////////////////////////////////////////////////////
type HelloFSTest struct {
clock timeutil.SimulatedClock
mfs *fuseutil.MountedFileSystem
}
var _ SetUpInterface = &HelloFSTest{}
var _ TearDownInterface = &HelloFSTest{}
func init() { RegisterTestSuite(&HelloFSTest{}) }
func (t *HelloFSTest) SetUp(ti *TestInfo) {
var err error
// Set up a fixed, non-zero time.
t.clock.AdvanceTime(time.Now().Sub(t.clock.Now()))
// Set up a temporary directory for mounting.
mountPoint, err := ioutil.TempDir("", "hello_fs_test")
if err != nil {
panic("ioutil.TempDir: " + err.Error())
}
// Mount a file system.
fs := &samples.HelloFS{
Clock: &t.clock,
}
if t.mfs, err = fuseutil.Mount(mountPoint, fs); err != nil {
panic("Mount: " + err.Error())
}
if err = t.mfs.WaitForReady(context.Background()); err != nil {
panic("MountedFileSystem.WaitForReady: " + err.Error())
}
}
func (t *HelloFSTest) TearDown() {
// Unmount the file system. Try again on "resource busy" errors.
delay := 10 * time.Millisecond
for {
err := t.mfs.Unmount()
if err == nil {
break
}
if strings.Contains(err.Error(), "resource busy") {
log.Println("Resource busy error while unmounting; trying again")
time.Sleep(delay)
delay = time.Duration(1.3 * float64(delay))
continue
}
panic("MountedFileSystem.Unmount: " + err.Error())
}
if err := t.mfs.Join(context.Background()); err != nil {
panic("MountedFileSystem.Join: " + err.Error())
}
}
////////////////////////////////////////////////////////////////////////
// Test functions
////////////////////////////////////////////////////////////////////////
func (t *HelloFSTest) ReadDir_Root() {
entries, err := ioutil.ReadDir(t.mfs.Dir())
AssertEq(nil, err)
AssertEq(2, len(entries))
var fi os.FileInfo
// dir
fi = entries[0]
ExpectEq("dir", fi.Name())
ExpectEq(0, fi.Size())
ExpectEq(os.ModeDir|0500, fi.Mode())
ExpectEq(t.clock.Now(), fi.ModTime())
ExpectTrue(fi.IsDir())
// hello
fi = entries[1]
ExpectEq("hello", fi.Name())
ExpectEq(len("Hello, world!"), fi.Size())
ExpectEq(0400, fi.Mode())
ExpectEq(t.clock.Now(), fi.ModTime())
ExpectFalse(fi.IsDir())
}
func (t *HelloFSTest) ReadDir_Dir() {
AssertTrue(false, "TODO")
}
func (t *HelloFSTest) ReadDir_NonExistent() {
AssertTrue(false, "TODO")
}
func (t *HelloFSTest) Stat_Hello() {
AssertTrue(false, "TODO")
}
func (t *HelloFSTest) Stat_Dir() {
AssertTrue(false, "TODO")
}
func (t *HelloFSTest) Stat_World() {
AssertTrue(false, "TODO")
}
func (t *HelloFSTest) Stat_NonExistent() {
AssertTrue(false, "TODO")
}
func (t *HelloFSTest) Read_Hello() {
AssertTrue(false, "TODO")
}
func (t *HelloFSTest) Read_World() {
AssertTrue(false, "TODO")
}
func (t *HelloFSTest) Open_NonExistent() {
AssertTrue(false, "TODO")
}

109
server.go Normal file
View File

@ -0,0 +1,109 @@
// Copyright 2015 Google Inc. All Rights Reserved.
// Author: jacobsa@google.com (Aaron Jacobs)
package fuseutil
import (
"fmt"
"io"
"log"
"golang.org/x/net/context"
"bazil.org/fuse"
)
// An object that terminates one end of the userspace <-> FUSE VFS connection.
type server struct {
logger *log.Logger
fs FileSystem
}
// Create a server that relays requests to the supplied file system.
func newServer(fs FileSystem) (s *server, err error) {
s = &server{
logger: getLogger(),
fs: fs,
}
return
}
// Serve the fuse connection by repeatedly reading requests from the supplied
// FUSE connection, responding as dictated by the file system. Return when the
// connection is closed or an unexpected error occurs.
func (s *server) Serve(c *fuse.Conn) (err error) {
// Read a message at a time, dispatching to goroutines doing the actual
// processing.
for {
var fuseReq fuse.Request
fuseReq, err = c.ReadRequest()
// ReadRequest returns EOF when the connection has been closed.
//
// TODO(jacobsa): Remove this and verify it's actually needed.
if err == io.EOF {
err = nil
return
}
// Otherwise, forward on errors.
if err != nil {
err = fmt.Errorf("Conn.ReadRequest: %v", err)
return
}
go s.handleFuseRequest(fuseReq)
}
}
func (s *server) handleFuseRequest(fuseReq fuse.Request) {
// Log the request.
s.logger.Println("Received:", fuseReq)
// TODO(jacobsa): Support cancellation when interrupted, if we can coax the
// system into reproducing such requests.
ctx := context.Background()
// Attempt to handle it.
switch typed := fuseReq.(type) {
case *fuse.InitRequest:
// Responding to this is required to make mounting work, at least on OS X.
// We don't currently expose the capability for the file system to
// intercept this.
fuseResp := &fuse.InitResponse{}
s.logger.Println("Responding:", fuseResp)
typed.Respond(fuseResp)
case *fuse.StatfsRequest:
// Responding to this is required to make mounting work, at least on OS X.
// We don't currently expose the capability for the file system to
// intercept this.
fuseResp := &fuse.StatfsResponse{}
s.logger.Println("Responding:", fuseResp)
typed.Respond(fuseResp)
case *fuse.OpenRequest:
// Convert the request.
req := &OpenRequest{
Inode: InodeID(typed.Header.Node),
Flags: typed.Flags,
}
// Call the file system.
if _, err := s.fs.Open(ctx, req); err != nil {
s.logger.Print("Responding:", err)
typed.RespondError(err)
return
}
// There is nothing interesting to convert in the response.
fuseResp := &fuse.OpenResponse{}
s.logger.Print("Responding:", fuseResp)
typed.Respond(fuseResp)
default:
s.logger.Println("Unhandled type. Returning ENOSYS.")
typed.RespondError(ENOSYS)
}
}