mirror of
https://github.com/rclone/rclone.git
synced 2025-12-06 00:03:32 +00:00
lib/transform adds the transform library, supporting advanced path name transformations for converting and renaming files and directories by applying prefixes, suffixes, and other alterations. It also adds the --name-transform flag for use with sync, copy, and move. Multiple transformations can be used in sequence, applied in the order they are specified on the command line. By default --name-transform will only apply to file names. The means only the leaf file name will be transformed. However some of the transforms would be better applied to the whole path or just directories. To choose which which part of the file path is affected some tags can be added to the --name-transform: file Only transform the leaf name of files (DEFAULT) dir Only transform name of directories - these may appear anywhere in the path all Transform the entire path for files and directories Example syntax: --name-transform file,prefix=ABC --name-transform dir,prefix=DEF
227 lines
5.8 KiB
Go
227 lines
5.8 KiB
Go
// Package transform holds functions for path name transformations
|
|
package transform
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"mime"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/lib/encoder"
|
|
"golang.org/x/text/encoding/charmap"
|
|
"golang.org/x/text/unicode/norm"
|
|
)
|
|
|
|
// Path transforms a path s according to the --name-transform options in use
|
|
//
|
|
// If no transforms are in use, s is returned unchanged
|
|
func Path(s string, isDir bool) string {
|
|
if !Transforming() {
|
|
return s
|
|
}
|
|
|
|
var err error
|
|
old := s
|
|
for _, t := range Opt.transforms {
|
|
if isDir && t.tag == file {
|
|
continue
|
|
}
|
|
baseOnly := !isDir && t.tag == file
|
|
if t.tag == dir && !isDir {
|
|
s, err = transformDir(s, t)
|
|
} else {
|
|
s, err = transformPath(s, t, baseOnly)
|
|
}
|
|
if err != nil {
|
|
fs.Error(s, err.Error()) // TODO: return err instead of logging it?
|
|
}
|
|
}
|
|
if old != s {
|
|
fs.Debugf(old, "transformed to: %v", s)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Transforming returns true when transforms are in use
|
|
func Transforming() bool {
|
|
return len(Opt.transforms) > 0
|
|
}
|
|
|
|
// transformPath transforms a path string according to the chosen TransformAlgo.
|
|
// Each path segment is transformed separately, to preserve path separators.
|
|
// If baseOnly is true, only the base will be transformed (useful for renaming while walking a dir tree recursively.)
|
|
// for example, "some/nested/path" -> "some/nested/CONVERTEDPATH"
|
|
// otherwise, the entire is path is transformed.
|
|
func transformPath(s string, t transform, baseOnly bool) (string, error) {
|
|
if s == "" || s == "/" || s == "\\" || s == "." {
|
|
return "", nil
|
|
}
|
|
|
|
if baseOnly {
|
|
transformedBase, err := transformPathSegment(path.Base(s), t)
|
|
if err := validateSegment(transformedBase); err != nil {
|
|
return "", err
|
|
}
|
|
return path.Join(path.Dir(s), transformedBase), err
|
|
}
|
|
|
|
segments := strings.Split(s, string(os.PathSeparator))
|
|
transformedSegments := make([]string, len(segments))
|
|
for _, seg := range segments {
|
|
convSeg, err := transformPathSegment(seg, t)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := validateSegment(convSeg); err != nil {
|
|
return "", err
|
|
}
|
|
transformedSegments = append(transformedSegments, convSeg)
|
|
}
|
|
return path.Join(transformedSegments...), nil
|
|
}
|
|
|
|
// transform all but the last path segment
|
|
func transformDir(s string, t transform) (string, error) {
|
|
dirPath, err := transformPath(path.Dir(s), t, false)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return path.Join(dirPath, path.Base(s)), nil
|
|
}
|
|
|
|
// transformPathSegment transforms one path segment (or really any string) according to the chosen TransformAlgo.
|
|
// It assumes path separators have already been trimmed.
|
|
func transformPathSegment(s string, t transform) (string, error) {
|
|
switch t.key {
|
|
case ConvNone:
|
|
return s, nil
|
|
case ConvToNFC:
|
|
return norm.NFC.String(s), nil
|
|
case ConvToNFD:
|
|
return norm.NFD.String(s), nil
|
|
case ConvToNFKC:
|
|
return norm.NFKC.String(s), nil
|
|
case ConvToNFKD:
|
|
return norm.NFKD.String(s), nil
|
|
case ConvBase64Encode:
|
|
return base64.URLEncoding.EncodeToString([]byte(s)), nil // URLEncoding to avoid slashes
|
|
case ConvBase64Decode:
|
|
if s == ".DS_Store" {
|
|
return s, nil
|
|
}
|
|
b, err := base64.URLEncoding.DecodeString(s)
|
|
if err != nil {
|
|
fs.Errorf(s, "base64 error")
|
|
}
|
|
return string(b), err
|
|
case ConvFindReplace:
|
|
split := strings.Split(t.value, ":")
|
|
if len(split) != 2 {
|
|
return s, fmt.Errorf("wrong number of values: %v", t.value)
|
|
}
|
|
return strings.ReplaceAll(s, split[0], split[1]), nil
|
|
case ConvPrefix:
|
|
return t.value + s, nil
|
|
case ConvSuffix:
|
|
return s + t.value, nil
|
|
case ConvSuffixKeepExtension:
|
|
return SuffixKeepExtension(s, t.value), nil
|
|
case ConvTrimPrefix:
|
|
return strings.TrimPrefix(s, t.value), nil
|
|
case ConvTrimSuffix:
|
|
return strings.TrimSuffix(s, t.value), nil
|
|
case ConvTruncate:
|
|
max, err := strconv.Atoi(t.value)
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
if max <= 0 {
|
|
return s, nil
|
|
}
|
|
if utf8.RuneCountInString(s) <= max {
|
|
return s, nil
|
|
}
|
|
runes := []rune(s)
|
|
return string(runes[:max]), nil
|
|
case ConvEncoder:
|
|
var enc encoder.MultiEncoder
|
|
err := enc.Set(t.value)
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
return enc.Encode(s), nil
|
|
case ConvDecoder:
|
|
var enc encoder.MultiEncoder
|
|
err := enc.Set(t.value)
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
return enc.Decode(s), nil
|
|
case ConvISO8859_1:
|
|
return encodeWithReplacement(s, charmap.ISO8859_1), nil
|
|
case ConvWindows1252:
|
|
return encodeWithReplacement(s, charmap.Windows1252), nil
|
|
case ConvMacintosh:
|
|
return encodeWithReplacement(s, charmap.Macintosh), nil
|
|
case ConvCharmap:
|
|
var cmapType fs.Enum[cmapChoices]
|
|
err := cmapType.Set(t.value)
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
c := charmapByID(cmapType)
|
|
return encodeWithReplacement(s, c), nil
|
|
case ConvLowercase:
|
|
return strings.ToLower(s), nil
|
|
case ConvUppercase:
|
|
return strings.ToUpper(s), nil
|
|
case ConvTitlecase:
|
|
return strings.ToTitle(s), nil
|
|
case ConvASCII:
|
|
return toASCII(s), nil
|
|
default:
|
|
return "", errors.New("this option is not yet implemented")
|
|
}
|
|
}
|
|
|
|
// SuffixKeepExtension adds a suffix while keeping extension
|
|
//
|
|
// i.e. file.txt becomes file_somesuffix.txt not file.txt_somesuffix
|
|
func SuffixKeepExtension(remote string, suffix string) string {
|
|
var (
|
|
base = remote
|
|
exts = ""
|
|
first = true
|
|
ext = path.Ext(remote)
|
|
)
|
|
for ext != "" {
|
|
// Look second and subsequent extensions in mime types.
|
|
// If they aren't found then don't keep it as an extension.
|
|
if !first && mime.TypeByExtension(ext) == "" {
|
|
break
|
|
}
|
|
base = base[:len(base)-len(ext)]
|
|
exts = ext + exts
|
|
first = false
|
|
ext = path.Ext(base)
|
|
}
|
|
return base + suffix + exts
|
|
}
|
|
|
|
// forbid transformations that add/remove path separators
|
|
func validateSegment(s string) error {
|
|
if s == "" {
|
|
return errors.New("transform cannot render path segments empty")
|
|
}
|
|
if strings.ContainsRune(s, '/') {
|
|
return fmt.Errorf("transform cannot add path separators: %v", s)
|
|
}
|
|
return nil
|
|
}
|