Published on

rclone 同步文件到 SFTP “InvalidUTF8” 问题 Troubleshooting

Authors
  • avatar
    Name
    Wang Zhiwei
    Twitter

问题初步分析

rclone 版本 v1.64.2

同步命令:

rclone -vv copy s3:/bucket/sftp_sync_test sftp:/sftp_sync_test

同步任务详细日志:

2024/03/15 13:10:33 DEBUG : test_2023225/(印章文件)广东省器官医学与技术学会消化道肿瘤规范化诊疗-首届中青年专家交流会会议通知(0).pdf: Need to transfer - File not found at Destination
2024/03/15 13:10:33 DEBUG : sftp://admin@azuressh2_sftp:22//sftp_sync_test: Waiting for checks to finish
2024/03/15 13:10:33 ERROR : test_2023225/(印章文件)广东省器官医学与技术学会消化道肿瘤规范化诊疗-首届中青年专家交流会会议通知(0).pdf: Failed to copy: Update Create failed: sftp: "InvalidUTF8: The input supplied is invalid UTF-8." (SSH_FX_BAD_MESSAGE)
2024/03/15 13:10:33 DEBUG : test_20240315/(印章文件)广东省器官医学与技术学会消化道肿瘤规范化诊疗-首届中青年专家交流会会议通知(0).pdf: Size and modification time the same (differ by 0s, within tolerance 1s)
2024/03/15 13:10:33 DEBUG : test_20240315/(印章文件)广东省器官医学与技术学会消化道肿瘤规范化诊疗-首届中青年专家交流会会议通知(0).pdf: Unchanged skipping
2024/03/15 13:10:33 DEBUG : sftp://admin@azuressh2_sftp:22//sftp_sync_test: Waiting for transfers to finish
2024/03/15 13:10:33 INFO  : test_2023225/(印章文件)广东省器官医学与技术学会消化道肿瘤规范化诊疗-首届中青年专家交流会�.pobafay0.partial: Failed to remove failed partial copy: stat failed: sftp: "InvalidUTF8: The input supplied is invalid UTF-8." (SSH_FX_BAD_MESSAGE)
2024/03/15 13:10:33 ERROR : Attempt 3/3 failed with 1 errors and: Update Create failed: sftp: "InvalidUTF8: The input supplied is invalid UTF-8." (SSH_FX_BAD_MESSAGE)

同步失败文件:

test_2023225/(印章文件)广东省器官医学与技术学会消化道肿瘤规范化诊疗-首届中青年专家交流会会议通知(0).pdf

在 Python 中对文件名进行编码测试,确认是否为非法 utf8 字符串。

In [1]: file = '(印章文件)广东省器官医学与技术学会消化道肿瘤规范化诊疗-首届中青年专家交流会会议通知(0).pdf'

In [2]: file.encode('utf8')
Out[2]: b'\xef\xbc\x88\xe5\x8d\xb0\xe7\xab\xa0\xe6\x96\x87\xe4\xbb\xb6\xef\xbc\x89\xe5\xb9\xbf\xe4\xb8\x9c\xe7\x9c\x81\xe5\x99\xa8\xe5\xae\x98\xe5\x8c\xbb\xe5\xad\xa6\xe4\xb8\x8e\xe6\x8a\x80\xe6\x9c\xaf\xe5\xad\xa6\xe4\xbc\x9a\xe6\xb6\x88\xe5\x8c\x96\xe9\x81\x93\xe8\x82\xbf\xe7\x98\xa4\xe8\xa7\x84\xe8\x8c\x83\xe5\x8c\x96\xe8\xaf\x8a\xe7\x96\x97-\xe9\xa6\x96\xe5\xb1\x8a\xe4\xb8\xad\xe9\x9d\x92\xe5\xb9\xb4\xe4\xb8\x93\xe5\xae\xb6\xe4\xba\xa4\xe6\xb5\x81\xe4\xbc\x9a\xe4\xbc\x9a\xe8\xae\xae\xe9\x80\x9a\xe7\x9f\xa5(0).pdf'

测试结果看起来没有问题,文件名是合法的 utf8。


仔细观察日志,发现这样一行:

2024/03/15 13:10:33 INFO  : test_2023225/(印章文件)广东省器官医学与技术学会消化道肿瘤规范化诊疗-首届中青年专家交流会�.pobafay0.partial: Failed to remove failed partial copy: stat failed: sftp: "InvalidUTF8: The input supplied is invalid UTF-8." (SSH_FX_BAD_MESSAGE)

可以看到这里在操作一个奇怪的文件 “印章文件)广东省器官医学与技术学会消化道肿瘤规范化诊疗-首届中青年专家交流会�.pobafay0.partial”,我们来源文件里并没有这样一个文件,对比原始文件名,像是被重命名了,不是很理解这里的逻辑。


到 rclone 源码中去查找原因

下载 rclone 源码

git clone https://github.com/rclone/rclone.git

在源码里搜索 .partial ,看到在 fs/operations/operations.go 代码中有相关逻辑引用到。

Untitled.png


跳转到代码实现部分,看起来在这里对文件名进行了截断并重命名,这里逻辑比较粗暴,直接取前 100 位字节,但是 utf8 编码中,一个汉字占用 3 个字节,很明显此处有些情况下很容易把合法 utf8 字符串结成非法的。

var (
		inplace       = true
		remotePartial = remote
	)
	if !ci.Inplace && f.Features().Move != nil && f.Features().PartialUploads && !strings.HasSuffix(remote, ".rclonelink") {
		// Avoid making the leaf name longer if it's already lengthy to avoid
		// trouble with file name length limits.
		suffix := "." + random.String(8) + ".partial"
		base := path.Base(remotePartial)
		if len(base) > 100 {
			remotePartial = remotePartial[:len(remotePartial)-len(suffix)] + suffix
		} else {
			remotePartial += suffix
		}
		inplace = false
	}

在后续代码中上传文件的操作也都是使用了定义在变量 remotePartial 中重新命名的文件名。


同样可以看到下面代码实现

// Used to remove a failed partial copy
//
// Returns whether the file was successfully removed or not
func removeFailedPartialCopy(ctx context.Context, f fs.Fs, remotePartial string) bool {
	o, err := f.NewObject(ctx, remotePartial)
	if errors.Is(err, fs.ErrorObjectNotFound) {
		return true
	} else if err != nil {
		fs.Infof(remotePartial, "Failed to remove failed partial copy: %s", err)
		return false
	}
	return removeFailedCopy(ctx, o)
}

对应日志记录

2024/03/15 13:10:33 INFO  : test_2023225/(印章文件)广东省器官医学与技术学会消化道肿瘤规范化诊疗-首届中青年专家交流会�.pobafay0.partial: Failed to remove failed partial copy: stat failed: sftp: "InvalidUTF8: The input supplied is invalid UTF-8." (SSH_FX_BAD_MESSAGE)

在 rclone 最新 v1.66.x 版本的代码中,这部分逻辑进行了重构,增加了一个配置参数 PartialSuffix ,默认值是 .partial,重命名文件名的代码实现放在了代码文件 fs/operations/copy.go 中。

// TruncateString s to n bytes.
//
// If s is valid UTF-8 then this may truncate to fewer than n bytes to
// make the returned string also valid UTF-8.
func TruncateString(s string, n int) string {
	truncated := s[:n]
	if !utf8.ValidString(s) {
		// If input string wasn't valid UTF-8 then just return the truncation
		return truncated
	}
	for len(truncated) > 0 {
		if utf8.ValidString(truncated) {
			return truncated
		}
		// Remove 1 byte until valid
		truncated = truncated[:len(truncated)-1]
	}
	return truncated
}

// Check to see if we should be using a partial name and return the name for the copy and the inplace flag
func (c *copy) checkPartial() (remoteForCopy string, inplace bool, err error) {
	remoteForCopy = c.remote
	if c.ci.Inplace || c.dstFeatures.Move == nil || !c.dstFeatures.PartialUploads || strings.HasSuffix(c.remote, ".rclonelink") {
		return remoteForCopy, true, nil
	}
	if len(c.ci.PartialSuffix) > 16 {
		return remoteForCopy, true, fmt.Errorf("expecting length of --partial-suffix to be not greater than %d but got %d", 16, len(c.ci.PartialSuffix))
	}
	// Avoid making the leaf name longer if it's already lengthy to avoid
	// trouble with file name length limits.
	suffix := "." + random.String(8) + c.ci.PartialSuffix
	base := path.Base(c.remoteForCopy)
	if len(base) > 100 {
		remoteForCopy = TruncateString(c.remoteForCopy, len(c.remoteForCopy)-len(suffix)) + suffix
	} else {
		remoteForCopy += suffix
	}
	return remoteForCopy, false, nil
}

可以看到在原先 v1.64.2 版本中粗暴的截断逻辑,增加了检测算法,以保证截断的字符同样是合法的 utf8 字符串。

不过,此处的实现也有一些问题,在判断文件名长度用来决定是否 truncate 时,检测的对象是 c.remoteForCopycopy 对象的 remoteForCopy 属性初始化时是空字符串,后续也没有赋值的逻辑,显然永远在检测一个空字符的长度,不符合逻辑,也许应该是检测 c.remote。在目前截止的 1.66.x 最新版本中,这里的代码逻辑依旧如此。

我到 rclone 论坛发帖咨询,从项目作者那得到了确定,这是一个 bug。

https://forum.rclone.org/t/why-use-c-remoteforcopy-instead-of-c-remote-to-check-length-in-copy-operation/45099


最后

显然只要将 rclone 版本从 v1.64.2 升级到 v.1.66.x 版本就可以避免同步时的 InvalidUTF8 报错。