406 lines · 10.7 KB · Go
RawDownload
// Package transform holds functions for path name transformations////go:generate go run gen_help.go transform.mdpackage transform import (	"bytes"	"context"	_ "embed"	"encoding/base64"	"errors"	"fmt"	"mime"	"net/url"	"os/exec"	"path"	"regexp"	"strconv"	"strings"	"time"	"unicode/utf8" 	"github.com/rclone/rclone/fs"	"github.com/rclone/rclone/fs/fserrors"	"github.com/rclone/rclone/lib/encoder"	"golang.org/x/text/encoding/charmap"	"golang.org/x/text/unicode/norm") //go:embed transform.mdvar help string // Help returns the help string cleaned up to simplify appendingfunc Help() string {	// Chop off auto generated message	nl := strings.IndexRune(help, '\n')	return strings.TrimSpace(help[nl:]) + "\n\n"} // Path transforms a path s according to the --name-transform options in use//// If no transforms are in use, s is returned unchangedfunc Path(ctx context.Context, s string, isDir bool) string {	if !Transforming(ctx) {		return s	} 	old := s	opt, err := getOptions(ctx)	if err != nil {		err = fs.CountError(ctx, err)		fs.Errorf(s, "Failed to parse transform flags: %v", err)	}	for _, t := range opt {		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 {			err = fs.CountError(ctx, fserrors.NoRetryError(err))			fs.Errorf(s, "Failed to transform: %v", err)		}	}	if old != s {		fs.Debugf(old, "transformed to: %v", s)	}	if strings.Count(old, "/") != strings.Count(s, "/") {		err = fs.CountError(ctx, fserrors.NoRetryError(fmt.Errorf("number of path segments must match: %v (%v), %v (%v)", old, strings.Count(old, "/"), s, strings.Count(s, "/"))))		fs.Errorf(old, "%v", err)		return old	}	return s} // 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, "/")	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 segmentfunc 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		}		return truncateChars(s, max, false)	case ConvTruncateKeepExtension:		max, err := strconv.Atoi(t.value)		if err != nil {			return s, err		}		return truncateChars(s, max, true)	case ConvTruncateBytes:		max, err := strconv.Atoi(t.value)		if err != nil {			return s, err		}		return truncateBytes(s, max, false)	case ConvTruncateBytesKeepExtension:		max, err := strconv.Atoi(t.value)		if err != nil {			return s, err		}		return truncateBytes(s, max, true)	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 CharmapChoices		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	case ConvURL:		return url.QueryEscape(s), nil	case ConvDate:		return s + AppyTimeGlobs(t.value, time.Now()), nil	case ConvRegex:		split := strings.Split(t.value, "/")		if len(split) != 2 {			return s, fmt.Errorf("regex syntax error: %v", t.value)		}		re := regexp.MustCompile(split[0])		return re.ReplaceAllString(s, split[1]), nil	case ConvCommand:		return mapper(s, t.value)	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_somesuffixfunc SuffixKeepExtension(remote string, suffix string) string {	base, exts := splitExtension(remote)	return base + suffix + exts} func splitExtension(remote string) (base, exts string) {	base = remote	var (		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, exts} func truncateChars(s string, max int, keepExtension bool) (string, error) {	if max <= 0 {		return s, nil	}	if utf8.RuneCountInString(s) <= max {		return s, nil	}	exts := ""	if keepExtension {		s, exts = splitExtension(s)	}	keep := max - utf8.RuneCountInString(exts)	if keep <= 0 {		return "", errors.New("extension is longer than the truncation limit")	}	runes := []rune(s)	return string(runes[:keep]) + exts, nil} // truncateBytes is like truncateChars but counts the number of bytes, not UTF-8 charactersfunc truncateBytes(s string, max int, keepExtension bool) (string, error) {	if max <= 0 {		return s, nil	}	if len(s) <= max {		return s, nil	}	exts := ""	if keepExtension {		s, exts = splitExtension(s)	} 	// ensure we don't split a multi-byte UTF-8 character	for i := max - len(exts); i > 0; i-- {		b := append([]byte(s)[:i], exts...)		if len(b) <= max && utf8.Valid(b) {			return string(b), nil		}	}	return "", errors.New("could not truncate to valid UTF-8")} // forbid transformations that add/remove path separatorsfunc validateSegment(s string) error {	if strings.TrimSpace(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} // ParseGlobs determines whether a string contains {brackets}// and returns the substring (including both brackets) for replacing// substring is first opening bracket to last closing bracket --// good for {{this}} but not {this}{this}func ParseGlobs(s string) (hasGlobs bool, substring string) {	open := strings.Index(s, "{")	close := strings.LastIndex(s, "}")	if open >= 0 && close > open {		return true, s[open : close+1]	}	return false, ""} // TrimBrackets converts {{this}} to thisfunc TrimBrackets(s string) string {	return strings.Trim(s, "{}")} // TimeFormat converts a user-supplied string to a Go time constant, if possiblefunc TimeFormat(timeFormat string) string {	switch timeFormat {	case "Layout":		timeFormat = time.Layout	case "ANSIC":		timeFormat = time.ANSIC	case "UnixDate":		timeFormat = time.UnixDate	case "RubyDate":		timeFormat = time.RubyDate	case "RFC822":		timeFormat = time.RFC822	case "RFC822Z":		timeFormat = time.RFC822Z	case "RFC850":		timeFormat = time.RFC850	case "RFC1123":		timeFormat = time.RFC1123	case "RFC1123Z":		timeFormat = time.RFC1123Z	case "RFC3339":		timeFormat = time.RFC3339	case "RFC3339Nano":		timeFormat = time.RFC3339Nano	case "Kitchen":		timeFormat = time.Kitchen	case "Stamp":		timeFormat = time.Stamp	case "StampMilli":		timeFormat = time.StampMilli	case "StampMicro":		timeFormat = time.StampMicro	case "StampNano":		timeFormat = time.StampNano	case "DateTime":		timeFormat = time.DateTime	case "DateOnly":		timeFormat = time.DateOnly	case "TimeOnly":		timeFormat = time.TimeOnly	case "MacFriendlyTime", "macfriendlytime", "mac":		timeFormat = "2006-01-02 0304PM" // not actually a Go constant -- but useful as macOS filenames can't have colons	case "YYYYMMDD":		timeFormat = "20060102"	}	return timeFormat} // AppyTimeGlobs converts "myfile-{DateOnly}.txt" to "myfile-2006-01-02.txt"func AppyTimeGlobs(s string, t time.Time) string {	hasGlobs, substring := ParseGlobs(s)	if !hasGlobs {		return s	}	timeString := t.Local().Format(TimeFormat(TrimBrackets(substring)))	return strings.ReplaceAll(s, substring, timeString)} func mapper(s string, command string) (string, error) {	out, err := exec.Command(command, s).CombinedOutput()	if err != nil {		out = bytes.TrimSpace(out)		return s, fmt.Errorf("%s: error running command %q: %v", out, command+" "+s, err)	}	return string(bytes.TrimSpace(out)), nil}