Skip to content

Commit 70bb231

Browse files
author
Martin Raiber
committed
Add callback/class to track upload and download progress
1 parent 220d66d commit 70bb231

File tree

3 files changed

+105
-51
lines changed

3 files changed

+105
-51
lines changed

.vscode/settings.json

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,52 @@
11
{
2-
"files.exclude": {
3-
".cache/": true,
4-
".venv/": true,
5-
"*.egg-info": true,
6-
"pip-wheel-metadata/": true,
7-
"**/__pycache__": true,
8-
"**/*.pyc": true,
9-
"**/.ipynb_checkpoints": true,
10-
"**/tmp/": true,
11-
"dist/": true,
12-
"htmlcov/": true,
13-
"notebooks/*.yml": true,
14-
"notebooks/files/": true,
15-
"notebooks/inventory/": true,
16-
"prof/": true,
17-
"site/": true,
18-
"geckodriver.log": true,
19-
"targets.log": true,
20-
"bin/verchew": true
21-
},
22-
"editor.formatOnSave": true,
23-
"pylint.args": ["--rcfile=.pylint.ini"],
24-
"cSpell.words": [
25-
"asdf",
26-
"builtins",
27-
"codecov",
28-
"codehilite",
29-
"choco",
30-
"cygstart",
31-
"cygwin",
32-
"dataclasses",
33-
"Graphviz",
34-
"ipython",
35-
"mkdocs",
36-
"noclasses",
37-
"pipx",
38-
"pyenv",
39-
"ruamel",
40-
"showfspath",
41-
"USERPROFILE",
42-
"venv",
43-
"verchew"
44-
]
45-
}
2+
"files.exclude": {
3+
".cache/": true,
4+
".venv/": true,
5+
"*.egg-info": true,
6+
"pip-wheel-metadata/": true,
7+
"**/__pycache__": true,
8+
"**/*.pyc": true,
9+
"**/.ipynb_checkpoints": true,
10+
"**/tmp/": true,
11+
"dist/": true,
12+
"htmlcov/": true,
13+
"notebooks/*.yml": true,
14+
"notebooks/files/": true,
15+
"notebooks/inventory/": true,
16+
"prof/": true,
17+
"site/": true,
18+
"geckodriver.log": true,
19+
"targets.log": true,
20+
"bin/verchew": true
21+
},
22+
"editor.formatOnSave": true,
23+
"pylint.args": [
24+
"--rcfile=.pylint.ini"
25+
],
26+
"cSpell.words": [
27+
"asdf",
28+
"builtins",
29+
"codecov",
30+
"codehilite",
31+
"choco",
32+
"cygstart",
33+
"cygwin",
34+
"dataclasses",
35+
"Graphviz",
36+
"ipython",
37+
"mkdocs",
38+
"noclasses",
39+
"pipx",
40+
"pyenv",
41+
"ruamel",
42+
"showfspath",
43+
"USERPROFILE",
44+
"venv",
45+
"verchew"
46+
],
47+
"python.testing.pytestArgs": [
48+
""
49+
],
50+
"python.testing.unittestEnabled": false,
51+
"python.testing.pytestEnabled": true
52+
}

filecloudapi/fcserver.py

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Copyright (c) 2024 FileCloud. All Rights Reserved.
22
import datetime
3+
import threading
34
import logging
45
import pathlib
56
import re
@@ -49,6 +50,35 @@ def str_to_bool(value):
4950

5051
log = logging.getLogger(__name__)
5152

53+
class Progress:
54+
"""
55+
Way to track progress of uploads/downloads.
56+
57+
Either use this object in another thread or
58+
override update() to get progress updates.
59+
"""
60+
61+
def __init__(self) -> None:
62+
self._completed_bytes = 0
63+
self._total_bytes = 0
64+
self._lock = threading.Lock()
65+
66+
"""
67+
Progress callback of uploads/downloads
68+
"""
69+
def update(self, completed_bytes: int, total_bytes: int, chunk_complete: bool) -> None:
70+
with self._lock:
71+
self._completed_bytes = completed_bytes
72+
self._total_bytes = total_bytes
73+
74+
def completed_bytes(self) -> int:
75+
with self._lock:
76+
return self._completed_bytes
77+
78+
def total_bytes(self) -> int:
79+
with self._lock:
80+
return self._total_bytes
81+
5282

5383
class FCServer:
5484
"""
@@ -496,7 +526,8 @@ def waitforfileremoval(self, path: str, maxwaits: float = 30):
496526
raise TimeoutError(f"File {path} not removed after {maxwaits} seconds")
497527

498528
def downloadfile_no_retry(
499-
self, path: str, dstPath: Union[pathlib.Path, str], redirect: bool = True
529+
self, path: str, dstPath: Union[pathlib.Path, str], redirect: bool = True,
530+
progress: Optional[Progress] = None
500531
) -> None:
501532
"""
502533
Download file at 'path' to local 'dstPath'
@@ -511,23 +542,29 @@ def downloadfile_no_retry(
511542
stream=True,
512543
) as resp:
513544
resp.raise_for_status()
545+
content_length = int(resp.headers.get("Content-Length", "-1"))
546+
completed_bytes = 0
514547
with open(dstPath, "wb") as dstF:
515548
for chunk in resp.iter_content(128 * 1024):
549+
completed_bytes += len(chunk)
516550
dstF.write(chunk)
551+
if progress is not None:
552+
progress.update(completed_bytes, content_length, False)
517553

518554
def downloadfile(
519-
self, path: str, dstPath: Union[pathlib.Path, str], redirect: bool = True
555+
self, path: str, dstPath: Union[pathlib.Path, str], redirect: bool = True,
556+
progress: Optional[Progress] = None
520557
) -> None:
521558
"""
522559
Download file at 'path' to local 'dstPath'. Retries.
523560
"""
524561
if self.retries is None:
525-
return self.downloadfile_no_retry(path, dstPath, redirect)
562+
return self.downloadfile_no_retry(path, dstPath, redirect, progress)
526563

527564
retries = self.retries
528565
while True:
529566
try:
530-
self.downloadfile_no_retry(path, dstPath, redirect)
567+
self.downloadfile_no_retry(path, dstPath, redirect, progress)
531568
return
532569
except:
533570
retries = retries.increment()
@@ -568,29 +605,32 @@ def upload_bytes(
568605
data: bytes,
569606
serverpath: str,
570607
datemodified: datetime.datetime = datetime.datetime.now(),
608+
progress: Optional[Progress] = None
571609
) -> None:
572610
"""
573611
Upload bytes 'data' to server at 'serverpath'.
574612
"""
575-
self.upload(BufferedReader(BytesIO(data)), serverpath, datemodified) # type: ignore
613+
self.upload(BufferedReader(BytesIO(data)), serverpath, datemodified, progress=progress) # type: ignore
576614

577615
def upload_str(
578616
self,
579617
data: str,
580618
serverpath: str,
581619
datemodified: datetime.datetime = datetime.datetime.now(),
620+
progress: Optional[Progress] = None
582621
) -> None:
583622
"""
584623
Upload str 'data' UTF-8 encoded to server at 'serverpath'.
585624
"""
586-
self.upload_bytes(data.encode("utf-8"), serverpath, datemodified)
625+
self.upload_bytes(data.encode("utf-8"), serverpath, datemodified, progress=progress)
587626

588627
def upload_file(
589628
self,
590629
localpath: pathlib.Path,
591630
serverpath: str,
592631
datemodified: datetime.datetime = datetime.datetime.now(),
593632
adminproxyuserid: Optional[str] = None,
633+
progress: Optional[Progress] = None
594634
) -> None:
595635
"""
596636
Upload file at 'localpath' to server at 'serverpath'.
@@ -601,6 +641,7 @@ def upload_file(
601641
serverpath,
602642
datemodified,
603643
adminproxyuserid=adminproxyuserid,
644+
progress=progress
604645
)
605646

606647
def _serverdatetime(self, dt: datetime.datetime):
@@ -619,6 +660,7 @@ def upload(
619660
serverpath: str,
620661
datemodified: datetime.datetime,
621662
adminproxyuserid: Optional[str] = None,
663+
progress: Optional[Progress] = None,
622664
) -> None:
623665
"""
624666
Upload seekable stream at uploadf to server at 'serverpath'
@@ -681,6 +723,8 @@ def read(self, size=-1):
681723
size = min(size, max_read)
682724
data = super().read(size)
683725
self.pos += len(data)
726+
if progress is not None:
727+
progress.update(self.pos, data_size, False)
684728
return data
685729

686730
def __len__(self) -> int:
@@ -811,6 +855,9 @@ def close(self):
811855

812856
pos += curr_slice_size
813857

858+
if progress is not None:
859+
progress.update(pos, data_size, True)
860+
814861
def share(self, path: str, adminproxyuserid: str = "") -> FCShare:
815862
"""
816863
Share 'path'

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[tool.poetry]
22

33
name = "filecloudapi-python"
4-
version = "0.1.2"
4+
version = "0.2"
55
description = "A Python library to connect to a Filecloud server"
66

77
packages = [{ include = "filecloudapi" }]

0 commit comments

Comments
 (0)