diff --git a/debug.go b/debug.go new file mode 100644 index 0000000..58fafdf --- /dev/null +++ b/debug.go @@ -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) +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..dba5d04 --- /dev/null +++ b/errors.go @@ -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 +) diff --git a/file_system.go b/file_system.go new file mode 100644 index 0000000..a3c9422 --- /dev/null +++ b/file_system.go @@ -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 { +} diff --git a/mounted_file_system.go b/mounted_file_system.go new file mode 100644 index 0000000..998952f --- /dev/null +++ b/mounted_file_system.go @@ -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 +} diff --git a/not_implemented_file_system.go b/not_implemented_file_system.go new file mode 100644 index 0000000..b2eb8eb --- /dev/null +++ b/not_implemented_file_system.go @@ -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 +} diff --git a/samples/hello_fs.go b/samples/hello_fs.go new file mode 100644 index 0000000..c604144 --- /dev/null +++ b/samples/hello_fs.go @@ -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 +} diff --git a/samples/hello_fs_test.go b/samples/hello_fs_test.go new file mode 100644 index 0000000..013f47e --- /dev/null +++ b/samples/hello_fs_test.go @@ -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") +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..4498801 --- /dev/null +++ b/server.go @@ -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) + } +}