Advertisement
ylSiew

download_all_components_action

May 6th, 2015
194
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 13.60 KB | None | 0 0
  1. #!/usr/bin/env python
  2. # -*- coding: UTF-8 -*-
  3.  
  4. """
  5. Module download_all_components_from_entity: This module registers an action
  6. that allows the user to download all components from an entity in ftrack.
  7. """
  8.  
  9. import argparse
  10. import getpass
  11. import logging
  12. import sys
  13. import traceback
  14. import urllib2
  15. import urlparse
  16. import ftrack
  17. import threading
  18. import os
  19.  
  20.  
  21. # add accessor to path and import it
  22. accessorPath = os.path.join(os.path.dirname(
  23.     os.path.dirname(
  24.         os.path.dirname(os.path.abspath(__file__)))), 'resolver')
  25.  
  26. sys.path.append(accessorPath)
  27.  
  28. # noinspection PyUnresolvedReferences
  29. import frostburnS3Accessor as fbS3
  30.  
  31.  
  32. class DownloadAllComponentsFromEntityAction(ftrack.Action):
  33.     """
  34.    This ``ftrack.Action`` subclass will allow the user to download all
  35.    components from an entity.
  36.    """
  37.  
  38.     # Action identifier
  39.     identifier = 'download-all-components-from-entity'
  40.  
  41.     # Action label
  42.     label = 'Download All Components'
  43.  
  44.     # Current user from OS environment
  45.     currentUser = getpass.getuser()
  46.  
  47.  
  48.     def discover(self, event):
  49.         """
  50.        This method is invoked when the user brings up the Actions menu from the
  51.        web UI. This action will only return the config when triggered on a single
  52.        ``ftrack.AssetVersion`` object.
  53.        """
  54.  
  55.         data = event['data']
  56.  
  57.         selection = data.get('selection', [])
  58.         self.logger.info('Got selection: {0}'.format(selection))
  59.  
  60.         # If selection contains more than one item return early
  61.         if len(selection) != 1 or selection[0]['entityType'] != 'assetversion':
  62.             return
  63.  
  64.         # Return data to show in web UI
  65.         return {
  66.             'items' : [{
  67.                 'label' : self.label,
  68.                 'actionIdentifier' : self.identifier
  69.             }]
  70.         }
  71.  
  72.  
  73.     def launch( self, event ):
  74.         """
  75.        This method is invoked when the user executes the Action. This brings up
  76.        a UI to choose where to save the components to.
  77.        """
  78.  
  79.         # This returns a dictionary containing the data returned from the server.
  80.         data = event['data']
  81.  
  82.         # Get list of all component urls attached to this AssetVersion
  83.         components = self.getAllComponentUrls(data)
  84.  
  85.         # Check for no Components attached to AssetVersion
  86.         if not components:
  87.             self.logger.error('### No components were found on the AssetVersion!')
  88.  
  89.             # Return early with status message
  90.             return {
  91.                 'success' : False,
  92.                 'message' : 'No Components found on this Asset Version!'
  93.             }
  94.  
  95.         # Format dict to be used in the Custom Actions UI
  96.         uiItems = {
  97.             'items' : [
  98.                 {
  99.                     'value' : '## Download Individual Components ##',
  100.                     'type' : 'label'
  101.                 }
  102.             ]
  103.         }
  104.  
  105.         # For each url, add a new download link label
  106.         for name, url in components.items():
  107.             urlLinkItem = {
  108.                 'value' : 'Download: [{0}]({1})'.format(name, url),
  109.                 'type' : 'label'
  110.             }
  111.  
  112.             uiItems['items'].append(urlLinkItem)
  113.  
  114.         # Add field for user-specified location to save all Components to
  115.  
  116.         saveToDirectoryHeader = {
  117.             'value' : '## Save All Components To Directory ##',
  118.             'type' : 'label'
  119.         }
  120.  
  121.         uriSaveDirectoryItem = {
  122.             'label' : 'Save all components to',
  123.             'type' : 'text',
  124.             'value' : 'Enter path to directory...',
  125.             'name' : 'saveDirectory'
  126.         }
  127.  
  128.         uiItems['items'].append(saveToDirectoryHeader)
  129.         uiItems['items'].append(uriSaveDirectoryItem)
  130.  
  131.         # Check to see if values have been returned
  132.         if 'values' in data:
  133.             self.logger.info('Action executed')
  134.  
  135.             # Open file browser to choose save directory
  136.             # Hide root widget
  137.             # rootDialog = Tkinter.Tk()
  138.             # rootDialog.withdraw()
  139.  
  140.             # Get user-specified location
  141.             # saveDirectory = tkFileDialog.askdirectory()
  142.  
  143.             saveDirectory = data['values']['saveDirectory']
  144.  
  145.             # check for valid directory
  146.             if not os.path.isdir(saveDirectory):
  147.                 self.logger.error('### The directory chosen is invalid: {0}'
  148.                                   .format(saveDirectory))
  149.  
  150.                 # return early and publish failure message
  151.                 return {
  152.                     'success' : False,
  153.                     'message' : 'Directory chosen is invalid!'
  154.                 }
  155.  
  156.             elif not os.access(saveDirectory, os.W_OK):
  157.                 self.logger.error('### You do not have write access to: {0}'
  158.                                   .format(saveDirectory))
  159.  
  160.                 # return early and publish failure message
  161.                 return {
  162.                     'success' : False,
  163.                     'message' : 'No permission to write to directory!'
  164.                 }
  165.  
  166.             self.logger.info('identifier')
  167.  
  168.             # Save all Components to the specified directory
  169.             initializeDownloads(urls=components.values(),
  170.                                 fileNames=None,
  171.                                 saveDirectory=saveDirectory)
  172.  
  173.             # Publish success message to UI
  174.             return {
  175.                 'success' : True,
  176.                 'message' : 'Successfully executed action: {0}!'.format(self.label)
  177.             }
  178.  
  179.         # return dict with formatted data for displaying the DAC UI
  180.         return uiItems
  181.  
  182.  
  183.     def register(self):
  184.         """
  185.        This overloads the base register() function to only subscribe to receive
  186.        certain events that have user information in them.
  187.  
  188.        Used to filter on users that initiated the action.
  189.        """
  190.  
  191.         self.logger.debug('register() called from: {0}'.format(__name__))
  192.  
  193.         # only subscribe/launch if this action's username match.
  194.         ftrack.EVENT_HUB.subscribe(
  195.             subscription='topic=ftrack.action.discover and source.user.username={0}'
  196.                 .format(self.currentUser),
  197.             callback=self.discover
  198.         )
  199.  
  200.         ftrack.EVENT_HUB.subscribe(
  201.             subscription='topic=ftrack.action.launch and data.actionIdentifier={0} '
  202.                          'and source.user.username={1}'
  203.                 .format(self.identifier, self.currentUser),
  204.             callback=self.launch
  205.         )
  206.  
  207.  
  208.     def getAllComponentUrls(self, data):
  209.         """
  210.        This method retrieves all available urls to ``ftrack.Component`` objects
  211.        from a ``ftrack.AssetVersion``. Returns the results as a ``list``.
  212.        :param data: ``dict`` containing the data returned from the ftrack server.
  213.        :type data: ``dict``
  214.        :return: ``namedtuple`` containing ``list`` objects of Component names and urls
  215.        :rtype: ``namedtuple``
  216.        """
  217.  
  218.         # make dictionary to store all data
  219.         componentData = {}
  220.  
  221.         # Query AssetVersion selection
  222.         selection = data.get('selection', [])
  223.  
  224.         # check for non-existent assetVersion selection
  225.         if not selection:
  226.             return
  227.  
  228.         for entity in selection:
  229.             # get assetVersion instance from id
  230.             assetVersion = ftrack.AssetVersion(id=entity['entityId'])
  231.  
  232.             # get all components attached to assetVersion
  233.             components = assetVersion.getComponents()
  234.  
  235.             for component in components:
  236.  
  237.                 assert isinstance(component, ftrack.Component), \
  238.                     '### {0} is not a ftrack Component!'.format(component)
  239.  
  240.                 # first attempt to get local component path
  241.                 componentPath = component.getFilesystemPath()
  242.  
  243.                 # get Component name to store in namedtuple
  244.                 componentName = component.getName()
  245.  
  246.                 if not componentPath:
  247.                     self.logger.info('Component {0} location is not local, retrieving from S3...'
  248.                         .format(component.getName()))
  249.  
  250.                     # get resource identifier for returning path from accessor
  251.                     componentId = component.getId()
  252.                     componentResourceIdentifier = component.getResourceIdentifier()
  253.  
  254.                     # get location instance from the assetVersion id
  255.                     location = ftrack.pickLocation(componentId)
  256.                     accessor = location.getAccessor()
  257.  
  258.                     if isinstance(accessor, ftrack.S3Accessor):
  259.                         # re-class to extended S3Accessor
  260.                         fbS3.FrostburnS3Accessor.extend(accessor)
  261.  
  262.                         assert isinstance(accessor, fbS3.FrostburnS3Accessor), \
  263.                             '### Re-classing {0} was unsuccessful!'
  264.  
  265.                     try:
  266.                         # check if the resource exists
  267.                         if accessor.exists(componentResourceIdentifier):
  268.                             # get url to the resource
  269.                             componentPath = accessor.getResolvedUrl(componentResourceIdentifier)
  270.  
  271.                     except ftrack.AccessorFilesystemPathError:
  272.                         self.logger.error('### Filesystem path could not be determined from '
  273.                                      'resourceIdentifier for component: {0}!!!\n{1}'
  274.                                      .format(component, traceback.print_exc()))
  275.                         return
  276.  
  277.                     except ftrack.AccessorUnsupportedOperationError:
  278.                         self.logger.error('### Retrieving filesystem paths is not supported by '
  279.                                      'this accessor for component: {0}!!!\n{1}'
  280.                                      .format(component, traceback.print_exc()))
  281.                         return
  282.  
  283.                 # store data in dict
  284.                 componentData[componentName] = componentPath
  285.  
  286.             self.logger.info('Data on AssetVersion: {0}'.format(componentData))
  287.             return componentData
  288.  
  289.  
  290. def async(fn):
  291.     """
  292.    This decorator method allows for running *fn* asynchronously.
  293.    :param fn:
  294.    :return:
  295.    """
  296.  
  297.     def wrapper(*args, **kwargs):
  298.         thread = threading.Thread(target=fn, args=args, kwargs=kwargs)
  299.         thread.start()
  300.     return wrapper
  301.  
  302.  
  303. def downloadFile(url, saveDirectory, fileName=None):
  304.     """
  305.    This method downloads a single file given a url.
  306.    """
  307.  
  308.     logger = logging.getLogger(__name__)
  309.  
  310.     # Download the file from the URL and get fileHandle to object
  311.     try: remoteFile = urllib2.urlopen(url)
  312.     except urllib2.HTTPError:
  313.         logger.error('### URL: {0} could not be downloaded!\n{1}'
  314.                           .format(url, traceback.print_exc()))
  315.         return None
  316.  
  317.     # if no filename is provided, generate one from the URL directly
  318.     if not fileName:
  319.  
  320.         parsedUrl = urlparse.urlparse(remoteFile.url)
  321.         fileName = parsedUrl[2].split('/')[-1]
  322.  
  323.     savePath = os.path.join(saveDirectory, fileName)
  324.  
  325.     logger.info('Saving file to: {0}'.format(savePath))
  326.  
  327.     # Write file to disk
  328.     try:
  329.         with open(savePath, 'w') as localFile:
  330.             localFile.write(remoteFile.read())
  331.  
  332.     except IOError:
  333.         logger.error('### Could not write to" {0}!\n{1}'.format(savePath, traceback.print_exc()))
  334.         raise IOError
  335.  
  336.     return localFile.name
  337.  
  338. def initializeDownloads(urls, saveDirectory, fileNames=None):
  339.  
  340.     # Make list for appending all threads to
  341.     threads = []
  342.  
  343.     # Iterate over each URL and start a thread to handle each download
  344.     for idx, url in enumerate(urls):
  345.  
  346.         # Check if list of specific fileNames are given as well
  347.         if fileNames:
  348.             fileName = fileNames[idx]
  349.  
  350.         else:
  351.             fileName = None
  352.  
  353.         # Construct thread and pass arguments
  354.         thread = threading.Thread(
  355.             target=downloadFile,
  356.             kwargs={
  357.               'url': url,
  358.               'saveDirectory': saveDirectory,
  359.               'fileName': fileName
  360.         })
  361.  
  362.         threads.append(thread)
  363.         thread.start()
  364.  
  365.  
  366. def register(registry, **kwargs):
  367.     """
  368.    Register action. Called when used as an event plugin.
  369.    :param registry:
  370.    :param kwargs:
  371.    :return:
  372.    """
  373.  
  374.     logger = logging.getLogger(__name__)
  375.  
  376.     logger.debug('Registering action: {0}'.format(
  377.         os.path.abspath(os.path.dirname(__file__))))
  378.  
  379.     action = DownloadAllComponentsFromEntityAction()
  380.     action.register()
  381.  
  382.  
  383. def main(arguments=None):
  384.     """
  385.    This entry point sets up logging and registers action when this is run
  386.    as a standalone action.
  387.    :param arguments:
  388.    :return:
  389.    """
  390.     if arguments is None:
  391.         arguments = []
  392.  
  393.     parser = argparse.ArgumentParser()
  394.     # Allow setting of logging level from arguments.
  395.     loggingLevels = {}
  396.     for level in (
  397.         logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING,
  398.         logging.ERROR, logging.CRITICAL
  399.     ):
  400.         loggingLevels[logging.getLevelName(level).lower()] = level
  401.  
  402.     parser.add_argument(
  403.         '-v', '--verbosity',
  404.         help='Set the logging output verbosity.',
  405.         choices=loggingLevels.keys(),
  406.         default='info'
  407.     )
  408.     namespace = parser.parse_args(arguments)
  409.  
  410.     # Set up basic logging
  411.     logging.basicConfig(level=loggingLevels[namespace.verbosity])
  412.  
  413.     # Subscribe to action.
  414.     ftrack.setup()
  415.     action = DownloadAllComponentsFromEntityAction()
  416.     action.register()
  417.  
  418.     # Wait for events
  419.     ftrack.EVENT_HUB.wait()
  420.  
  421.  
  422. if __name__ == '__main__':
  423.     raise SystemExit(main(sys.argv[1:]))
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement