local: use lib/encoder

This commit is contained in:
Fabian Möller 2018-11-02 13:12:51 +01:00 committed by Nick Craig-Wood
parent c09b62a088
commit a98a750fc9
6 changed files with 184 additions and 244 deletions

View file

@ -0,0 +1,9 @@
//+build !windows
package local
import (
"github.com/rclone/rclone/fs/encodings"
)
const enc = encodings.LocalUnix

View file

@ -0,0 +1,9 @@
//+build windows
package local
import (
"github.com/rclone/rclone/fs/encodings"
)
const enc = encodings.LocalWindows

View file

@ -142,19 +142,19 @@ type Fs struct {
dev uint64 // device number of root node dev uint64 // device number of root node
precisionOk sync.Once // Whether we need to read the precision precisionOk sync.Once // Whether we need to read the precision
precision time.Duration // precision of local filesystem precision time.Duration // precision of local filesystem
wmu sync.Mutex // used for locking access to 'warned'. warnedMu sync.Mutex // used for locking access to 'warned'.
warned map[string]struct{} // whether we have warned about this string warned map[string]struct{} // whether we have warned about this string
// do os.Lstat or os.Stat // do os.Lstat or os.Stat
lstat func(name string) (os.FileInfo, error) lstat func(name string) (os.FileInfo, error)
dirNames *mapper // directory name mapping
objectHashesMu sync.Mutex // global lock for Object.hashes objectHashesMu sync.Mutex // global lock for Object.hashes
} }
// Object represents a local filesystem object // Object represents a local filesystem object
type Object struct { type Object struct {
fs *Fs // The Fs this object is part of fs *Fs // The Fs this object is part of
remote string // The remote path - properly UTF-8 encoded - for rclone remote string // The remote path (encoded path)
path string // The local path - may not be properly UTF-8 encoded - for OS path string // The local path (OS path)
size int64 // file metadata - always present size int64 // file metadata - always present
mode os.FileMode mode os.FileMode
modTime time.Time modTime time.Time
@ -188,9 +188,8 @@ func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
warned: make(map[string]struct{}), warned: make(map[string]struct{}),
dev: devUnset, dev: devUnset,
lstat: os.Lstat, lstat: os.Lstat,
dirNames: newMapper(),
} }
f.root = f.cleanPath(root) f.root = cleanRootPath(root, f.opt.NoUNC)
f.features = (&fs.Features{ f.features = (&fs.Features{
CaseInsensitive: f.caseInsensitive(), CaseInsensitive: f.caseInsensitive(),
CanHaveEmptyDirectories: true, CanHaveEmptyDirectories: true,
@ -235,12 +234,12 @@ func (f *Fs) Name() string {
// Root of the remote (as passed into NewFs) // Root of the remote (as passed into NewFs)
func (f *Fs) Root() string { func (f *Fs) Root() string {
return f.root return enc.ToStandardPath(filepath.ToSlash(f.root))
} }
// String converts this Fs to a string // String converts this Fs to a string
func (f *Fs) String() string { func (f *Fs) String() string {
return fmt.Sprintf("Local file system at %s", f.root) return fmt.Sprintf("Local file system at %s", f.Root())
} }
// Features returns the optional features of this Fs // Features returns the optional features of this Fs
@ -268,33 +267,27 @@ func (f *Fs) caseInsensitive() bool {
// and returns a new path, removing the suffix as needed, // and returns a new path, removing the suffix as needed,
// It also returns whether this is a translated link at all // It also returns whether this is a translated link at all
// //
// for regular files, dstPath is returned unchanged // for regular files, localPath is returned unchanged
func translateLink(remote, dstPath string) (newDstPath string, isTranslatedLink bool) { func translateLink(remote, localPath string) (newLocalPath string, isTranslatedLink bool) {
isTranslatedLink = strings.HasSuffix(remote, linkSuffix) isTranslatedLink = strings.HasSuffix(remote, linkSuffix)
newDstPath = strings.TrimSuffix(dstPath, linkSuffix) newLocalPath = strings.TrimSuffix(localPath, linkSuffix)
return newDstPath, isTranslatedLink return newLocalPath, isTranslatedLink
} }
// newObject makes a half completed Object // newObject makes a half completed Object
// func (f *Fs) newObject(remote string) *Object {
// if dstPath is empty then it is made from remote
func (f *Fs) newObject(remote, dstPath string) *Object {
translatedLink := false translatedLink := false
localPath := f.localPath(remote)
if dstPath == "" {
dstPath = f.cleanPath(filepath.Join(f.root, remote))
}
remote = f.cleanRemote(remote)
if f.opt.TranslateSymlinks { if f.opt.TranslateSymlinks {
// Possibly receive a new name for dstPath // Possibly receive a new name for localPath
dstPath, translatedLink = translateLink(remote, dstPath) localPath, translatedLink = translateLink(remote, localPath)
} }
return &Object{ return &Object{
fs: f, fs: f,
remote: remote, remote: remote,
path: dstPath, path: localPath,
translatedLink: translatedLink, translatedLink: translatedLink,
} }
} }
@ -302,8 +295,8 @@ func (f *Fs) newObject(remote, dstPath string) *Object {
// Return an Object from a path // Return an Object from a path
// //
// May return nil if an error occurred // May return nil if an error occurred
func (f *Fs) newObjectWithInfo(remote, dstPath string, info os.FileInfo) (fs.Object, error) { func (f *Fs) newObjectWithInfo(remote string, info os.FileInfo) (fs.Object, error) {
o := f.newObject(remote, dstPath) o := f.newObject(remote)
if info != nil { if info != nil {
o.setMetadata(info) o.setMetadata(info)
} else { } else {
@ -332,7 +325,7 @@ func (f *Fs) newObjectWithInfo(remote, dstPath string, info os.FileInfo) (fs.Obj
// NewObject finds the Object at remote. If it can't be found // NewObject finds the Object at remote. If it can't be found
// it returns the error ErrorObjectNotFound. // it returns the error ErrorObjectNotFound.
func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) { func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
return f.newObjectWithInfo(remote, "", nil) return f.newObjectWithInfo(remote, nil)
} }
// List the objects and directories in dir into entries. The // List the objects and directories in dir into entries. The
@ -345,10 +338,7 @@ func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
// This should return ErrDirNotFound if the directory isn't // This should return ErrDirNotFound if the directory isn't
// found. // found.
func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) { func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
fsDirPath := f.localPath(dir)
dir = f.dirNames.Load(dir)
fsDirPath := f.cleanPath(filepath.Join(f.root, dir))
remote := f.cleanRemote(dir)
_, err = os.Stat(fsDirPath) _, err = os.Stat(fsDirPath)
if err != nil { if err != nil {
return nil, fs.ErrorDirNotFound return nil, fs.ErrorDirNotFound
@ -410,11 +400,11 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
for _, fi := range fis { for _, fi := range fis {
name := fi.Name() name := fi.Name()
mode := fi.Mode() mode := fi.Mode()
newRemote := path.Join(remote, name) newRemote := f.cleanRemote(dir, name)
newPath := filepath.Join(fsDirPath, name)
// Follow symlinks if required // Follow symlinks if required
if f.opt.FollowSymlinks && (mode&os.ModeSymlink) != 0 { if f.opt.FollowSymlinks && (mode&os.ModeSymlink) != 0 {
fi, err = os.Stat(newPath) localPath := filepath.Join(fsDirPath, name)
fi, err = os.Stat(localPath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
// Skip bad symlinks // Skip bad symlinks
err = fserrors.NoRetryError(errors.Wrap(err, "symlink")) err = fserrors.NoRetryError(errors.Wrap(err, "symlink"))
@ -431,7 +421,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
// Ignore directories which are symlinks. These are junction points under windows which // Ignore directories which are symlinks. These are junction points under windows which
// are kind of a souped up symlink. Unix doesn't have directories which are symlinks. // are kind of a souped up symlink. Unix doesn't have directories which are symlinks.
if (mode&os.ModeSymlink) == 0 && f.dev == readDevice(fi, f.opt.OneFileSystem) { if (mode&os.ModeSymlink) == 0 && f.dev == readDevice(fi, f.opt.OneFileSystem) {
d := fs.NewDir(f.dirNames.Save(newRemote, f.cleanRemote(newRemote)), fi.ModTime()) d := fs.NewDir(newRemote, fi.ModTime())
entries = append(entries, d) entries = append(entries, d)
} }
} else { } else {
@ -439,7 +429,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
if f.opt.TranslateSymlinks && fi.Mode()&os.ModeSymlink != 0 { if f.opt.TranslateSymlinks && fi.Mode()&os.ModeSymlink != 0 {
newRemote += linkSuffix newRemote += linkSuffix
} }
fso, err := f.newObjectWithInfo(newRemote, newPath, fi) fso, err := f.newObjectWithInfo(newRemote, fi)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -452,67 +442,28 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
return entries, nil return entries, nil
} }
// cleanRemote makes string a valid UTF-8 string for remote strings. func (f *Fs) cleanRemote(dir, filename string) (remote string) {
// remote = path.Join(dir, enc.ToStandardName(filename))
// Any invalid UTF-8 characters will be replaced with utf8.RuneError
// It also normalises the UTF-8 and converts the slashes if necessary. if !utf8.ValidString(filename) {
func (f *Fs) cleanRemote(name string) string { f.warnedMu.Lock()
if !utf8.ValidString(name) { if _, ok := f.warned[remote]; !ok {
f.wmu.Lock() fs.Logf(f, "Replacing invalid UTF-8 characters in %q", remote)
if _, ok := f.warned[name]; !ok { f.warned[remote] = struct{}{}
fs.Logf(f, "Replacing invalid UTF-8 characters in %q", name)
f.warned[name] = struct{}{}
} }
f.wmu.Unlock() f.warnedMu.Unlock()
name = string([]rune(name))
} }
name = filepath.ToSlash(name) return
return name
} }
// mapper maps raw to cleaned directory names func (f *Fs) localPath(name string) string {
type mapper struct { return filepath.Join(f.root, filepath.FromSlash(enc.FromStandardPath(name)))
mu sync.RWMutex // mutex to protect the below
m map[string]string // map of un-normalised directory names
}
func newMapper() *mapper {
return &mapper{
m: make(map[string]string),
}
}
// Lookup a directory name to make a local name (reverses
// cleanDirName)
//
// FIXME this is temporary before we make a proper Directory object
func (m *mapper) Load(in string) string {
m.mu.RLock()
out, ok := m.m[in]
m.mu.RUnlock()
if ok {
return out
}
return in
}
// Cleans a directory name recording if it needed to be altered
//
// FIXME this is temporary before we make a proper Directory object
func (m *mapper) Save(in, out string) string {
if in != out {
m.mu.Lock()
m.m[out] = in
m.mu.Unlock()
}
return out
} }
// Put the Object to the local filesystem // Put the Object to the local filesystem
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
remote := src.Remote()
// Temporary Object under construction - info filled in by Update() // Temporary Object under construction - info filled in by Update()
o := f.newObject(remote, "") o := f.newObject(src.Remote())
err := o.Update(ctx, in, src, options...) err := o.Update(ctx, in, src, options...)
if err != nil { if err != nil {
return nil, err return nil, err
@ -528,13 +479,13 @@ func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, opt
// Mkdir creates the directory if it doesn't exist // Mkdir creates the directory if it doesn't exist
func (f *Fs) Mkdir(ctx context.Context, dir string) error { func (f *Fs) Mkdir(ctx context.Context, dir string) error {
// FIXME: https://github.com/syncthing/syncthing/blob/master/lib/osutil/mkdirall_windows.go // FIXME: https://github.com/syncthing/syncthing/blob/master/lib/osutil/mkdirall_windows.go
root := f.cleanPath(filepath.Join(f.root, dir)) localPath := f.localPath(dir)
err := os.MkdirAll(root, 0777) err := os.MkdirAll(localPath, 0777)
if err != nil { if err != nil {
return err return err
} }
if dir == "" { if dir == "" {
fi, err := f.lstat(root) fi, err := f.lstat(localPath)
if err != nil { if err != nil {
return err return err
} }
@ -547,8 +498,7 @@ func (f *Fs) Mkdir(ctx context.Context, dir string) error {
// //
// If it isn't empty it will return an error // If it isn't empty it will return an error
func (f *Fs) Rmdir(ctx context.Context, dir string) error { func (f *Fs) Rmdir(ctx context.Context, dir string) error {
root := f.cleanPath(filepath.Join(f.root, dir)) return os.Remove(f.localPath(dir))
return os.Remove(root)
} }
// Precision of the file system // Precision of the file system
@ -644,7 +594,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
} }
// Temporary Object under construction // Temporary Object under construction
dstObj := f.newObject(remote, "") dstObj := f.newObject(remote)
// Check it is a file if it exists // Check it is a file if it exists
err := dstObj.lstat() err := dstObj.lstat()
@ -701,8 +651,8 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
fs.Debugf(srcFs, "Can't move directory - not same remote type") fs.Debugf(srcFs, "Can't move directory - not same remote type")
return fs.ErrorCantDirMove return fs.ErrorCantDirMove
} }
srcPath := f.cleanPath(filepath.Join(srcFs.root, srcRemote)) srcPath := srcFs.localPath(srcRemote)
dstPath := f.cleanPath(filepath.Join(f.root, dstRemote)) dstPath := f.localPath(dstRemote)
// Check if destination exists // Check if destination exists
_, err := os.Lstat(dstPath) _, err := os.Lstat(dstPath)
@ -836,13 +786,6 @@ func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
// Storable returns a boolean showing if this object is storable // Storable returns a boolean showing if this object is storable
func (o *Object) Storable() bool { func (o *Object) Storable() bool {
// Check for control characters in the remote name and show non storable
for _, c := range o.Remote() {
if c >= 0x00 && c < 0x20 || c == 0x7F {
fs.Logf(o.fs, "Can't store file with control characters: %q", o.Remote())
return false
}
}
mode := o.mode mode := o.mode
if mode&os.ModeSymlink != 0 && !o.fs.opt.TranslateSymlinks { if mode&os.ModeSymlink != 0 && !o.fs.opt.TranslateSymlinks {
if !o.fs.opt.SkipSymlinks { if !o.fs.opt.SkipSymlinks {
@ -1087,7 +1030,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
// It truncates any existing object // It truncates any existing object
func (f *Fs) OpenWriterAt(ctx context.Context, remote string, size int64) (fs.WriterAtCloser, error) { func (f *Fs) OpenWriterAt(ctx context.Context, remote string, size int64) (fs.WriterAtCloser, error) {
// Temporary Object under construction // Temporary Object under construction
o := f.newObject(remote, "") o := f.newObject(remote)
err := o.mkdirAll() err := o.mkdirAll()
if err != nil { if err != nil {
@ -1139,49 +1082,32 @@ func (o *Object) Remove(ctx context.Context) error {
return remove(o.path) return remove(o.path)
} }
// cleanPathFragment cleans an OS path fragment which is part of a func cleanRootPath(s string, noUNC bool) string {
// bigger path and not necessarily absolute
func cleanPathFragment(s string) string {
if s == "" {
return s
}
s = filepath.Clean(s)
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
s = strings.Replace(s, `/`, `\`, -1) s = filepath.ToSlash(s)
} vol := filepath.VolumeName(s)
return s s = vol + enc.FromStandardPath(s[len(vol):])
} s = filepath.FromSlash(s)
// cleanPath cleans and makes absolute the path passed in and returns
// an OS path.
//
// The input might be in OS form or rclone form or a mixture, but the
// output is in OS form.
//
// On windows it makes the path UNC also and replaces any characters
// Windows can't deal with with their replacements.
func (f *Fs) cleanPath(s string) string {
s = cleanPathFragment(s)
if runtime.GOOS == "windows" {
if !filepath.IsAbs(s) && !strings.HasPrefix(s, "\\") { if !filepath.IsAbs(s) && !strings.HasPrefix(s, "\\") {
s2, err := filepath.Abs(s) s2, err := filepath.Abs(s)
if err == nil { if err == nil {
s = s2 s = s2
} }
} }
if !f.opt.NoUNC { if !noUNC {
// Convert to UNC // Convert to UNC
s = uncPath(s) s = uncPath(s)
} }
s = cleanWindowsName(f, s) return s
} else { }
if !filepath.IsAbs(s) { if !filepath.IsAbs(s) {
s2, err := filepath.Abs(s) s2, err := filepath.Abs(s)
if err == nil { if err == nil {
s = s2 s = s2
} }
} }
} s = enc.FromStandardPath(s)
return s return s
} }
@ -1190,63 +1116,21 @@ var isAbsWinDrive = regexp.MustCompile(`^[a-zA-Z]\:\\`)
// uncPath converts an absolute Windows path // uncPath converts an absolute Windows path
// to a UNC long path. // to a UNC long path.
func uncPath(s string) string { func uncPath(l string) string {
// UNC can NOT use "/", so convert all to "\"
s = strings.Replace(s, `/`, `\`, -1)
// If prefix is "\\", we already have a UNC path or server. // If prefix is "\\", we already have a UNC path or server.
if strings.HasPrefix(s, `\\`) { if strings.HasPrefix(l, `\\`) {
// If already long path, just keep it // If already long path, just keep it
if strings.HasPrefix(s, `\\?\`) { if strings.HasPrefix(l, `\\?\`) {
return s return l
} }
// Trim "\\" from path and add UNC prefix. // Trim "\\" from path and add UNC prefix.
return `\\?\UNC\` + strings.TrimPrefix(s, `\\`) return `\\?\UNC\` + strings.TrimPrefix(l, `\\`)
} }
if isAbsWinDrive.MatchString(s) { if isAbsWinDrive.MatchString(l) {
return `\\?\` + s return `\\?\` + l
} }
return s return l
}
// cleanWindowsName will clean invalid Windows characters replacing them with _
func cleanWindowsName(f *Fs, name string) string {
original := name
var name2 string
if strings.HasPrefix(name, `\\?\`) {
name2 = `\\?\`
name = strings.TrimPrefix(name, `\\?\`)
}
if strings.HasPrefix(name, `//?/`) {
name2 = `//?/`
name = strings.TrimPrefix(name, `//?/`)
}
// Colon is allowed as part of a drive name X:\
colonAt := strings.Index(name, ":")
if colonAt > 0 && colonAt < 3 && len(name) > colonAt+1 {
// Copy to name2, which is unfiltered
name2 += name[0 : colonAt+1]
name = name[colonAt+1:]
}
name2 += strings.Map(func(r rune) rune {
switch r {
case '<', '>', '"', '|', '?', '*', ':':
return '_'
}
return r
}, name)
if name2 != original && f != nil {
f.wmu.Lock()
if _, ok := f.warned[name]; !ok {
fs.Logf(f, "Replacing invalid characters in %q to %q", name, name2)
f.warned[name] = struct{}{}
}
f.wmu.Unlock()
}
return name2
} }
// Check the interfaces are satisfied // Check the interfaces are satisfied

View file

@ -25,19 +25,6 @@ func TestMain(m *testing.M) {
fstest.TestMain(m) fstest.TestMain(m)
} }
func TestMapper(t *testing.T) {
m := newMapper()
assert.Equal(t, m.m, map[string]string{})
assert.Equal(t, "potato", m.Save("potato", "potato"))
assert.Equal(t, m.m, map[string]string{})
assert.Equal(t, "-r'áö", m.Save("-r?'a´o¨", "-r'áö"))
assert.Equal(t, m.m, map[string]string{
"-r'áö": "-r?'a´o¨",
})
assert.Equal(t, "potato", m.Load("potato"))
assert.Equal(t, "-r?'a´o¨", m.Load("-r'áö"))
}
// Test copy with source file that's updating // Test copy with source file that's updating
func TestUpdatingCheck(t *testing.T) { func TestUpdatingCheck(t *testing.T) {
r := fstest.NewRun(t) r := fstest.NewRun(t)

View file

@ -1,29 +1,26 @@
package local package local
import ( import (
"runtime"
"testing" "testing"
) )
var uncTestPaths = []string{ var uncTestPaths = []string{
"C:\\Ba*d\\P|a?t<h>\\Windows\\Folder", `C:\Ba*d\P|a?t<h>\Windows\Folder`,
"C:/Ba*d/P|a?t<h>/Windows\\Folder", `C:\Windows\Folder`,
"C:\\Windows\\Folder", `\\?\C:\Windows\Folder`,
"\\\\?\\C:\\Windows\\Folder", `\\?\UNC\server\share\Desktop`,
"//?/C:/Windows/Folder", `\\?\unC\server\share\Desktop\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path`,
"\\\\?\\UNC\\server\\share\\Desktop", `\\server\share\Desktop\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path`,
"\\\\?\\unC\\server\\share\\Desktop\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path", `C:\Desktop\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path`,
"\\\\server\\share\\Desktop\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path", `C:\AbsoluteToRoot\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path\Very Long path`,
"C:\\Desktop\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path", `\\server\share\Desktop`,
"C:\\AbsoluteToRoot\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path\\Very Long path", `\\?\UNC\\share\folder\Desktop`,
"\\\\server\\share\\Desktop", `\\server\share`,
"\\\\?\\UNC\\\\share\\folder\\Desktop",
"\\\\server\\share",
} }
var uncTestPathsResults = []string{ var uncTestPathsResults = []string{
`\\?\C:\Ba*d\P|a?t<h>\Windows\Folder`, `\\?\C:\Ba*d\P|a?t<h>\Windows\Folder`,
`\\?\C:\Ba*d\P|a?t<h>\Windows\Folder`,
`\\?\C:\Windows\Folder`,
`\\?\C:\Windows\Folder`, `\\?\C:\Windows\Folder`,
`\\?\C:\Windows\Folder`, `\\?\C:\Windows\Folder`,
`\\?\UNC\server\share\Desktop`, `\\?\UNC\server\share\Desktop`,
@ -51,38 +48,23 @@ func TestUncPaths(t *testing.T) {
} }
} }
var utf8Tests = [][2]string{
{"ABC", "ABC"},
{string([]byte{0x80}), "<22>"},
{string([]byte{'a', 0x80, 'b'}), "a<>b"},
}
func TestCleanRemote(t *testing.T) {
f := &Fs{}
f.warned = make(map[string]struct{})
for _, test := range utf8Tests {
got := f.cleanRemote(test[0])
expect := test[1]
if got != expect {
t.Fatalf("got %q, expected %q", got, expect)
}
}
}
// Test Windows character replacements // Test Windows character replacements
var testsWindows = [][2]string{ var testsWindows = [][2]string{
{`c:\temp`, `c:\temp`}, {`c:\temp`, `c:\temp`},
{`\\?\UNC\theserver\dir\file.txt`, `\\?\UNC\theserver\dir\file.txt`}, {`\\?\UNC\theserver\dir\file.txt`, `\\?\UNC\theserver\dir\file.txt`},
{`//?/UNC/theserver/dir\file.txt`, `//?/UNC/theserver/dir\file.txt`}, {`//?/UNC/theserver/dir\file.txt`, `\\?\UNC\theserver\dir\file.txt`},
{"c:/temp", "c:/temp"}, {`c:/temp`, `c:\temp`},
{"/temp/file.txt", "/temp/file.txt"}, {`/temp/file.txt`, `\temp\file.txt`},
{`!\"#¤%&/()=;:*^?+-`, "!\\_#¤%&/()=;__^_+-"}, {`c:\!\"#¤%&/()=;:*^?+-`, `c:\!\#¤%&\()=;^+-`},
{`<>"|?*:&\<>"|?*:&\<>"|?*:&`, "_______&\\_______&\\_______&"}, {`c:\<>"|?*:&\<>"|?*:&\<>"|?*:&`, `c:\&\&\&`},
} }
func TestCleanWindows(t *testing.T) { func TestCleanWindows(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skipf("windows only")
}
for _, test := range testsWindows { for _, test := range testsWindows {
got := cleanWindowsName(nil, test[0]) got := cleanRootPath(test[0], true)
expect := test[1] expect := test[1]
if got != expect { if got != expect {
t.Fatalf("got %q, expected %q", got, expect) t.Fatalf("got %q, expected %q", got, expect)

View file

@ -24,8 +24,8 @@ on OS X.
### Filenames ### ### Filenames ###
Filenames are expected to be encoded in UTF-8 on disk. This is the Filenames should be encoded in UTF-8 on disk. This is the normal case
normal case for Windows and OS X. for Windows and OS X.
There is a bit more uncertainty in the Linux world, but new There is a bit more uncertainty in the Linux world, but new
distributions will have UTF-8 encoded files names. If you are using an distributions will have UTF-8 encoded files names. If you are using an
@ -34,13 +34,82 @@ can use the `convmv` tool to convert the filesystem to UTF-8. This
tool is available in most distributions' package managers. tool is available in most distributions' package managers.
If an invalid (non-UTF8) filename is read, the invalid characters will If an invalid (non-UTF8) filename is read, the invalid characters will
be replaced with the unicode replacement character, '<27>'. `rclone` be replaced with a quoted representation of the invalid bytes. The name
will emit a debug message in this case (use `-v` to see), eg `gro\xdf` will be transferred as `groDF`. `rclone` will emit a debug
message in this case (use `-v` to see), eg
``` ```
Local file system at .: Replacing invalid UTF-8 characters in "gro\xdf" Local file system at .: Replacing invalid UTF-8 characters in "gro\xdf"
``` ```
#### Restricted characters
On non Windows platforms the following characters are replaced when
handling file names.
| Character | Value | Replacement |
| --------- |:-----:|:-----------:|
| NUL | 0x00 | ␀ |
| / | 0x2F | |
When running on Windows the following characters are replaced. This
list is based on the [Windows file naming conventions](https://docs.microsoft.com/de-de/windows/desktop/FileIO/naming-a-file#naming-conventions).
| Character | Value | Replacement |
| --------- |:-----:|:-----------:|
| NUL | 0x00 | ␀ |
| SOH | 0x01 | ␁ |
| STX | 0x02 | ␂ |
| ETX | 0x03 | ␃ |
| EOT | 0x04 | ␄ |
| ENQ | 0x05 | ␅ |
| ACK | 0x06 | ␆ |
| BEL | 0x07 | ␇ |
| BS | 0x08 | ␈ |
| HT | 0x09 | ␉ |
| LF | 0x0A | ␊ |
| VT | 0x0B | ␋ |
| FF | 0x0C | ␌ |
| CR | 0x0D | ␍ |
| SO | 0x0E | ␎ |
| SI | 0x0F | ␏ |
| DLE | 0x10 | ␐ |
| DC1 | 0x11 | ␑ |
| DC2 | 0x12 | ␒ |
| DC3 | 0x13 | ␓ |
| DC4 | 0x14 | ␔ |
| NAK | 0x15 | ␕ |
| SYN | 0x16 | ␖ |
| ETB | 0x17 | ␗ |
| CAN | 0x18 | ␘ |
| EM | 0x19 | ␙ |
| SUB | 0x1A | ␚ |
| ESC | 0x1B | ␛ |
| FS | 0x1C | ␜ |
| GS | 0x1D | ␝ |
| RS | 0x1E | ␞ |
| US | 0x1F | ␟ |
| / | 0x2F | |
| " | 0x22 | |
| * | 0x2A | |
| : | 0x3A | |
| < | 0x3C | |
| > | 0x3E | |
| ? | 0x3F | |
| \ | 0x5C | |
| \| | 0x7C | |
File names on Windows can also not end with the following characters.
These only get replaced if they are last character in the name:
| Character | Value | Replacement |
| --------- |:-----:|:-----------:|
| SP | 0x20 | ␠ |
| . | 0x2E | |
Invalid UTF-8 bytes will also be [replaced](/overview/#invalid-utf8),
as they can't be converted to UTF-16.
### Long paths on Windows ### ### Long paths on Windows ###
Rclone handles long paths automatically, by converting all paths to long Rclone handles long paths automatically, by converting all paths to long