mirror of
https://github.com/rclone/rclone.git
synced 2024-12-22 23:22:08 +01:00
mountlib: use procfs to validate mount on linux - #5593
Current way of checking whether mountpoint has been already mounted (directory list) can result in race if rclone runs under Automount (classic or systemd). This patch adopts Linux ProcFS for the check. Note that mountpoint is considered empty if it's tagged as "mounted" by autofs. Also ProcFS is used to check whether rclone mount was successful (ie. tagged by a string containing "rclone"). On macOS/BSD where ProcFS is unavailable the old method is still used. This patch also moves a few utility functions unchanged to utils.go: CheckOverlap, CheckAllowings, SetVolumeName.
This commit is contained in:
parent
68be24c88d
commit
8c10dee510
77
cmd/mountlib/check_linux.go
Normal file
77
cmd/mountlib/check_linux.go
Normal file
@ -0,0 +1,77 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package mountlib
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/artyom/mtab"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
mtabPath = "/proc/mounts"
|
||||
pollInterval = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
// CheckMountEmpty checks if folder is not already a mountpoint.
|
||||
// On Linux we use the OS-specific /proc/mount API so the check won't access the path.
|
||||
// Directories marked as "mounted" by autofs are considered not mounted.
|
||||
func CheckMountEmpty(mountpoint string) error {
|
||||
const msg = "Directory already mounted, use --allow-non-empty to mount anyway: %s"
|
||||
|
||||
mountpointAbs, err := filepath.Abs(mountpoint)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "cannot get absolute path: %s", mountpoint)
|
||||
}
|
||||
|
||||
entries, err := mtab.Entries(mtabPath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "cannot read %s", mtabPath)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.Dir == mountpointAbs && entry.Type != "autofs" {
|
||||
return errors.Errorf(msg, mountpointAbs)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckMountReady checks whether mountpoint is mounted by rclone.
|
||||
// Only mounts with type "rclone" or "fuse.rclone" count.
|
||||
func CheckMountReady(mountpoint string) error {
|
||||
mountpointAbs, err := filepath.Abs(mountpoint)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "cannot get absolute path: %s", mountpoint)
|
||||
}
|
||||
entries, err := mtab.Entries(mtabPath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "cannot read %s", mtabPath)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.Dir == mountpointAbs && strings.Contains(entry.Type, "rclone") {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("mount not ready")
|
||||
}
|
||||
|
||||
// WaitMountReady waits until mountpoint is mounted by rclone.
|
||||
func WaitMountReady(mountpoint string, timeout time.Duration) (err error) {
|
||||
endTime := time.Now().Add(timeout)
|
||||
for {
|
||||
err = CheckMountReady(mountpoint)
|
||||
delay := time.Until(endTime)
|
||||
if err == nil || delay <= 0 {
|
||||
break
|
||||
}
|
||||
if delay > pollInterval {
|
||||
delay = pollInterval
|
||||
}
|
||||
time.Sleep(delay)
|
||||
}
|
||||
return
|
||||
}
|
47
cmd/mountlib/check_other.go
Normal file
47
cmd/mountlib/check_other.go
Normal file
@ -0,0 +1,47 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
package mountlib
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rclone/rclone/fs"
|
||||
)
|
||||
|
||||
// CheckMountEmpty checks if mountpoint folder is empty.
|
||||
// On non-Linux unixes we list directory to ensure that.
|
||||
func CheckMountEmpty(mountpoint string) error {
|
||||
fp, err := os.Open(mountpoint)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Can not open: %s", mountpoint)
|
||||
}
|
||||
defer fs.CheckClose(fp, &err)
|
||||
|
||||
_, err = fp.Readdirnames(1)
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
|
||||
const msg = "Directory is not empty, use --allow-non-empty to mount anyway: %s"
|
||||
if err == nil {
|
||||
return errors.Errorf(msg, mountpoint)
|
||||
}
|
||||
return errors.Wrapf(err, msg, mountpoint)
|
||||
}
|
||||
|
||||
// CheckMountReady should check if mountpoint is mounted by rclone.
|
||||
// The check is implemented only for Linux so this does nothing.
|
||||
func CheckMountReady(mountpoint string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// WaitMountReady should wait until mountpoint is mounted by rclone.
|
||||
// The check is implemented only for Linux so we just sleep a little.
|
||||
func WaitMountReady(mountpoint string, timeout time.Duration) error {
|
||||
time.Sleep(timeout)
|
||||
return nil
|
||||
}
|
@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -199,58 +198,6 @@ func (m *MountPoint) Mount() (daemonized bool, err error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// CheckOverlap checks that root doesn't overlap with mountpoint
|
||||
func (m *MountPoint) CheckOverlap() error {
|
||||
name := m.Fs.Name()
|
||||
if name != "" && name != "local" {
|
||||
return nil
|
||||
}
|
||||
rootAbs := absPath(m.Fs.Root())
|
||||
mountpointAbs := absPath(m.MountPoint)
|
||||
if strings.HasPrefix(rootAbs, mountpointAbs) || strings.HasPrefix(mountpointAbs, rootAbs) {
|
||||
const msg = "mount point %q and directory to be mounted %q mustn't overlap"
|
||||
return errors.Errorf(msg, m.MountPoint, m.Fs.Root())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// absPath is a helper function for MountPoint.CheckOverlap
|
||||
func absPath(path string) string {
|
||||
if abs, err := filepath.EvalSymlinks(path); err == nil {
|
||||
path = abs
|
||||
}
|
||||
if abs, err := filepath.Abs(path); err == nil {
|
||||
path = abs
|
||||
}
|
||||
path = filepath.ToSlash(path)
|
||||
if !strings.HasSuffix(path, "/") {
|
||||
path += "/"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// CheckAllowings informs about ignored flags on Windows. If not on Windows
|
||||
// and not --allow-non-empty flag is used, verify that mountpoint is empty.
|
||||
func (m *MountPoint) CheckAllowings() error {
|
||||
opt := &m.MountOpt
|
||||
if runtime.GOOS == "windows" {
|
||||
if opt.AllowNonEmpty {
|
||||
fs.Logf(nil, "--allow-non-empty flag does nothing on Windows")
|
||||
}
|
||||
if opt.AllowRoot {
|
||||
fs.Logf(nil, "--allow-root flag does nothing on Windows")
|
||||
}
|
||||
if opt.AllowOther {
|
||||
fs.Logf(nil, "--allow-other flag does nothing on Windows")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !opt.AllowNonEmpty {
|
||||
return CheckMountEmpty(m.MountPoint)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wait for mount end
|
||||
func (m *MountPoint) Wait() error {
|
||||
// Unmount on exit
|
||||
@ -303,22 +250,3 @@ func (m *MountPoint) Wait() error {
|
||||
func (m *MountPoint) Unmount() (err error) {
|
||||
return m.UnmountFn()
|
||||
}
|
||||
|
||||
// SetVolumeName with sensible default
|
||||
func (m *MountPoint) SetVolumeName(vol string) {
|
||||
if vol == "" {
|
||||
vol = m.Fs.Name() + ":" + m.Fs.Root()
|
||||
}
|
||||
m.MountOpt.SetVolumeName(vol)
|
||||
}
|
||||
|
||||
// SetVolumeName removes special characters from volume name if necessary
|
||||
func (opt *Options) SetVolumeName(vol string) {
|
||||
vol = strings.ReplaceAll(vol, ":", " ")
|
||||
vol = strings.ReplaceAll(vol, "/", " ")
|
||||
vol = strings.TrimSpace(vol)
|
||||
if runtime.GOOS == "windows" && len(vol) > 32 {
|
||||
vol = vol[:32]
|
||||
}
|
||||
opt.VolumeName = vol
|
||||
}
|
||||
|
@ -1,36 +1,14 @@
|
||||
package mountlib
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rclone/rclone/fs"
|
||||
)
|
||||
|
||||
// CheckMountEmpty checks if folder is empty
|
||||
func CheckMountEmpty(mountpoint string) error {
|
||||
fp, fpErr := os.Open(mountpoint)
|
||||
|
||||
if fpErr != nil {
|
||||
return errors.Wrap(fpErr, "Can not open: "+mountpoint)
|
||||
}
|
||||
defer fs.CheckClose(fp, &fpErr)
|
||||
|
||||
_, fpErr = fp.Readdirnames(1)
|
||||
|
||||
if fpErr == io.EOF {
|
||||
return nil
|
||||
}
|
||||
|
||||
msg := "Directory is not empty: " + mountpoint + " If you want to mount it anyway use: --allow-non-empty option"
|
||||
if fpErr == nil {
|
||||
return errors.New(msg)
|
||||
}
|
||||
return errors.Wrap(fpErr, msg)
|
||||
}
|
||||
|
||||
// ClipBlocks clips the blocks pointed to the OS max
|
||||
func ClipBlocks(b *uint64) {
|
||||
var max uint64
|
||||
@ -53,3 +31,74 @@ func ClipBlocks(b *uint64) {
|
||||
*b = max
|
||||
}
|
||||
}
|
||||
|
||||
// CheckOverlap checks that root doesn't overlap with mountpoint
|
||||
func (m *MountPoint) CheckOverlap() error {
|
||||
name := m.Fs.Name()
|
||||
if name != "" && name != "local" {
|
||||
return nil
|
||||
}
|
||||
rootAbs := absPath(m.Fs.Root())
|
||||
mountpointAbs := absPath(m.MountPoint)
|
||||
if strings.HasPrefix(rootAbs, mountpointAbs) || strings.HasPrefix(mountpointAbs, rootAbs) {
|
||||
const msg = "mount point %q and directory to be mounted %q mustn't overlap"
|
||||
return errors.Errorf(msg, m.MountPoint, m.Fs.Root())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// absPath is a helper function for MountPoint.CheckOverlap
|
||||
func absPath(path string) string {
|
||||
if abs, err := filepath.EvalSymlinks(path); err == nil {
|
||||
path = abs
|
||||
}
|
||||
if abs, err := filepath.Abs(path); err == nil {
|
||||
path = abs
|
||||
}
|
||||
path = filepath.ToSlash(path)
|
||||
if !strings.HasSuffix(path, "/") {
|
||||
path += "/"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// CheckAllowings informs about ignored flags on Windows. If not on Windows
|
||||
// and not --allow-non-empty flag is used, verify that mountpoint is empty.
|
||||
func (m *MountPoint) CheckAllowings() error {
|
||||
opt := &m.MountOpt
|
||||
if runtime.GOOS == "windows" {
|
||||
if opt.AllowNonEmpty {
|
||||
fs.Logf(nil, "--allow-non-empty flag does nothing on Windows")
|
||||
}
|
||||
if opt.AllowRoot {
|
||||
fs.Logf(nil, "--allow-root flag does nothing on Windows")
|
||||
}
|
||||
if opt.AllowOther {
|
||||
fs.Logf(nil, "--allow-other flag does nothing on Windows")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !opt.AllowNonEmpty {
|
||||
return CheckMountEmpty(m.MountPoint)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetVolumeName with sensible default
|
||||
func (m *MountPoint) SetVolumeName(vol string) {
|
||||
if vol == "" {
|
||||
vol = m.Fs.Name() + ":" + m.Fs.Root()
|
||||
}
|
||||
m.MountOpt.SetVolumeName(vol)
|
||||
}
|
||||
|
||||
// SetVolumeName removes special characters from volume name if necessary
|
||||
func (o *Options) SetVolumeName(vol string) {
|
||||
vol = strings.ReplaceAll(vol, ":", " ")
|
||||
vol = strings.ReplaceAll(vol, "/", " ")
|
||||
vol = strings.TrimSpace(vol)
|
||||
if runtime.GOOS == "windows" && len(vol) > 32 {
|
||||
vol = vol[:32]
|
||||
}
|
||||
o.VolumeName = vol
|
||||
}
|
||||
|
1
go.mod
1
go.mod
@ -14,6 +14,7 @@ require (
|
||||
github.com/aalpar/deheap v0.0.0-20200318053559-9a0c2883bd56
|
||||
github.com/abbot/go-http-auth v0.4.0
|
||||
github.com/anacrolix/dms v1.2.2
|
||||
github.com/artyom/mtab v0.0.0-20141107123140-74b6fd01d416
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
github.com/aws/aws-sdk-go v1.40.27
|
||||
github.com/billziss-gh/cgofuse v1.5.0
|
||||
|
2
go.sum
2
go.sum
@ -106,6 +106,8 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/artyom/mtab v0.0.0-20141107123140-74b6fd01d416 h1:8VH5S3f48ca549Ij9/mIIzwp5kkBio0enC+Zte5xBr4=
|
||||
github.com/artyom/mtab v0.0.0-20141107123140-74b6fd01d416/go.mod h1:4/w3KGZo0/xcC5ghHHq/Ij/CRCfK8s00v8oTmB7UTO0=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aws/aws-sdk-go v1.40.27 h1:8fWW0CpmBZ8WWduNwl4vE9t07nMYFrhAsUHjPj81qUM=
|
||||
|
Loading…
Reference in New Issue
Block a user