Invitation improvements
[pithos] / tools / psync
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()