github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/cmd/bisync/bilib/canonical.go (about)

     1  // Package bilib provides common stuff for bisync and bisync_test
     2  package bilib
     3  
     4  import (
     5  	"context"
     6  	"os"
     7  	"path/filepath"
     8  	"regexp"
     9  	"runtime"
    10  	"strings"
    11  
    12  	"github.com/rclone/rclone/fs"
    13  	"github.com/rclone/rclone/fs/operations"
    14  )
    15  
    16  // FsPath converts Fs to a suitable rclone argument
    17  func FsPath(f fs.Info) string {
    18  	name, path, slash := f.Name(), f.Root(), "/"
    19  	if name == "local" {
    20  		slash = string(os.PathSeparator)
    21  		if runtime.GOOS == "windows" {
    22  			path = strings.ReplaceAll(path, "/", slash)
    23  			path = strings.TrimPrefix(path, `\\?\`)
    24  		}
    25  	} else {
    26  		path = name + ":" + path
    27  	}
    28  	if !strings.HasSuffix(path, slash) {
    29  		path += slash
    30  	}
    31  	return path
    32  }
    33  
    34  // CanonicalPath converts a remote to a suitable base file name
    35  func CanonicalPath(remote string) string {
    36  	trimmed := strings.Trim(remote, `\/`)
    37  	return nonCanonicalChars.ReplaceAllString(trimmed, "_")
    38  }
    39  
    40  var nonCanonicalChars = regexp.MustCompile(`[\s\\/:?*]`)
    41  
    42  // SessionName makes a unique base name for the sync operation
    43  func SessionName(fs1, fs2 fs.Fs) string {
    44  	return StripHexString(CanonicalPath(FsPath(fs1))) + ".." + StripHexString(CanonicalPath(FsPath(fs2)))
    45  }
    46  
    47  // StripHexString strips the (first) canonical {hexstring} suffix
    48  func StripHexString(path string) string {
    49  	open := strings.IndexRune(path, '{')
    50  	close := strings.IndexRune(path, '}')
    51  	if open >= 0 && close > open {
    52  		return path[:open] + path[close+1:] // (trailing underscore)
    53  	}
    54  	return path
    55  }
    56  
    57  // HasHexString returns true if path contains at least one canonical {hexstring} suffix
    58  func HasHexString(path string) bool {
    59  	open := strings.IndexRune(path, '{')
    60  	if open >= 0 && strings.IndexRune(path, '}') > open {
    61  		return true
    62  	}
    63  	return false
    64  }
    65  
    66  // BasePath joins the workDir with the SessionName, stripping {hexstring} suffix if necessary
    67  func BasePath(ctx context.Context, workDir string, fs1, fs2 fs.Fs) string {
    68  	suffixedSession := CanonicalPath(FsPath(fs1)) + ".." + CanonicalPath(FsPath(fs2))
    69  	suffixedBasePath := filepath.Join(workDir, suffixedSession)
    70  	listing1 := suffixedBasePath + ".path1.lst"
    71  	listing2 := suffixedBasePath + ".path2.lst"
    72  
    73  	sessionName := SessionName(fs1, fs2)
    74  	basePath := filepath.Join(workDir, sessionName)
    75  
    76  	// Normalize to non-canonical version for overridden configs
    77  	// to ensure that backend-specific flags don't change the listing filename.
    78  	// For backward-compatibility, we first check if we found a listing file with the suffixed version.
    79  	// If so, we rename it (and overwrite non-suffixed version, if any.)
    80  	// If not, we carry on with the non-suffixed version.
    81  	// We should only find a suffixed version if bisync v1.66 or older created it.
    82  	if HasHexString(suffixedSession) && FileExists(listing1) {
    83  		fs.Infof(listing1, "renaming to: %s", basePath+".path1.lst")
    84  		if !operations.SkipDestructive(ctx, listing1, "rename to "+basePath+".path1.lst") {
    85  			_ = os.Rename(listing1, basePath+".path1.lst")
    86  		}
    87  	}
    88  	if HasHexString(suffixedSession) && FileExists(listing2) {
    89  		fs.Infof(listing2, "renaming to: %s", basePath+".path2.lst")
    90  		if !operations.SkipDestructive(ctx, listing1, "rename to "+basePath+".path2.lst") {
    91  			_ = os.Rename(listing2, basePath+".path2.lst")
    92  		} else {
    93  			return suffixedBasePath
    94  		}
    95  	}
    96  	return basePath
    97  }