Skip to content

Commit 2faf3b8

Browse files
Merge pull request #2 from codelathe/feature/Add_progress_callback
Add callback/class to track upload and download progress
2 parents 220d66d + 9cbfe9c commit 2faf3b8

File tree

4 files changed

+119
-53
lines changed

4 files changed

+119
-53
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: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44
import pathlib
55
import re
6+
import threading
67
import time
78
import xml.etree.ElementTree as ET
89
from io import SEEK_CUR, SEEK_END, SEEK_SET, BufferedReader, BytesIO
@@ -50,6 +51,39 @@ def str_to_bool(value):
5051
log = logging.getLogger(__name__)
5152

5253

54+
class Progress:
55+
"""
56+
Way to track progress of uploads/downloads.
57+
58+
Either use this object in another thread or
59+
override update() to get progress updates.
60+
"""
61+
62+
def __init__(self) -> None:
63+
self._completed_bytes = 0
64+
self._total_bytes = 0
65+
self._lock = threading.Lock()
66+
67+
"""
68+
Progress callback of uploads/downloads
69+
"""
70+
71+
def update(
72+
self, completed_bytes: int, total_bytes: int, chunk_complete: bool
73+
) -> None:
74+
with self._lock:
75+
self._completed_bytes = completed_bytes
76+
self._total_bytes = total_bytes
77+
78+
def completed_bytes(self) -> int:
79+
with self._lock:
80+
return self._completed_bytes
81+
82+
def total_bytes(self) -> int:
83+
with self._lock:
84+
return self._total_bytes
85+
86+
5387
class FCServer:
5488
"""
5589
FileCloud Server API
@@ -496,7 +530,11 @@ def waitforfileremoval(self, path: str, maxwaits: float = 30):
496530
raise TimeoutError(f"File {path} not removed after {maxwaits} seconds")
497531

498532
def downloadfile_no_retry(
499-
self, path: str, dstPath: Union[pathlib.Path, str], redirect: bool = True
533+
self,
534+
path: str,
535+
dstPath: Union[pathlib.Path, str],
536+
redirect: bool = True,
537+
progress: Optional[Progress] = None,
500538
) -> None:
501539
"""
502540
Download file at 'path' to local 'dstPath'
@@ -511,23 +549,32 @@ def downloadfile_no_retry(
511549
stream=True,
512550
) as resp:
513551
resp.raise_for_status()
552+
content_length = int(resp.headers.get("Content-Length", "-1"))
553+
completed_bytes = 0
514554
with open(dstPath, "wb") as dstF:
515555
for chunk in resp.iter_content(128 * 1024):
556+
completed_bytes += len(chunk)
516557
dstF.write(chunk)
558+
if progress is not None:
559+
progress.update(completed_bytes, content_length, False)
517560

518561
def downloadfile(
519-
self, path: str, dstPath: Union[pathlib.Path, str], redirect: bool = True
562+
self,
563+
path: str,
564+
dstPath: Union[pathlib.Path, str],
565+
redirect: bool = True,
566+
progress: Optional[Progress] = None,
520567
) -> None:
521568
"""
522569
Download file at 'path' to local 'dstPath'. Retries.
523570
"""
524571
if self.retries is None:
525-
return self.downloadfile_no_retry(path, dstPath, redirect)
572+
return self.downloadfile_no_retry(path, dstPath, redirect, progress)
526573

527574
retries = self.retries
528575
while True:
529576
try:
530-
self.downloadfile_no_retry(path, dstPath, redirect)
577+
self.downloadfile_no_retry(path, dstPath, redirect, progress)
531578
return
532579
except:
533580
retries = retries.increment()
@@ -568,29 +615,34 @@ def upload_bytes(
568615
data: bytes,
569616
serverpath: str,
570617
datemodified: datetime.datetime = datetime.datetime.now(),
618+
progress: Optional[Progress] = None,
571619
) -> None:
572620
"""
573621
Upload bytes 'data' to server at 'serverpath'.
574622
"""
575-
self.upload(BufferedReader(BytesIO(data)), serverpath, datemodified) # type: ignore
623+
self.upload(BufferedReader(BytesIO(data)), serverpath, datemodified, progress=progress) # type: ignore
576624

577625
def upload_str(
578626
self,
579627
data: str,
580628
serverpath: str,
581629
datemodified: datetime.datetime = datetime.datetime.now(),
630+
progress: Optional[Progress] = None,
582631
) -> None:
583632
"""
584633
Upload str 'data' UTF-8 encoded to server at 'serverpath'.
585634
"""
586-
self.upload_bytes(data.encode("utf-8"), serverpath, datemodified)
635+
self.upload_bytes(
636+
data.encode("utf-8"), serverpath, datemodified, progress=progress
637+
)
587638

588639
def upload_file(
589640
self,
590641
localpath: pathlib.Path,
591642
serverpath: str,
592643
datemodified: datetime.datetime = datetime.datetime.now(),
593644
adminproxyuserid: Optional[str] = None,
645+
progress: Optional[Progress] = None,
594646
) -> None:
595647
"""
596648
Upload file at 'localpath' to server at 'serverpath'.
@@ -601,6 +653,7 @@ def upload_file(
601653
serverpath,
602654
datemodified,
603655
adminproxyuserid=adminproxyuserid,
656+
progress=progress,
604657
)
605658

606659
def _serverdatetime(self, dt: datetime.datetime):
@@ -619,6 +672,7 @@ def upload(
619672
serverpath: str,
620673
datemodified: datetime.datetime,
621674
adminproxyuserid: Optional[str] = None,
675+
progress: Optional[Progress] = None,
622676
) -> None:
623677
"""
624678
Upload seekable stream at uploadf to server at 'serverpath'
@@ -681,6 +735,8 @@ def read(self, size=-1):
681735
size = min(size, max_read)
682736
data = super().read(size)
683737
self.pos += len(data)
738+
if progress is not None:
739+
progress.update(self.pos, data_size, False)
684740
return data
685741

686742
def __len__(self) -> int:
@@ -811,6 +867,9 @@ def close(self):
811867

812868
pos += curr_slice_size
813869

870+
if progress is not None:
871+
progress.update(pos, data_size, True)
872+
814873
def share(self, path: str, adminproxyuserid: str = "") -> FCShare:
815874
"""
816875
Share 'path'

poetry.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 2 deletions
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" }]
@@ -33,7 +33,7 @@ python = "^3.11"
3333
click = "*"
3434
requests = "*"
3535

36-
[tool.poetry.dev-dependencies]
36+
[tool.poetry.group.dev.dependencies]
3737

3838
# Formatters
3939
black = "^22.1"

0 commit comments

Comments
 (0)