Skip to content

Commit b0a165c

Browse files
committed
Basic logging of sync process to .mergin/client-log.txt
1 parent 9c3dc16 commit b0a165c

File tree

4 files changed

+79
-19
lines changed

4 files changed

+79
-19
lines changed

mergin/client_pull.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
import copy
1313
import math
1414
import os
15+
import pprint
1516
import shutil
1617

1718
import concurrent.futures
18-
import threading
1919

2020
from .common import CHUNK_SIZE, ClientError
2121
from .merginproject import MerginProject
@@ -76,14 +76,14 @@ def _download_items(file, directory, diff_only=False):
7676
return items
7777

7878

79-
def _do_download(item, mc, project_path, job):
79+
def _do_download(item, mc, mp, project_path, job):
8080
""" runs in worker thread """
8181
if job.is_cancelled:
8282
return
8383

8484
# TODO: make download_blocking / save_to_file cancellable so that we can cancel as soon as possible
8585

86-
item.download_blocking(mc, project_path)
86+
item.download_blocking(mc, mp, project_path)
8787
job.transferred_size += item.size
8888

8989

@@ -99,9 +99,13 @@ def download_project_async(mc, project_path, directory):
9999
os.makedirs(directory)
100100
mp = MerginProject(directory)
101101

102+
mp.log.info(f"--- start download {project_path}")
103+
102104
project_info = mc.project_info(project_path)
103105
version = project_info['version'] if project_info['version'] else 'v0'
104106

107+
mp.log.info(f"got project info. version {version}")
108+
105109
# prepare download
106110
update_tasks = [] # stuff to do at the end of download
107111
for file in project_info['files']:
@@ -116,14 +120,16 @@ def download_project_async(mc, project_path, directory):
116120
download_list.extend(task.download_queue_items)
117121
for item in task.download_queue_items:
118122
total_size += item.size
123+
124+
mp.log.info(f"will download {len(update_tasks)} files in {len(download_list)} chunks, total size {total_size}")
119125

120126
job = DownloadJob(project_path, total_size, version, update_tasks, download_list, directory, mp, project_info)
121127

122128
# start download
123129
job.executor = concurrent.futures.ThreadPoolExecutor(max_workers=4)
124130
job.futures = []
125131
for item in download_list:
126-
future = job.executor.submit(_do_download, item, mc, project_path, job)
132+
future = job.executor.submit(_do_download, item, mc, mp, project_path, job)
127133
job.futures.append(future)
128134

129135
return job
@@ -167,6 +173,8 @@ def download_project_finalize(job):
167173
if future.exception() is not None:
168174
raise future.exception()
169175

176+
job.mp.log.info("--- download finished")
177+
170178
for task in job.update_tasks:
171179

172180
# right now only copy tasks...
@@ -235,9 +243,10 @@ def __repr__(self):
235243
return "<DownloadQueueItem path={} version={} diff_only={} part_index={} size={} dest={}>".format(
236244
self.file_path, self.version, self.diff_only, self.part_index, self.size, self.download_file_path)
237245

238-
def download_blocking(self, mc, project_path):
246+
def download_blocking(self, mc, mp, project_path):
239247
""" Starts download and only returns once the file has been fully downloaded and saved """
240248

249+
mp.log.debug(f"Downloading {self.file_path} version={self.version} diff={self.diff_only} part={self.part_index}")
241250
start = self.part_index * (1 + CHUNK_SIZE)
242251
resp = mc.get("/v1/project/raw/{}".format(project_path), data={
243252
"file": self.file_path,
@@ -248,9 +257,11 @@ def download_blocking(self, mc, project_path):
248257
}
249258
)
250259
if resp.status in [200, 206]:
260+
mp.log.debug(f"Download finished: {self.file_path}")
251261
save_to_file(resp, self.download_file_path)
252262
else:
253-
raise ClientError('Failed to download part {} of file {}'.format(part, basename))
263+
mp.log.error(f"Download failed: {self.file_path}")
264+
raise ClientError('Failed to download part {} of file {}'.format(self.part_index, self.file_path))
254265

255266

256267
class PullJob:
@@ -290,19 +301,27 @@ def pull_project_async(mc, directory):
290301
mp = MerginProject(directory)
291302
project_path = mp.metadata["name"]
292303
local_version = mp.metadata["version"]
304+
305+
mp.log.info(f"--- start pull {project_path}")
306+
293307
server_info = mc.project_info(project_path, since=local_version)
294-
if local_version == server_info["version"]:
308+
server_version = server_info["version"]
309+
310+
mp.log.info(f"got project info. version {server_version}")
311+
312+
if local_version == server_version:
313+
mp.log.info("--- pull - nothing to do (already at server version)")
295314
return # Project is up to date
296315

297316
# we either download a versioned file using diffs (strongly preferred),
298317
# but if we don't have history with diffs (e.g. uploaded without diffs)
299318
# then we just download the whole file
300319
_pulling_file_with_diffs = lambda f: 'diffs' in f and len(f['diffs']) != 0
301320

302-
server_version = server_info["version"]
303321
temp_dir = mp.fpath_meta(f'fetch_{local_version}-{server_version}')
304322
os.makedirs(temp_dir, exist_ok=True)
305323
pull_changes = mp.get_pull_changes(server_info["files"])
324+
mp.log.debug("pull changes:\n" + pprint.pformat(pull_changes))
306325
fetch_files = []
307326
for f in pull_changes["added"]:
308327
f['version'] = server_version
@@ -364,13 +383,15 @@ def pull_project_async(mc, directory):
364383
for item in file_to_merge.downloaded_items:
365384
total_size += item.size
366385

386+
mp.log.info(f"will download {len(download_list)} chunks, total size {total_size}")
387+
367388
job = PullJob(project_path, pull_changes, total_size, server_version, files_to_merge, download_list, temp_dir, mp, server_info, basefiles_to_patch)
368389

369390
# start download
370391
job.executor = concurrent.futures.ThreadPoolExecutor(max_workers=4)
371392
job.futures = []
372393
for item in download_list:
373-
future = job.executor.submit(_do_download, item, mc, project_path, job)
394+
future = job.executor.submit(_do_download, item, mc, mp, project_path, job)
374395
job.futures.append(future)
375396

376397
return job
@@ -450,6 +471,8 @@ def pull_project_finalize(job):
450471
if future.exception() is not None:
451472
raise future.exception()
452473

474+
job.mp.log.info("finalizing pull")
475+
453476
# merge downloaded chunks
454477
for file_to_merge in job.files_to_merge:
455478
file_to_merge.merge()
@@ -478,6 +501,8 @@ def pull_project_finalize(job):
478501
'version': job.version if job.version else "v0", # for new projects server version is ""
479502
'files': job.project_info['files']
480503
}
481-
504+
505+
job.mp.log.info("--- pull finished")
506+
482507
shutil.rmtree(job.temp_dir)
483508
return conflicts

mergin/client_push.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111

1212
import json
1313
import hashlib
14+
import pprint
1415
import concurrent.futures
15-
import threading
1616

1717
from .common import UPLOAD_CHUNK_SIZE, ClientError, SyncError
1818
from .merginproject import MerginProject
@@ -53,18 +53,21 @@ def __init__(self, file_path, size, transaction_id, chunk_id, chunk_index):
5353
self.chunk_index = chunk_index # index (starting from zero) of the chunk within the file
5454
self.transaction_id = transaction_id # ID of the transaction
5555

56-
def upload_blocking(self, mc):
56+
def upload_blocking(self, mc, mp):
5757

5858
file_handle = open(self.file_path, 'rb')
5959
file_handle.seek(self.chunk_index * UPLOAD_CHUNK_SIZE)
6060
data = file_handle.read(UPLOAD_CHUNK_SIZE)
6161

6262
checksum = hashlib.sha1()
6363
checksum.update(data)
64-
64+
65+
mp.log.debug(f"Uploading {self.file_path} part={self.chunk_index}")
66+
6567
headers = {"Content-Type": "application/octet-stream"}
6668
resp = mc.post("/v1/project/push/chunk/{}/{}".format(self.transaction_id, self.chunk_id), data, headers)
6769
resp_dict = json.load(resp)
70+
mp.log.debug(f"Upload finished: {self.file_path}")
6871
if not (resp_dict['size'] == len(data) and resp_dict['checksum'] == checksum.hexdigest()):
6972
mc.post("/v1/project/push/cancel/{}".format(self.transaction_id))
7073
raise ClientError("Mismatch between uploaded file chunk {} and local one".format(self.chunk_id))
@@ -77,19 +80,31 @@ def push_project_async(mc, directory):
7780
mp = MerginProject(directory)
7881
project_path = mp.metadata["name"]
7982
local_version = mp.metadata["version"]
83+
84+
mp.log.info(f"--- start push {project_path}")
85+
8086
server_info = mc.project_info(project_path)
8187
server_version = server_info["version"] if server_info["version"] else "v0"
88+
89+
mp.log.info(f"got project info. version {server_version}")
90+
8291
if local_version != server_version:
83-
raise ClientError("Update your local repository")
92+
mp.log.error(f"--- push {project_path} - not up to date (local {local_version} vs server {server_version})")
93+
raise ClientError("There is a new version of the project on the server. Please update your local copy." +
94+
f"\n\nLocal version: {local_version}\nServer version: {server_version}")
8495

8596
changes = mp.get_push_changes()
97+
mp.log.debug("push changes:\n" + pprint.pformat(changes))
8698
enough_free_space, freespace = mc.enough_storage_available(changes)
8799
if not enough_free_space:
88100
freespace = int(freespace/(1024*1024))
101+
mp.log.error(f"--- push {project_path} - not enough space")
89102
raise SyncError("Storage limit has been reached. Only " + str(freespace) + "MB left")
90103

91104
if not sum(len(v) for v in changes.values()):
105+
mp.log.info(f"--- push {project_path} - nothing to do")
92106
return
107+
93108
# drop internal info from being sent to server
94109
for item in changes['updated']:
95110
item.pop('origin_checksum', None)
@@ -107,10 +122,13 @@ def push_project_async(mc, directory):
107122
job = UploadJob(project_path, changes, transaction_id, mp, mc)
108123

109124
if not upload_files:
125+
mp.log.info("not uploading any files")
110126
job.server_resp = server_resp
111127
push_project_finalize(job)
112128
return None # all done - no pending job
113-
129+
130+
mp.log.info(f"got transaction ID {transaction_id}")
131+
114132
upload_queue_items = []
115133
total_size = 0
116134
# prepare file chunks for upload
@@ -129,7 +147,9 @@ def push_project_async(mc, directory):
129147

130148
job.total_size = total_size
131149
job.upload_queue_items = upload_queue_items
132-
150+
151+
mp.log.info(f"will upload {len(upload_queue_items)} items with total size {total_size}")
152+
133153
# start uploads in background
134154
job.executor = concurrent.futures.ThreadPoolExecutor(max_workers=4)
135155
for item in upload_queue_items:
@@ -186,17 +206,19 @@ def push_project_finalize(job):
186206

187207
if with_upload_of_files:
188208
try:
209+
job.mp.log.info(f"Finishing transaction {job.transaction_id}")
189210
resp = job.mc.post("/v1/project/push/finish/%s" % job.transaction_id)
190211
job.server_resp = json.load(resp)
191212
except ClientError as err:
192213
job.mc.post("/v1/project/push/cancel/%s" % job.transaction_id)
193214
# server returns various error messages with filename or something generic
194215
# it would be better if it returned list of failed files (and reasons) whenever possible
195216
return {'error': str(err)}
196-
217+
197218
if 'error' in job.server_resp:
198219
#TODO would be good to get some detailed info from server so user could decide what to do with it
199220
# e.g. diff conflicts, basefiles issues, or any other failure
221+
job.mp.log.error("push failed. server response: " + job.server_resp['error'])
200222
raise ClientError(job.server_resp['error'])
201223

202224
job.mp.metadata = {
@@ -206,6 +228,8 @@ def push_project_finalize(job):
206228
}
207229
job.mp.apply_push_changes(job.changes)
208230

231+
job.mp.log.info("--- push finished")
232+
209233

210234
def push_project_cancel(job):
211235
"""
@@ -224,5 +248,5 @@ def _do_upload(item, job):
224248
if job.is_cancelled:
225249
return
226250

227-
item.upload_blocking(job.mc)
251+
item.upload_blocking(job.mc, job.mp)
228252
job.transferred_size += item.size

mergin/merginproject.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11

22
import json
3+
import logging
34
import math
45
import os
56
import re
@@ -48,6 +49,16 @@ def __init__(self, directory):
4849
if not os.path.exists(self.meta_dir):
4950
os.mkdir(self.meta_dir)
5051

52+
# setup logging into project directory's .mergin/client-log.txt file
53+
self.log = logging.getLogger('mergin.' + directory)
54+
self.log.setLevel(logging.DEBUG) # log everything (it would otherwise log just warnings+errors)
55+
if not self.log.handlers:
56+
# we only need to set the handler once
57+
# (otherwise we would get things logged multiple times as loggers are cached)
58+
log_handler = logging.FileHandler(os.path.join(self.meta_dir, "client-log.txt"))
59+
log_handler.setFormatter(logging.Formatter('%(asctime)s %(message)s'))
60+
self.log.addHandler(log_handler)
61+
5162
def fpath(self, file, other_dir=None):
5263
"""
5364
Helper function to get absolute path of project file. Defaults to project dir but

mergin/test/test_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ def test_push_pull_changes(mc):
166166
f_conflict_checksum = generate_checksum(os.path.join(project_dir_2, f_updated))
167167

168168
# not at latest server version
169-
with pytest.raises(ClientError, match='Update your local repository'):
169+
with pytest.raises(ClientError, match='Please update your local copy'):
170170
mc.push_project(project_dir_2)
171171

172172
# check changes in project_dir_2 before applied

0 commit comments

Comments
 (0)