@@ -43,6 +43,11 @@ class DownloadType(str, Enum):
4343 PACKAGED_FILES = "qfield-files"
4444
4545
46+ class FileTransferType (Enum ):
47+ PROJECT = "project"
48+ PACKAGE = "package"
49+
50+
4651class JobTypes (str , Enum ):
4752 PACKAGE = "package"
4853 APPLY_DELTAS = "delta_apply"
@@ -170,6 +175,92 @@ def delete_project(self, project_id: str):
170175
171176 return resp
172177
178+ def upload_files (
179+ self ,
180+ project_id : str ,
181+ upload_type : FileTransferType ,
182+ project_path : str ,
183+ filter_glob : str ,
184+ exit_on_error : bool = False ,
185+ show_progress : bool = False ,
186+ job_id : str = "" ,
187+ ) -> List [Dict ]:
188+ """Upload files to a QFieldCloud project"""
189+ if not filter_glob :
190+ filter_glob = "*"
191+
192+ files = self .get_files_list (project_path , filter_glob )
193+
194+ if not files :
195+ return files
196+
197+ for file in files :
198+ try :
199+ local_filename = Path (file ["name" ])
200+ remote_filename = local_filename .relative_to (project_path )
201+ self .upload_file (
202+ project_id ,
203+ upload_type ,
204+ local_filename ,
205+ remote_filename ,
206+ show_progress ,
207+ job_id ,
208+ )
209+ file ["status" ] = UploadStatus .SUCCESS
210+ except Exception as err :
211+ file ["status" ] = UploadStatus .FAILED
212+ file ["error" ] = err
213+
214+ if exit_on_error :
215+ raise err
216+ else :
217+ continue
218+
219+ return files
220+
221+ def upload_file (
222+ self ,
223+ project_id : str ,
224+ upload_type : FileTransferType ,
225+ local_filename : Path ,
226+ remote_filename : Path ,
227+ show_progress : bool ,
228+ job_id : str = "" ,
229+ ) -> requests .Response :
230+ with open (local_filename , "rb" ) as local_file :
231+ upload_file = local_file
232+ if show_progress :
233+ from tqdm import tqdm
234+ from tqdm .utils import CallbackIOWrapper
235+
236+ progress_bar = tqdm (
237+ total = local_filename .stat ().st_size ,
238+ unit_scale = True ,
239+ desc = local_filename .stem ,
240+ )
241+ upload_file = CallbackIOWrapper (progress_bar .update , local_file , "read" )
242+ else :
243+ logging .info (f'Uploading file "{ remote_filename } "…' )
244+
245+ if upload_type == FileTransferType .PROJECT :
246+ url = f"files/{ project_id } /{ remote_filename } "
247+ elif upload_type == FileTransferType .PACKAGE :
248+ if not job_id :
249+ raise QfcException (
250+ 'When the upload type is "package", you must pass the "job_id" parameter.'
251+ )
252+
253+ url = f"packages/{ project_id } /{ job_id } /files/{ remote_filename } "
254+ else :
255+ raise NotImplementedError ()
256+
257+ return self ._request (
258+ "POST" ,
259+ url ,
260+ files = {
261+ "file" : upload_file ,
262+ },
263+ )
173264
174265 def download_files (
175266 self ,
@@ -422,6 +513,33 @@ def _download_files(
422513
423514 return files_to_download
424515
516+ def get_files_list (
517+ self , project_path : str , filter_glob : str
518+ ) -> List [Dict [str , Any ]]:
519+ if not filter_glob :
520+ filter_glob = "*"
521+
522+ files : List [Dict [str , Any ]] = []
523+ for path in Path (project_path ).rglob (filter_glob ):
524+ if not path .is_file ():
525+ continue
526+
527+ if str (path .relative_to (project_path )).startswith (".qfieldsync" ):
528+ continue
529+
530+ files .append (
531+ {
532+ "name" : str (path ),
533+ "status" : UploadStatus .PENDING ,
534+ "error" : None ,
535+ }
536+ )
537+
538+ # upload the QGIS project file at the end
539+ files .sort (key = lambda f : Path (f ["name" ]).suffix .lower () in (".qgs" , ".qgz" ))
540+
541+ return files
542+
425543 def _request (
426544 self ,
427545 method : str ,
0 commit comments