From 0dee75ec876cd485a13d039cd1ffb8885e2a28fe Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 1 Sep 2021 19:20:50 +0100 Subject: [PATCH] Dropbox Provider --- .../sync_server/providers/dropbox.py | 366 ++++++++++++++++++ .../sync_server/providers/lib.py | 2 + .../providers/resources/dropbox.png | Bin 0 -> 2081 bytes .../schema_project_syncserver.json | 41 +- 4 files changed, 404 insertions(+), 5 deletions(-) create mode 100644 openpype/modules/default_modules/sync_server/providers/dropbox.py create mode 100644 openpype/modules/default_modules/sync_server/providers/resources/dropbox.png diff --git a/openpype/modules/default_modules/sync_server/providers/dropbox.py b/openpype/modules/default_modules/sync_server/providers/dropbox.py new file mode 100644 index 0000000000..31459f1074 --- /dev/null +++ b/openpype/modules/default_modules/sync_server/providers/dropbox.py @@ -0,0 +1,366 @@ +import os + +import dropbox + +from openpype.api import Logger +from .abstract_provider import AbstractProvider +from ..utils import EditableScopes + +log = Logger().get_logger("SyncServer") + + +class DropboxHandler(AbstractProvider): + CODE = 'dropbox' + LABEL = 'Dropbox' + + def __init__(self, project_name, site_name, tree=None, presets=None): + self.active = False + self.site_name = site_name + self.presets = presets + + if not self.presets: + log.info( + "Sync Server: There are no presets for {}.".format(site_name) + ) + return + + provider_presets = self.presets.get(self.CODE) + if not provider_presets: + msg = "Sync Server: No provider presets for {}".format(self.CODE) + log.info(msg) + return + + token = self.presets[self.CODE].get("token", "") + if not token: + msg = "Sync Server: No access token for dropbox provider" + log.info(msg) + return + + team_folder_name = self.presets[self.CODE].get("team_folder_name", "") + if not team_folder_name: + msg = "Sync Server: No team folder name for dropbox provider" + log.info(msg) + return + + acting_as_member = self.presets[self.CODE].get("acting_as_member", "") + if not acting_as_member: + msg = ( + "Sync Server: No acting member for dropbox provider" + ) + log.info(msg) + return + + self.dbx = None + try: + self.dbx = self._get_service( + token, acting_as_member, team_folder_name + ) + except Exception as e: + log.info("Could not establish dropbox object: {}".format(e)) + return + + super(AbstractProvider, self).__init__() + + def _get_service(self, token, acting_as_member, team_folder_name): + dbx = dropbox.DropboxTeam(token) + + # Getting member id. + member_id = None + member_names = [] + for member in dbx.team_members_list().members: + member_names.append(member.profile.name.display_name) + if member.profile.name.display_name == acting_as_member: + member_id = member.profile.team_member_id + + if member_id is None: + raise ValueError( + "Could not find member \"{}\". Available members: {}".format( + acting_as_member, member_names + ) + ) + + # Getting team folder id. + team_folder_id = None + team_folder_names = [] + for entry in dbx.team_team_folder_list().team_folders: + team_folder_names.append(entry.name) + if entry.name == team_folder_name: + team_folder_id = entry.team_folder_id + + if team_folder_id is None: + raise ValueError( + "Could not find team folder \"{}\". Available folders: " + "{}".format( + team_folder_name, team_folder_names + ) + ) + + # Establish dropbox object. + path_root = dropbox.common.PathRoot.namespace_id(team_folder_id) + return dropbox.DropboxTeam( + token + ).with_path_root(path_root).as_user(member_id) + + def is_active(self): + """ + Returns True if provider is activated, eg. has working credentials. + Returns: + (boolean) + """ + return self.dbx is not None + + @classmethod + def get_configurable_items(cls): + """ + Returns filtered dict of editable properties + + + Returns: + (dict) + """ + editable = { + 'token': { + 'scope': [EditableScopes.PROJECT], + 'label': "Access Token", + 'type': 'text', + 'namespace': ( + '{project_settings}/global/sync_server/sites/{site}/token' + ) + }, + 'team_folder_name': { + 'scope': [EditableScopes.PROJECT], + 'label': "Team Folder Name", + 'type': 'text', + 'namespace': ( + '{project_settings}/global/sync_server/sites/{site}' + '/team_folder_name' + ) + }, + 'acting_as_member': { + 'scope': [EditableScopes.PROJECT, EditableScopes.LOCAL], + 'label': "Acting As Member", + 'type': 'text', + 'namespace': ( + '{project_settings}/global/sync_server/sites/{site}' + '/acting_as_member' + ) + } + } + return editable + + def _path_exists(self, path): + try: + entries = self.dbx.files_list_folder( + path=os.path.dirname(path) + ).entries + except dropbox.exceptions.ApiError: + return False + + for entry in entries: + if entry.name == os.path.basename(path): + return True + + return False + + def upload_file(self, source_path, path, + server, collection, file, representation, site, + overwrite=False): + """ + Copy file from 'source_path' to 'target_path' on provider. + Use 'overwrite' boolean to rewrite existing file on provider + + Args: + source_path (string): + path (string): absolute path with or without name of the file + overwrite (boolean): replace existing file + + arguments for saving progress: + server (SyncServer): server instance to call update_db on + collection (str): name of collection + file (dict): info about uploaded file (matches structure from db) + representation (dict): complete repre containing 'file' + site (str): site name + Returns: + (string) file_id of created file, raises exception + """ + # Check source path. + if not os.path.exists(source_path): + raise FileNotFoundError( + "Source file {} doesn't exist.".format(source_path) + ) + + if self._path_exists(path) and not overwrite: + raise FileExistsError( + "File already exists, use 'overwrite' argument" + ) + + mode = dropbox.files.WriteMode("add", None) + if overwrite: + mode = dropbox.files.WriteMode.overwrite + + with open(source_path, "rb") as f: + self.dbx.files_upload(f.read(), path, mode=mode) + + server.update_db( + collection=collection, + new_file_id=None, + file=file, + representation=representation, + site=site, + progress=100 + ) + + return path + + def download_file(self, source_path, local_path, + server, collection, file, representation, site, + overwrite=False): + """ + Download file from provider into local system + + Args: + source_path (string): absolute path on provider + local_path (string): absolute path with or without name of the file + overwrite (boolean): replace existing file + + arguments for saving progress: + server (SyncServer): server instance to call update_db on + collection (str): name of collection + file (dict): info about uploaded file (matches structure from db) + representation (dict): complete repre containing 'file' + site (str): site name + Returns: + None + """ + # Check source path. + if not self._path_exists(source_path): + raise FileNotFoundError( + "Source file {} doesn't exist.".format(source_path) + ) + + if os.path.exists(local_path) and not overwrite: + raise FileExistsError( + "File already exists, use 'overwrite' argument" + ) + + if os.path.exists(local_path) and overwrite: + os.unlink(local_path) + + self.dbx.files_download_to_file(local_path, source_path) + + server.update_db( + collection=collection, + new_file_id=None, + file=file, + representation=representation, + site=site, + progress=100 + ) + + return os.path.basename(source_path) + + def delete_file(self, path): + """ + Deletes file from 'path'. Expects path to specific file. + + Args: + path (string): absolute path to particular file + + Returns: + None + """ + if not self._path_exists(path): + raise FileExistsError("File {} doesn't exist".format(path)) + + self.dbx.files_delete(path) + + def list_folder(self, folder_path): + """ + List all files and subfolders of particular path non-recursively. + Args: + folder_path (string): absolut path on provider + + Returns: + (list) + """ + if not self._path_exists(folder_path): + raise FileExistsError( + "Folder \"{}\" does not exist".format(folder_path) + ) + + entry_names = [] + for entry in self.dbx.files_list_folder(path=folder_path).entries: + entry_names.append(entry.name) + return entry_names + + def create_folder(self, folder_path): + """ + Create all nonexistent folders and subfolders in 'path'. + + Args: + path (string): absolute path + + Returns: + (string) folder id of lowest subfolder from 'path' + """ + if self._path_exists(folder_path): + return folder_path + + self.dbx.files_create_folder_v2(folder_path) + + return folder_path + + def get_tree(self): + """ + Creates folder structure for providers which do not provide + tree folder structure (GDrive has no accessible tree structure, + only parents and their parents) + """ + pass + + def get_roots_config(self, anatomy=None): + """ + Returns root values for path resolving + + Takes value from Anatomy which takes values from Settings + overridden by Local Settings + + Returns: + (dict) - {"root": {"root": "/My Drive"}} + OR + {"root": {"root_ONE": "value", "root_TWO":"value}} + Format is importing for usage of python's format ** approach + """ + return self.presets['root'] + + def resolve_path(self, path, root_config=None, anatomy=None): + """ + Replaces all root placeholders with proper values + + Args: + path(string): root[work]/folder... + root_config (dict): {'work': "c:/..."...} + anatomy (Anatomy): object of Anatomy + Returns: + (string): proper url + """ + if not root_config: + root_config = self.get_roots_config(anatomy) + + if root_config and not root_config.get("root"): + root_config = {"root": root_config} + + try: + if not root_config: + raise KeyError + + path = path.format(**root_config) + except KeyError: + try: + path = anatomy.fill_root(path) + except KeyError: + msg = "Error in resolving local root from anatomy" + log.error(msg) + raise ValueError(msg) + + return path diff --git a/openpype/modules/default_modules/sync_server/providers/lib.py b/openpype/modules/default_modules/sync_server/providers/lib.py index 816ccca981..192562b48b 100644 --- a/openpype/modules/default_modules/sync_server/providers/lib.py +++ b/openpype/modules/default_modules/sync_server/providers/lib.py @@ -1,4 +1,5 @@ from .gdrive import GDriveHandler +from .dropbox import DropboxHandler from .local_drive import LocalDriveHandler @@ -103,4 +104,5 @@ factory = ProviderFactory() # 7 denotes number of files that could be synced in single loop - learned by # trial and error factory.register_provider(GDriveHandler.CODE, GDriveHandler, 7) +factory.register_provider(DropboxHandler.CODE, DropboxHandler, 10) factory.register_provider(LocalDriveHandler.CODE, LocalDriveHandler, 50) diff --git a/openpype/modules/default_modules/sync_server/providers/resources/dropbox.png b/openpype/modules/default_modules/sync_server/providers/resources/dropbox.png new file mode 100644 index 0000000000000000000000000000000000000000..6f56e3335b2871feb40dda6dc46ce5c88170cbe8 GIT binary patch literal 2081 zcmbVN2~gBl7+$p~h@g0Zwqs)u#A7$v17rgW#RX-xiwvwn<&bQWg^atKm;`qL6-K#b ztAaY#GK!9()E4o^6A`6qrOsH9qhpavpw@#Ks*YEi<=D}VwNo?m@?PHi-uK^s^1{Ow zI*pz>S|AWOsX~Jz_}R@m>__tNdnUzmesZ8gzhVUfXHV-Gw)e5eD}lhyh15iI(dsV| zj53H&oYE2^vw`N(0)c;^nMSdd1P5q|MAE1LAD^uP0TNe$QQm4;O)H5cGBlMTB2&XO zSn5hlj)Q>#fWH~x2@C{>0%n8W$RcJ1*w2gbYwI=y0{swfr2-7H3Iw9n;ee822*5`K z3o#h>0p`g?II2Z4t#&RTfhA%HmO)~f5QY({9FfU@!3X5UF}Mzi2wpHKjQ>)ANgPKb z5M(l$L?)?-ViF;-TrP)T2_%sSd4!NnF>Ru?V)>)DOfl8;(w9^!>td3?lS|fiQ9`j}_anbP~l;Y!dY^ z)VAYy1n|C9t8FsgtBb*46TxyJt9dg99r9i@t4W~=D1uKNP9(A(a7%L?i>eqDUi7nb=uxT_A!{j!=Nss0m@Y5Eg61-iUY}BK4jFixC(e zfT}5+)TO)^Dt`yZCk&2q=!cOwhUh59fbyo21~id?Xk#J>*c^x`DLuvTB6+=}Z||#= z%5a9#k$V1sjaWDzP=zRc#Bv`Wp+qF^S5&P=R7RFVjToT{R)D-)L?nqLTCEh5VuVD9 z>AbZdZs^wr9Jg}7KYPm23W3k;JOOm7WCp69qvrYXK-g%o=$D2M4#wkM_U-ttpA%Nk5Pg12C4-aVC@@ zf)aUe{b$BOyz-E>R0r&U{%K}hJt zvzP}lEvHty?UIdB*?*ppZO+hCJAl<`-8HYjX~}Pn-S2eC>m=&elVrEhrEjf&?n^lT z*HpX4O^4~Yp1VG4^6d7T)?Ugxm*H7{MRko{nsJC84rvbGdH8%$czzyIRA)3MU^rfk_0hdby|OW#)9aoxej+dDeK{PX@=G@+{SPOE$F$$-je(fRbz zt(lqOueQG2vmp(bSYxC#bx(2UCB)S zsk|n}eB8}&aauv0dWqwU-l>mf*DkI&CT{ZGVCNez$r)B$eL-|TElcXbGPBaZ4zUPg z@(-bGR*+N9#(SO3Rm;mu+>aB}+nu!&M6GBH?vIPF7aUz4wqo(2O>L9?8$5lpPB1IR zv^w}(VtSK5HGT4+v(T=!DX(c~*sQ=4&1&%_ zU<06aimwHJzTR-PZcyjjTWs}>NXtUt@ zVwFXjw9hlJ>&O_Wt|k^;7tWOBmg(~I4cytA&x)3{wu_3}FFO_P3Cn!+dBMc?!nBIT zVNoNxJkmM}6?g0QepeT>#q`3nN7?pp{;>&FeYe}vW4}H8!|7dDGp#>yRY+KH`TT_S Fe*@&?2sr=% literal 0 HcmV?d00001 diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_syncserver.json b/openpype/settings/entities/schemas/projects_schema/schema_project_syncserver.json index cb2cc9c9d1..577efcaf13 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_syncserver.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_syncserver.json @@ -48,10 +48,41 @@ "type": "dict", "children": [ { - "type": "path", - "key": "credentials_url", - "label": "Credentials url", - "multiplatform": true + "type": "dict", + "key": "gdrive", + "label": "Google Drive", + "collapsible": true, + "children": [ + { + "type": "path", + "key": "credentials_url", + "label": "Credentials url", + "multiplatform": true + } + ] + }, + { + "type": "dict", + "key": "dropbox", + "label": "Dropbox", + "collapsible": true, + "children": [ + { + "type": "text", + "key": "token", + "label": "Access Token" + }, + { + "type": "text", + "key": "team_folder_name", + "label": "Team Folder Name" + }, + { + "type": "text", + "key": "acting_as_member", + "label": "Acting As Member" + } + ] }, { "type": "dict-modifiable", @@ -61,7 +92,7 @@ "collapsable_key": false, "object_type": "text" } - ] + ] } } ]