LoginSignup
8
2

More than 5 years have passed since last update.

積年の「shutil.copy はメタデータをコピーしません」の回避策

Last updated at Posted at 2018-10-30

Pythonには、ファイル操作のための標準ライブラリ shutil が用意されていますが、そのドキュメントの冒頭には物々しい警告が書かれています。

警告 高水準のファイルコピー関数 (shutil.copy(), shutil.copy2()) でも、ファイルのメタデータの全てをコピーすることはできません。

POSIXプラットフォームでは、これはACLやファイルのオーナー、グループが失われることを意味しています。 Mac OSでは、リソースフォーク(resource fork)やその他のメタデータが利用されません。これは、リソースが失われ、ファイルタイプや生成者コード(creator code)が正しくなくなることを意味しています。 Windowsでは、ファイルオーナー、ACL、代替データストリームがコピーされません。

https://docs.python.jp/3/library/shutil.html

しかし、赤字で警告しているわりに「ではどうすればいいか」が書かれていないため、読む側としては困惑するばかりです。Python 2.6の頃からこうのようです。なんだかな〜。

そこで、具体的にはどんなワークアラウンドをとればいいのかを調べてみました。

TL;DR

  1. shutil.copy, shutil.copy2 では、オーナーや日時がコピーされない
  2. オーナーは shutil.chown でコピーすればOK
  3. 最終手段 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.copyshutil.copy2 ユーザー・オーナーをコピーしない
  • shutil.copyshutil.copy2 は各種日時もコピーしない
  • shutil.copyshutil.copy2cp とも cp -p と振る舞いが異なる

そのため、例えばシェルスクリプトを、Pythonで書き直す時、単純に cpshutil.copy に置換すると振る舞いが変わってしまいます。

cp -pを完全再現する(日時までもコピーしたい)

osshutilを見る限り、最終アクセス日時や最終変更日時をコピーする関数はありません。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.copyshutil.copy2 はファイルのパーミッションをコピーしてくれますが、パーミッションを変えるにはos.chmodを使います。

import shutil, os
shutil.copy2('src', 'dst-workaround3')
os.chmod('dst-workaround3', 0x755)

また、(あまりないと思いますが)cp のように、umask を適用したパーミッションにしたいという場合は、os.umask で値を取得できます。なお、os.umaskPOSIXの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)

  1. よく見ると最終アクセス日時が最初に stat src したときと変わっていますが、実際にアクセスがあったので仕方がありません。 

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2