diff --git a/cli/command_mount.go b/cli/command_mount.go index 5949080b2..4e3f7b796 100644 --- a/cli/command_mount.go +++ b/cli/command_mount.go @@ -21,6 +21,9 @@ var ( mountPoint = mountCommand.Arg("mountPoint", "Mount point").Default("*").String() mountPointBrowse = mountCommand.Flag("browse", "Open file browser").Bool() mountTraceFS = mountCommand.Flag("trace-fs", "Trace filesystem operations").Bool() + + mountFuseAllowOther = mountCommand.Flag("fuse-allow-other", "Allows other users to access the file system.").Bool() + mountFuseAllowNonEmptyMount = mountCommand.Flag("fuse-allow-non-empty-mount", "Allows the mounting over a non-empty directory. The files in it will be shadowed by the freshly created mount.").Bool() ) func runMountCommand(ctx context.Context, rep repo.Repository) error { @@ -42,7 +45,11 @@ func runMountCommand(ctx context.Context, rep repo.Repository) error { entry = cachefs.Wrap(entry, newFSCache()).(fs.Directory) - ctrl, mountErr := mount.Directory(ctx, entry, *mountPoint) + ctrl, mountErr := mount.Directory(ctx, entry, *mountPoint, + mount.Options{ + FuseAllowOther: *mountFuseAllowOther, + FuseAllowNonEmptyMount: *mountFuseAllowNonEmptyMount, + }) if mountErr != nil { return errors.Wrap(mountErr, "mount error") @@ -68,15 +75,27 @@ func runMountCommand(ctx context.Context, rep repo.Repository) error { onCtrlC(func() { close(ctrlCPressed) }) + select { case <-ctrlCPressed: log(ctx).Infof("Unmounting...") - return ctrl.Unmount(ctx) + // TODO: Consider lazy unmounting (-z) and polling till the filesystem is unmounted instead of failing with: + // "unmount error: exit status 1: fusermount: failed to unmount /tmp/kopia-mount719819963: Device or resource busy, try --help" + err := ctrl.Unmount(ctx) + if err != nil { + return err + } case <-ctrl.Done(): log(ctx).Infof("Unmounted.") return nil } + + // Reporting clean unmount in case of interrupt signal. + <-ctrl.Done() + log(ctx).Infof("Unmounted.") + + return nil } func init() { diff --git a/go.mod b/go.mod index 9065a0f8d..05bb51d6c 100644 --- a/go.mod +++ b/go.mod @@ -54,4 +54,5 @@ require ( google.golang.org/protobuf v1.25.0 gopkg.in/alecthomas/kingpin.v2 v2.2.6 gopkg.in/kothar/go-backblaze.v0 v0.0.0-20191215213626-7594ed38700f + mvdan.cc/gofumpt v0.0.0-20200927160801-5bfeb2e70dd6 // indirect ) diff --git a/go.sum b/go.sum index 7a5f95df5..0cedfd750 100644 --- a/go.sum +++ b/go.sum @@ -597,6 +597,7 @@ github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w github.com/rjeczalik/notify v0.9.2/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -944,6 +945,7 @@ golang.org/x/tools v0.0.0-20200606014950-c42cb6316fb6/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200608174601-1b747fd94509/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200731060945-b5fad4ed8dd6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200814172026-c4923e618c08/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -1127,6 +1129,8 @@ honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +mvdan.cc/gofumpt v0.0.0-20200927160801-5bfeb2e70dd6 h1:z+/YqapuV7VZPvBb3GYmuEJbA88M3PFUxaHilHYVCpQ= +mvdan.cc/gofumpt v0.0.0-20200927160801-5bfeb2e70dd6/go.mod h1:bzrjFmaD6+xqohD3KYP0H2FEuxknnBmyyOxdhLdaIws= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= diff --git a/internal/mount/mount.go b/internal/mount/mount.go index f4933a8a6..7f3554ded 100644 --- a/internal/mount/mount.go +++ b/internal/mount/mount.go @@ -15,3 +15,12 @@ type Controller interface { MountPath() string Done() <-chan struct{} } + +// Options cary set of flags passed to the mount layer. +type Options struct { + // AllowOther allows other users to access the file system. Supported on FUSE" + FuseAllowOther bool + // Allows the mounting over a non-empty directory. The files in it will be shadowed by the freshly created mount. + // Supported only on Fuse. + FuseAllowNonEmptyMount bool +} diff --git a/internal/mount/mount_fuse.go b/internal/mount/mount_fuse.go index 23abd672c..def1a731c 100644 --- a/internal/mount/mount_fuse.go +++ b/internal/mount/mount_fuse.go @@ -23,8 +23,27 @@ func (r *root) Root() (fusefs.Node, error) { return r.Node, nil } +func (mo *Options) toFuseMountOptions() []fuse.MountOption { + options := []fuse.MountOption{ + fuse.ReadOnly(), + fuse.FSName("kopia"), + fuse.Subtype("kopia"), + fuse.VolumeName("Kopia"), + } + + if mo.FuseAllowOther { + options = append(options, fuse.AllowOther()) + } + + if mo.FuseAllowNonEmptyMount { + options = append(options, fuse.AllowNonEmptyMount()) + } + + return options +} + // Directory mounts the given directory using FUSE. -func Directory(ctx context.Context, entry fs.Directory, mountPoint string) (Controller, error) { +func Directory(ctx context.Context, entry fs.Directory, mountPoint string, mountOptions Options) (Controller, error) { isTempDir := false if mountPoint == "*" { @@ -40,13 +59,14 @@ func Directory(ctx context.Context, entry fs.Directory, mountPoint string) (Cont rootNode := fusemount.NewDirectoryNode(entry) - fuseConnection, err := fuse.Mount( - mountPoint, + options := append( + mountOptions.toFuseMountOptions(), fuse.ReadOnly(), fuse.FSName("kopia"), fuse.Subtype("kopia"), - fuse.VolumeName("Kopia"), - ) + fuse.VolumeName("Kopia")) + + fuseConnection, err := fuse.Mount(mountPoint, options...) if err != nil { return nil, errors.Wrap(err, "error creating fuse connection") } diff --git a/internal/mount/mount_net_use.go b/internal/mount/mount_net_use.go index 91b9d8aeb..707e36b9b 100644 --- a/internal/mount/mount_net_use.go +++ b/internal/mount/mount_net_use.go @@ -14,7 +14,7 @@ import ( ) // Directory mounts a given directory under a provided drive letter. -func Directory(ctx context.Context, entry fs.Directory, driveLetter string) (Controller, error) { +func Directory(ctx context.Context, entry fs.Directory, driveLetter string, _ Options) (Controller, error) { if !isValidWindowsDriveOrAsterisk(driveLetter) { return nil, errors.Errorf("must be a valid drive letter or asteris") } diff --git a/internal/server/api_mount.go b/internal/server/api_mount.go index 1bc5ae594..f1b10b393 100644 --- a/internal/server/api_mount.go +++ b/internal/server/api_mount.go @@ -31,7 +31,7 @@ func (s *Server) handleMountCreate(ctx context.Context, r *http.Request, body [] log(ctx).Debugf("mount controller for %v not found, starting", oid) var err error - c, err = mount.Directory(ctx, snapshotfs.DirectoryEntry(s.rep, oid, nil), "*") + c, err = mount.Directory(ctx, snapshotfs.DirectoryEntry(s.rep, oid, nil), "*", mount.Options{}) if err != nil { return nil, internalServerError(err)