Statistics
| Branch: | Tag: | Revision:

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()