Refs #1171
[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 cStringIO import StringIO
41 from hashlib import md5
42
43 from lib.client import Pithos_Client, Fault
44
45
46 SQL_CREATE_TABLE = '''CREATE TABLE IF NOT EXISTS files (
47                         path TEXT PRIMARY KEY, hash TEXT)'''
48
49
50 class LocalState(object):
51     def __init__(self):
52         dbpath = os.path.expanduser('~/.psyncdb')
53         self.conn = sqlite3.connect(dbpath)
54         self.conn.execute(SQL_CREATE_TABLE)
55         self.conn.commit()
56     
57     def get(self, path):
58         sql = 'SELECT hash FROM files WHERE path = ?'
59         ret = self.conn.execute(sql, (path,)).fetchone()
60         return ret[0] if ret else ''
61     
62     def put(self, path, hash):
63         sql = 'INSERT OR REPLACE INTO files VALUES (?, ?)'
64         self.conn.execute(sql, (path, hash))
65         self.conn.commit()
66
67
68 class CurrentState(object):
69     def __init__(self, dir):
70         self.dir = dir
71     
72     def list(self):
73         return os.listdir(self.dir)
74         
75     def get(self, path):
76         fullpath = os.path.join(self.dir, path)
77         if os.path.exists(fullpath):
78             with open(fullpath) as f:
79                 data = f.read()
80                 return md5(data).hexdigest()
81         else:
82             return ''
83
84     def read(self, path):
85         fullpath = os.path.join(self.dir, path)
86         if not os.path.exists(fullpath):
87             return None
88         with open(fullpath) as f:
89             return f.read()
90     
91     def write(self, path, data):
92         fullpath = os.path.join(self.dir, path)
93         if data is None:
94             os.remove(fullpath)
95         else:
96             with open(fullpath, 'w') as f:
97                 f.write(data)
98     
99     def resolve_conflict(self, path):
100         fullpath = os.path.join(self.dir, path)
101         os.rename(fullpath, fullpath + '.local')
102
103
104 class RemoteState(object):
105     def __init__(self):
106         host = os.environ['PITHOS_SERVER']
107         user = os.environ['PITHOS_USER']
108         token = os.environ['PITHOS_AUTH']
109         self.container = 'pithos'
110         self.client = Pithos_Client(host, token, user)
111
112     def list(self):
113         return self.client.list_objects(self.container)
114         
115     def get(self, path):
116         try:
117             meta = self.client.retrieve_object_metadata(self.container, path)
118         except Fault:
119             return ''
120         return meta['etag']
121     
122     def read(self, path):
123         try:
124             return self.client.retrieve_object(self.container, path)
125         except Fault:
126             return None
127     
128     def write(self, path, data):
129         if data is None:
130             self.client.delete_object(self.container, path)
131         else:
132             f = StringIO(data)
133             self.client.create_object(self.container, path, f=f)
134
135
136 def sync(path, lstate, cstate, rstate):
137     s0 = lstate.get(path)
138     s1 = cstate.get(path)
139     s = rstate.get(path)
140
141     if s1 == s0:
142         # No local changes
143         if s != s0:
144             data = rstate.read(path)
145             cstate.write(path, data)
146             assert cstate.get(path) == s
147             lstate.put(path, s)
148         return
149     
150     if s == s0:
151         # No remote changes
152         if s1 != s0:
153             data = cstate.read(path)
154             rstate.write(path, data)
155             assert rstate.get(path) == s1
156             lstate.put(path, s1)
157         return
158     
159     # At this point both local and remote states have changes since last sync
160
161     if s1 == s:
162         # We were lucky, both had the same change
163         lstate.put(path, s)
164     else:
165         # Conflict, try to resolve it
166         cstate.resolve_conflict(path)
167         data = rstate.read(path)
168         cstate.write(path, data)
169         assert cstate.get(path) == s
170         lstate.put(path, s)
171
172
173 def main():
174     if len(sys.argv) != 2:
175         print 'syntax: %s <dir>' % sys.argv[0]
176         sys.exit(1)
177     
178     lstate = LocalState()
179     cstate = CurrentState(sys.argv[1])
180     rstate = RemoteState()
181
182     local_files = set(cstate.list())
183     remote_files = set(rstate.list())
184
185     for path in local_files | remote_files:
186         sync(path, lstate, cstate, rstate)
187
188
189 if __name__ == '__main__':
190     main()