forked from TrueCloudLab/rclone
local: use lib/encoder
This commit is contained in:
parent
c09b62a088
commit
a98a750fc9
6 changed files with 184 additions and 244 deletions
9
backend/local/encode_other.go
Normal file
9
backend/local/encode_other.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
//+build !windows
|
||||||
|
|
||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rclone/rclone/fs/encodings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const enc = encodings.LocalUnix
|
9
backend/local/encode_windows.go
Normal file
9
backend/local/encode_windows.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
//+build windows
|
||||||
|
|
||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rclone/rclone/fs/encodings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const enc = encodings.LocalWindows
|
|
@ -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
|
||||||
|
@ -183,14 +183,13 @@ func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
f := &Fs{
|
f := &Fs{
|
||||||
name: name,
|
name: name,
|
||||||
opt: *opt,
|
opt: *opt,
|
||||||
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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 `gro‛DF`. `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
|
||||||
|
|
Loading…
Reference in a new issue