~kevang/mnemosyne-proj/grade-shortcuts-improvements

« back to all changes in this revision

Viewing changes to mnemosyne/openSM2sync/client.py

  • Committer: pbienst
  • Date: 2006-02-09 16:13:13 UTC
  • Revision ID: svn-v3-trunk0:e5e6b78b-db40-0410-9517-b98c64f8d2c1:trunk:2
Initial revision

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#
2
 
# client.py - Max Usachev <maxusachev@gmail.com>
3
 
#             Ed Bartosh <bartosh@gmail.com>
4
 
#             Peter Bienstman <Peter.Bienstman@UGent.be>
5
 
#
6
 
 
7
 
import os
8
 
import socket
9
 
import urllib
10
 
import tarfile
11
 
import httplib
12
 
 
13
 
from partner import Partner
14
 
from text_formats.xml_format import XMLFormat
15
 
from utils import traceback_string, SyncError, SeriousSyncError
16
 
 
17
 
# Avoid delays caused by Nagle's algorithm.
18
 
# http://www.cmlenz.net/archives/2008/03/python-httplib-performance-problems
19
 
 
20
 
realsocket = socket.socket
21
 
def socketwrap(family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0):
22
 
    sockobj = realsocket(family, type, proto)
23
 
    sockobj.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
24
 
    return sockobj
25
 
socket.socket = socketwrap
26
 
 
27
 
# Buffer the response socket.
28
 
# http://mail.python.org/pipermail/python-bugs-list/2006-September/035156.html
29
 
# Fix included here for systems running Python 2.5.
30
 
 
31
 
class HTTPResponse(httplib.HTTPResponse):
32
 
 
33
 
    def __init__(self, sock, **kw):
34
 
        httplib.HTTPResponse.__init__(self, sock, **kw)
35
 
        self.fp = sock.makefile("rb") # Was unbuffered: sock.makefile("rb", 0)
36
 
 
37
 
httplib.HTTPConnection.response_class = HTTPResponse
38
 
 
39
 
# Register binary formats.
40
 
 
41
 
from binary_formats.mnemosyne_format import MnemosyneFormat
42
 
BinaryFormats = [MnemosyneFormat]
43
 
 
44
 
 
45
 
class Client(Partner):
46
 
 
47
 
    program_name = "unknown-SRS-app"
48
 
    program_version = "unknown"
49
 
    BUFFER_SIZE = 8192
50
 
    # The capabilities supported by the client. Note that we assume that the
51
 
    # server supports "mnemosyne_dynamic_cards".
52
 
    capabilities = "mnemosyne_dynamic_cards"  # "facts", "cards"
53
 
    # The following setting can be set to False to speed up the syncing
54
 
    # process on e.g. mobile clients where the media files don't get edited
55
 
    # externally.
56
 
    check_for_edited_local_media_files = True
57
 
    # Setting the following to False will speed up the initial sync, but in that
58
 
    # case the client will not have access to all of the review history in order
59
 
    # to e.g. display statistics. Also, it will not be possible to keep the
60
 
    # local database when there are sync conflicts. If a client makes it
61
 
    # possible for the user to change this value, doing so should result in
62
 
    # redownloading the entire database from scratch.
63
 
    interested_in_old_reps = True
64
 
    # Store prerendered question, answer and tag fields in database. The only
65
 
    # benefit of this is fast operation for a 'browse cards' dialog which
66
 
    # directly operates at the SQL level. If you don't use this, set to False
67
 
    # to reduce the database size.
68
 
    store_pregenerated_data = True
69
 
    # On mobile clients with slow SD cards copying a large database for the
70
 
    # backup before sync can take longer than the sync itself, so we offer
71
 
    # reckless users the possibility to skip this.
72
 
    do_backup = True
73
 
    # Setting this to False will leave all the uploading of anonymous science
74
 
    # logs to the sync server. Recommended to set this to False for mobile
75
 
    # clients, which are not always guaranteed to have an internet connection.
76
 
    upload_science_logs = True
77
 
 
78
 
    def __init__(self, machine_id, database, ui):
79
 
        Partner.__init__(self, ui)
80
 
        self.machine_id = machine_id
81
 
        self.database = database
82
 
        self.text_format = XMLFormat()
83
 
        self.server_info = {}
84
 
        self.con = None
85
 
        self.behind_proxy = None  # Explicit variable for testability.
86
 
        self.proxy = None
87
 
 
88
 
    def request_connection(self):
89
 
 
90
 
        """If we are not behind a proxy, create the connection once and reuse
91
 
        it for all requests. If we are behind a proxy, we need to revert to
92
 
        HTTP 1.0 and use a separate connection for each request.
93
 
 
94
 
        """
95
 
 
96
 
        # If we haven't done so, determine whether we're behind a proxy.
97
 
        if self.behind_proxy is None:
98
 
            import urllib
99
 
            proxies = urllib.getproxies()
100
 
            if "http" in proxies:
101
 
                self.behind_proxy = True
102
 
                self.proxy = proxies["http"]
103
 
            else:
104
 
                self.behind_proxy = False
105
 
        # Create a new connection or reuse an existing one.
106
 
        if self.behind_proxy:
107
 
            httplib.HTTPConnection._http_vsn = 10
108
 
            httplib.HTTPConnection._http_vsn_str = "HTTP/1.0"
109
 
            if self.proxy is not None:
110
 
                self.con = httplib.HTTPConnection(self.proxy, self.port)
111
 
            else:  # Testsuite has set self.behind_proxy to True to simulate
112
 
                # being behind a proxy.
113
 
                self.con = httplib.HTTPConnection(self.server, self.port)
114
 
        else:
115
 
            httplib.HTTPConnection._http_vsn = 11
116
 
            httplib.HTTPConnection._http_vsn_str = "HTTP/1.1"
117
 
            if not self.con:
118
 
                self.con = httplib.HTTPConnection(self.server, self.port)
119
 
 
120
 
    def url(self, url_string):
121
 
        if self.behind_proxy and self.proxy:
122
 
            url_string = self.server + ":/" + url_string
123
 
        return url_string
124
 
 
125
 
    def sync(self, server, port, username, password):
126
 
        try:
127
 
            self.server = socket.gethostbyname(server)
128
 
            self.port = port
129
 
            if self.do_backup:
130
 
                self.ui.set_progress_text("Creating backup...")
131
 
                backup_file = self.database.backup()
132
 
            # We check if files were edited outside of the program. This can
133
 
            # generate EDITED_MEDIA_FILES log entries, so it should be done
134
 
            # first.
135
 
            if self.check_for_edited_local_media_files:
136
 
                self.ui.set_progress_text("Checking for edited media files...")
137
 
                self.database.check_for_edited_media_files()
138
 
                self.ui.set_progress_text("Dynamically creating media files...")
139
 
                self.database.dynamically_create_media_files()
140
 
            # Set timeout long enough for e.g. a slow NAS waking from 
141
 
            # hibernation.
142
 
            socket.setdefaulttimeout(45)
143
 
            self.login(username, password)
144
 
            # Generating media files at the server side could take some time,
145
 
            # so we update the timeout.
146
 
            self.con = None
147
 
            socket.setdefaulttimeout(15*60)
148
 
            self.get_server_check_media_files()
149
 
            # Do a full sync after either the client or the server has restored
150
 
            # from a backup.
151
 
            if self.database.is_sync_reset_needed(\
152
 
                self.server_info["machine_id"]) or \
153
 
                self.server_info["sync_reset_needed"] == True:
154
 
                self.resolve_conflicts(restored_from_backup=True)
155
 
            # First sync, fetch database from server.
156
 
            elif self.database.is_empty():
157
 
                self.get_server_media_files()
158
 
                if self.server_info["supports_binary_transfer"]:
159
 
                    self.get_server_entire_database_binary()
160
 
                else:
161
 
                    self.get_server_entire_database()
162
 
                self.get_sync_finish()
163
 
                # Fetch config settings.
164
 
                self.login(username, password)
165
 
                self.get_server_generate_log_entries_for_settings()
166
 
                self.get_server_log_entries()
167
 
                self.get_sync_finish()
168
 
            # First sync, put binary database to server if supported.
169
 
            elif not self.database.is_empty() and \
170
 
                    self.server_info["is_database_empty"] and \
171
 
                    self.supports_binary_upload():
172
 
                self.put_client_media_files(reupload_all=True)
173
 
                self.put_client_entire_database_binary()
174
 
                self.get_sync_finish()
175
 
                # Upload config settings.
176
 
                self.login(username, password)
177
 
                self.database.generate_log_entries_for_settings()
178
 
                self.put_client_log_entries()
179
 
                self.get_server_log_entries()
180
 
                self.get_sync_finish()
181
 
            else:
182
 
                # Upload local changes and check for conflicts.
183
 
                result = self.put_client_log_entries()
184
 
                if result == "OK":
185
 
                    self.put_client_media_files()
186
 
                    self.get_server_media_files()
187
 
                    self.get_server_log_entries()
188
 
                    self.get_sync_finish()
189
 
                else:
190
 
                    self.resolve_conflicts()
191
 
            self.ui.show_information("Sync finished!")
192
 
        except Exception, exception:
193
 
            self.ui.close_progress()
194
 
            serious = True
195
 
            if type(exception) == type(socket.gaierror()):
196
 
                self.ui.show_error("Could not find server!")
197
 
                serious = False
198
 
            elif type(exception) == type(socket.error()):
199
 
                self.ui.show_error("Could not connect to server!")
200
 
                serious = False
201
 
            elif type(exception) == type(socket.timeout()):
202
 
                self.ui.show_error("Timeout while waiting for server!")
203
 
            elif type(exception) == type(SyncError()):
204
 
                self.ui.show_error(str(exception))
205
 
                serious = False
206
 
            elif type(exception) == type(SeriousSyncError()):
207
 
                self.ui.show_error(str(exception))
208
 
            else:
209
 
                self.ui.show_error(traceback_string())
210
 
            if serious and self.do_backup:
211
 
                # Only serious errors should result in the need for a full
212
 
                # sync next time.
213
 
                self.ui.show_error("Sync failed, restoring from backup. " + \
214
 
                    "The next sync will need to be a full sync.")
215
 
                if backup_file:
216
 
                    self.database.restore(backup_file)
217
 
        finally:
218
 
            if self.con:
219
 
                self.con.close()
220
 
            self.ui.close_progress()
221
 
 
222
 
    def supports_binary_upload(self):
223
 
        return self.capabilities == "mnemosyne_dynamic_cards" and \
224
 
            self.interested_in_old_reps and self.store_pregenerated_data \
225
 
            and self.program_name == self.server_info["program_name"] and \
226
 
            self.program_version == self.server_info["program_version"]
227
 
 
228
 
    def resolve_conflicts(self, restored_from_backup=False):
229
 
        if restored_from_backup:
230
 
            message = "The database was restored from a backup, either " + \
231
 
                "automatically because of an aborted sync or manually by " + \
232
 
                "the user.\nFor safety, a full sync needs to happen and " + \
233
 
                "you need to choose which copy of the database to keep and " + \
234
 
                "which copy to discard.\n"
235
 
        else:
236
 
            message = "Conflicts detected during sync! Choose which version to keep and which version to discard."
237
 
        # Ask for conflict resolution direction.
238
 
        if self.supports_binary_upload():
239
 
            result = self.ui.show_question(message,
240
 
                "Keep local version", "Fetch remote version", "Cancel")
241
 
            results = {0: "KEEP_LOCAL", 1: "KEEP_REMOTE", 2: "CANCEL"}
242
 
            result = results[result]
243
 
        else:
244
 
            message += " " + "Your client only stores part of the server " + \
245
 
                "database or uses a different software version, " + \
246
 
                "so you can only fetch the remote version."
247
 
            result = self.ui.show_question(message,
248
 
                "Fetch remote version", "Cancel", "")
249
 
            results = {0: "KEEP_REMOTE", 1: "CANCEL"}
250
 
            result = results[result]
251
 
        # Keep remote. Reset the partnerships afterwards, such that syncing
252
 
        # with a third party will also trigger a full sync.
253
 
        if result == "KEEP_REMOTE":
254
 
            self.get_server_media_files(redownload_all=True)
255
 
            if self.server_info["supports_binary_transfer"]:
256
 
                self.get_server_entire_database_binary()
257
 
            else:
258
 
                self.get_server_entire_database()
259
 
            self.database.reset_partnerships()
260
 
            self.get_sync_finish()
261
 
        # Keep local.
262
 
        elif result == "KEEP_LOCAL":
263
 
            self.put_client_media_files(reupload_all=True)
264
 
            self.put_client_entire_database_binary()
265
 
            self.get_sync_finish()
266
 
        # Cancel.
267
 
        elif result == "CANCEL":
268
 
            self.get_sync_cancel()
269
 
 
270
 
    def _check_response_for_errors(self, response, can_consume_response=True):
271
 
        # Check for non-Mnemosyne error messages.
272
 
        if response.status != httplib.OK:
273
 
            raise SeriousSyncError("Internal server error:\n" + response.read())
274
 
        if can_consume_response == False:
275
 
            return
276
 
        # Check for Mnemosyne error messages.
277
 
        message, traceback = self.text_format.parse_message(response.read())
278
 
        if "server error" in message.lower():
279
 
            raise SeriousSyncError(message + "\n" + traceback)
280
 
 
281
 
    def login(self, username, password):
282
 
        self.ui.set_progress_text("Logging in...")
283
 
        client_info = {}
284
 
        client_info["username"] = username
285
 
        client_info["password"] = password
286
 
        client_info["user_id"] = self.database.user_id()
287
 
        client_info["machine_id"] = self.machine_id
288
 
        client_info["program_name"] = self.program_name
289
 
        client_info["program_version"] = self.program_version
290
 
        client_info["database_name"] = self.database.name()
291
 
        client_info["database_version"] = self.database.version
292
 
        client_info["capabilities"] = self.capabilities
293
 
        client_info["partners"] = self.database.partners()
294
 
        client_info["interested_in_old_reps"] = self.interested_in_old_reps
295
 
        client_info["store_pregenerated_data"] = self.store_pregenerated_data
296
 
        client_info["upload_science_logs"] = self.upload_science_logs
297
 
        # Signal if the database is empty, so that the server does not give a
298
 
        # spurious sync cycle warning if the client database was reset.
299
 
        client_info["is_database_empty"] = self.database.is_empty()
300
 
        # Not yet implemented: preferred renderer.
301
 
        client_info["render_chain"] = ""
302
 
        # Add optional program-specific information.
303
 
        client_info = self.database.append_to_sync_partner_info(client_info)
304
 
        # Try to establish a connection, but don't force a restore from backup
305
 
        # if we can't login.
306
 
        try:
307
 
            self.request_connection()
308
 
            self.con.request("PUT", self.url("/login"),
309
 
                self.text_format.repr_partner_info(client_info).\
310
 
                encode("utf-8") + "\n")
311
 
            response = self.con.getresponse()
312
 
        except Exception, e:
313
 
            raise SyncError("Could not connect to server!")
314
 
        # Check for errors, but don't force a restore from backup if we can't
315
 
        # login.
316
 
        try:
317
 
            self._check_response_for_errors(\
318
 
                response, can_consume_response=False)
319
 
        except SeriousSyncError:
320
 
            raise SyncError("Logging in: server error.")
321
 
        response = response.read()
322
 
        if "message" in response:
323
 
            message, traceback = self.text_format.parse_message(response)
324
 
            message = message.lower()
325
 
            if "server error" in message:
326
 
                raise SyncError("Logging in: server error.")
327
 
            if "access denied" in message:
328
 
                raise SyncError("Wrong username or password.")
329
 
            if "cycle" in message:
330
 
                raise SyncError(\
331
 
                    "Sync cycle detected. Sync through intermediate partner.")
332
 
            if "same machine ids" in message:
333
 
                raise SyncError(\
334
 
"You have manually copied the data directory before sync. Sync needs to start from an empty database.")
335
 
        self.server_info = self.text_format.parse_partner_info(response)
336
 
        self.database.set_sync_partner_info(self.server_info)
337
 
        if self.database.is_empty():
338
 
            self.database.change_user_id(self.server_info["user_id"])
339
 
        elif self.server_info["user_id"] != client_info["user_id"] and \
340
 
            self.server_info["is_database_empty"] == False:
341
 
            raise SyncError("Error: mismatched user ids.\n" + \
342
 
                "The first sync should happen on an empty database.")
343
 
        self.database.create_if_needed_partnership_with(\
344
 
            self.server_info["machine_id"])
345
 
        self.database.merge_partners(self.server_info["partners"])
346
 
 
347
 
    def get_server_check_media_files(self):
348
 
        self.ui.set_progress_text("Asking server to check for updated media files...")
349
 
        self.request_connection()
350
 
        self.con.request("GET", self.url(\
351
 
            "/server_check_media_files?" + \
352
 
            "session_token=%s" % (self.server_info["session_token"], )))
353
 
        response = self.con.getresponse()
354
 
        self._check_response_for_errors(response, can_consume_response=True)
355
 
 
356
 
    def put_client_log_entries(self):
357
 
 
358
 
        """Contrary to binary files, the size of the log is not known until we
359
 
        create it. In order to save memory on mobile devices, we don't want to
360
 
        construct the entire log in memory before sending it on to the server.
361
 
        However, chunked uploads are in the grey area of the WSGI spec and are
362
 
        also not supported by older HTTP 1.0 proxies (e.g. Squid before 3.1).
363
 
        Therefore, as a compromise, rather then streaming chunks in a single
364
 
        message, we break up the entire log in different messages of size
365
 
        self.BUFFER_SIZE.
366
 
 
367
 
        """
368
 
 
369
 
        number_of_entries = self.database.number_of_log_entries_to_sync_for(\
370
 
            self.server_info["machine_id"])
371
 
        if number_of_entries == 0:
372
 
            return
373
 
        self.ui.set_progress_text("Sending log entries...")
374
 
        self.ui.set_progress_range(number_of_entries)
375
 
        self.ui.set_progress_update_interval(number_of_entries/20)
376
 
        buffer = ""
377
 
        count = 0
378
 
        for log_entry in self.database.log_entries_to_sync_for(\
379
 
                self.server_info["machine_id"]):
380
 
            buffer += self.text_format.repr_log_entry(log_entry)
381
 
            count += 1
382
 
            self.ui.increase_progress(1)
383
 
            if len(buffer) > self.BUFFER_SIZE or count == number_of_entries:
384
 
                buffer = \
385
 
                    self.text_format.log_entries_header(number_of_entries) \
386
 
                    + buffer + self.text_format.log_entries_footer()
387
 
                self.request_connection()
388
 
                self.con.request("PUT", self.url(\
389
 
                    "/client_log_entries?session_token=%s" \
390
 
                    % (self.server_info["session_token"],)),
391
 
                    buffer.encode("utf-8"))
392
 
                buffer = ""
393
 
                response = self.con.getresponse()
394
 
                self._check_response_for_errors(response,
395
 
                    can_consume_response=False)
396
 
                response = response.read()
397
 
                message, traceback = self.text_format.parse_message(response)
398
 
                message = message.lower()
399
 
                if "server error" in message:
400
 
                    raise SeriousSyncError(message)
401
 
                if "conflict" in message:
402
 
                    return "conflict"
403
 
        return "OK"
404
 
 
405
 
    def put_client_entire_database_binary(self):
406
 
        self.ui.set_progress_text("Sending entire binary database...")
407
 
        for BinaryFormat in BinaryFormats:
408
 
            binary_format = BinaryFormat(self.database)
409
 
            if binary_format.supports(self.server_info["program_name"],
410
 
                self.server_info["program_version"],
411
 
                self.server_info["database_version"]):
412
 
                assert self.store_pregenerated_data == True
413
 
                assert self.interested_in_old_reps == True
414
 
                filename = binary_format.binary_filename(\
415
 
                    self.store_pregenerated_data, self.interested_in_old_reps)
416
 
                break
417
 
        self.request_connection()
418
 
        self.con.putrequest("PUT",
419
 
                self.url("/client_entire_database_binary?session_token=%s" \
420
 
                % (self.server_info["session_token"], )))
421
 
        self.con.putheader("content-length", os.path.getsize(filename))
422
 
        self.con.endheaders()
423
 
        for buffer in self.stream_binary_file(filename):
424
 
            self.con.send(buffer)
425
 
        binary_format.clean_up()
426
 
        self._check_response_for_errors(self.con.getresponse())
427
 
 
428
 
    def _download_log_entries(self, stream):
429
 
        element_loop = self.text_format.parse_log_entries(stream)
430
 
        number_of_entries = int(element_loop.next())
431
 
        if number_of_entries == 0:
432
 
            return
433
 
        self.ui.set_progress_range(number_of_entries)
434
 
        self.ui.set_progress_update_interval(number_of_entries/50)
435
 
        for log_entry in element_loop:
436
 
            self.database.apply_log_entry(log_entry)
437
 
            self.ui.increase_progress(1)
438
 
        self.ui.set_progress_value(number_of_entries)
439
 
 
440
 
    def get_server_log_entries(self):
441
 
        self.ui.set_progress_text("Getting log entries...")
442
 
        if self.upload_science_logs:
443
 
            self.database.dump_to_science_log()
444
 
        self.request_connection()
445
 
        self.con.request("GET", self.url(\
446
 
            "/server_log_entries?session_token=%s" \
447
 
            % (self.server_info["session_token"], )))
448
 
        response = self.con.getresponse()
449
 
        self._check_response_for_errors(response, can_consume_response=False)
450
 
        self._download_log_entries(response)
451
 
        # The server will always upload the science logs of the log events
452
 
        # which originated at the server side.
453
 
        self.database.skip_science_log()
454
 
 
455
 
    def get_server_entire_database(self):
456
 
        self.ui.set_progress_text("Getting entire database...")
457
 
        filename = self.database.path()
458
 
        # Create a new database. Note that this also resets the
459
 
        # partnerships, as required.
460
 
        self.database.new(filename)
461
 
        self.request_connection()
462
 
        self.con.request("GET", self.url("/server_entire_database?" + \
463
 
            "session_token=%s" % (self.server_info["session_token"], )))
464
 
        response = self.con.getresponse()
465
 
        self._check_response_for_errors(response, can_consume_response=False)
466
 
        self._download_log_entries(response)
467
 
        self.database.load(filename)
468
 
        # The server will always upload the science logs of the log events
469
 
        # which originated at the server side.
470
 
        self.database.skip_science_log()
471
 
        # Since we start from a new database, we need to create the
472
 
        # partnership again.
473
 
        self.database.create_if_needed_partnership_with(\
474
 
            self.server_info["machine_id"])
475
 
 
476
 
    def get_server_entire_database_binary(self):
477
 
        self.ui.set_progress_text("Getting entire binary database...")
478
 
        filename = self.database.path()
479
 
        self.database.abandon()
480
 
        self.request_connection()
481
 
        self.con.request("GET", self.url(\
482
 
            "/server_entire_database_binary?" + \
483
 
            "session_token=%s" % (self.server_info["session_token"], )))
484
 
        response = self.con.getresponse()
485
 
        self._check_response_for_errors(response, can_consume_response=False)
486
 
        file_size = int(response.getheader("mnemosyne-content-length"))
487
 
        self.download_binary_file(response, filename, file_size)
488
 
        self.database.load(filename)
489
 
        self.database.create_if_needed_partnership_with(\
490
 
            self.server_info["machine_id"])
491
 
        self.database.remove_partnership_with(self.machine_id)
492
 
 
493
 
    def get_server_generate_log_entries_for_settings(self):
494
 
        self.ui.set_progress_text("Getting settings...")
495
 
        self.request_connection()
496
 
        self.con.request("GET", self.url(\
497
 
            "/server_generate_log_entries_for_settings?" + \
498
 
            "session_token=%s" % (self.server_info["session_token"], )))
499
 
        response = self.con.getresponse()
500
 
        self._check_response_for_errors(response, can_consume_response=True)
501
 
 
502
 
    def put_client_media_files(self, reupload_all=False):
503
 
        self.ui.set_progress_text("Sending media files...")
504
 
        if reupload_all:
505
 
            filenames = list(self.database.all_media_filenames())
506
 
        else:
507
 
            filenames = list(self.database.media_filenames_to_sync_for(\
508
 
                self.server_info["machine_id"]))
509
 
        total_size = 0
510
 
        for filename in filenames:
511
 
            total_size += os.path.getsize(os.path.join(\
512
 
                self.database.media_dir(), filename))
513
 
        self.ui.set_progress_range(total_size)
514
 
        self.ui.set_progress_update_interval(total_size/50)
515
 
        for filename in filenames:
516
 
            self.request_connection()
517
 
            self.con.putrequest("PUT",
518
 
                self.url("/client_media_file?session_token=%s&filename=%s" \
519
 
                % (self.server_info["session_token"],
520
 
                urllib.quote(filename.encode("utf-8"), ""))))
521
 
            full_path = os.path.join(self.database.media_dir(), filename)
522
 
            file_size = os.path.getsize(full_path)
523
 
            self.con.putheader("content-length", file_size)
524
 
            self.con.endheaders()
525
 
            for buffer in self.stream_binary_file(full_path, progress_bar=False):
526
 
                self.con.send(buffer)
527
 
                self.ui.increase_progress(len(buffer))
528
 
            self._check_response_for_errors(self.con.getresponse())
529
 
        self.ui.set_progress_value(total_size)
530
 
 
531
 
    def get_server_media_files(self, redownload_all=False):
532
 
        self.ui.set_progress_text("Getting list of media files to download...")
533
 
        # Get list of names of all media files to download.
534
 
        media_url = "/server_media_filenames?session_token=%s" \
535
 
            % (self.server_info["session_token"], )
536
 
        if redownload_all:
537
 
             media_url += "&redownload_all=1"
538
 
        self.request_connection()
539
 
        self.con.request("GET", self.url(media_url))
540
 
        response = self.con.getresponse()
541
 
        self._check_response_for_errors(response, can_consume_response=False)
542
 
        total_size = int(response.getheader("mnemosyne-content-length"))
543
 
        if total_size == 0:
544
 
            # Make sure to read the full message, even if it's empty,
545
 
            # since we reuse our connection.
546
 
            response.read()
547
 
            return
548
 
        # Download each media file.
549
 
        self.ui.set_progress_text("Getting media files...")
550
 
        self.ui.set_progress_range(total_size)
551
 
        self.ui.set_progress_update_interval(total_size/50)
552
 
        for filename in response.read().split("\n"):
553
 
            filename = unicode(filename, "utf-8")
554
 
            self.request_connection()
555
 
            self.con.request("GET",
556
 
                self.url("/server_media_file?session_token=%s&filename=%s" \
557
 
                % (self.server_info["session_token"],
558
 
                urllib.quote(filename.encode("utf-8"), ""))))
559
 
            response = self.con.getresponse()
560
 
            self._check_response_for_errors(response,
561
 
                can_consume_response=False)
562
 
            file_size = int(response.getheader("mnemosyne-content-length"))
563
 
            # Make sure a malicious server cannot overwrite anything outside
564
 
            # of the media directory.
565
 
            filename = filename.replace("../", "").replace("..\\", "")
566
 
            filename = filename.replace("/..", "").replace("\\..", "")
567
 
            filename = os.path.join(self.database.media_dir(), filename)
568
 
            self.download_binary_file(response, filename,
569
 
                                      file_size, progress_bar=False)
570
 
            self.ui.increase_progress(file_size)
571
 
        self.ui.set_progress_value(total_size)
572
 
 
573
 
    def get_sync_cancel(self):
574
 
        self.ui.set_progress_text("Cancelling sync...")
575
 
        self.request_connection()
576
 
        self.con.request("GET", self.url("/sync_cancel?session_token=%s" \
577
 
            % (self.server_info["session_token"], )),
578
 
            headers={"connection": "close"})
579
 
        self._check_response_for_errors(self.con.getresponse())
580
 
 
581
 
    def get_sync_finish(self):
582
 
        self.ui.set_progress_text("Finishing sync...")
583
 
        self.request_connection()
584
 
        self.con.request("GET", self.url("/sync_finish?session_token=%s" \
585
 
            % (self.server_info["session_token"], )),
586
 
            headers={"connection": "close"})
587
 
        self._check_response_for_errors(self.con.getresponse())
588
 
        # Only update after we are sure there have been no errors.
589
 
        self.database.update_last_log_index_synced_for(\
590
 
            self.server_info["machine_id"])