mirror of
https://github.com/rclone/rclone.git
synced 2025-08-17 09:01:33 +02:00
lib/transform: add transform library and --name-transform flag
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
This commit is contained in:
@ -39,6 +39,7 @@ import (
|
||||
"github.com/rclone/rclone/lib/pacer"
|
||||
"github.com/rclone/rclone/lib/random"
|
||||
"github.com/rclone/rclone/lib/readers"
|
||||
"github.com/rclone/rclone/lib/transform"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
@ -424,6 +425,8 @@ func MoveTransfer(ctx context.Context, fdst fs.Fs, dst fs.Object, remote string,
|
||||
|
||||
// move - see Move for help
|
||||
func move(ctx context.Context, fdst fs.Fs, dst fs.Object, remote string, src fs.Object, isTransfer bool) (newDst fs.Object, err error) {
|
||||
origRemote := remote // avoid double-transform on fallback to copy
|
||||
remote = transform.Path(ctx, remote, false)
|
||||
ci := fs.GetConfig(ctx)
|
||||
var tr *accounting.Transfer
|
||||
if isTransfer {
|
||||
@ -447,12 +450,14 @@ func move(ctx context.Context, fdst fs.Fs, dst fs.Object, remote string, src fs.
|
||||
if doMove := fdst.Features().Move; doMove != nil && (SameConfig(src.Fs(), fdst) || (SameRemoteType(src.Fs(), fdst) && (fdst.Features().ServerSideAcrossConfigs || ci.ServerSideAcrossConfigs))) {
|
||||
// Delete destination if it exists and is not the same file as src (could be same file while seemingly different if the remote is case insensitive)
|
||||
if dst != nil {
|
||||
remote = dst.Remote()
|
||||
remote = transform.Path(ctx, dst.Remote(), false)
|
||||
if !SameObject(src, dst) {
|
||||
err = DeleteFile(ctx, dst)
|
||||
if err != nil {
|
||||
return newDst, err
|
||||
}
|
||||
} else if src.Remote() == remote {
|
||||
return newDst, nil
|
||||
} else if needsMoveCaseInsensitive(fdst, fdst, remote, src.Remote(), false) {
|
||||
doMove = func(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
|
||||
return MoveCaseInsensitive(ctx, fdst, fdst, remote, src.Remote(), false, src)
|
||||
@ -488,7 +493,7 @@ func move(ctx context.Context, fdst fs.Fs, dst fs.Object, remote string, src fs.
|
||||
}
|
||||
}
|
||||
// Move not found or didn't work so copy dst <- src
|
||||
newDst, err = Copy(ctx, fdst, dst, remote, src)
|
||||
newDst, err = Copy(ctx, fdst, dst, origRemote, src)
|
||||
if err != nil {
|
||||
fs.Errorf(src, "Not deleting source as copy failed: %v", err)
|
||||
return newDst, err
|
||||
@ -516,24 +521,7 @@ func SuffixName(ctx context.Context, remote string) string {
|
||||
return remote
|
||||
}
|
||||
if ci.SuffixKeepExtension {
|
||||
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 + ci.Suffix + exts
|
||||
return transform.SuffixKeepExtension(remote, ci.Suffix)
|
||||
}
|
||||
return remote + ci.Suffix
|
||||
}
|
||||
@ -1994,12 +1982,12 @@ func MoveCaseInsensitive(ctx context.Context, fdst fs.Fs, fsrc fs.Fs, dstFileNam
|
||||
}
|
||||
|
||||
// moveOrCopyFile moves or copies a single file possibly to a new name
|
||||
func moveOrCopyFile(ctx context.Context, fdst fs.Fs, fsrc fs.Fs, dstFileName string, srcFileName string, cp bool) (err error) {
|
||||
func moveOrCopyFile(ctx context.Context, fdst fs.Fs, fsrc fs.Fs, dstFileName string, srcFileName string, cp bool, allowOverlap bool) (err error) {
|
||||
ci := fs.GetConfig(ctx)
|
||||
logger, usingLogger := GetLogger(ctx)
|
||||
dstFilePath := path.Join(fdst.Root(), dstFileName)
|
||||
srcFilePath := path.Join(fsrc.Root(), srcFileName)
|
||||
if fdst.Name() == fsrc.Name() && dstFilePath == srcFilePath {
|
||||
if fdst.Name() == fsrc.Name() && dstFilePath == srcFilePath && !allowOverlap {
|
||||
fs.Debugf(fdst, "don't need to copy/move %s, it is already at target location", dstFileName)
|
||||
if usingLogger {
|
||||
srcObj, _ := fsrc.NewObject(ctx, srcFileName)
|
||||
@ -2106,7 +2094,14 @@ func moveOrCopyFile(ctx context.Context, fdst fs.Fs, fsrc fs.Fs, dstFileName str
|
||||
//
|
||||
// This is treated as a transfer.
|
||||
func MoveFile(ctx context.Context, fdst fs.Fs, fsrc fs.Fs, dstFileName string, srcFileName string) (err error) {
|
||||
return moveOrCopyFile(ctx, fdst, fsrc, dstFileName, srcFileName, false)
|
||||
return moveOrCopyFile(ctx, fdst, fsrc, dstFileName, srcFileName, false, false)
|
||||
}
|
||||
|
||||
// TransformFile transforms a file in place using --name-transform
|
||||
//
|
||||
// This is treated as a transfer.
|
||||
func TransformFile(ctx context.Context, fdst fs.Fs, srcFileName string) (err error) {
|
||||
return moveOrCopyFile(ctx, fdst, fdst, srcFileName, srcFileName, false, true)
|
||||
}
|
||||
|
||||
// SetTier changes tier of object in remote
|
||||
@ -2211,50 +2206,10 @@ func (l *ListFormat) SetOutput(output []func(entry *ListJSONItem) string) {
|
||||
|
||||
// AddModTime adds file's Mod Time to output
|
||||
func (l *ListFormat) AddModTime(timeFormat string) {
|
||||
switch timeFormat {
|
||||
case "":
|
||||
if timeFormat == "" {
|
||||
timeFormat = "2006-01-02 15:04:05"
|
||||
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 // missing in go1.19
|
||||
timeFormat = "2006-01-02 15:04:05"
|
||||
case "DateOnly":
|
||||
// timeFormat = time.DateOnly // missing in go1.19
|
||||
timeFormat = "2006-01-02"
|
||||
case "TimeOnly":
|
||||
// timeFormat = time.TimeOnly // missing in go1.19
|
||||
timeFormat = "15:04:05"
|
||||
} else {
|
||||
timeFormat = transform.TimeFormat(timeFormat)
|
||||
}
|
||||
l.AppendOutput(func(entry *ListJSONItem) string {
|
||||
return entry.ModTime.When.Local().Format(timeFormat)
|
||||
|
Reference in New Issue
Block a user