Statistics
| Branch: | Tag: | Revision:

root / tools / pithos-sync @ 390c7730

History | View | Annotate | Download (9.8 kB)

1 d8c26d94 Giorgos Verigakis
#!/usr/bin/env python
2 d8c26d94 Giorgos Verigakis
3 eaa6fb55 Antony Chazapis
# Copyright 2011 GRNET S.A. All rights reserved.
4 eaa6fb55 Antony Chazapis
# 
5 eaa6fb55 Antony Chazapis
# Redistribution and use in source and binary forms, with or
6 eaa6fb55 Antony Chazapis
# without modification, are permitted provided that the following
7 eaa6fb55 Antony Chazapis
# conditions are met:
8 eaa6fb55 Antony Chazapis
# 
9 eaa6fb55 Antony Chazapis
#   1. Redistributions of source code must retain the above
10 eaa6fb55 Antony Chazapis
#      copyright notice, this list of conditions and the following
11 eaa6fb55 Antony Chazapis
#      disclaimer.
12 eaa6fb55 Antony Chazapis
# 
13 eaa6fb55 Antony Chazapis
#   2. Redistributions in binary form must reproduce the above
14 eaa6fb55 Antony Chazapis
#      copyright notice, this list of conditions and the following
15 eaa6fb55 Antony Chazapis
#      disclaimer in the documentation and/or other materials
16 eaa6fb55 Antony Chazapis
#      provided with the distribution.
17 eaa6fb55 Antony Chazapis
# 
18 eaa6fb55 Antony Chazapis
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19 eaa6fb55 Antony Chazapis
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 eaa6fb55 Antony Chazapis
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 eaa6fb55 Antony Chazapis
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22 eaa6fb55 Antony Chazapis
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 eaa6fb55 Antony Chazapis
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 eaa6fb55 Antony Chazapis
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25 eaa6fb55 Antony Chazapis
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26 eaa6fb55 Antony Chazapis
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 eaa6fb55 Antony Chazapis
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 eaa6fb55 Antony Chazapis
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 eaa6fb55 Antony Chazapis
# POSSIBILITY OF SUCH DAMAGE.
30 eaa6fb55 Antony Chazapis
# 
31 eaa6fb55 Antony Chazapis
# The views and conclusions contained in the software and
32 eaa6fb55 Antony Chazapis
# documentation are those of the authors and should not be
33 eaa6fb55 Antony Chazapis
# interpreted as representing official policies, either expressed
34 eaa6fb55 Antony Chazapis
# or implied, of GRNET S.A.
35 eaa6fb55 Antony Chazapis
36 d8c26d94 Giorgos Verigakis
import os
37 d8c26d94 Giorgos Verigakis
import sqlite3
38 d8c26d94 Giorgos Verigakis
import sys
39 99d0e277 Antony Chazapis
import shutil
40 db3c7c26 Antony Chazapis
import pickle
41 d8c26d94 Giorgos Verigakis
42 68d89033 Giorgos Verigakis
from lib import transfer
43 d8c26d94 Giorgos Verigakis
from lib.client import Pithos_Client, Fault
44 db3c7c26 Antony Chazapis
from lib.hashmap import HashMap, merkle
45 68d89033 Giorgos Verigakis
from lib.util import get_user, get_auth, get_server
46 68d89033 Giorgos Verigakis
47 d8c26d94 Giorgos Verigakis
48 b0762fe3 Antony Chazapis
DEFAULT_CONTAINER = 'pithos'
49 b0762fe3 Antony Chazapis
50 b0762fe3 Antony Chazapis
def get_container():
51 b0762fe3 Antony Chazapis
    try:
52 b0762fe3 Antony Chazapis
        return os.environ['PITHOS_SYNC_CONTAINER']
53 b0762fe3 Antony Chazapis
    except KeyError:
54 b0762fe3 Antony Chazapis
        return DEFAULT_CONTAINER
55 b0762fe3 Antony Chazapis
56 d8c26d94 Giorgos Verigakis
57 99d0e277 Antony Chazapis
def create_dir(path):
58 99d0e277 Antony Chazapis
    if not os.path.exists(path):
59 99d0e277 Antony Chazapis
        os.makedirs(path)
60 99d0e277 Antony Chazapis
    if not os.path.isdir(path):
61 99d0e277 Antony Chazapis
        raise RuntimeError("Cannot open '%s'" % (path,))
62 99d0e277 Antony Chazapis
63 d8c26d94 Giorgos Verigakis
64 db3c7c26 Antony Chazapis
def copy_file(src, dst):
65 3ea35aa0 Antony Chazapis
    print '***', 'COPYING', src, dst
66 3ea35aa0 Antony Chazapis
    path = os.path.dirname(dst)
67 db3c7c26 Antony Chazapis
    create_dir(path)
68 db3c7c26 Antony Chazapis
    shutil.copyfile(src, dst)
69 db3c7c26 Antony Chazapis
70 db3c7c26 Antony Chazapis
71 68d89033 Giorgos Verigakis
client = None
72 db3c7c26 Antony Chazapis
conf = None
73 db3c7c26 Antony Chazapis
confdir = None
74 99d0e277 Antony Chazapis
trash = None
75 68d89033 Giorgos Verigakis
lstate = None
76 68d89033 Giorgos Verigakis
cstate = None
77 68d89033 Giorgos Verigakis
rstate = None
78 68d89033 Giorgos Verigakis
79 db3c7c26 Antony Chazapis
80 99d0e277 Antony Chazapis
class Trash(object):
81 99d0e277 Antony Chazapis
    def __init__(self):
82 db3c7c26 Antony Chazapis
        self.path = os.path.join(confdir, 'trash')
83 db3c7c26 Antony Chazapis
        create_dir(self.path)
84 99d0e277 Antony Chazapis
        
85 db3c7c26 Antony Chazapis
        dbpath = os.path.join(confdir, 'trash.db')
86 99d0e277 Antony Chazapis
        self.conn = sqlite3.connect(dbpath)
87 99d0e277 Antony Chazapis
        sql = '''CREATE TABLE IF NOT EXISTS files (
88 99d0e277 Antony Chazapis
                    path TEXT PRIMARY KEY, hash TEXT)'''
89 99d0e277 Antony Chazapis
        self.conn.execute(sql)
90 99d0e277 Antony Chazapis
        self.conn.commit()
91 99d0e277 Antony Chazapis
    
92 3ea35aa0 Antony Chazapis
    def put(self, fullpath, path, hash):
93 3ea35aa0 Antony Chazapis
        copy_file(fullpath, os.path.join(self.path, path))
94 3ea35aa0 Antony Chazapis
        os.remove(fullpath)
95 99d0e277 Antony Chazapis
        sql = 'INSERT OR REPLACE INTO files VALUES (?, ?)'
96 db3c7c26 Antony Chazapis
        self.conn.execute(sql, (path, hash))
97 99d0e277 Antony Chazapis
        self.conn.commit()
98 99d0e277 Antony Chazapis
    
99 99d0e277 Antony Chazapis
    def search(self, hash):
100 99d0e277 Antony Chazapis
        sql = 'SELECT path FROM files WHERE hash = ?'
101 99d0e277 Antony Chazapis
        ret = self.conn.execute(sql, (hash,)).fetchone()
102 99d0e277 Antony Chazapis
        return ret[0] if ret else None
103 99d0e277 Antony Chazapis
    
104 99d0e277 Antony Chazapis
    def empty(self):
105 99d0e277 Antony Chazapis
        sql = 'DELETE FROM files'
106 99d0e277 Antony Chazapis
        self.conn.execute(sql)
107 99d0e277 Antony Chazapis
        self.conn.commit()
108 db3c7c26 Antony Chazapis
        shutil.rmtree(self.path)
109 99d0e277 Antony Chazapis
    
110 99d0e277 Antony Chazapis
    def fullpath(self, path):
111 db3c7c26 Antony Chazapis
        return os.path.join(self.path, path)
112 99d0e277 Antony Chazapis
113 d8c26d94 Giorgos Verigakis
114 d8c26d94 Giorgos Verigakis
class LocalState(object):
115 db3c7c26 Antony Chazapis
    def __init__(self, path):
116 db3c7c26 Antony Chazapis
        self.path = path
117 db3c7c26 Antony Chazapis
        
118 db3c7c26 Antony Chazapis
        dbpath = os.path.join(confdir, 'state.db')
119 d8c26d94 Giorgos Verigakis
        self.conn = sqlite3.connect(dbpath)
120 99d0e277 Antony Chazapis
        sql = '''CREATE TABLE IF NOT EXISTS files (
121 99d0e277 Antony Chazapis
                    path TEXT PRIMARY KEY, hash TEXT)'''
122 99d0e277 Antony Chazapis
        self.conn.execute(sql)
123 d8c26d94 Giorgos Verigakis
        self.conn.commit()
124 d8c26d94 Giorgos Verigakis
    
125 d8c26d94 Giorgos Verigakis
    def get(self, path):
126 d8c26d94 Giorgos Verigakis
        sql = 'SELECT hash FROM files WHERE path = ?'
127 d8c26d94 Giorgos Verigakis
        ret = self.conn.execute(sql, (path,)).fetchone()
128 68d89033 Giorgos Verigakis
        return ret[0] if ret else 'DEL'
129 d8c26d94 Giorgos Verigakis
    
130 d8c26d94 Giorgos Verigakis
    def put(self, path, hash):
131 d8c26d94 Giorgos Verigakis
        sql = 'INSERT OR REPLACE INTO files VALUES (?, ?)'
132 d8c26d94 Giorgos Verigakis
        self.conn.execute(sql, (path, hash))
133 d8c26d94 Giorgos Verigakis
        self.conn.commit()
134 d8c26d94 Giorgos Verigakis
135 99d0e277 Antony Chazapis
    def search(self, hash):
136 99d0e277 Antony Chazapis
        sql = 'SELECT path FROM files WHERE hash = ?'
137 99d0e277 Antony Chazapis
        ret = self.conn.execute(sql, (hash,)).fetchone()
138 99d0e277 Antony Chazapis
        return ret[0] if ret else None
139 db3c7c26 Antony Chazapis
    
140 db3c7c26 Antony Chazapis
    def fullpath(self, path):
141 db3c7c26 Antony Chazapis
        return os.path.join(self.path, path)
142 99d0e277 Antony Chazapis
143 d8c26d94 Giorgos Verigakis
144 d8c26d94 Giorgos Verigakis
class CurrentState(object):
145 db3c7c26 Antony Chazapis
    def __init__(self, path):
146 db3c7c26 Antony Chazapis
        self.path = path
147 d8c26d94 Giorgos Verigakis
    
148 d8c26d94 Giorgos Verigakis
    def get(self, path):
149 db3c7c26 Antony Chazapis
        fullpath = os.path.join(self.path, path)
150 d8c26d94 Giorgos Verigakis
        if os.path.exists(fullpath):
151 68d89033 Giorgos Verigakis
            if os.path.isdir(fullpath):
152 68d89033 Giorgos Verigakis
                return 'DIR'
153 68d89033 Giorgos Verigakis
            else:
154 db3c7c26 Antony Chazapis
                return merkle(fullpath, conf['blocksize'], conf['blockhash'])
155 d8c26d94 Giorgos Verigakis
        else:
156 68d89033 Giorgos Verigakis
            return 'DEL'
157 d8c26d94 Giorgos Verigakis
    
158 68d89033 Giorgos Verigakis
    def fullpath(self, path):
159 db3c7c26 Antony Chazapis
        return os.path.join(self.path, path)
160 d8c26d94 Giorgos Verigakis
161 d8c26d94 Giorgos Verigakis
162 d8c26d94 Giorgos Verigakis
class RemoteState(object):
163 68d89033 Giorgos Verigakis
    def __init__(self, client):
164 68d89033 Giorgos Verigakis
        self.client = client
165 b0762fe3 Antony Chazapis
        self.container = get_container()
166 68d89033 Giorgos Verigakis
    
167 d8c26d94 Giorgos Verigakis
    def get(self, path):
168 d8c26d94 Giorgos Verigakis
        try:
169 d8c26d94 Giorgos Verigakis
            meta = self.client.retrieve_object_metadata(self.container, path)
170 d8c26d94 Giorgos Verigakis
        except Fault:
171 68d89033 Giorgos Verigakis
            return 'DEL'
172 68d89033 Giorgos Verigakis
        if meta.get('content-type', None) == 'application/directory':
173 68d89033 Giorgos Verigakis
            return 'DIR'
174 d8c26d94 Giorgos Verigakis
        else:
175 3ea35aa0 Antony Chazapis
            return meta['x-object-hash']
176 68d89033 Giorgos Verigakis
177 d8c26d94 Giorgos Verigakis
178 99d0e277 Antony Chazapis
def update_local(path, S):
179 3ea35aa0 Antony Chazapis
    # XXX If something is already here, put it in trash and delete it.
180 3ea35aa0 Antony Chazapis
    # XXX If we have a directory already here, put all files in trash.
181 68d89033 Giorgos Verigakis
    fullpath = cstate.fullpath(path)
182 68d89033 Giorgos Verigakis
    if S == 'DEL':
183 3ea35aa0 Antony Chazapis
        trash.put(fullpath, path, S)
184 68d89033 Giorgos Verigakis
    elif S == 'DIR':
185 68d89033 Giorgos Verigakis
        if os.path.exists(fullpath):
186 3ea35aa0 Antony Chazapis
            trash.put(fullpath, path, S)
187 3ea35aa0 Antony Chazapis
        # XXX Strip trailing slash (or escape).
188 68d89033 Giorgos Verigakis
        os.mkdir(fullpath)
189 68d89033 Giorgos Verigakis
    else:
190 db3c7c26 Antony Chazapis
        # First, search for local copy
191 db3c7c26 Antony Chazapis
        file = lstate.search(S)
192 db3c7c26 Antony Chazapis
        if file:
193 db3c7c26 Antony Chazapis
            copy_file(lstate.fullpath(file), fullpath)
194 db3c7c26 Antony Chazapis
        else:
195 db3c7c26 Antony Chazapis
            # Search for copy in trash
196 db3c7c26 Antony Chazapis
            file = trash.search(S)
197 db3c7c26 Antony Chazapis
            if file:
198 3ea35aa0 Antony Chazapis
                # XXX Move from trash (not copy).
199 db3c7c26 Antony Chazapis
                copy_file(trash.fullpath(file), fullpath)
200 db3c7c26 Antony Chazapis
            else:
201 db3c7c26 Antony Chazapis
                # Download
202 db3c7c26 Antony Chazapis
                transfer.download(client, get_container(), path, fullpath)
203 68d89033 Giorgos Verigakis
        assert cstate.get(path) == S
204 68d89033 Giorgos Verigakis
205 68d89033 Giorgos Verigakis
206 99d0e277 Antony Chazapis
def update_remote(path, S):
207 68d89033 Giorgos Verigakis
    fullpath = cstate.fullpath(path)
208 68d89033 Giorgos Verigakis
    if S == 'DEL':
209 b0762fe3 Antony Chazapis
        client.delete_object(get_container(), path)
210 68d89033 Giorgos Verigakis
    elif S == 'DIR':
211 b0762fe3 Antony Chazapis
        client.create_directory_marker(get_container(), path)
212 68d89033 Giorgos Verigakis
    else:
213 68d89033 Giorgos Verigakis
        prefix, name = os.path.split(path)
214 68d89033 Giorgos Verigakis
        if prefix:
215 68d89033 Giorgos Verigakis
            prefix += '/'
216 b0762fe3 Antony Chazapis
        transfer.upload(client, fullpath, get_container(), prefix, name)
217 68d89033 Giorgos Verigakis
        assert rstate.get(path) == S
218 d8c26d94 Giorgos Verigakis
219 d8c26d94 Giorgos Verigakis
220 68d89033 Giorgos Verigakis
def resolve_conflict(path):
221 3ea35aa0 Antony Chazapis
    # XXX Check if this works with dirs.
222 68d89033 Giorgos Verigakis
    fullpath = cstate.fullpath(path)
223 68d89033 Giorgos Verigakis
    if os.path.exists(fullpath):
224 68d89033 Giorgos Verigakis
        os.rename(fullpath, fullpath + '.local')
225 68d89033 Giorgos Verigakis
226 68d89033 Giorgos Verigakis
227 68d89033 Giorgos Verigakis
def sync(path):
228 68d89033 Giorgos Verigakis
    L = lstate.get(path)
229 68d89033 Giorgos Verigakis
    C = cstate.get(path)
230 68d89033 Giorgos Verigakis
    R = rstate.get(path)
231 68d89033 Giorgos Verigakis
232 68d89033 Giorgos Verigakis
    if C == L:
233 d8c26d94 Giorgos Verigakis
        # No local changes
234 68d89033 Giorgos Verigakis
        if R != L:
235 99d0e277 Antony Chazapis
            update_local(path, R)
236 68d89033 Giorgos Verigakis
            lstate.put(path, R)
237 d8c26d94 Giorgos Verigakis
        return
238 d8c26d94 Giorgos Verigakis
    
239 68d89033 Giorgos Verigakis
    if R == L:
240 d8c26d94 Giorgos Verigakis
        # No remote changes
241 68d89033 Giorgos Verigakis
        if C != L:
242 99d0e277 Antony Chazapis
            update_remote(path, C)
243 68d89033 Giorgos Verigakis
            lstate.put(path, C)
244 d8c26d94 Giorgos Verigakis
        return
245 d8c26d94 Giorgos Verigakis
    
246 d8c26d94 Giorgos Verigakis
    # At this point both local and remote states have changes since last sync
247 d8c26d94 Giorgos Verigakis
248 68d89033 Giorgos Verigakis
    if C == R:
249 d8c26d94 Giorgos Verigakis
        # We were lucky, both had the same change
250 68d89033 Giorgos Verigakis
        lstate.put(path, R)
251 d8c26d94 Giorgos Verigakis
    else:
252 d8c26d94 Giorgos Verigakis
        # Conflict, try to resolve it
253 68d89033 Giorgos Verigakis
        resolve_conflict(path)
254 99d0e277 Antony Chazapis
        update_local(path, R)
255 68d89033 Giorgos Verigakis
        lstate.put(path, R)
256 68d89033 Giorgos Verigakis
257 68d89033 Giorgos Verigakis
258 68d89033 Giorgos Verigakis
def walk(dir):
259 68d89033 Giorgos Verigakis
    pending = ['']
260 68d89033 Giorgos Verigakis
    
261 68d89033 Giorgos Verigakis
    while pending:
262 68d89033 Giorgos Verigakis
        dirs = set()
263 68d89033 Giorgos Verigakis
        files = set()
264 68d89033 Giorgos Verigakis
        root = pending.pop(0)
265 68d89033 Giorgos Verigakis
        if root:
266 68d89033 Giorgos Verigakis
            yield root
267 68d89033 Giorgos Verigakis
        
268 68d89033 Giorgos Verigakis
        dirpath = os.path.join(dir, root)
269 68d89033 Giorgos Verigakis
        if os.path.exists(dirpath):
270 68d89033 Giorgos Verigakis
            for filename in os.listdir(dirpath):
271 68d89033 Giorgos Verigakis
                path = os.path.join(root, filename)
272 68d89033 Giorgos Verigakis
                if os.path.isdir(os.path.join(dir, path)):
273 68d89033 Giorgos Verigakis
                    dirs.add(path)
274 68d89033 Giorgos Verigakis
                else:
275 68d89033 Giorgos Verigakis
                    files.add(path)
276 68d89033 Giorgos Verigakis
        
277 b0762fe3 Antony Chazapis
        for object in client.list_objects(get_container(), prefix=root,
278 68d89033 Giorgos Verigakis
                                            delimiter='/', format='json'):
279 3ea35aa0 Antony Chazapis
            # XXX Check subdirs.
280 68d89033 Giorgos Verigakis
            if 'subdir' in object:
281 68d89033 Giorgos Verigakis
                continue
282 68d89033 Giorgos Verigakis
            name = str(object['name'])
283 68d89033 Giorgos Verigakis
            if object['content_type'] == 'application/directory':
284 68d89033 Giorgos Verigakis
                dirs.add(name)
285 68d89033 Giorgos Verigakis
            else:
286 68d89033 Giorgos Verigakis
                files.add(name)
287 68d89033 Giorgos Verigakis
        
288 68d89033 Giorgos Verigakis
        pending += sorted(dirs)
289 68d89033 Giorgos Verigakis
        for path in files:
290 68d89033 Giorgos Verigakis
            yield path
291 d8c26d94 Giorgos Verigakis
292 d8c26d94 Giorgos Verigakis
293 d8c26d94 Giorgos Verigakis
def main():
294 db3c7c26 Antony Chazapis
    global client, conf, confdir, trash, lstate, cstate, rstate
295 68d89033 Giorgos Verigakis
    
296 d8c26d94 Giorgos Verigakis
    if len(sys.argv) != 2:
297 d8c26d94 Giorgos Verigakis
        print 'syntax: %s <dir>' % sys.argv[0]
298 d8c26d94 Giorgos Verigakis
        sys.exit(1)
299 d8c26d94 Giorgos Verigakis
    
300 68d89033 Giorgos Verigakis
    dir = sys.argv[1]
301 68d89033 Giorgos Verigakis
    client = Pithos_Client(get_server(), get_auth(), get_user())
302 68d89033 Giorgos Verigakis
    
303 db3c7c26 Antony Chazapis
    container = get_container()
304 db3c7c26 Antony Chazapis
    try:
305 db3c7c26 Antony Chazapis
        meta = client.retrieve_container_metadata(container)
306 db3c7c26 Antony Chazapis
    except Fault:
307 db3c7c26 Antony Chazapis
        raise RuntimeError("Cannot open container '%s'" % (container,))
308 db3c7c26 Antony Chazapis
    
309 db3c7c26 Antony Chazapis
    conf = {'local': dir,
310 db3c7c26 Antony Chazapis
            'remote': container,
311 db3c7c26 Antony Chazapis
            'blocksize': int(meta['x-container-block-size']),
312 db3c7c26 Antony Chazapis
            'blockhash': meta['x-container-block-hash']}
313 db3c7c26 Antony Chazapis
    confdir = os.path.expanduser('~/.pithos-sync/')
314 db3c7c26 Antony Chazapis
    
315 db3c7c26 Antony Chazapis
    conffile = os.path.join(confdir, 'config')
316 db3c7c26 Antony Chazapis
    if os.path.isfile(conffile):
317 db3c7c26 Antony Chazapis
        try:
318 db3c7c26 Antony Chazapis
            if (conf != pickle.loads(open(conffile, 'rb').read())):
319 db3c7c26 Antony Chazapis
                raise ValueError
320 db3c7c26 Antony Chazapis
        except:
321 db3c7c26 Antony Chazapis
            shutil.rmtree(confdir)
322 db3c7c26 Antony Chazapis
    create_dir(confdir)
323 99d0e277 Antony Chazapis
    
324 99d0e277 Antony Chazapis
    trash = Trash()
325 db3c7c26 Antony Chazapis
    lstate = LocalState(dir)
326 68d89033 Giorgos Verigakis
    cstate = CurrentState(dir)
327 68d89033 Giorgos Verigakis
    rstate = RemoteState(client)
328 68d89033 Giorgos Verigakis
    
329 68d89033 Giorgos Verigakis
    for path in walk(dir):
330 68d89033 Giorgos Verigakis
        print 'Syncing', path
331 68d89033 Giorgos Verigakis
        sync(path)
332 db3c7c26 Antony Chazapis
    
333 db3c7c26 Antony Chazapis
    f = open(conffile, 'wb')
334 db3c7c26 Antony Chazapis
    f.write(pickle.dumps(conf))
335 db3c7c26 Antony Chazapis
    f.close()
336 db3c7c26 Antony Chazapis
    
337 db3c7c26 Antony Chazapis
    trash.empty()
338 d8c26d94 Giorgos Verigakis
339 d8c26d94 Giorgos Verigakis
340 d8c26d94 Giorgos Verigakis
if __name__ == '__main__':
341 d8c26d94 Giorgos Verigakis
    main()