Featured image of post Rclone、rsync、Docker 的 COPY/ADD:加不加「/」的含义大不相同!

Rclone、rsync、Docker 的 COPY/ADD:加不加「/」的含义大不相同!

详细解析了在使用 Rclone、rsync 和 Docker COPY/ADD命令时,路径末尾是否添加斜杠(`/`)对文件复制和同步行为的影响。通过多个示例,文章展示了不同工具在处理文件夹到文件夹、文件到文件夹、文件到文件等场景时的差异。总结要点包括:Rclone 对斜杠不敏感,rsync 斜杠影响源路径行为,而 Docker COPY/ADD 斜杠决定目标路径是文件还是文件夹。

日常中可能经常会使用到与同步或文件复制有关的命令,针对不同的场合分为很多种情况,比如复制文件到文件夹、复制文件夹到文件夹、复制文件夹里的内容到文件夹等。如果是文件夹的话可能需要考虑需不需要加斜杠,例如名为 dest 的文件夹是写成dest/还是dest好。而实际上不同的工具或软件对于加不加斜杠的处理大有不同。

以如下的场景为例:

1
2
3
4
5
6
.
├── dest_folder
│   └── dest_content.txt
└── src_folder
    ├── src_content1.txt
    └── src_content2.txt

注意下文的每一次运行命令,默认都是在如上的文件结构上运行的。

对于如同command /path/a /path/b的命令来说,我们把/path/a称作前一个路径,/path/b称为后一个路径。

Rclone 的场合

Rclone 应该来说各种场合是一致性最强的了。一句话总结就是:斜杠无关紧要。第一个路径可以是文件或文件夹,后一个路径只能是文件夹。

文件夹->文件夹

前后两个目录都是文件夹时,不管有没有斜杠,Rclone 的语义始终是「把前面那个文件夹底下的所有文件都复制到后一个文件夹底下」。

运行命令:

1
rclone copy src_folder dest_folder

结果:

1
2
3
4
5
6
7
8
.
├── dest_folder
│   ├── dest_content.txt
│   ├── src_content1.txt
│   └── src_content2.txt
└── src_folder
    ├── src_content1.txt
    └── src_content2.txt

可以看到把 src_folder 里的文件都复制到 dest_folder 下面了,而非形成类似 dest_folder/src_folder 的结构。

文件->文件夹

运行命令:

1
rclone copy src_folder/src_content1.txt dest_folder/a.txt

结果:

1
2
3
4
5
6
7
8
.
├── dest_folder
│   ├── a.txt
│   │   └── src_content1.txt
│   └── dest_content.txt
└── src_folder
    ├── src_content1.txt
    └── src_content2.txt

可以看到并不会把 src_content1.txt 复制到 dest_folder 底下改名成 a.txt,而是把后一个路径始终看成是文件夹,因为不存在而新建了一个名为 a.txt 的文件夹。

文件->文件

想使用 Rclone 把一个文件复制到另一个文件夹底下并改名,这点是做不到的。原因上面已经说了,Rclone 后一个路径只能是文件夹。

rsync 的场合

rsync 的斜杠之和前一个路径参数(原)有关,后一个路径参数(目的地)加不加斜杆都无所谓。

文件夹->文件夹

前一个路径不加斜杠

运行命令:

1
rsync -av src_folder dest_folder

结果:

1
2
3
4
5
6
7
8
9
.
├── dest_folder
│   ├── dest_content.txt
│   └── src_folder
│       ├── src_content1.txt
│       └── src_content2.txt
└── src_folder
    ├── src_content1.txt
    └── src_content2.txt

可以看到前一个路径没加斜杠,就把整个 src_folder 连文件夹带文件都拷贝到 dest_folder 底下了。

运行命令:

1
rsync -av src_folder dest_folder/new_folder

结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
.
├── dest_folder
│   ├── dest_content.txt
│   └── new_folder
│       └── src_folder
│           ├── src_content1.txt
│           └── src_content2.txt
└── src_folder
    ├── src_content1.txt
    └── src_content2.txt

还是连文件夹带文件拷贝,不过会自动新建后一个路径不存在的 new_folder 文件夹。

前一个路径加斜杠

运行命令:

1
rsync -av src_folder/ dest_folder

结果:

1
2
3
4
5
6
7
8
.
├── dest_folder
│   ├── dest_content.txt
│   ├── src_content1.txt
│   └── src_content2.txt
└── src_folder
    ├── src_content1.txt
    └── src_content2.txt

前一个路径是文件夹且后面加斜杠,意思就是把该文件夹下的所有文件拷贝到后一个目录下,不带文件夹本身。

运行命令:

1
rsync -av src_folder/ dest_folder/new_folder

结果:

1
2
3
4
5
6
7
8
9
.
├── dest_folder
│   ├── dest_content.txt
│   └── new_folder
│       ├── src_content1.txt
│       └── src_content2.txt
└── src_folder
    ├── src_content1.txt
    └── src_content2.txt

这次是拷贝 src_folder 下的所有文件,后一个路径中的 new_folder 由于不存在所以会自动新建。

文件->文件夹 / 文件->文件

如果前一个路径是文件的话,这时候对于后一个路径来说要分几种情况。

为了方便说明我们把诸如 aa/bb/cc 的目录看作以/隔开的 seg,其中 aa 是一个 seg、bb 是一个 seg、cc 是一个 seg。因此对于这个路径来说,cc 就是它的最后一个 seg。下面分几种情况:

  • 后一个路径的最后一个 seg 不存在:把最后一个 seg 作为文件名,用前一个路径的文件内容写进来

  • 后一个路径的最后一个 seg 存在:

    • 后一个路径是文件夹:把前一个路径的文件拷贝到后一个路径的文件夹下
    • 后一个路径是文件:用前一个路径的文件覆盖后一个路径的文件
  • 后一个路径的超过一个 seg 不存在:复制失败(并不会进行mkdir -p类似的操作)

后一个路径的最后一个 seg 不存在

运行命令:

1
rsync -av src_folder/src_content1.txt dest_folder/aaa.txt

结果:

1
2
3
4
5
6
7
.
├── dest_folder
│   ├── aaa.txt
│   └── dest_content.txt
└── src_folder
    ├── src_content1.txt
    └── src_content2.txt

src _content1.txt 的内容被写到了 aaa.txt 中。

后一个路径的最后一个 seg 存在

后一个路径是文件夹

运行命令:

1
rsync -av src_folder/src_content1.txt dest_folder

结果:

1
2
3
4
5
6
7
.
├── dest_folder
│   ├── dest_content.txt
│   └── src_content1.txt
└── src_folder
    ├── src_content1.txt
    └── src_content2.txt

src_content.txt 文件被复制到了 dest_folder 中。

后一个路径是文件

运行命令:

1
2
echo src_content1_content > src_folder/src_content1.txt
rsync -av src_folder/src_content1.txt dest_folder/dest_content.txt

结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
root@dev ~/test# tree
.
├── dest_folder
│   └── dest_content.txt
└── src_folder
    ├── src_content1.txt
    └── src_content2.txt

2 directories, 3 files

root@dev ~/test# cat dest_folder/dest_content.txt 
src_content1_content

可以看到 dest_content.txt 的文件内容被 src_content1.txt 的内容覆盖。

后一个路径的超过一个 seg 不存在

如下所示,会报错:

1
2
3
4
root@dev ~/test# rsync -av src_folder/src_content1.txt dest_folder/aa/bb
sending incremental file list
rsync: [Receiver] change_dir#3 "/root/test/dest_folder/aa" failed: No such file or directory (2)
rsync error: errors selecting input/output files, dirs (code 3) at main.c(829) [Receiver=3.2.7]

Docker 的场合

Docker 的情况是,使用 COPY 或 ADD 命令时,前一个路径只看是文件还是文件夹,是否有斜杠无关紧要。后一个路径可以是文件或文件夹,通过是否有斜杠来判断。

如果 Docker 中后一个路径的文件夹不存在,不管有几层都会自动新建。

我们这次用以下的目录结构做测试:

1
2
3
4
5
6
.
└── src_folder
    ├── src_content1.txt
    ├── src_content2.txt
    └── sub_folder
        └── new.txt

文件夹->文件夹

运行命令:

1
COPY src_folder /test/dest_folder

结果:

1
2
3
4
5
6
/test
`-- dest_folder
    |-- src_content1.txt
    |-- src_content2.txt
    `-- sub_folder
        `-- new.txt

不管前面加不加斜杠,都是把 src_folder 目录下的所有内容复制到/test/dest_folder 底下去,不包括 src_folder 本身。那如果一定要保留 src_folder 怎么办?只能自己在后面加一个文件夹名了,比如:

1
COPY src_folder /test/dest_folder/src_folder

文件->文件夹 / 文件->文件

如果前一个目录是文件,需要分两种情况来讨论。

后一个路径加斜杠(文件->文件夹)

运行命令:

1
COPY src_folder/src_content1.txt /test/dest_folder/

结果:

1
2
3
/test
`-- dest_folder
    `-- src_content1.txt

可以看到如果后一个路径加了斜杆的话,会把它当做文件夹看待,而把前一个路径的文件复制到指定的文件夹下。

后一个路径不加斜杠(文件->文件)

运行命令:

1
COPY src_folder/src_content1.txt /test/dest_folder

结果:

1
2
/test
`-- dest_folder

这里出现了非预期行为。实际上由于后一个路径没有加斜杠,Docker 把后一个路径当做了文件,而把 src_content1.txt 的内容拷贝了过来。这里 tree 命令显示的 dest_folder 其实是一个文件,不是文件夹,其文件内容是 src_content1.txt 的内容。

这种情况其实还能同时复制多个文件,比如官方文档中的例子:

1
COPY file1.txt file2.txt /usr/src/things/

会把这两个文件复制到 things 文件夹下。同时,前一个路径是通配符也是可以的,比如file*.txt

参考:https://docs.docker.com/reference/dockerfile/#copy

Licensed under CC BY-NC-SA 4.0