33import logging
44import pathlib
55import re
6+ import threading
67import time
78import xml .etree .ElementTree as ET
89from io import SEEK_CUR , SEEK_END , SEEK_SET , BufferedReader , BytesIO
@@ -50,6 +51,39 @@ def str_to_bool(value):
5051log = 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+
5387class 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'
0 commit comments