Pythonには、ファイル操作のための標準ライブラリ shutil
が用意されていますが、そのドキュメントの冒頭には物々しい警告が書かれています。
警告 高水準のファイルコピー関数 (shutil.copy(), shutil.copy2()) でも、ファイルのメタデータの全てをコピーすることはできません。
POSIXプラットフォームでは、これはACLやファイルのオーナー、グループが失われることを意味しています。 Mac OSでは、リソースフォーク(resource fork)やその他のメタデータが利用されません。これは、リソースが失われ、ファイルタイプや生成者コード(creator code)が正しくなくなることを意味しています。 Windowsでは、ファイルオーナー、ACL、代替データストリームがコピーされません。
しかし、赤字で警告しているわりに**「ではどうすればいいか」が書かれていない**ため、読む側としては困惑するばかりです。Python 2.6の頃からこうのようです。なんだかな〜。
そこで、具体的にはどんなワークアラウンドをとればいいのかを調べてみました。
TL;DR
-
shutil.copy
,shutil.copy2
では、オーナーや日時がコピーされない - オーナーは
shutil.chown
でコピーすればOK - 最終手段
subprocess.run(['cp', '-p', src, dst])
で全てをコピーできる(でも、そんな必要ある?)
どんなメタデータがある?
OS独自・ファイルシステム独自のメタデータを調べだすとキリがなさそうなので、今回は stat
コマンドで調べられるものだけ考えます。
Dockerで環境を作り stat
コマンドを実行してみます。
$ docker run --rm --workdir=/root -it python /bin/bash
root@3d7f163bbb17:~# groupadd foo
root@3d7f163bbb17:~# useradd bar -G foo
root@3d7f163bbb17:~# echo hello > src
root@3d7f163bbb17:~# chown bar:foo src
root@3d7f163bbb17:~# chmod 777 src
root@3d7f163bbb17:~# stat src
File: src
Size: 6 Blocks: 8 IO Block: 4096 regular file
Device: 68h/104d Inode: 119 Links: 1
Access: (0777/-rwxrwxrwx) Uid: ( 1000/ bar) Gid: ( 1000/ foo)
Access: 2018-10-30 03:14:01.198071691 +0000
Modify: 2018-10-30 03:14:01.198071691 +0000
Change: 2018-10-30 03:15:41.571717800 +0000
Birth: -
いわゆるメタデータとしては以下の属性があるようです。
- パーミッション(Access)
- ユーザー(Uid)
- 最終アクセス日時(Access)
- 最終修正日時(Modify)
- 最終状態変更日時(Change)
Blocks
などはファイルの格納状況についての指標で、コピーの対象ではないので無視します。作成日時(Birth)は、今回は取得できていないので無視します。
shutil.copy
はどこまではコピーしてくれるのか
ドキュメントの説明によると、コピー内容は以下のようです。
-
shutil.copy
: ファイル本体 + パーミッション -
shutil.copy2
: ファイル本体 + stat(パーミッション、最終アクセス時間、最終変更時間、その他)
実際に実行してみると、以下のことが分かります:
- ユーザー(Uid)が、コピーをしたユーザー(root)になっている
- グループ(Gid)が、コピーをしたユーザー(root)になっている
- 最終アクセス日時(Access)が現在時刻になっている
- 最終状態変更日時(Change)が現在時刻になっている
また、shutil.copy
では最終修正日時(Modify)も現在日時になっています。
root@3d7f163bbb17:~# python -c 'import shutil; shutil.copy("src", "dst1"); shutil.copy2("src", "dst2")'
root@3d7f163bbb17:~# stat dst1 dst2
File: dst1
Size: 6 Blocks: 8 IO Block: 4096 regular file
Device: 68h/104d Inode: 597 Links: 1
Access: (0777/-rwxrwxrwx) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2018-10-30 03:18:51.788627495 +0000
Modify: 2018-10-30 03:18:51.788627495 +0000
Change: 2018-10-30 03:18:51.788627495 +0000
Birth: -
File: dst2
Size: 6 Blocks: 8 IO Block: 4096 regular file
Device: 68h/104d Inode: 598 Links: 1
Access: (0777/-rwxrwxrwx) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2018-10-30 03:18:51.788627495 +0000
Modify: 2018-10-30 03:14:01.198071691 +0000
Change: 2018-10-30 03:18:51.788627495 +0000
Birth: -
cp
コマンドの場合は?
cp
コマンドでコピーしてみると、ユーザー・グループ・各種日時が変わっています。また、Linux(Posix)では、パーミッションが umask でマスクされるので、これも変更されます(0o777 & 0o022 == 0o755
)。
root@3d7f163bbb17:~# cp src dst-cp1
root@3d7f163bbb17:~# stat dst-cp1
File: dst-cp1
Size: 6 Blocks: 8 IO Block: 4096 regular file
Device: 68h/104d Inode: 604 Links: 1
Access: (0755/-rwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2018-10-30 03:24:16.760432208 +0000
Modify: 2018-10-30 03:24:16.760432208 +0000
Change: 2018-10-30 03:24:16.760432208 +0000
Birth: -
root@3d7f163bbb17:~# umask
0022
一方、-p
オプションをつけると、メタデータもコピーしてくれます。ユーザー、グループ、パーミッション、最終アクセス日時、最終変更日時がコピーされているようです1。
root@3d7f163bbb17:~# cp -p src dst-cp2
root@3d7f163bbb17:~# stat dst-cp2
File: dst-cp2
Size: 6 Blocks: 8 IO Block: 4096 regular file
Device: 68h/104d Inode: 605 Links: 1
Access: (0777/-rwxrwxrwx) Uid: ( 1000/ bar) Gid: ( 1000/ foo)
Access: 2018-10-30 03:18:51.788627495 +0000
Modify: 2018-10-30 03:14:01.198071691 +0000
Change: 2018-10-30 03:28:58.270658164 +0000
Birth: -
結局、どうすればいいのか?
調べた結果から以下のことが分かります。
-
shutil.copy
とshutil.copy2
ユーザー・オーナーをコピーしない -
shutil.copy
とshutil.copy2
は各種日時もコピーしない -
shutil.copy
とshutil.copy2
はcp
ともcp -p
と振る舞いが異なる
そのため、例えばシェルスクリプトを、Pythonで書き直す時、単純に cp
を shutil.copy
に置換すると振る舞いが変わってしまいます。
cp -p
を完全再現する(日時までもコピーしたい)
osやshutilを見る限り、最終アクセス日時や最終変更日時をコピーする関数はありません。cp -p
を直接使うしかなさそうです。
import subprocess
subprocess.run(['cp', '-p', 'src', 'dst-workaround-1'])
実行して見ると(当たり前ですが)日時もコピーできています。
しかし、ディスクバックアップツールなどの特殊なケース以外は最終変更日時や最終アクセス日時をコピーする必要はないと思います。
ユーザーやグループをコピーする
shutil.chownを使います。ユーザー・グループは文字列ででもIDででも指定できます。
また、元ファイルのユーザー・グループは os.stat
で取得できます。
import shutil, os
shutil.copy2('src', 'dst-workaround2')
# ユーザー/グループがあらかじめ分かっている場合。
shutil.chown('dst-workaround2', 'bar', 'foo')
# ユーザー/グループを、元ファイルからコピーする場合
st = os.stat('src')
shutil.chown('dst-workaround2', st.st_uid, st.st_gid)
パーミッションを変更する
shutil.copy
や shutil.copy2
はファイルのパーミッションをコピーしてくれますが、パーミッションを変えるにはos.chmodを使います。
import shutil, os
shutil.copy2('src', 'dst-workaround3')
os.chmod('dst-workaround3', 0x755)
また、(あまりないと思いますが)cp
のように、umask
を適用したパーミッションにしたいという場合は、os.umask
で値を取得できます。なお、os.umask
は POSIXのumask(2) の仕様を反映し「値を変更して以前の値を返す」という使いにくいインターフェースになっています。
import shutil, os
shutil.copy('src', 'dst-workaround4')
umask = os.umask(0) # 現在の値を取得するために、適当な値で呼び出す
os.umask(umask) # 値を元に戻す
# パーミッションを umask でマスクする。
mode = os.stat('src').st_mode & ~umask
os.chmod('dst-workaround4', mode)
-
よく見ると最終アクセス日時が最初に
stat src
したときと変わっていますが、実際にアクセスがあったので仕方がありません。 ↩