root / tools / pithos-sync @ 91560b09
History | View | Annotate | Download (6.5 kB)
1 |
#!/usr/bin/env python |
---|---|
2 |
|
3 |
# Copyright 2011 GRNET S.A. All rights reserved. |
4 |
# |
5 |
# Redistribution and use in source and binary forms, with or |
6 |
# without modification, are permitted provided that the following |
7 |
# conditions are met: |
8 |
# |
9 |
# 1. Redistributions of source code must retain the above |
10 |
# copyright notice, this list of conditions and the following |
11 |
# disclaimer. |
12 |
# |
13 |
# 2. Redistributions in binary form must reproduce the above |
14 |
# copyright notice, this list of conditions and the following |
15 |
# disclaimer in the documentation and/or other materials |
16 |
# provided with the distribution. |
17 |
# |
18 |
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS |
19 |
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
20 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
21 |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR |
22 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
23 |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
24 |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
25 |
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
26 |
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
27 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
28 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
29 |
# POSSIBILITY OF SUCH DAMAGE. |
30 |
# |
31 |
# The views and conclusions contained in the software and |
32 |
# documentation are those of the authors and should not be |
33 |
# interpreted as representing official policies, either expressed |
34 |
# or implied, of GRNET S.A. |
35 |
|
36 |
import os |
37 |
import sqlite3 |
38 |
import sys |
39 |
|
40 |
from lib import transfer |
41 |
from lib.client import Pithos_Client, Fault |
42 |
from lib.hashmap import merkle |
43 |
from lib.util import get_user, get_auth, get_server |
44 |
|
45 |
|
46 |
DEFAULT_CONTAINER = 'pithos' |
47 |
|
48 |
def get_container(): |
49 |
try: |
50 |
return os.environ['PITHOS_SYNC_CONTAINER'] |
51 |
except KeyError: |
52 |
return DEFAULT_CONTAINER |
53 |
|
54 |
|
55 |
SQL_CREATE_TABLE = '''CREATE TABLE IF NOT EXISTS files ( |
56 |
path TEXT PRIMARY KEY, hash TEXT)''' |
57 |
|
58 |
client = None |
59 |
lstate = None |
60 |
cstate = None |
61 |
rstate = None |
62 |
|
63 |
|
64 |
class LocalState(object): |
65 |
def __init__(self): |
66 |
dbpath = os.path.expanduser('~/.psyncdb') |
67 |
self.conn = sqlite3.connect(dbpath) |
68 |
self.conn.execute(SQL_CREATE_TABLE) |
69 |
self.conn.commit() |
70 |
|
71 |
def get(self, path): |
72 |
sql = 'SELECT hash FROM files WHERE path = ?' |
73 |
ret = self.conn.execute(sql, (path,)).fetchone() |
74 |
return ret[0] if ret else 'DEL' |
75 |
|
76 |
def put(self, path, hash): |
77 |
sql = 'INSERT OR REPLACE INTO files VALUES (?, ?)' |
78 |
self.conn.execute(sql, (path, hash)) |
79 |
self.conn.commit() |
80 |
|
81 |
|
82 |
class CurrentState(object): |
83 |
def __init__(self, dir): |
84 |
self.dir = dir |
85 |
|
86 |
def get(self, path): |
87 |
fullpath = os.path.join(self.dir, path) |
88 |
if os.path.exists(fullpath): |
89 |
if os.path.isdir(fullpath): |
90 |
return 'DIR' |
91 |
else: |
92 |
return merkle(fullpath) |
93 |
else: |
94 |
return 'DEL' |
95 |
|
96 |
def fullpath(self, path): |
97 |
return os.path.join(self.dir, path) |
98 |
|
99 |
|
100 |
class RemoteState(object): |
101 |
def __init__(self, client): |
102 |
self.client = client |
103 |
self.container = get_container() |
104 |
|
105 |
def get(self, path): |
106 |
try: |
107 |
meta = self.client.retrieve_object_metadata(self.container, path) |
108 |
except Fault: |
109 |
return 'DEL' |
110 |
if meta.get('content-type', None) == 'application/directory': |
111 |
return 'DIR' |
112 |
else: |
113 |
return meta['etag'] |
114 |
|
115 |
|
116 |
def download(path, S): |
117 |
fullpath = cstate.fullpath(path) |
118 |
if S == 'DEL': |
119 |
os.remove(fullpath) |
120 |
elif S == 'DIR': |
121 |
if os.path.exists(fullpath): |
122 |
os.remove(fullpath) |
123 |
os.mkdir(fullpath) |
124 |
else: |
125 |
transfer.download(client, get_container(), path, fullpath) |
126 |
assert cstate.get(path) == S |
127 |
|
128 |
|
129 |
def upload(path, S): |
130 |
fullpath = cstate.fullpath(path) |
131 |
if S == 'DEL': |
132 |
client.delete_object(get_container(), path) |
133 |
elif S == 'DIR': |
134 |
client.create_directory_marker(get_container(), path) |
135 |
else: |
136 |
prefix, name = os.path.split(path) |
137 |
if prefix: |
138 |
prefix += '/' |
139 |
transfer.upload(client, fullpath, get_container(), prefix, name) |
140 |
assert rstate.get(path) == S |
141 |
|
142 |
|
143 |
def resolve_conflict(path): |
144 |
fullpath = cstate.fullpath(path) |
145 |
if os.path.exists(fullpath): |
146 |
os.rename(fullpath, fullpath + '.local') |
147 |
|
148 |
|
149 |
def sync(path): |
150 |
L = lstate.get(path) |
151 |
C = cstate.get(path) |
152 |
R = rstate.get(path) |
153 |
|
154 |
if C == L: |
155 |
# No local changes |
156 |
if R != L: |
157 |
download(path, R) |
158 |
lstate.put(path, R) |
159 |
return |
160 |
|
161 |
if R == L: |
162 |
# No remote changes |
163 |
if C != L: |
164 |
upload(path, C) |
165 |
lstate.put(path, C) |
166 |
return |
167 |
|
168 |
# At this point both local and remote states have changes since last sync |
169 |
|
170 |
if C == R: |
171 |
# We were lucky, both had the same change |
172 |
lstate.put(path, R) |
173 |
else: |
174 |
# Conflict, try to resolve it |
175 |
resolve_conflict(path) |
176 |
download(path, R) |
177 |
lstate.put(path, R) |
178 |
|
179 |
|
180 |
def walk(dir): |
181 |
pending = [''] |
182 |
|
183 |
while pending: |
184 |
dirs = set() |
185 |
files = set() |
186 |
root = pending.pop(0) |
187 |
if root: |
188 |
yield root |
189 |
|
190 |
dirpath = os.path.join(dir, root) |
191 |
if os.path.exists(dirpath): |
192 |
for filename in os.listdir(dirpath): |
193 |
path = os.path.join(root, filename) |
194 |
if os.path.isdir(os.path.join(dir, path)): |
195 |
dirs.add(path) |
196 |
else: |
197 |
files.add(path) |
198 |
|
199 |
for object in client.list_objects(get_container(), prefix=root, |
200 |
delimiter='/', format='json'): |
201 |
if 'subdir' in object: |
202 |
continue |
203 |
name = str(object['name']) |
204 |
if object['content_type'] == 'application/directory': |
205 |
dirs.add(name) |
206 |
else: |
207 |
files.add(name) |
208 |
|
209 |
pending += sorted(dirs) |
210 |
for path in files: |
211 |
yield path |
212 |
|
213 |
|
214 |
def main(): |
215 |
global client, lstate, cstate, rstate |
216 |
|
217 |
if len(sys.argv) != 2: |
218 |
print 'syntax: %s <dir>' % sys.argv[0] |
219 |
sys.exit(1) |
220 |
|
221 |
dir = sys.argv[1] |
222 |
client = Pithos_Client(get_server(), get_auth(), get_user()) |
223 |
|
224 |
lstate = LocalState() |
225 |
cstate = CurrentState(dir) |
226 |
rstate = RemoteState(client) |
227 |
|
228 |
for path in walk(dir): |
229 |
print 'Syncing', path |
230 |
sync(path) |
231 |
|
232 |
|
233 |
if __name__ == '__main__': |
234 |
main() |