フォルダ同期ツール「Harmonize.py」
フォルダ・ファイル構成を同じに保ちます。
tags: | python, filesync, utility |
---|---|
created: | 2007-02-05T20:24:24 |
フォルダ同期ツール 2つのフォルダを指定することで同じ状態になるよう同期させます。
概要
2つのフォルダペアを指定すると、その2フォルダの中身が同じになるよう ファイルの転送などを行います。
コマンドラインユーティリティとして作ってありますので、 バッチファイルやシェルからの利用がしやすいでしょう。
良くある同期ツールと違って特徴的なのは「 削除 」機能を持っていることです。
2つのフォルダをレフト、ライトと呼びます。
流れとしては、
- 起動時にレフト、ライトそれぞれのフォルダ内容のリストを取得します。
- 最後のファイルリストにあってレフトにないものはライトから削除。
- 最後のファイルリストにあってライトにないものはレフトから削除。
- レフトにあってライトにないものはライトにコピー。
- ライトにあってレフトにないものはレフトにコピー。
- 共通してあるものは日付をみて古いほうを新しいほうで上書き。
- レフトとライトは同じになったので(無視するものを除き)そのリストを保存。
という手順をとります。
保存したリストがない場合、もしくは「-d」オプションが指定されていない場合は、 「2.」、「3.」の手順は行いません。
警告
削除機能はうまく働くうちは便利なのですが、あくまで慎重に使ってください。 そのうち、ごみ箱へ移動するように変更するつもりです。
ソースコード
若干だらだらと長くなってしまいましたが・・・。
単発ツールはファイル1つの方が使い勝手が良いので。
harmonize.py( ダウンロード )
# -*- encoding: utf-8 -*-
import sys
import os
import pickle
import shutil
import imp
if hasattr(sys,"setdefaultencoding"):
sys.setdefaultencoding("utf-8")
def is_frozen():
return (hasattr(sys, "frozen") or # new py2exe
hasattr(sys, "importers") # old py2exe
or imp.is_frozen("__main__")) # tools/freeze
def executable():
if is_frozen():
return os.path.abspath(sys.executable)
return os.path.abspath(sys.argv[0])
class Harmonize:
def __init__(self, conf_file=''):
self.conf_file = conf_file
self.conf = {
'ignore_dirs': ['RCS', 'CVS', 'tags', '.svn'],
'ignore_file_ext': ['.pyc', '.pyo', '.obj', '.db', '.pdb'],
'folders': {}
}
if os.path.exists(conf_file):
self.load()
def load(self):
self.conf = pickle.load(file(self.conf_file))
def save(self):
pickle.dump(self.conf, file(self.conf_file, 'w'))
def listdir(self, path):
ans = {}
for item in os.listdir(path):
fullpath = os.path.join(path, item)
if os.path.isdir(fullpath):
if item in self.conf['ignore_dirs']:
continue
ans[item+os.sep] = self.listdir(fullpath)
else:
fname, ext = os.path.splitext(item)
if ext.lower() in self.conf['ignore_file_ext']:
continue
ans[item] = ()#os.path.getmtime(fullpath), os.path.getsize(fullpath))
return ans
def subtract(self, include, exclude):
ans = {}
for k,v in include.iteritems():
if isinstance(v,dict):
if v!=exclude.get(k,None):
ans[k] = self.subtract(v, exclude.get(k,{}))
else:
if not exclude.has_key(k):
ans[k] = tuple(v)
return ans
def merge(self, include, extend):
ans = self.subtract(include, {})
for k,v in extend.iteritems():
if isinstance(v,dict):
ans[k] = self.merge(v, include.get(k, {}))
else:
ans[k] = tuple(v)
return ans
def append(self, src, dst):
if self.conf['folders'].has_key((src,dst)):
return
if self.conf['folders'].has_key((dst,src)):
return
self.conf['folders'][(src,dst)] = {}
self.save()
def list(self):
return self.conf['folders'].keys()
def remove(self, src, dst):
del self.conf['folders'][(src, dst)]
self.save()
def index_list(self, path, index):
for key, value in index.iteritems():
child_path = os.path.join(path,key)
yield child_path
if isinstance(value, dict):
for item in self.index_list(child_path, value):
yield item
def copy(self, src_path, dst_path, index):
if not os.path.exists(dst_path):
print 'make folder:', dst_path
os.makedirs(dst_path)
for key, value in index.iteritems():
src_path_ = os.path.join(src_path, key)
dst_path_ = os.path.join(dst_path, key)
if isinstance(value, dict):
self.copy(src_path_, dst_path_, value)
else:
print 'copy file:', src_path_
shutil.copy2(src_path_, dst_path_)
def move(self, src_path, dst_path, index):
if not os.path.exists(dst_path):
print 'make folder:', dst_path
os.makedirs(dst_path)
for key, value in index.iteritems():
src_path_ = os.path.join(src_path, key)
dst_path_ = os.path.join(dst_path, key)
if isinstance(value, dict):
self.move(src_path_, dst_path_, value)
else:
print 'move file:', src_path_
shutil.move(src_path_, dst_path_)
def delete(self, path, original, reduced):
for key, value in original.iteritems():
path_ = os.path.join(path, key)
if isinstance(value, dict):
if reduced.has_key(key):
self.delete(path_, value, reduced[key])
else:
shutil.rmtree(path_, True)
print 'delete folder:', path_
else:
if not reduced.has_key(key):
try:
os.remove(path_)
print 'delete file:', path_
except OSError:
pass
def update(self, src_path, dst_path, index):
for key, value in index.iteritems():
src_path_ = os.path.join(src_path, key)
dst_path_ = os.path.join(dst_path, key)
if isinstance(value, dict):
self.update(src_path_, dst_path_, value)
else:
d = os.path.getmtime(src_path_) - os.path.getmtime(dst_path_)
if d>0:
print 'update -->:', src_path_
shutil.copy2(src_path_, dst_path_)
elif d<0:
print 'update <--:', src_path_
shutil.copy2(dst_path_, src_path_)
def synchronize(self, src_path, dst_path, delete=False):
print 'Synchronize :',src_path, dst_path
index={}
loaded = True
try:
index = self.conf['folders'][(src_path, dst_path)]
except KeyError:
try:
index = self.conf['folders'].get((dst_path, src_path), {})
except KeyError:
loaded = False
sub = self.subtract
src = self.listdir(src_path)
dst = self.listdir(dst_path)
src_remove = sub(index, dst)
dst_remove = sub(index, src)
common = self.merge(src, dst)
common = sub(common, sub(src, dst))
common = sub(common, sub(dst, src))
common = sub(common, src_remove)
common = sub(common, dst_remove)
to_dst = sub(src, dst)
to_src = sub(dst, src)
if delete:
self.delete(src_path, index, dst)
self.delete(dst_path, index, src)
to_dst = sub(to_dst, src_remove)
to_src = sub(to_src, dst_remove)
self.copy(src_path, dst_path, to_dst)
self.copy(dst_path, src_path, to_src)
self.update(src_path, dst_path, common)
if loaded:
self.conf['folders'][(src_path, dst_path)] = self.listdir(src_path)
self.save()
def all_synchronize(self, delete=False):
for s,d in self.conf['folders'].iterkeys():
self.synchronize(s, d, delete)
def main():
default_conf = os.path.splitext(executable())[0] + '.conf'
print 'config file:', default_conf
from optparse import OptionParser
parser = OptionParser(usage="%prog [-l] [-a] [-m] [-r n] [-c CONFIG] folder_1 folder_2", version="%prog 1.0")
parser.add_option("-m", "--manage", action="store_true", default=False,
help="append harmonize pair")
parser.add_option("-r", "--remove",
help="remove harmonize pair.", metavar="INDEX")
parser.add_option("-c", "--config", default=default_conf,
help="arrow config file.", metavar="CONFIG_FILENAME")
parser.add_option("-a", "--all",
action="store_true", default=False, help="all synchronize.")
parser.add_option("-d", "--delete",
action="store_true", default=False, help="enable deletion.")
parser.add_option("-l", "--list",
action="store_true", default=False, help="managed listing.")
(options, args) = parser.parse_args()
harmonize = Harmonize(options.config)
if options.all:
harmonize.all_synchronize(options.delete)
elif options.list:
print 'listing harmonize pair(s):'
for num, (s,d) in enumerate(harmonize.list()):
print ' %2d:' % num,s,d
elif options.remove:
s,d = harmonize.list()[int(options.remove)]
print 'remove harmonize pair:', s,d
harmonize.remove(s,d)
else:
if len(args)!=2:
parser.error("need for harmonize pair path or use -a option")
src_path = unicode(args[0], sys.getfilesystemencoding())
if not src_path.endswith(os.sep):
src_path += os.sep
dst_path = unicode(args[1], sys.getfilesystemencoding())
if not dst_path.endswith(os.sep):
dst_path += os.sep
if options.manage:
harmonize.append(src_path, dst_path)
harmonize.synchronize(src_path, dst_path, options.delete)
if __name__ == '__main__':
main()
使い方
同期フォルダペアの登録
初めて同期をするフォルダのとき、
harmonize.py -m hoge moge
としてください。
削除の発生しないモードよる同期の後、 harmonize.confというファイルにファイルリストが保存されます。
同期させたいフォルダがいくつもあるなら上記のコマンドをそれぞれ繰り返してください。
ヘルプメッセージ
harmonize.py -h
以下のような説明が表示されます。
usage: harmonize.py [-l] [-a] [-m] [-r n] [-c CONFIG] folder_1 folder_2
options:
--version show program's version number and exit
-h, --help show this help message and exit
-m, --manage append harmonize pair
-r INDEX, --remove=INDEX
remove harmonize pair.
-c CONFIG_FILENAME, --config=CONFIG_FILENAME
arrow config file.
-a, --all all synchronize.
-d, --delete enable deletion.
-l, --list managed listing.
結論
このソースにあるミソは、
- ネストされたdictオブジェクトの差分やマージ
- OptionParserの例
というところでしょうか。
ファイル群のリストアップはレフトとライトそれぞれ一回だけに行うようになっているので、 ネットワークフォルダ相手でも意外に高速に同期できます。
filecmp.dircmpを使おうと最初検討していたんですが、 削除リストの取得ができなさそうだったので自作となりました。
途中でのエラーリカバリなどをあまり留意して作っていないので利用するときは注意してください。