Fix wrong autoincrement column. Make mysql tables use UTF-8
[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 CONTAINER = 'pithos'
47
48 SQL_CREATE_TABLE = '''CREATE TABLE IF NOT EXISTS files (
49                         path TEXT PRIMARY KEY, hash TEXT)'''
50
51 client = None
52 lstate = None
53 cstate = None
54 rstate = None
55
56
57 class LocalState(object):
58     def __init__(self):
59         dbpath = os.path.expanduser('~/.psyncdb')
60         self.conn = sqlite3.connect(dbpath)
61         self.conn.execute(SQL_CREATE_TABLE)
62         self.conn.commit()
63     
64     def get(self, path):
65         sql = 'SELECT hash FROM files WHERE path = ?'
66         ret = self.conn.execute(sql, (path,)).fetchone()
67         return ret[0] if ret else 'DEL'
68     
69     def put(self, path, hash):
70         sql = 'INSERT OR REPLACE INTO files VALUES (?, ?)'
71         self.conn.execute(sql, (path, hash))
72         self.conn.commit()
73
74
75 class CurrentState(object):
76     def __init__(self, dir):
77         self.dir = dir
78     
79     def get(self, path):
80         fullpath = os.path.join(self.dir, path)
81         if os.path.exists(fullpath):
82             if os.path.isdir(fullpath):
83                 return 'DIR'
84             else:
85                 return merkle(fullpath)
86         else:
87             return 'DEL'
88     
89     def fullpath(self, path):
90         return os.path.join(self.dir, path)
91
92
93 class RemoteState(object):
94     def __init__(self, client):
95         self.container = 'pithos'
96         self.client = client
97         self.container = CONTAINER
98     
99     def get(self, path):
100         try:
101             meta = self.client.retrieve_object_metadata(self.container, path)
102         except Fault:
103             return 'DEL'
104         if meta.get('content-type', None) == 'application/directory':
105             return 'DIR'
106         else:
107             return meta['etag']
108
109
110 def download(path, S):
111     fullpath = cstate.fullpath(path)
112     if S == 'DEL':
113         os.remove(fullpath)
114     elif S == 'DIR':
115         if os.path.exists(fullpath):
116             os.remove(fullpath)
117         os.mkdir(fullpath)
118     else:
119         transfer.download(client, CONTAINER, path, fullpath)
120         assert cstate.get(path) == S
121
122
123 def upload(path, S):
124     fullpath = cstate.fullpath(path)
125     if S == 'DEL':
126         client.delete_object(CONTAINER, path)
127     elif S == 'DIR':
128         client.create_directory_marker(CONTAINER, path)
129     else:
130         prefix, name = os.path.split(path)
131         if prefix:
132             prefix += '/'
133         transfer.upload(client, fullpath, CONTAINER, prefix, name)
134         assert rstate.get(path) == S
135
136
137 def resolve_conflict(path):
138     fullpath = cstate.fullpath(path)
139     if os.path.exists(fullpath):
140         os.rename(fullpath, fullpath + '.local')
141
142
143 def sync(path):
144     L = lstate.get(path)
145     C = cstate.get(path)
146     R = rstate.get(path)
147
148     if C == L:
149         # No local changes
150         if R != L:
151             download(path, R)
152             lstate.put(path, R)
153         return
154     
155     if R == L:
156         # No remote changes
157         if C != L:
158             upload(path, C)
159             lstate.put(path, C)
160         return
161     
162     # At this point both local and remote states have changes since last sync
163
164     if C == R:
165         # We were lucky, both had the same change
166         lstate.put(path, R)
167     else:
168         # Conflict, try to resolve it
169         resolve_conflict(path)
170         download(path, R)
171         lstate.put(path, R)
172
173
174 def walk(dir):
175     pending = ['']
176     
177     while pending:
178         dirs = set()
179         files = set()
180         root = pending.pop(0)
181         if root:
182             yield root
183         
184         dirpath = os.path.join(dir, root)
185         if os.path.exists(dirpath):
186             for filename in os.listdir(dirpath):
187                 path = os.path.join(root, filename)
188                 if os.path.isdir(os.path.join(dir, path)):
189                     dirs.add(path)
190                 else:
191                     files.add(path)
192         
193         for object in client.list_objects(CONTAINER, prefix=root,
194                                             delimiter='/', format='json'):
195             if 'subdir' in object:
196                 continue
197             name = str(object['name'])
198             if object['content_type'] == 'application/directory':
199                 dirs.add(name)
200             else:
201                 files.add(name)
202         
203         pending += sorted(dirs)
204         for path in files:
205             yield path
206
207
208 def main():
209     global client, lstate, cstate, rstate
210     
211     if len(sys.argv) != 2:
212         print 'syntax: %s <dir>' % sys.argv[0]
213         sys.exit(1)
214     
215     dir = sys.argv[1]
216     client = Pithos_Client(get_server(), get_auth(), get_user())
217     
218     lstate = LocalState()
219     cstate = CurrentState(dir)
220     rstate = RemoteState(client)
221     
222     for path in walk(dir):
223         print 'Syncing', path
224         sync(path)
225
226
227 if __name__ == '__main__':
228     main()