Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
bfb73e6
Formatting change
kalisp Feb 20, 2026
8ab721f
Formatting changes
kalisp Feb 20, 2026
e935db4
Formatting change
kalisp Feb 20, 2026
da30e81
Vendorized python-management-console-api-module temporarily
kalisp Feb 23, 2026
537e5a6
Added Settings for resilio provider
kalisp Feb 23, 2026
527eb64
First iteration of resilio provider
kalisp Feb 23, 2026
868c738
Resilio provider registered
kalisp Feb 23, 2026
4157233
Formatting change
kalisp Feb 25, 2026
498d5bf
Resilio site model could not be overridden by users
kalisp Feb 25, 2026
f4f7a1a
Do not locally override anything for Resilio sites
kalisp Feb 25, 2026
2abf932
Moved Resilio credentials for local site out
kalisp Feb 25, 2026
28d1d8c
Handle aborts
kalisp Feb 25, 2026
ac69ce0
Reworked querying of source and target agent ids
kalisp Feb 25, 2026
b2753c6
Refactored to reusable methods
kalisp Feb 25, 2026
6962e1e
Formatting change
kalisp Feb 25, 2026
523bc85
Return {} for root overrides even for other than studio/local
kalisp Feb 26, 2026
360257f
Reworked getting agent_ids from active/remote sites
kalisp Feb 27, 2026
28d86bf
Added resilio icon
kalisp Feb 27, 2026
70ba286
Use milliseconds to separate multiframe jobs
kalisp Feb 27, 2026
0aa9d48
Use only 3 milliseconds
kalisp Feb 27, 2026
5060625
Rework get_site_root_overrides to look for root values in sites confi…
kalisp Mar 6, 2026
732a686
Translate to local only real local sites, not configured in Studio Se…
kalisp Mar 6, 2026
ecde74b
Reorganized imports
kalisp Mar 6, 2026
f6d5324
Changed to use global addon translation to local site
kalisp Mar 6, 2026
db5e4f3
Normalize paths
kalisp Mar 6, 2026
3e71623
Raise exception if same agent ids
kalisp Mar 6, 2026
0effb5a
Wait first then check
kalisp Mar 6, 2026
71afc92
Fix log to 100 maximum
kalisp Mar 6, 2026
78cdc27
Fix normpath to be used everywhere
kalisp Mar 6, 2026
052c446
Removed max_depth and reworked recursion
kalisp Mar 6, 2026
ec9954e
Added default value for link_type
kalisp Mar 6, 2026
676c177
Added linked representations to add_site by default
kalisp Mar 6, 2026
c18a18d
Added linked representations to remove_site by default
kalisp Mar 6, 2026
1a434ad
Refactored _get_entity_id_by_path
kalisp Mar 25, 2026
0d3d226
Removed debug logging
kalisp Mar 31, 2026
691e51c
Add check if project is enabled
kalisp Apr 10, 2026
1a9c459
Merge develop
kalisp Apr 13, 2026
890910c
Set use site settings script
kalisp Apr 16, 2026
3ccaad2
Watch for errors too
kalisp Apr 16, 2026
65aa43f
Bumped version to 1.2.6+dev.1
kalisp Apr 16, 2026
f88e2fb
Separate adding site to linked and regular repre
kalisp Apr 17, 2026
6639ac5
Reworked job name to more readable
kalisp Apr 17, 2026
2a9c777
Reworked errors handling, added ignored ones
kalisp Apr 17, 2026
2d69950
Updated SiteSettings script to create site_id
kalisp Apr 17, 2026
b275aa6
Bumped version to 1.2.6+dev.2
kalisp Apr 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 172 additions & 14 deletions client/ayon_sitesync/addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
SyncStatus,
SiteAlreadyPresentError,
SiteSyncStatus,
get_linked_representation_id,
)

SYNC_ADDON_DIR = os.path.dirname(os.path.abspath(__file__))
Expand Down Expand Up @@ -160,7 +161,8 @@ def add_site(
site_name=None,
file_id=None,
force=False,
status=SiteSyncStatus.QUEUED
status=SiteSyncStatus.QUEUED,
link_type="reference",
):
"""Adds new site to representation to be synced.

Expand All @@ -171,6 +173,8 @@ def add_site(

Use 'force' to reset existing site.

If link_type is provided, also adds site to all linked representations.

Args:
project_name (str): Project name.
representation_id (str): Representation id.
Expand All @@ -179,6 +183,8 @@ def add_site(
force (bool): Reset site if exists.
status (SiteSyncStatus): Current status,
default SiteSyncStatus.QUEUED
link_type (str): Type of link to follow (e.g. 'reference').
If provided, will also add site to all linked representations.

Raises:
SiteAlreadyPresentError: If adding already existing site and
Expand All @@ -196,6 +202,78 @@ def add_site(
project_name, representation_id
)

files = representation.get("files", [])
if not files:
self.log.debug("No files for {}".format(representation_id))
return

# Collect all representation IDs to process (original + linked)
representation_ids = [representation_id]

self._add_site_to_representation(
project_name,
representation_id,
site_name,
file_id,
force,
status
)

# If link_type is provided, find all linked representations
if link_type:
linked_repre_ids = get_linked_representation_id(
project_name, representation, link_type
)
self.log.debug(
"Found {} linked representations for {}".format(
len(linked_repre_ids), representation_id
)
)
# Add site to each representation
for repre_id in linked_repre_ids:
try:
self._add_site_to_representation(
project_name,
repre_id,
site_name,
file_id,
force,
status
)
except SiteAlreadyPresentError:
self.log.warning(
f"Site {site_name} already present on {repre_id}"
)


def _add_site_to_representation(
self,
project_name,
representation_id,
site_name,
file_id,
force,
status
):
"""Internal method to add site to a single representation.

Args:
project_name (str): Project name.
representation_id (str): Representation id.
site_name (str): Site name of configured site.
file_id (str): File id.
force (bool): Reset site if exists.
status (SiteSyncStatus): Current status.

Raises:
SiteAlreadyPresentError: If adding already existing site and
not 'force'
ValueError: other errors (repre not found, misconfiguration)
"""
representation = get_representation_by_id(
project_name, representation_id
)

files = representation.get("files", [])
if not files:
self.log.debug("No files for {}".format(representation_id))
Expand Down Expand Up @@ -242,16 +320,22 @@ def remove_site(
project_name,
representation_id,
site_name,
remove_local_files=False
remove_local_files=False,
link_type="reference",
):
"""Removes site for particular representation in project.

If link_type is provided, also removes site from all linked representations.
Also removes the remote site from project settings for all representations.

Args:
project_name (str): project name (must match DB)
representation_id (str): MongoDB _id value
site_name (str): name of configured and active site
remove_local_files (bool): remove only files for 'local_id'
site
link_type (str): Type of link to follow (e.g. 'reference').
If provided, will also remove site from all linked representations.

Raises:
ValueError: Throws if any issue.
Expand All @@ -260,6 +344,66 @@ def remove_site(
if not self.get_sync_project_setting(project_name):
raise ValueError("Project not configured")

representation = get_representation_by_id(
project_name, representation_id
)

# Collect all representation IDs to process (original + linked)
representation_ids = [representation_id]

# If link_type is provided, find all linked representations
if link_type:
linked_repre_ids = get_linked_representation_id(
project_name, representation, link_type
)
representation_ids.extend(linked_repre_ids)
self.log.debug(
"Found {} linked representations for {}".format(
len(linked_repre_ids), representation_id
)
)

# Get remote site from project settings
remote_site = self.get_remote_site(project_name)

# Remove site from each representation (both local and remote)
for repre_id in representation_ids:
# Remove local site
self._remove_site_from_representation(
project_name,
repre_id,
site_name,
remove_local_files
)
# Remove remote site if different from local site
if remote_site and remote_site != site_name:
self._remove_site_from_representation(
project_name,
repre_id,
remote_site,
remove_local_files
)

def _remove_site_from_representation(
self,
project_name,
representation_id,
site_name,
remove_local_files=False
):
"""Internal method to remove site from a single representation.

Args:
project_name (str): project name (must match DB)
representation_id (str): MongoDB _id value
site_name (str): name of configured and active site
remove_local_files (bool): remove only files for 'local_id'
site

Raises:
ValueError: Throws if any issue.

"""
sync_info = self.get_repre_sync_state(
project_name,
representation_id,
Expand Down Expand Up @@ -781,30 +925,36 @@ def get_site_root_overrides(
{"work": "c:/projects_local"}
"""

# Validate that site name is valid
if site_name not in ("studio", "local"):
# Consider local site id as 'local'
if site_name != get_local_site_id():
raise ValueError((
"Root overrides are available only for"
" default sites not for \"{}\""
).format(site_name))
site_name = "local"

sitesync_settings = self.get_sync_project_setting(project_name)

roots = {}
if not sitesync_settings["enabled"]:
return roots
local_project_settings = sitesync_settings["local_setting"]
# look for local roots overrides
if site_name == "local":
for root_info in local_project_settings["local_roots"]:
roots[root_info["name"]] = root_info["path"]

# check if there are roots in Studio settings
# (background process doesn't have local settings,
# but it should have roots for local site in Studio settings)
if not roots:
for setting_site_name, site_info in sitesync_settings["sites"].items():
if setting_site_name == site_name:
site_roots = site_info.get("root")
if not site_roots:
continue
if isinstance(site_roots, dict):
roots = site_roots
else:
for root_info in site_roots:
platform_key = platform.system().lower()
roots[root_info["name"]] = root_info[platform_key]
return roots

def get_local_normalized_site(self, site_name):
"""Normlize local site name.
"""Normalize local site name.

Return 'local' if 'site_name' is local id.

Expand All @@ -815,7 +965,13 @@ def get_local_normalized_site(self, site_name):
str: Normalized site name.

"""
if site_name == get_local_site_id():
studio_site_names = self._transform_sites_from_settings(
self.sync_studio_settings
).keys()
if (
site_name not in studio_site_names and
site_name == get_local_site_id()
):
site_name = self.LOCAL_SITE

return site_name
Expand Down Expand Up @@ -1252,6 +1408,8 @@ def _transform_sites_from_settings(self, settings):
"""Transforms list of 'sites' from Setting to dict.

It processes both System and Project Settings as they have same format.
Returns:
dict[str, dict]: {'site_name': {site_info}...}
"""
sites = {}
if not self.enabled:
Expand Down
4 changes: 4 additions & 0 deletions client/ayon_sitesync/plugins/publish/integrate_site_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ class IntegrateSiteSync(pyblish.api.InstancePlugin):
order = pyblish.api.IntegratorOrder + 0.2
label = "Integrate Site Sync state"

@classmethod
def apply_settings(cls, project_settings):
cls.enabled = project_settings["sitesync"]["enabled"]

def process(self, instance):
published_representations = instance.data.get(
"published_representations")
Expand Down
3 changes: 2 additions & 1 deletion client/ayon_sitesync/providers/lib.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .gdrive import GDriveHandler
from .dropbox import DropboxHandler
from .local_drive import LocalDriveHandler
from .resilio import ResilioHandler
from .sftp import SFTPHandler
from .rclone import RCloneHandler

Expand All @@ -11,7 +12,6 @@ class ProviderFactory:
Each new implementation needs to be registered and added to Providers
enum.
"""

def __init__(self):
self.providers = {} # {'PROVIDER_LABEL: {cls, int},..}

Expand Down Expand Up @@ -107,4 +107,5 @@ class and batch limit.
factory.register_provider(DropboxHandler.CODE, DropboxHandler, 10)
factory.register_provider(LocalDriveHandler.CODE, LocalDriveHandler, 50)
factory.register_provider(SFTPHandler.CODE, SFTPHandler, 20)
factory.register_provider(ResilioHandler.CODE, ResilioHandler, 50)
factory.register_provider(RCloneHandler.CODE, RCloneHandler, 20)
26 changes: 10 additions & 16 deletions client/ayon_sitesync/providers/local_drive.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
import threading
import time

from ayon_core.lib import Logger
from ayon_core.addon import AddonsManager
from ayon_core.lib import Logger, get_local_site_id
from ayon_core.pipeline import Anatomy
from .abstract_provider import AbstractProvider

from ayon_core.addon import AddonsManager
from .abstract_provider import AbstractProvider

log = Logger.get_logger("SiteSync")

Expand Down Expand Up @@ -136,19 +136,19 @@ def get_roots_config(self, anatomy=None):
{"root": {"root_ONE": "value", "root_TWO":"value}}
Format is importing for usage of python's format ** approach
"""
site_name = self._normalize_site_name(self.site_name)
manager = AddonsManager()
sitesync_addon = manager.get_enabled_addon("sitesync")
if not sitesync_addon:
raise RuntimeError("No SiteSync addon")

site_name = sitesync_addon.get_local_normalized_site(self.site_name)
if not anatomy:
anatomy = Anatomy(self.project_name,
site_name)
anatomy = Anatomy(self.project_name, site_name)

# TODO cleanup when Anatomy will implement siteRoots method
roots = anatomy.roots
root_values = [root.value for root in roots.values()]
if not all(root_values):
manager = AddonsManager()
sitesync_addon = manager.get_enabled_addon("sitesync")
if not sitesync_addon:
raise RuntimeError("No SiteSync addon")
roots = sitesync_addon._get_project_root_overrides_by_site_id(
self.project_name, site_name)

Expand Down Expand Up @@ -206,9 +206,3 @@ def _mark_progress(
except FileNotFoundError:
pass
time.sleep(0.5)

def _normalize_site_name(self, site_name):
"""Transform user id to 'local' for Local settings"""
if site_name != 'studio':
return 'local'
return site_name
Loading
Loading