Revision d8c26d94

b/tools/psync
1
#!/usr/bin/env python
2

  
3
import os
4
import sqlite3
5
import sys
6

  
7
from cStringIO import StringIO
8
from hashlib import md5
9

  
10
from lib.client import Pithos_Client, Fault
11

  
12

  
13
SQL_CREATE_TABLE = '''CREATE TABLE IF NOT EXISTS files (
14
                        path TEXT PRIMARY KEY, hash TEXT)'''
15

  
16

  
17
class LocalState(object):
18
    def __init__(self):
19
        dbpath = os.path.expanduser('~/.psyncdb')
20
        self.conn = sqlite3.connect(dbpath)
21
        self.conn.execute(SQL_CREATE_TABLE)
22
        self.conn.commit()
23
    
24
    def get(self, path):
25
        sql = 'SELECT hash FROM files WHERE path = ?'
26
        ret = self.conn.execute(sql, (path,)).fetchone()
27
        return ret[0] if ret else ''
28
    
29
    def put(self, path, hash):
30
        sql = 'INSERT OR REPLACE INTO files VALUES (?, ?)'
31
        self.conn.execute(sql, (path, hash))
32
        self.conn.commit()
33

  
34

  
35
class CurrentState(object):
36
    def __init__(self, dir):
37
        self.dir = dir
38
    
39
    def list(self):
40
        return os.listdir(self.dir)
41
        
42
    def get(self, path):
43
        fullpath = os.path.join(self.dir, path)
44
        if os.path.exists(fullpath):
45
            with open(fullpath) as f:
46
                data = f.read()
47
                return md5(data).hexdigest()
48
        else:
49
            return ''
50

  
51
    def read(self, path):
52
        fullpath = os.path.join(self.dir, path)
53
        if not os.path.exists(fullpath):
54
            return None
55
        with open(fullpath) as f:
56
            return f.read()
57
    
58
    def write(self, path, data):
59
        fullpath = os.path.join(self.dir, path)
60
        if data is None:
61
            os.remove(fullpath)
62
        else:
63
            with open(fullpath, 'w') as f:
64
                f.write(data)
65
    
66
    def resolve_conflict(self, path):
67
        fullpath = os.path.join(self.dir, path)
68
        os.rename(fullpath, fullpath + '.local')
69

  
70

  
71
class RemoteState(object):
72
    def __init__(self):
73
        host = os.environ['PITHOS_SERVER']
74
        user = os.environ['PITHOS_USER']
75
        token = os.environ['PITHOS_AUTH']
76
        self.container = 'pithos'
77
        self.client = Pithos_Client(host, token, user)
78

  
79
    def list(self):
80
        return self.client.list_objects(self.container)
81
        
82
    def get(self, path):
83
        try:
84
            meta = self.client.retrieve_object_metadata(self.container, path)
85
        except Fault:
86
            return ''
87
        return meta['etag']
88
    
89
    def read(self, path):
90
        try:
91
            return self.client.retrieve_object(self.container, path)
92
        except Fault:
93
            return None
94
    
95
    def write(self, path, data):
96
        if data is None:
97
            self.client.delete_object(self.container, path)
98
        else:
99
            f = StringIO(data)
100
            self.client.create_object(self.container, path, f=f)
101

  
102

  
103
def sync(path, lstate, cstate, rstate):
104
    s0 = lstate.get(path)
105
    s1 = cstate.get(path)
106
    s = rstate.get(path)
107

  
108
    if s1 == s0:
109
        # No local changes
110
        if s != s0:
111
            data = rstate.read(path)
112
            cstate.write(path, data)
113
            assert cstate.get(path) == s
114
            lstate.put(path, s)
115
        return
116
    
117
    if s == s0:
118
        # No remote changes
119
        if s1 != s0:
120
            data = cstate.read(path)
121
            rstate.write(path, data)
122
            assert rstate.get(path) == s1
123
            lstate.put(path, s1)
124
        return
125
    
126
    # At this point both local and remote states have changes since last sync
127

  
128
    if s1 == s:
129
        # We were lucky, both had the same change
130
        lstate.put(path, s)
131
    else:
132
        # Conflict, try to resolve it
133
        cstate.resolve_conflict(path)
134
        data = rstate.read(path)
135
        cstate.write(path, data)
136
        assert cstate.get(path) == s
137
        lstate.put(path, s)
138

  
139

  
140
def main():
141
    if len(sys.argv) != 2:
142
        print 'syntax: %s <dir>' % sys.argv[0]
143
        sys.exit(1)
144
    
145
    lstate = LocalState()
146
    cstate = CurrentState(sys.argv[1])
147
    rstate = RemoteState()
148

  
149
    local_files = set(cstate.list())
150
    remote_files = set(rstate.list())
151

  
152
    for path in local_files | remote_files:
153
        sync(path, lstate, cstate, rstate)
154

  
155

  
156
if __name__ == '__main__':
157
    main()

Also available in: Unified diff