diff --git a/client/ayon_sitesync/addon.py b/client/ayon_sitesync/addon.py index 3d9c88a..df2c0ba 100644 --- a/client/ayon_sitesync/addon.py +++ b/client/ayon_sitesync/addon.py @@ -1259,8 +1259,17 @@ def _transform_sites_from_settings(self, settings): for whole_site_info in settings.get("sites", []): site_name = whole_site_info["name"] + provider = whole_site_info.get("provider") + if not provider or provider not in whole_site_info: + # site saved in Settings without provider selected would + # break settings preparation for all projects + self.log.warning( + "Site '{}' does not have valid provider configured" + " ('{}'), skipping it.".format(site_name, provider) + ) + continue provider_specific = copy.deepcopy( - whole_site_info[whole_site_info["provider"]] + whole_site_info[provider] ) configured_site = { "enabled": True, @@ -1812,6 +1821,9 @@ def get_representations_sites_sync_state( Returns: list[dict]: dicts follow RepresentationSiteStateModel """ + if not representation_ids: + return [] + endpoint = "{}/{}/state/representations".format( self.endpoint_prefix, project_name ) diff --git a/client/ayon_sitesync/providers/dropbox.py b/client/ayon_sitesync/providers/dropbox.py index 3bec6e4..543de85 100644 --- a/client/ayon_sitesync/providers/dropbox.py +++ b/client/ayon_sitesync/providers/dropbox.py @@ -331,7 +331,16 @@ def get_roots_config(self, anatomy=None): Format is importing for usage of python's format ** approach """ # TODO implement multiple roots - return {"root": {"work": self.presets['root']}} + # Projects may define several roots (work/publish/cache/...) but the + # provider preset holds a single root value - map any requested root + # name onto it so path templates using other roots still resolve. + class _SingleRoot(dict): + def __missing__(self, key): + return self["work"] + + root_map = _SingleRoot() + root_map["work"] = self.presets['root'] + return {"root": root_map} def resolve_path(self, path, root_config=None, anatomy=None): """ diff --git a/client/ayon_sitesync/providers/gdrive.py b/client/ayon_sitesync/providers/gdrive.py index 10871ed..8171198 100644 --- a/client/ayon_sitesync/providers/gdrive.py +++ b/client/ayon_sitesync/providers/gdrive.py @@ -128,7 +128,16 @@ def get_roots_config(self, anatomy=None): """ # GDrive roots cannot be locally overridden # TODO implement multiple roots - return {"root": {"work": self.presets["root"]}} + # Projects may define several roots (work/publish/cache/...) but the + # provider preset holds a single root value - map any requested root + # name onto it so path templates using other roots still resolve. + class _SingleRoot(dict): + def __missing__(self, key): + return self["work"] + + root_map = _SingleRoot() + root_map["work"] = self.presets["root"] + return {"root": root_map} def get_tree(self): """ @@ -508,8 +517,16 @@ def list_files(self): def folder_path_exists(self, file_path): """ - Checks if path from 'file_path' exists. If so, return its + Checks if the folder path 'file_path' exists. If so, return its folder id. + + 'file_path' is expected to be a folder path (no file name). The + folder is resolved purely against the known folder tree instead of + guessing folder-vs-file from the extension. Folder names may + legitimately contain dots (e.g. 'girish.pv', 'mani.j', + 'bhavana.darsi'), which the previous ``os.path.splitext`` based + check misread as files - that made the dotted folder unresolvable, + so it was never created and files landed in the parent folder. Args: file_path (string): gdrive path with / as a separator Returns: @@ -518,15 +535,11 @@ def folder_path_exists(self, file_path): if not file_path: return False - root, ext = os.path.splitext(file_path) - if not ext: - file_path += "/" - - dir_path = os.path.dirname(file_path) + dir_path = file_path.rstrip("/") - path = self.get_tree().get(dir_path, None) - if path: - return path["id"] + folder = self.get_tree().get(dir_path, None) + if folder: + return folder["id"] return False @@ -539,9 +552,12 @@ def file_path_exists(self, file_path): Returns: (dictionary|boolean) file metadata | False if not found """ - folder_id = self.folder_path_exists(file_path) + # 'folder_path_exists' now resolves only real folder paths, so strip + # the file name here and look the file up inside its parent folder. + clean_path = file_path.rstrip("/") + folder_id = self.folder_path_exists(os.path.dirname(clean_path)) if folder_id: - return self.file_exists(os.path.basename(file_path), folder_id) + return self.file_exists(os.path.basename(clean_path), folder_id) return False def file_exists(self, file_name, folder_id): diff --git a/client/ayon_sitesync/providers/rclone.py b/client/ayon_sitesync/providers/rclone.py index 7ecf328..f90c465 100644 --- a/client/ayon_sitesync/providers/rclone.py +++ b/client/ayon_sitesync/providers/rclone.py @@ -255,7 +255,16 @@ def get_tree(self): def get_roots_config(self, anatomy=None) -> dict: """Returns root values for path resolving.""" - return {"root": {"work": self.presets.get("root", "/")}} + # Projects may define several roots (work/publish/cache/...) but the + # provider preset holds a single root value - map any requested root + # name onto it so path templates using other roots still resolve. + class _SingleRoot(dict): + def __missing__(self, key): + return self["work"] + + root_map = _SingleRoot() + root_map["work"] = self.presets.get("root", "/") + return {"root": root_map} # Helper methods def _manage_web_config(self): diff --git a/client/ayon_sitesync/providers/sftp.py b/client/ayon_sitesync/providers/sftp.py index 2603372..fea691d 100644 --- a/client/ayon_sitesync/providers/sftp.py +++ b/client/ayon_sitesync/providers/sftp.py @@ -87,7 +87,16 @@ def get_roots_config(self, anatomy=None): Format is importing for usage of python's format ** approach """ # TODO implement multiple roots - return {"root": {"work": self.presets["root"]}} + # Projects may define several roots (work/publish/cache/...) but the + # provider preset holds a single root value - map any requested root + # name onto it so path templates using other roots still resolve. + class _SingleRoot(dict): + def __missing__(self, key): + return self["work"] + + root_map = _SingleRoot() + root_map["work"] = self.presets["root"] + return {"root": root_map} def get_tree(self): """ diff --git a/client/ayon_sitesync/sitesync.py b/client/ayon_sitesync/sitesync.py index 815b8c9..d9845e1 100644 --- a/client/ayon_sitesync/sitesync.py +++ b/client/ayon_sitesync/sitesync.py @@ -375,7 +375,18 @@ async def sync_loop(self): project_name = None enabled_projects = self.addon.get_enabled_projects() for project_name in enabled_projects: - await self._sync_project(project_name) + try: + await self._sync_project(project_name) + except (ConnectionResetError, ResumableError): + raise + except Exception: + # one broken project must not stop syncing of the + # other projects, skip it until next loop + self.log.warning( + "Unhandled exception while syncing project " + "'{}', skipping it until next loop".format( + project_name), + exc_info=True) duration = time.time() - start_time self.log.debug("One loop took {:.2f}s".format(duration)) @@ -399,10 +410,12 @@ async def sync_loop(self): "ResumableError in sync loop, trying next loop", exc_info=True) except Exception: - self.stop() + # do not kill the whole sync server on unexpected error, + # log it and retry on next loop after a delay self.log.warning( - "Unhandled except. in sync loop, stopping server", + "Unhandled except. in sync loop, trying next loop", exc_info=True) + await asyncio.sleep(60) def stop(self): """Sets is_running flag to false, 'check_shutdown' shuts server down""" @@ -459,8 +472,15 @@ def _working_sites(self, project_name, sync_config): local_site, remote_site)) return None, None - local_site_config = sync_config.get("sites")[local_site] - remote_site_config = sync_config.get("sites")[remote_site] + site_configs = sync_config.get("sites") or {} + local_site_config = site_configs.get(local_site) + remote_site_config = site_configs.get(remote_site) + if local_site_config is None or remote_site_config is None: + self.log.warning( + "Active or remote site '{}'/'{}' is not configured for " + "project '{}', skipping".format( + local_site, remote_site, project_name)) + return None, None if not all([ _site_is_working( self.addon, project_name, local_site, local_site_config diff --git a/client/ayon_sitesync/version.py b/client/ayon_sitesync/version.py index 4944b71..9d279ae 100644 --- a/client/ayon_sitesync/version.py +++ b/client/ayon_sitesync/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'sitesync' version.""" -__version__ = "1.3.1+dev" +__version__ = "1.3.1+lbs.4" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e726458..eb15c68 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "sitesync-addon", - "version": "1.3.1+dev", + "version": "1.3.1+lbs.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sitesync-addon", - "version": "1.3.1+dev", + "version": "1.3.1+lbs.4", "dependencies": { "@types/styled-components": "^5.1.25", "@ynput/ayon-react-addon-provider": "^0.0.6", diff --git a/frontend/package.json b/frontend/package.json index 53408ff..41d70af 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "sitesync-addon", "private": true, - "version": "1.3.1+dev", + "version": "1.3.1+lbs.4", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 88bfdb9..923f1e4 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -14,7 +14,7 @@ const AddonWrapper = () => { // const addonName = useContext(AddonContext).addonName const addonName = 'sitesync' // const addonVersion = useContext(AddonContext).addonVersion - const addonVersion = '1.3.1+dev' + const addonVersion = '1.3.1+lbs.4' const accessToken = useContext(AddonContext).accessToken const projectName = useContext(AddonContext).projectName const userName = useContext(AddonContext).userName diff --git a/package.py b/package.py index b27769b..9017d32 100644 --- a/package.py +++ b/package.py @@ -2,7 +2,7 @@ """Package declaring addon version.""" name = "sitesync" title = "SiteSync" -version = "1.3.1+dev" +version = "1.3.1+lbs.4" client_dir = "ayon_sitesync" ayon_launcher_version = ">=1.4.3" diff --git a/pyproject.toml b/pyproject.toml index f42a1d1..9c53074 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "SiteSync" -version = "1.3.1+dev" +version = "1.3.1+lbs.4" description = "SiteSync Addon" authors = ["Ynput s.r.o. "] license = "MIT License" diff --git a/server/__init__.py b/server/__init__.py index b443cd9..82413e9 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -225,7 +225,7 @@ async def get_site_sync_state( await check_sync_status_table(project_name) conditions = [] - if representationIds is not None: + if representationIds: conditions.append(f"r.id IN {SQLTool.array(representationIds)}") if folderFilter: @@ -552,6 +552,9 @@ async def get_representations_site_sync_state( """List all sites on all representations and their state""" await check_sync_status_table(project_name) + if not representationIds: + return [] + conditions = [ f"representation_id IN {SQLTool.array(representationIds)}" ]