Handle exporting and importing repository capsules with new ExportRepositoryManager and ImportRepositoryMnager classes respectively.

This commit is contained in:
Greg Von Kuster
2014-06-24 13:11:24 -04:00
parent 808b12000e
commit bba63a24b0
10 changed files with 971 additions and 894 deletions
@@ -10,14 +10,14 @@ from galaxy import web
from galaxy.web.base.controller import BaseAPIController
from galaxy.web.base.controller import HTTPBadRequest
from galaxy.web.framework.helpers import time_ago
from tool_shed.capsule import capsule_manager
import tool_shed.repository_types.util as rt_util
import tool_shed.util.shed_util_common as suc
from tool_shed.util import basic_util
from tool_shed.util import encoding_util
from tool_shed.util import hg_util
from tool_shed.util import import_util
from tool_shed.util import metadata_util
from tool_shed.util import repository_maintenance_util
from tool_shed.util import shed_util_common as suc
from tool_shed.util import tool_util
log = logging.getLogger( __name__ )
@@ -248,10 +248,14 @@ class RepositoriesController( BaseAPIController ):
except tarfile.ReadError, e:
log.debug( 'Error opening capsule file %s: %s' % ( str( capsule_file_name ), str( e ) ) )
return {}
irm = capsule_manager.ImportRepositoryManager( trans.app,
trans.request.host,
trans.user,
trans.user_is_admin() )
capsule_dict[ 'tar_archive' ] = tar_archive
capsule_dict[ 'capsule_file_name' ] = capsule_file_name
capsule_dict = import_util.extract_capsule_files( **capsule_dict )
capsule_dict = import_util.validate_capsule( **capsule_dict )
capsule_dict = irm.extract_capsule_files( **capsule_dict )
capsule_dict = irm.validate_capsule( **capsule_dict )
status = capsule_dict.get( 'status', 'error' )
if status == 'error':
log.debug( 'The capsule contents are invalid and cannot be imported:<br/>%s' % \
@@ -263,15 +267,12 @@ class RepositoriesController( BaseAPIController ):
return {}
file_path = encoding_util.tool_shed_decode( encoded_file_path )
export_info_file_path = os.path.join( file_path, 'export_info.xml' )
export_info_dict = import_util.get_export_info_dict( export_info_file_path )
export_info_dict = irm.get_export_info_dict( export_info_file_path )
manifest_file_path = os.path.join( file_path, 'manifest.xml' )
# The manifest.xml file has already been validated, so no error_message should be returned here.
repository_info_dicts, error_message = import_util.get_repository_info_from_manifest( manifest_file_path )
repository_info_dicts, error_message = irm.get_repository_info_from_manifest( manifest_file_path )
# Determine the status for each exported repository archive contained within the capsule.
repository_status_info_dicts = import_util.get_repository_status_from_tool_shed( trans.app,
trans.user,
trans.user_is_admin(),
repository_info_dicts )
repository_status_info_dicts = irm.get_repository_status_from_tool_shed( repository_info_dicts )
# Generate a list of repository name / import results message tuples for display after the capsule is imported.
import_results_tups = []
# Only create repositories that do not yet exist and that the current user is authorized to create. The
@@ -280,12 +281,9 @@ class RepositoriesController( BaseAPIController ):
# Add the capsule_file_name and encoded_file_path to the repository_status_info_dict.
repository_status_info_dict[ 'capsule_file_name' ] = capsule_file_name
repository_status_info_dict[ 'encoded_file_path' ] = encoded_file_path
import_results_tups = import_util.create_repository_and_import_archive( trans.app,
trans.request.host,
trans.user,
repository_status_info_dict,
import_results_tups )
import_util.check_status_and_reset_downloadable( trans.app, import_results_tups )
import_results_tups = irm.create_repository_and_import_archive( repository_status_info_dict,
import_results_tups )
irm.check_status_and_reset_downloadable( import_results_tups )
basic_util.remove_dir( file_path )
# NOTE: the order of installation is defined in import_results_tups, but order will be lost
# when transferred to return_dict.
@@ -5,7 +5,7 @@ from galaxy import web
from galaxy import util
from galaxy.model.orm import and_, not_, select
from galaxy.web.base.controller import BaseAPIController, HTTPBadRequest
from tool_shed.util import export_util
from tool_shed.capsule import capsule_manager
from tool_shed.util import hg_util
import tool_shed.util.shed_util_common as suc
@@ -53,15 +53,14 @@ class RepositoryRevisionsController( BaseAPIController ):
log.debug( error_message )
return None, error_message
repository_id = trans.security.encode_id( repository.id )
return export_util.export_repository( trans.app,
trans.user,
tool_shed_url,
repository_id,
str( repository.name ),
changeset_revision,
file_type,
export_repository_dependencies,
api=True )
erm = capsule_manager.ExportRepositoryManager( app=trans.app,
user=trans.user,
tool_shed_url=tool_shed_url,
repository=repository,
changeset_revision=changeset_revision,
export_repository_dependencies=export_repository_dependencies,
using_api=True )
return erm.export_repository()
def __get_value_mapper( self, trans ):
value_mapper = { 'id' : trans.security.encode_id,
@@ -14,20 +14,19 @@ from galaxy.web.form_builder import CheckboxField
from galaxy.web.framework.helpers import grids
from galaxy.util import json
from galaxy.model.orm import and_
import tool_shed.util.shed_util_common as suc
from tool_shed.capsule import capsule_manager
from tool_shed.util import basic_util
from tool_shed.util import common_util
from tool_shed.util import container_util
from tool_shed.util import encoding_util
from tool_shed.util import export_util
from tool_shed.util import hg_util
from tool_shed.util import import_util
from tool_shed.util import metadata_util
from tool_shed.util import readme_util
from tool_shed.util import repository_dependency_util
from tool_shed.util import repository_maintenance_util
from tool_shed.util import review_util
from tool_shed.util import search_util
from tool_shed.util import shed_util_common as suc
from tool_shed.util import tool_dependency_util
from tool_shed.util import tool_util
from tool_shed.util import workflow_util
@@ -1149,7 +1148,7 @@ class RepositoryController( BaseUIController, ratings_util.ItemRatings ):
# server account's .hgrc file to include the following setting:
# [web]
# allow_archive = bz2, gz, zip
file_type_str = export_util.get_file_type_str( changeset_revision, file_type )
file_type_str = basic_util.get_file_type_str( changeset_revision, file_type )
repository.times_downloaded += 1
trans.sa_session.add( repository )
trans.sa_session.flush()
@@ -1172,15 +1171,14 @@ class RepositoryController( BaseUIController, ratings_util.ItemRatings ):
file_type = 'gz'
export_repository_dependencies = CheckboxField.is_checked( export_repository_dependencies )
tool_shed_url = web.url_for( '/', qualified=True )
repositories_archive, error_message = export_util.export_repository( trans.app,
trans.user,
tool_shed_url,
repository_id,
str( repository.name ),
changeset_revision,
file_type,
export_repository_dependencies,
api=False )
erm = capsule_manager.ExportRepositoryManager( app=trans.app,
user=trans.user,
tool_shed_url=tool_shed_url,
repository=repository,
changeset_revision=changeset_revision,
export_repository_dependencies=export_repository_dependencies,
using_api=False )
repositories_archive, error_message = erm.export_repository()
repositories_archive_filename = os.path.basename( repositories_archive.name )
if error_message:
message = error_message
@@ -1969,15 +1967,16 @@ class RepositoryController( BaseUIController, ratings_util.ItemRatings ):
encoded_file_path = kwd.get( 'encoded_file_path', None )
file_path = encoding_util.tool_shed_decode( encoded_file_path )
export_info_file_path = os.path.join( file_path, 'export_info.xml' )
export_info_dict = import_util.get_export_info_dict( export_info_file_path )
irm = capsule_manager.ImportRepositoryManager( trans.app,
trans.request.host,
trans.user,
trans.user_is_admin() )
export_info_dict = irm.get_export_info_dict( export_info_file_path )
manifest_file_path = os.path.join( file_path, 'manifest.xml' )
# The manifest.xml file has already been validated, so no error_message should be returned here.
repository_info_dicts, error_message = import_util.get_repository_info_from_manifest( manifest_file_path )
repository_info_dicts, error_message = irm.get_repository_info_from_manifest( manifest_file_path )
# Determine the status for each exported repository archive contained within the capsule.
repository_status_info_dicts = import_util.get_repository_status_from_tool_shed( trans.app,
trans.user,
trans.user_is_admin(),
repository_info_dicts )
repository_status_info_dicts = irm.get_repository_status_from_tool_shed( repository_info_dicts )
if 'import_capsule_button' in kwd:
# Generate a list of repository name / import results message tuples for display after the capsule is imported.
import_results_tups = []
@@ -1987,12 +1986,9 @@ class RepositoryController( BaseUIController, ratings_util.ItemRatings ):
# Add the capsule_file_name and encoded_file_path to the repository_status_info_dict.
repository_status_info_dict[ 'capsule_file_name' ] = capsule_file_name
repository_status_info_dict[ 'encoded_file_path' ] = encoded_file_path
import_results_tups = import_util.create_repository_and_import_archive( trans.app,
trans.request.host,
trans.user,
repository_status_info_dict,
import_results_tups )
import_util.check_status_and_reset_downloadable( trans.app, import_results_tups )
import_results_tups = irm.create_repository_and_import_archive( repository_status_info_dict,
import_results_tups )
irm.check_status_and_reset_downloadable( import_results_tups )
basic_util.remove_dir( file_path )
return trans.fill_template( '/webapps/tool_shed/repository/import_capsule_results.mako',
export_info_dict=export_info_dict,
@@ -3081,13 +3077,17 @@ class RepositoryController( BaseUIController, ratings_util.ItemRatings ):
status = kwd.get( 'status', 'done' )
url = kwd.get( 'url', '' )
if 'upload_capsule_button' in kwd:
capsule_dict = import_util.upload_capsule( **kwd )
irm = capsule_manager.ImportRepositoryManager( trans.app,
trans.request.host,
trans.user,
trans.user_is_admin() )
capsule_dict = irm.upload_capsule( **kwd )
status = capsule_dict.get( 'status', 'error' )
if status == 'error':
message = capsule_dict.get( 'error_message', '' )
else:
capsule_dict = import_util.extract_capsule_files( **capsule_dict )
capsule_dict = import_util.validate_capsule( **capsule_dict )
capsule_dict = irm.extract_capsule_files( **capsule_dict )
capsule_dict = irm.validate_capsule( **capsule_dict )
status = capsule_dict.get( 'status', 'error' )
if status == 'ok':
return trans.response.send_redirect( web.url_for( controller='repository',
View File
+867
View File
@@ -0,0 +1,867 @@
import logging
import os
import shutil
import tarfile
import tempfile
import threading
import urllib
from time import gmtime
from time import strftime
import tool_shed.repository_types.util as rt_util
from galaxy import web
from galaxy.util import asbool
from galaxy.util import CHUNK_SIZE
from galaxy.util.odict import odict
from tool_shed.dependencies import dependency_manager
from tool_shed.util import basic_util
from tool_shed.util import commit_util
from tool_shed.util import common_util
from tool_shed.util import encoding_util
from tool_shed.util import hg_util
from tool_shed.util import metadata_util
from tool_shed.util import repository_dependency_util
from tool_shed.util import repository_maintenance_util
from tool_shed.util import shed_util_common as suc
from tool_shed.util import xml_util
from tool_shed.galaxy_install.repository_dependencies.repository_dependency_manager import RepositoryDependencyManager
log = logging.getLogger( __name__ )
class ExportedRepositoryRegistry( object ):
def __init__( self ):
self.exported_repository_elems = []
class ExportRepositoryManager( object ):
def __init__( self, app, user, tool_shed_url, repository, changeset_revision, export_repository_dependencies, using_api ):
self.app = app
self.capsule_filename = 'capsule'
self.capsule_with_dependencies_filename = 'capsule_with_dependencies'
self.changeset_revision = changeset_revision
self.export_repository_dependencies = asbool( export_repository_dependencies )
self.file_type = 'gz'
self.repository = repository
self.repository_id = self.app.security.encode_id( repository.id )
self.tool_shed_url = tool_shed_url
self.user = user
self.using_api = using_api
def export_repository( self ):
repositories_archive_filename = self.generate_repository_archive_filename( use_tmp_archive_dir=True )
if self.export_repository_dependencies:
repo_info_dicts = self.get_repo_info_dicts()
repository_ids = self.get_repository_ids( repo_info_dicts )
ordered_repository_ids, ordered_repositories, ordered_changeset_revisions = \
self.order_components_for_import( repository_ids, repo_info_dicts )
else:
ordered_repository_ids = []
ordered_repositories = []
ordered_changeset_revisions = []
if self.repository:
repository_metadata = \
suc.get_current_repository_metadata_for_changeset_revision( self.app,
self.repository,
self.changeset_revision )
if repository_metadata:
ordered_repository_ids = [ self.repository_id ]
ordered_repositories = [ self.repository ]
ordered_changeset_revisions = [ repository_metadata.changeset_revision ]
repositories_archive = None
error_messages = ''
lock = threading.Lock()
lock.acquire( True )
try:
repositories_archive = tarfile.open( repositories_archive_filename, "w:%s" % self.file_type )
exported_repository_registry = ExportedRepositoryRegistry()
for index, repository_id in enumerate( ordered_repository_ids ):
work_dir = tempfile.mkdtemp( prefix="tmp-toolshed-export-er" )
ordered_repository = ordered_repositories[ index ]
ordered_changeset_revision = ordered_changeset_revisions[ index ]
repository_archive, error_message = self.generate_repository_archive( ordered_repository,
ordered_changeset_revision,
work_dir )
if error_message:
error_messages = '%s %s' % ( error_messages, error_message )
else:
archive_name = str( os.path.basename( repository_archive.name ) )
repositories_archive.add( repository_archive.name, arcname=archive_name )
attributes, sub_elements = self.get_repository_attributes_and_sub_elements( ordered_repository,
archive_name )
elem = xml_util.create_element( 'repository', attributes=attributes, sub_elements=sub_elements )
exported_repository_registry.exported_repository_elems.append( elem )
basic_util.remove_dir( work_dir )
# Keep information about the export in a file named export_info.xml in the archive.
sub_elements = self.generate_export_elem()
export_elem = xml_util.create_element( 'export_info', attributes=None, sub_elements=sub_elements )
tmp_export_info = xml_util.create_and_write_tmp_file( export_elem, use_indent=True )
repositories_archive.add( tmp_export_info, arcname='export_info.xml' )
# Write the manifest, which must preserve the order in which the repositories should be imported.
exported_repository_root = xml_util.create_element( 'repositories' )
for exported_repository_elem in exported_repository_registry.exported_repository_elems:
exported_repository_root.append( exported_repository_elem )
tmp_manifest = xml_util.create_and_write_tmp_file( exported_repository_root, use_indent=True )
repositories_archive.add( tmp_manifest, arcname='manifest.xml' )
except Exception, e:
log.exception( str( e ) )
finally:
lock.release()
if repositories_archive is not None:
repositories_archive.close()
if self.using_api:
encoded_repositories_archive_name = encoding_util.tool_shed_encode( repositories_archive_filename )
params = '?encoded_repositories_archive_name=%s' % encoded_repositories_archive_name
download_url = common_util.url_join( web.url_for( '/', qualified=True ),
'repository/export_via_api%s' % params )
return dict( download_url=download_url, error_messages=error_messages )
return repositories_archive, error_messages
def generate_export_elem( self ):
sub_elements = odict()
sub_elements[ 'export_time' ] = strftime( '%a, %d %b %Y %H:%M:%S +0000', gmtime() )
sub_elements[ 'tool_shed' ] = str( self.tool_shed_url.rstrip( '/' ) )
sub_elements[ 'repository_name' ] = str( self.repository.name )
sub_elements[ 'repository_owner' ] = str( self.repository.user.username )
sub_elements[ 'changeset_revision' ] = str( self.changeset_revision )
sub_elements[ 'export_repository_dependencies' ] = str( self.export_repository_dependencies )
sub_elements[ 'exported_via_api' ] = str( self.using_api )
return sub_elements
def generate_repository_archive( self, repository, changeset_revision, work_dir ):
rdah = dependency_manager.RepositoryDependencyAttributeHandler( self.app, unpopulate=True )
tdah = dependency_manager.ToolDependencyAttributeHandler( self.app, unpopulate=True )
file_type_str = basic_util.get_file_type_str( changeset_revision, self.file_type )
file_name = '%s-%s' % ( repository.name, file_type_str )
return_code, error_message = hg_util.archive_repository_revision( self.app,
repository,
work_dir,
changeset_revision )
if return_code:
return None, error_message
repository_archive_name = os.path.join( work_dir, file_name )
# Create a compressed tar archive that will contain only valid files and possibly altered dependency definition files.
repository_archive = tarfile.open( repository_archive_name, "w:%s" % self.file_type )
for root, dirs, files in os.walk( work_dir ):
if root.find( '.hg' ) < 0 and root.find( 'hgrc' ) < 0:
for dir in dirs:
if dir in commit_util.UNDESIRABLE_DIRS:
dirs.remove( dir )
for name in files:
name = str( name )
if str( name ) in commit_util.UNDESIRABLE_FILES:
continue
full_path = os.path.join( root, name )
relative_path = full_path.replace( work_dir, '' ).lstrip( '/' )
# See if we have a repository dependencies defined.
if name == rt_util.REPOSITORY_DEPENDENCY_DEFINITION_FILENAME:
# Eliminate the toolshed, and changeset_revision attributes from all <repository> tags.
altered, root_elem, error_message = rdah.handle_tag_attributes( full_path )
if error_message:
return None, error_message
if altered:
tmp_filename = xml_util.create_and_write_tmp_file( root_elem, use_indent=True )
shutil.move( tmp_filename, full_path )
elif name == rt_util.TOOL_DEPENDENCY_DEFINITION_FILENAME:
# Eliminate the toolshed, and changeset_revision attributes from all <repository> tags.
altered, root_elem, error_message = tdah.handle_tag_attributes( full_path )
if error_message:
return None, error_message
if altered:
tmp_filename = xml_util.create_and_write_tmp_file( root_elem, use_indent=True )
shutil.move( tmp_filename, full_path )
repository_archive.add( full_path, arcname=relative_path )
repository_archive.close()
return repository_archive, error_message
def generate_repository_archive_filename( self, use_tmp_archive_dir=False ):
tool_shed = self.remove_protocol_from_tool_shed_url()
file_type_str = basic_util.get_file_type_str( self.changeset_revision, self.file_type )
if self.export_repository_dependencies:
repositories_archive_filename = '%s_%s_%s_%s_%s' % ( self.capsule_with_dependencies_filename,
tool_shed,
str( self.repository.name ),
str( self.repository.user.username ),
file_type_str )
else:
repositories_archive_filename = '%s_%s_%s_%s_%s' % ( self.capsule_filename,
tool_shed,
str( self.repository.name ),
str( self.repository.user.username ),
file_type_str )
if use_tmp_archive_dir:
tmp_archive_dir = tempfile.mkdtemp( prefix="tmp-toolshed-arcdir" )
repositories_archive_filename = os.path.join( tmp_archive_dir, repositories_archive_filename )
return repositories_archive_filename
def get_components_from_repo_info_dict( self, repo_info_dict ):
"""
Return the repository and the associated latest installable changeset_revision (including
# updates) for the repository defined by the received repo_info_dict.
"""
for repository_name, repo_info_tup in repo_info_dict.items():
# There should only be one entry in the received repo_info_dict.
description, \
repository_clone_url, \
changeset_revision, \
ctx_rev, \
repository_owner, \
repository_dependencies, \
tool_dependencies = \
suc.get_repo_info_tuple_contents( repo_info_tup )
repository = suc.get_repository_by_name_and_owner( self.app, repository_name, repository_owner )
repository_metadata = suc.get_current_repository_metadata_for_changeset_revision( self.app,
repository,
changeset_revision )
if repository_metadata:
return repository, repository_metadata.changeset_revision
return None, None
def get_repo_info_dict_for_import( self, encoded_repository_id, encoded_repository_ids, repo_info_dicts ):
"""
The received encoded_repository_ids and repo_info_dicts are lists that contain associated
elements at each location in the list. This method will return the element from repo_info_dicts
associated with the received encoded_repository_id by determining its location in the received
encoded_repository_ids list.
"""
for index, repository_id in enumerate( encoded_repository_ids ):
if repository_id == encoded_repository_id:
repo_info_dict = repo_info_dicts[ index ]
return repo_info_dict
return None
def get_repo_info_dicts( self ):
"""
Return a list of dictionaries defining repositories that are required by the repository
associated with self.repository_id.
"""
rdm = RepositoryDependencyManager( self.app )
repository = suc.get_repository_in_tool_shed( self.app, self.repository_id )
repository_metadata = suc.get_repository_metadata_by_changeset_revision( self.app,
self.repository_id,
self.changeset_revision )
# Get a dictionary of all repositories upon which the contents of the current repository_metadata record depend.
toolshed_base_url = str( web.url_for( '/', qualified=True ) ).rstrip( '/' )
repository_dependencies = \
repository_dependency_util.get_repository_dependencies_for_changeset_revision( app=self.app,
repository=self.repository,
repository_metadata=repository_metadata,
toolshed_base_url=toolshed_base_url,
key_rd_dicts_to_be_processed=None,
all_repository_dependencies=None,
handled_key_rd_dicts=None )
repo = hg_util.get_repo_for_repository( self.app,
repository=self.repository,
repo_path=None,
create=False )
ctx = hg_util.get_changectx_for_changeset( repo, self.changeset_revision )
repo_info_dict = {}
# Cast unicode to string.
repo_info_dict[ str( repository.name ) ] = ( str( self.repository.description ),
common_util.generate_clone_url_for_repository_in_tool_shed( self.user,
self.repository ),
str( self.changeset_revision ),
str( ctx.rev() ),
str( self.repository.user.username ),
repository_dependencies,
None )
all_required_repo_info_dict = rdm.get_required_repo_info_dicts( self.tool_shed_url, [ repo_info_dict ] )
all_repo_info_dicts = all_required_repo_info_dict.get( 'all_repo_info_dicts', [] )
return all_repo_info_dicts
def get_repository_attributes_and_sub_elements( self, repository, archive_name ):
"""
Get the information about a repository to create and populate an XML tag set. The
generated attributes will be contained within the <repository> tag, while the sub_elements
will be tag sets contained within the <repository> tag set.
"""
attributes = odict()
sub_elements = odict()
attributes[ 'name' ] = str( repository.name )
attributes[ 'type' ] = str( repository.type )
# We have to associate the public username since the user_id will be different between tool sheds.
attributes[ 'username' ] = str( repository.user.username )
# Don't coerce description or long description from unicode to string because the fields are free text.
sub_elements[ 'description' ] = repository.description
sub_elements[ 'long_description' ] = repository.long_description
sub_elements[ 'archive' ] = archive_name
# Keep track of Category associations.
categories = []
for rca in repository.categories:
category = rca.category
categories.append( ( 'category', str( category.name ) ) )
sub_elements[ 'categories' ] = categories
return attributes, sub_elements
def get_repository_ids( self, repo_info_dicts ):
"""Return a list of repository ids associated with each dictionary in the received repo_info_dicts."""
repository_ids = []
for repo_info_dict in repo_info_dicts:
for repository_name, repo_info_tup in repo_info_dict.items():
description, \
repository_clone_url, \
changeset_revision, \
ctx_rev, \
repository_owner, \
repository_dependencies, \
tool_dependencies = \
suc.get_repo_info_tuple_contents( repo_info_tup )
repository = suc.get_repository_by_name_and_owner( self.app, repository_name, repository_owner )
repository_ids.append( self.app.security.encode_id( repository.id ) )
return repository_ids
def order_components_for_import( self, repository_ids, repo_info_dicts ):
"""
Some repositories may have repository dependencies that must be imported and have metadata set on
them before the dependent repository is imported. This method will inspect the list of repositories
about to be exported and make sure to order them appropriately for proper import. For each repository
about to be exported, if required repositories are not contained in the list of repositories about to
be exported, then they are not considered. Repository dependency definitions that contain circular
dependencies should not result in an infinite loop, but obviously ordering the list will not be handled
for one or more of the repositories that require prior import.
"""
# The received list of repository_ids are the ids of all of the primary exported repository's
# repository dependencies. The primary repository will always be last in the returned lists.
ordered_repository_ids = []
ordered_repositories = []
ordered_changeset_revisions = []
# Create a dictionary whose keys are the received repository_ids and whose values are a list of
# repository_ids, each of which is contained in the received list of repository_ids and whose associated
# repository must be imported prior to the repository associated with the repository_id key.
prior_import_required_dict = suc.get_prior_import_or_install_required_dict( self.app,
repository_ids,
repo_info_dicts )
processed_repository_ids = []
# Process the list of repository dependencies defined for the primary exported repository.
while len( processed_repository_ids ) != len( prior_import_required_dict.keys() ):
repository_id = suc.get_next_prior_import_or_install_required_dict_entry( prior_import_required_dict,
processed_repository_ids )
if repository_id == self.repository_id:
# Append self.repository_id without processing it since it must be returned last in the order.
# It will be processed below after all dependencies are processed.
processed_repository_ids.append( self.repository_id )
continue
processed_repository_ids.append( repository_id )
if repository_id not in ordered_repository_ids:
prior_import_required_ids = prior_import_required_dict[ repository_id ]
for prior_import_required_id in prior_import_required_ids:
if prior_import_required_id not in ordered_repository_ids:
# Import the associated repository dependency first.
prior_repo_info_dict = \
self.get_repo_info_dict_for_import( prior_import_required_id,
repository_ids,
repo_info_dicts )
prior_repository, prior_import_changeset_revision = \
self.get_components_from_repo_info_dict( prior_repo_info_dict )
if prior_repository and prior_import_changeset_revision:
ordered_repository_ids.append( prior_import_required_id )
ordered_repositories.append( prior_repository )
ordered_changeset_revisions.append( prior_import_changeset_revision )
repo_info_dict = self.get_repo_info_dict_for_import( repository_id, repository_ids, repo_info_dicts )
repository, changeset_revision = self.get_components_from_repo_info_dict( repo_info_dict )
if repository and changeset_revision:
ordered_repository_ids.append( repository_id )
ordered_repositories.append( repository )
ordered_changeset_revisions.append( changeset_revision )
# Process the repository associated with self.repository_id last.
repo_info_dict = self.get_repo_info_dict_for_import( self.repository_id, repository_ids, repo_info_dicts )
repository, changeset_revision = self.get_components_from_repo_info_dict( repo_info_dict )
if repository and changeset_revision:
ordered_repository_ids.append( repository_id )
ordered_repositories.append( repository )
ordered_changeset_revisions.append( changeset_revision )
return ordered_repository_ids, ordered_repositories, ordered_changeset_revisions
def remove_protocol_from_tool_shed_url( self ):
protocol, base = self.tool_shed_url.split( '://' )
base = base.replace( ':', '_colon_' )
base = base.rstrip( '/' )
return base
class ImportRepositoryManager( object ):
def __init__( self, app, host, user, user_is_admin ):
self.app = app
self.host = host
self.user = user
self.user_is_admin = user_is_admin
def check_status_and_reset_downloadable( self, import_results_tups ):
"""Check the status of each imported repository and set downloadable to False if errors."""
sa_session = self.app.model.context.current
flush = False
for import_results_tup in import_results_tups:
ok, name_owner, message = import_results_tup
name, owner = name_owner
if not ok:
repository = suc.get_repository_by_name_and_owner( self.app, name, owner )
if repository is not None:
# Do not allow the repository to be automatically installed if population resulted in errors.
tip_changeset_revision = repository.tip( self.app )
repository_metadata = suc.get_repository_metadata_by_changeset_revision( self.app,
self.app.security.encode_id( repository.id ),
tip_changeset_revision )
if repository_metadata:
if repository_metadata.downloadable:
repository_metadata.downloadable = False
sa_session.add( repository_metadata )
if not flush:
flush = True
# Do not allow dependent repository revisions to be automatically installed if population
# resulted in errors.
dependent_downloadable_revisions = suc.get_dependent_downloadable_revisions( self.app, repository_metadata )
for dependent_downloadable_revision in dependent_downloadable_revisions:
if dependent_downloadable_revision.downloadable:
dependent_downloadable_revision.downloadable = False
sa_session.add( dependent_downloadable_revision )
if not flush:
flush = True
if flush:
sa_session.flush()
def create_repository_and_import_archive( self, repository_archive_dict, import_results_tups ):
"""
Create a new repository in the tool shed and populate it with the contents of a gzip compressed
tar archive that was exported as part or all of the contents of a capsule.
"""
results_message = ''
name = repository_archive_dict.get( 'name', None )
username = repository_archive_dict.get( 'owner', None )
if name is None or username is None:
ok = False
results_message += 'Import failed: required repository name <b>%s</b> or owner <b>%s</b> is missing.' % \
( str( name ), str( username ))
import_results_tups.append( ( ok, ( str( name ), str( username ) ), results_message ) )
else:
status = repository_archive_dict.get( 'status', None )
if status is None:
# The repository does not yet exist in this Tool Shed and the current user is authorized to import
# the current archive file.
type = repository_archive_dict.get( 'type', 'unrestricted' )
description = repository_archive_dict.get( 'description', '' )
long_description = repository_archive_dict.get( 'long_description', '' )
# The owner entry in the repository_archive_dict is the public username of the user associated with
# the exported repository archive.
user = suc.get_user_by_username( self.app, username )
if user is None:
ok = False
results_message += 'Import failed: repository owner <b>%s</b> does not have an account in this Tool Shed.' % \
str( username )
import_results_tups.append( ( ok, ( str( name ), str( username ) ), results_message ) )
else:
user_id = user.id
# The categories entry in the repository_archive_dict is a list of category names. If a name does not
# exist in the current Tool Shed, the category will not be created, so it will not be associated with
# the repository.
category_ids = []
category_names = repository_archive_dict.get( 'category_names', [] )
for category_name in category_names:
category = suc.get_category_by_name( self.app, category_name )
if category is None:
results_message += 'This Tool Shed does not have the category <b>%s</b> so it ' % str( category_name )
results_message += 'will not be associated with this repository.'
else:
category_ids.append( self.app.security.encode_id( category.id ) )
# Create the repository record in the database.
repository, create_message = repository_maintenance_util.create_repository( self.app,
name,
type,
description,
long_description,
user_id=user_id,
category_ids=category_ids )
if create_message:
results_message += create_message
# Populate the new repository with the contents of exported repository archive.
results_dict = self.import_repository_archive( repository, repository_archive_dict )
ok = results_dict.get( 'ok', False )
error_message = results_dict.get( 'error_message', '' )
if error_message:
results_message += error_message
import_results_tups.append( ( ok, ( str( name ), str( username ) ), results_message ) )
else:
# The repository either already exists in this Tool Shed or the current user is not authorized to create it.
ok = True
results_message += 'Import not necessary: repository status for this Tool Shed is: %s.' % str( status )
import_results_tups.append( ( ok, ( str( name ), str( username ) ), results_message ) )
return import_results_tups
def extract_capsule_files( self, **kwd ):
"""
Extract the uploaded capsule archive into a temporary location for inspection, validation
and potential import.
"""
return_dict = {}
tar_archive = kwd.get( 'tar_archive', None )
capsule_file_name = kwd.get( 'capsule_file_name', None )
if tar_archive is not None and capsule_file_name is not None:
return_dict.update( kwd )
extract_directory_path = tempfile.mkdtemp( prefix="tmp-capsule-ecf" )
if capsule_file_name.endswith( '.tar.gz' ):
extract_directory_name = capsule_file_name.replace( '.tar.gz', '' )
elif capsule_file_name.endswith( '.tar' ):
extract_directory_name = capsule_file_name.replace( '.tar', '' )
else:
extract_directory_name = capsule_file_name
file_path = os.path.join( extract_directory_path, extract_directory_name )
return_dict[ 'encoded_file_path' ] = encoding_util.tool_shed_encode( file_path )
tar_archive.extractall( path=file_path )
try:
tar_archive.close()
except Exception, e:
log.exception( "Cannot close tar_archive: %s" % str( e ) )
del return_dict[ 'tar_archive' ]
return return_dict
def get_archives_from_manifest( self, manifest_file_path ):
"""
Return the list of archive names defined in the capsule manifest. This method will validate
the manifest by ensuring all <repository> tag sets contain a valid <archive> sub-element.
"""
archives = []
error_message = ''
manifest_tree, error_message = xml_util.parse_xml( manifest_file_path )
if error_message:
return archives, error_message
manifest_root = manifest_tree.getroot()
for elem in manifest_root:
# <repository name="package_lapack_3_4" type="tool_dependency_definition" username="test">
if elem.tag != 'repository':
error_message = 'All level one sub-elements in the manifest.xml file must be <repository> tag sets. '
error_message += 'The tag <b><%s></b> is invalid.' % str( elem.tag )
return [], error_message
archive_file_name = None
for repository_elem in elem:
if repository_elem.tag == 'archive':
# <archive>package_lapack_3_4-9e7a45ad3522.tar.gz</archive>
archive_file_name = repository_elem.text
break
if archive_file_name is None:
error_message = 'The %s tag set is missing a required <archive> sub-element.' % str( elem.tag )
return [], error_message
archives.append( archive_file_name )
return archives, error_message
def get_export_info_dict( self, export_info_file_path ):
"""
Parse the export_info.xml file contained within the capsule and return a dictionary
containing its entries.
"""
export_info_tree, error_message = xml_util.parse_xml( export_info_file_path )
export_info_root = export_info_tree.getroot()
export_info_dict = {}
for elem in export_info_root:
if elem.tag == 'export_time':
export_info_dict[ 'export_time' ] = elem.text
elif elem.tag == 'tool_shed':
export_info_dict[ 'tool_shed' ] = elem.text
elif elem.tag == 'repository_name':
export_info_dict[ 'repository_name' ] = elem.text
elif elem.tag == 'repository_owner':
export_info_dict[ 'repository_owner' ] = elem.text
elif elem.tag == 'changeset_revision':
export_info_dict[ 'changeset_revision' ] = elem.text
elif elem.tag == 'export_repository_dependencies':
if asbool( elem.text ):
export_info_dict[ 'export_repository_dependencies' ] = 'Yes'
else:
export_info_dict[ 'export_repository_dependencies' ] = 'No'
return export_info_dict
def get_repository_info_from_manifest( self, manifest_file_path ):
"""
Parse the capsule manifest and return a list of dictionaries containing information about
each exported repository archive contained within the capsule.
"""
repository_info_dicts = []
manifest_tree, error_message = xml_util.parse_xml( manifest_file_path )
if error_message:
return repository_info_dicts, error_message
manifest_root = manifest_tree.getroot()
for elem in manifest_root:
# <repository name="package_lapack_3_4" type="tool_dependency_definition" username="test">
if elem.tag != 'repository':
error_message = 'All level one sub-elements in the manifest.xml file must be <repository> tag sets. '
error_message += 'The tag <b><%s></b> is invalid.' % str( elem.tag )
return [], error_message
name = elem.get( 'name', None )
owner = elem.get( 'username', None )
type = elem.get( 'type', None )
if name is None or owner is None or type is None:
error_message = 'Missing required name, type, owner attributes from the tag %s' % str( elem.tag )
return [], error_message
repository_info_dict = dict( name=name, owner=owner, type=type )
for repository_elem in elem:
if repository_elem.tag == 'archive':
# <archive>package_lapack_3_4-9e7a45ad3522.tar.gz</archive>
archive_file_name = repository_elem.text
repository_info_dict[ 'archive_file_name' ] = archive_file_name
items = archive_file_name.split( '-' )
changeset_revision = items[ 1 ].rstrip( '.tar.gz' )
repository_info_dict [ 'changeset_revision' ] = changeset_revision
elif repository_elem.tag == 'categories':
category_names = []
for category_elem in repository_elem:
if category_elem.tag == 'category':
category_names.append( category_elem.text )
repository_info_dict[ 'category_names' ] = category_names
elif repository_elem.tag == 'description':
repository_info_dict[ 'description' ] = repository_elem.text
elif repository_elem.tag == 'long_description':
repository_info_dict[ 'long_description' ] = repository_elem.text
repository_info_dicts.append( repository_info_dict )
return repository_info_dicts, error_message
def get_repository_status_from_tool_shed( self, repository_info_dicts ):
"""
For each exported repository archive contained in the capsule, inspect the Tool Shed to
see if that repository already exists or if the current user is authorized to create the
repository and set a status appropriately. If repository dependencies are included in the
capsule, repositories may have various owners. We will keep repositories associated with
owners, so we need to restrict created repositories to those the current user can create.
If the current user is an admin or a member of the IUC, all repositories will be created
no matter the owner. Otherwise only repositories whose associated owner is the current
user will be created.
"""
repository_status_info_dicts = []
for repository_info_dict in repository_info_dicts:
repository = suc.get_repository_by_name_and_owner( self.app,
repository_info_dict[ 'name' ],
repository_info_dict[ 'owner' ] )
if repository:
if repository.deleted:
repository_info_dict[ 'status' ] = 'Exists, deleted'
elif repository.deprecated:
repository_info_dict[ 'status' ] = 'Exists, deprecated'
else:
repository_info_dict[ 'status' ] = 'Exists'
else:
# No repository with the specified name and owner currently exists, so make sure
# the current user can create one.
if self.user_is_admin:
repository_info_dict[ 'status' ] = None
elif self.app.security_agent.user_can_import_repository_archive( self.user,
repository_info_dict[ 'owner' ] ):
repository_info_dict[ 'status' ] = None
else:
repository_info_dict[ 'status' ] = 'Not authorized to import'
repository_status_info_dicts.append( repository_info_dict )
return repository_status_info_dicts
def import_repository_archive( self, repository, repository_archive_dict ):
"""Import a repository archive contained within a repository capsule."""
rdah = dependency_manager.RepositoryDependencyAttributeHandler( self.app, unpopulate=False )
tdah = dependency_manager.ToolDependencyAttributeHandler( self.app, unpopulate=False )
archive_file_name = repository_archive_dict.get( 'archive_file_name', None )
capsule_file_name = repository_archive_dict[ 'capsule_file_name' ]
encoded_file_path = repository_archive_dict[ 'encoded_file_path' ]
file_path = encoding_util.tool_shed_decode( encoded_file_path )
results_dict = dict( ok=True, error_message='' )
archive_file_path = os.path.join( file_path, archive_file_name )
archive = tarfile.open( archive_file_path, 'r:*' )
repo_dir = repository.repo_path( self.app )
repo = hg_util.get_repo_for_repository( self.app, repository=None, repo_path=repo_dir, create=False )
undesirable_dirs_removed = 0
undesirable_files_removed = 0
ok, error_message = commit_util.check_archive( repository, archive )
if ok:
full_path = os.path.abspath( repo_dir )
filenames_in_archive = []
for tarinfo_obj in archive.getmembers():
# Check files and directories in the archive.
ok = os.path.basename( tarinfo_obj.name ) not in commit_util.UNDESIRABLE_FILES
if ok:
for file_path_item in tarinfo_obj.name.split( '/' ):
if file_path_item in commit_util.UNDESIRABLE_DIRS:
undesirable_dirs_removed += 1
error_message = 'Import failed: invalid file path <b>%s</b> in archive <b>%s</b>' % \
( str( file_path_item ), str( archive_file_name ) )
results_dict[ 'ok' ] = False
results_dict[ 'error_message' ] += error_message
return results_dict
filenames_in_archive.append( tarinfo_obj.name )
else:
undesirable_files_removed += 1
# Extract the uploaded archive to the repository root.
archive.extractall( path=full_path )
archive.close()
for filename in filenames_in_archive:
uploaded_file_name = os.path.join( full_path, filename )
if os.path.split( uploaded_file_name )[ -1 ] == rt_util.REPOSITORY_DEPENDENCY_DEFINITION_FILENAME:
# Inspect the contents of the file to see if toolshed or changeset_revision attributes
# are missing and if so, set them appropriately.
altered, root_elem, error_message = rdah.handle_tag_attributes( uploaded_file_name )
if error_message:
results_dict[ 'ok' ] = False
results_dict[ 'error_message' ] += error_message
if altered:
tmp_filename = xml_util.create_and_write_tmp_file( root_elem )
shutil.move( tmp_filename, uploaded_file_name )
elif os.path.split( uploaded_file_name )[ -1 ] == rt_util.TOOL_DEPENDENCY_DEFINITION_FILENAME:
# Inspect the contents of the file to see if toolshed or changeset_revision
# attributes are missing and if so, set them appropriately.
altered, root_elem, error_message = tdah.handle_tag_attributes( uploaded_file_name )
if error_message:
results_dict[ 'ok' ] = False
results_dict[ 'error_message' ] += error_message
if altered:
tmp_filename = xml_util.create_and_write_tmp_file( root_elem )
shutil.move( tmp_filename, uploaded_file_name )
commit_message = 'Imported from capsule %s' % str( capsule_file_name )
# Send email notification to those that have registered to receive alerts for new repositories in this Tool Shed.
new_repo_alert = True
# Since the repository is new, the following must be False.
remove_repo_files_not_in_tar = False
ok, error_message, files_to_remove, content_alert_str, undesirable_dirs_removed, undesirable_files_removed = \
commit_util.handle_directory_changes( self.app,
self.host,
self.user.username,
repository,
full_path,
filenames_in_archive,
remove_repo_files_not_in_tar,
new_repo_alert,
commit_message,
undesirable_dirs_removed,
undesirable_files_removed )
if error_message:
results_dict[ 'ok' ] = False
results_dict[ 'error_message' ] += error_message
try:
status, error_message = metadata_util.set_repository_metadata_due_to_new_tip( self.app,
self.host,
self.user,
repository,
content_alert_str=content_alert_str )
if error_message:
results_dict[ 'ok' ] = False
results_dict[ 'error_message' ] += error_message
except Exception, e:
log.debug( "Error setting metadata on repository %s created from imported archive %s: %s" % \
( str( repository.name ), str( archive_file_name ), str( e ) ) )
else:
archive.close()
results_dict[ 'ok' ] = False
results_dict[ 'error_message' ] += error_message
return results_dict
def upload_capsule( self, **kwd ):
"""Upload and prepare an exported repository capsule for validation."""
file_data = kwd.get( 'file_data', '' )
url = kwd.get( 'url', '' )
uploaded_file = None
return_dict = dict( error_message='',
encoded_file_path=None,
status='ok',
tar_archive=None,
uploaded_file=None,
capsule_file_name=None )
if file_data == '' and url == '':
message = 'No files were entered on the import form.'
status = 'error'
elif url:
valid_url = True
try:
stream = urllib.urlopen( url )
except Exception, e:
valid_url = False
message = 'Error importing file via http: %s' % str( e )
status = 'error'
if valid_url:
fd, uploaded_file_name = tempfile.mkstemp()
uploaded_file = open( uploaded_file_name, 'wb' )
while 1:
chunk = stream.read( CHUNK_SIZE )
if not chunk:
break
uploaded_file.write( chunk )
uploaded_file.flush()
uploaded_file_filename = url.split( '/' )[ -1 ]
isempty = os.path.getsize( os.path.abspath( uploaded_file_name ) ) == 0
elif file_data not in ( '', None ):
uploaded_file = file_data.file
uploaded_file_name = uploaded_file.name
uploaded_file_filename = os.path.split( file_data.filename )[ -1 ]
isempty = os.path.getsize( os.path.abspath( uploaded_file_name ) ) == 0
if uploaded_file is not None:
if isempty:
uploaded_file.close()
return_dict[ 'error_message' ] = 'Your uploaded capsule file is empty.'
return_dict[ 'status' ] = 'error'
return return_dict
try:
# Open for reading with transparent compression.
tar_archive = tarfile.open( uploaded_file_name, 'r:*' )
except tarfile.ReadError, e:
error_message = 'Error opening file %s: %s' % ( str( uploaded_file_name ), str( e ) )
log.exception( error_message )
return_dict[ 'error_message' ] = error_message
return_dict[ 'status' ] = 'error'
uploaded_file.close()
return return_dict
return_dict[ 'tar_archive' ] = tar_archive
return_dict[ 'capsule_file_name' ] = uploaded_file_filename
uploaded_file.close()
else:
return_dict[ 'error_message' ] = 'No files were entered on the import form.'
return_dict[ 'status' ] = 'error'
return return_dict
return return_dict
def validate_capsule( self, **kwd ):
"""
Inspect the uploaded capsule's manifest and its contained files to ensure it is a valid
repository capsule.
"""
capsule_dict = {}
capsule_dict.update( kwd )
encoded_file_path = capsule_dict.get( 'encoded_file_path', '' )
file_path = encoding_util.tool_shed_decode( encoded_file_path )
# The capsule must contain a valid XML file named export_info.xml.
export_info_file_path = os.path.join( file_path, 'export_info.xml' )
export_info_tree, error_message = xml_util.parse_xml( export_info_file_path )
if error_message:
capsule_dict[ 'error_message' ] = error_message
capsule_dict[ 'status' ] = 'error'
return capsule_dict
# The capsule must contain a valid XML file named manifest.xml.
manifest_file_path = os.path.join( file_path, 'manifest.xml' )
# Validate the capsule manifest by inspecting name, owner, changeset_revision and type
# information contained within each <repository> tag set.
repository_info_dicts, error_message = self.get_repository_info_from_manifest( manifest_file_path )
if error_message:
capsule_dict[ 'error_message' ] = error_message
capsule_dict[ 'status' ] = 'error'
return capsule_dict
# Validate the capsule manifest by ensuring all <repository> tag sets contain a valid
# <archive> sub-element.
archives, error_message = self.get_archives_from_manifest( manifest_file_path )
if error_message:
capsule_dict[ 'error_message' ] = error_message
capsule_dict[ 'status' ] = 'error'
return capsule_dict
# Validate the capsule manifest by ensuring each defined archive file name exists within
# the capsule.
error_message = self.verify_archives_in_capsule( file_path, archives )
if error_message:
capsule_dict[ 'error_message' ] = error_message
capsule_dict[ 'status' ] = 'error'
return capsule_dict
capsule_dict[ 'status' ] = 'ok'
return capsule_dict
def verify_archives_in_capsule( self, file_path, archives ):
"""
Inspect the files contained within the capsule and make sure each is defined correctly
in the capsule manifest.
"""
error_message = ''
for archive_file_name in archives:
full_path = os.path.join( file_path, archive_file_name )
if not os.path.exists( full_path ):
error_message = 'The uploaded capsule is invalid because the contained manifest.xml '
error_message += 'file defines an archive file named <b>%s</b> which ' % str( archive_file_name )
error_message += 'is not contained within the capsule.'
break
return error_message
+41 -9
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python
"""
Export a specified repository revision and optionally all of its defined repository dependencies from the tool shed into a compressed archive.
Export a specified repository revision and optionally all of its defined repository
dependencies from the tool shed into a compressed archive.
Here is a working example of how to use this script to export a repository from the tool shed.
./export.py --url http://testtoolshed.g2.bx.psu.edu --name chemicaltoolbox --owner bgruening --revision 4133dbf7ff4d --export_repository_dependencies True --download_dir /tmp
@@ -8,15 +9,45 @@ Here is a working example of how to use this script to export a repository from
import os
import sys
import tempfile
import argparse
import urllib2
sys.path.insert( 0, os.path.dirname( __file__ ) )
from common import display
from common import submit
from tool_shed.util import export_util
from tool_shed.util import basic_util
CAPSULE_FILENAME = 'capsule'
CAPSULE_WITH_DEPENDENCIES_FILENAME = 'capsule_with_dependencies'
CHUNK_SIZE = 2**20 # 1Mb
def generate_repository_archive_filename( tool_shed_url, name, owner, changeset_revision, file_type,
export_repository_dependencies, use_tmp_archive_dir=False ):
tool_shed = remove_protocol_from_tool_shed_url( tool_shed_url )
file_type_str = basic_util.get_file_type_str( changeset_revision, file_type )
if self.export_repository_dependencies:
repositories_archive_filename = '%s_%s_%s_%s_%s' % ( CAPSULE_WITH_DEPENDENCIES_FILENAME,
tool_shed,
name,
owner,
file_type_str )
else:
repositories_archive_filename = '%s_%s_%s_%s_%s' % ( CAPSULE_FILENAME,
tool_shed,
name,
owner,
file_type_str )
if use_tmp_archive_dir:
tmp_archive_dir = tempfile.mkdtemp( prefix="tmp-toolshed-arcdir" )
repositories_archive_filename = os.path.join( tmp_archive_dir, repositories_archive_filename )
return repositories_archive_filename
def remove_protocol_from_tool_shed_url( tool_shed_url ):
protocol, base = tool_shed_url.split( '://' )
base = base.replace( ':', '_colon_' )
base = base.rstrip( '/' )
return base
def string_as_bool( string ):
if str( string ).lower() in ( 'true', 'yes', 'on' ):
return True
@@ -50,14 +81,15 @@ def main( options ):
if error_messages:
print "Error attempting to export revision ", options.changeset_revision, " of repository ", options.name, " owned by ", options.owner, ":\n", error_messages
else:
export_repository_dependencies = string_as_bool( options.export_repository_dependencies )
repositories_archive_filename = \
export_util.generate_repository_archive_filename( base_tool_shed_url,
options.name,
options.owner,
options.changeset_revision,
file_type,
export_repository_dependencies=string_as_bool( options.export_repository_dependencies ),
use_tmp_archive_dir=False )
generate_repository_archive_filename( base_tool_shed_url,
options.name,
options.owner,
options.changeset_revision,
file_type,
export_repository_dependencies=export_repository_dependencies,
use_tmp_archive_dir=False )
download_url = export_dict[ 'download_url' ]
download_dir = os.path.abspath( options.download_dir )
file_path = os.path.join( download_dir, repositories_archive_filename )
+11
View File
@@ -45,6 +45,17 @@ def get_env_var_values( install_environment ):
env_var_dict[ '__is64bit__' ] = sys.maxsize > 2**32
return env_var_dict
def get_file_type_str( changeset_revision, file_type ):
if file_type == 'zip':
file_type_str = '%s.zip' % changeset_revision
elif file_type == 'bz2':
file_type_str = '%s.tar.bz2' % changeset_revision
elif file_type == 'gz':
file_type_str = '%s.tar.gz' % changeset_revision
else:
file_type_str = ''
return file_type_str
def move_file( current_dir, source, destination, rename_to=None ):
source_path = os.path.abspath( os.path.join( current_dir, source ) )
source_file = os.path.basename( source_path )
-355
View File
@@ -1,355 +0,0 @@
import logging
import os
import shutil
import tarfile
import tempfile
import threading
from time import gmtime
from time import strftime
import tool_shed.repository_types.util as rt_util
from galaxy import web
from galaxy.util.odict import odict
from tool_shed.dependencies import dependency_manager
from tool_shed.util import basic_util
from tool_shed.util import commit_util
from tool_shed.util import common_util
from tool_shed.util import encoding_util
from tool_shed.util import hg_util
from tool_shed.util import repository_dependency_util
from tool_shed.util import shed_util_common as suc
from tool_shed.util import xml_util
from tool_shed.galaxy_install.repository_dependencies.repository_dependency_manager import RepositoryDependencyManager
log = logging.getLogger( __name__ )
CAPSULE_FILENAME = 'capsule'
CAPSULE_WITH_DEPENDENCIES_FILENAME = 'capsule_with_dependencies'
class ExportedRepositoryRegistry( object ):
def __init__( self ):
self.exported_repository_elems = []
def export_repository( app, user, tool_shed_url, repository_id, repository_name, changeset_revision, file_type,
export_repository_dependencies, api=False ):
repository = suc.get_repository_in_tool_shed( app, repository_id )
repositories_archive_filename = generate_repository_archive_filename( tool_shed_url,
str( repository.name ),
str( repository.user.username ),
changeset_revision,
file_type,
export_repository_dependencies=export_repository_dependencies,
use_tmp_archive_dir=True )
if export_repository_dependencies:
repo_info_dicts = get_repo_info_dicts( app, user, tool_shed_url, repository_id, changeset_revision )
repository_ids = get_repository_ids( app, repo_info_dicts )
ordered_repository_ids, ordered_repositories, ordered_changeset_revisions = \
order_components_for_import( app, repository_id, repository_ids, repo_info_dicts )
else:
ordered_repository_ids = []
ordered_repositories = []
ordered_changeset_revisions = []
if repository:
repository_metadata = suc.get_current_repository_metadata_for_changeset_revision( app,
repository,
changeset_revision )
if repository_metadata:
ordered_repository_ids = [ repository_id ]
ordered_repositories = [ repository ]
ordered_changeset_revisions = [ repository_metadata.changeset_revision ]
repositories_archive = None
error_messages = ''
lock = threading.Lock()
lock.acquire( True )
try:
repositories_archive = tarfile.open( repositories_archive_filename, "w:%s" % file_type )
exported_repository_registry = ExportedRepositoryRegistry()
for index, repository_id in enumerate( ordered_repository_ids ):
work_dir = tempfile.mkdtemp( prefix="tmp-toolshed-export-er" )
ordered_repository = ordered_repositories[ index ]
ordered_changeset_revision = ordered_changeset_revisions[ index ]
repository_archive, error_message = generate_repository_archive( app,
work_dir,
tool_shed_url,
ordered_repository,
ordered_changeset_revision,
file_type )
if error_message:
error_messages = '%s %s' % ( error_messages, error_message )
else:
archive_name = str( os.path.basename( repository_archive.name ) )
repositories_archive.add( repository_archive.name, arcname=archive_name )
attributes, sub_elements = get_repository_attributes_and_sub_elements( ordered_repository, archive_name )
elem = xml_util.create_element( 'repository', attributes=attributes, sub_elements=sub_elements )
exported_repository_registry.exported_repository_elems.append( elem )
basic_util.remove_dir( work_dir )
# Keep information about the export in a file name export_info.xml in the archive.
sub_elements = generate_export_elem( tool_shed_url, repository, changeset_revision, export_repository_dependencies, api )
export_elem = xml_util.create_element( 'export_info', attributes=None, sub_elements=sub_elements )
tmp_export_info = xml_util.create_and_write_tmp_file( export_elem, use_indent=True )
repositories_archive.add( tmp_export_info, arcname='export_info.xml' )
# Write the manifest, which must preserve the order in which the repositories should be imported.
exported_repository_root = xml_util.create_element( 'repositories' )
for exported_repository_elem in exported_repository_registry.exported_repository_elems:
exported_repository_root.append( exported_repository_elem )
tmp_manifest = xml_util.create_and_write_tmp_file( exported_repository_root, use_indent=True )
repositories_archive.add( tmp_manifest, arcname='manifest.xml' )
except Exception, e:
log.exception( str( e ) )
finally:
lock.release()
repositories_archive.close()
if api:
encoded_repositories_archive_name = encoding_util.tool_shed_encode( repositories_archive_filename )
params = '?encoded_repositories_archive_name=%s' % encoded_repositories_archive_name
download_url = common_util.url_join( web.url_for( '/', qualified=True ),
'repository/export_via_api%s' % params )
return dict( download_url=download_url, error_messages=error_messages )
return repositories_archive, error_messages
def generate_repository_archive( app, work_dir, tool_shed_url, repository, changeset_revision, file_type ):
rdah = dependency_manager.RepositoryDependencyAttributeHandler( app, unpopulate=True )
tdah = dependency_manager.ToolDependencyAttributeHandler( app, unpopulate=True )
file_type_str = get_file_type_str( changeset_revision, file_type )
file_name = '%s-%s' % ( repository.name, file_type_str )
return_code, error_message = hg_util.archive_repository_revision( app,
repository,
work_dir,
changeset_revision )
if return_code:
return None, error_message
repository_archive_name = os.path.join( work_dir, file_name )
# Create a compressed tar archive that will contain only valid files and possibly altered dependency definition files.
repository_archive = tarfile.open( repository_archive_name, "w:%s" % file_type )
for root, dirs, files in os.walk( work_dir ):
if root.find( '.hg' ) < 0 and root.find( 'hgrc' ) < 0:
for dir in dirs:
if dir in commit_util.UNDESIRABLE_DIRS:
dirs.remove( dir )
for name in files:
name = str( name )
if str( name ) in commit_util.UNDESIRABLE_FILES:
continue
full_path = os.path.join( root, name )
relative_path = full_path.replace( work_dir, '' ).lstrip( '/' )
# See if we have a repository dependencies defined.
if name == rt_util.REPOSITORY_DEPENDENCY_DEFINITION_FILENAME:
# Eliminate the toolshed, and changeset_revision attributes from all <repository> tags.
altered, root_elem, error_message = rdah.handle_tag_attributes( full_path )
if error_message:
return None, error_message
if altered:
tmp_filename = xml_util.create_and_write_tmp_file( root_elem, use_indent=True )
shutil.move( tmp_filename, full_path )
elif name == rt_util.TOOL_DEPENDENCY_DEFINITION_FILENAME:
# Eliminate the toolshed, and changeset_revision attributes from all <repository> tags.
altered, root_elem, error_message = tdah.handle_tag_attributes( full_path )
if error_message:
return None, error_message
if altered:
tmp_filename = xml_util.create_and_write_tmp_file( root_elem, use_indent=True )
shutil.move( tmp_filename, full_path )
repository_archive.add( full_path, arcname=relative_path )
repository_archive.close()
return repository_archive, error_message
def generate_repository_archive_filename( tool_shed_url, name, owner, changeset_revision, file_type,
export_repository_dependencies=False, use_tmp_archive_dir=False ):
tool_shed = remove_protocol_from_tool_shed_url( tool_shed_url )
file_type_str = get_file_type_str( changeset_revision, file_type )
if export_repository_dependencies:
repositories_archive_filename = '%s_%s_%s_%s_%s' % ( CAPSULE_WITH_DEPENDENCIES_FILENAME, tool_shed, name, owner, file_type_str )
else:
repositories_archive_filename = '%s_%s_%s_%s_%s' % ( CAPSULE_FILENAME, tool_shed, name, owner, file_type_str )
if use_tmp_archive_dir:
tmp_archive_dir = tempfile.mkdtemp( prefix="tmp-toolshed-arcdir" )
repositories_archive_filename = os.path.join( tmp_archive_dir, repositories_archive_filename )
return repositories_archive_filename
def generate_export_elem( tool_shed_url, repository, changeset_revision, export_repository_dependencies, api ):
sub_elements = odict()
sub_elements[ 'export_time' ] = strftime( '%a, %d %b %Y %H:%M:%S +0000', gmtime() )
sub_elements[ 'tool_shed' ] = str( tool_shed_url.rstrip( '/' ) )
sub_elements[ 'repository_name' ] = str( repository.name )
sub_elements[ 'repository_owner' ] = str( repository.user.username )
sub_elements[ 'changeset_revision' ] = str( changeset_revision )
sub_elements[ 'export_repository_dependencies' ] = str( export_repository_dependencies )
sub_elements[ 'exported_via_api' ] = str( api )
return sub_elements
def get_components_from_repo_info_dict( app, repo_info_dict ):
"""
Return the repository and the associated latest installable changeset_revision (including updates) for the
repository defined by the received repo_info_dict.
"""
for repository_name, repo_info_tup in repo_info_dict.items():
# There should only be one entry in the received repo_info_dict.
description, repository_clone_url, changeset_revision, ctx_rev, repository_owner, repository_dependencies, tool_dependencies = \
suc.get_repo_info_tuple_contents( repo_info_tup )
repository = suc.get_repository_by_name_and_owner( app, repository_name, repository_owner )
repository_metadata = suc.get_current_repository_metadata_for_changeset_revision( app,
repository,
changeset_revision )
if repository_metadata:
return repository, repository_metadata.changeset_revision
return None, None
def get_file_type_str( changeset_revision, file_type ):
if file_type == 'zip':
file_type_str = '%s.zip' % changeset_revision
elif file_type == 'bz2':
file_type_str = '%s.tar.bz2' % changeset_revision
elif file_type == 'gz':
file_type_str = '%s.tar.gz' % changeset_revision
else:
file_type_str = ''
return file_type_str
def get_repo_info_dict_for_import( encoded_repository_id, encoded_repository_ids, repo_info_dicts ):
"""
The received encoded_repository_ids and repo_info_dicts are lists that contain associated elements at each
location in the list. This method will return the element from repo_info_dicts associated with the received
encoded_repository_id by determining its location in the received encoded_repository_ids list.
"""
for index, repository_id in enumerate( encoded_repository_ids ):
if repository_id == encoded_repository_id:
repo_info_dict = repo_info_dicts[ index ]
return repo_info_dict
return None
def get_repo_info_dicts( app, user, tool_shed_url, repository_id, changeset_revision ):
"""
Return a list of dictionaries defining repositories that are required by the repository associated with the
received repository_id.
"""
rdm = RepositoryDependencyManager( app )
repository = suc.get_repository_in_tool_shed( app, repository_id )
repository_metadata = suc.get_repository_metadata_by_changeset_revision( app, repository_id, changeset_revision )
# Get a dictionary of all repositories upon which the contents of the current repository_metadata record depend.
toolshed_base_url = str( web.url_for( '/', qualified=True ) ).rstrip( '/' )
repository_dependencies = \
repository_dependency_util.get_repository_dependencies_for_changeset_revision( app=app,
repository=repository,
repository_metadata=repository_metadata,
toolshed_base_url=toolshed_base_url,
key_rd_dicts_to_be_processed=None,
all_repository_dependencies=None,
handled_key_rd_dicts=None )
repo = hg_util.get_repo_for_repository( app, repository=repository, repo_path=None, create=False )
ctx = hg_util.get_changectx_for_changeset( repo, changeset_revision )
repo_info_dict = {}
# Cast unicode to string.
repo_info_dict[ str( repository.name ) ] = ( str( repository.description ),
common_util.generate_clone_url_for_repository_in_tool_shed( user, repository ),
str( changeset_revision ),
str( ctx.rev() ),
str( repository.user.username ),
repository_dependencies,
None )
all_required_repo_info_dict = rdm.get_required_repo_info_dicts( tool_shed_url, [ repo_info_dict ] )
all_repo_info_dicts = all_required_repo_info_dict.get( 'all_repo_info_dicts', [] )
return all_repo_info_dicts
def get_repository_attributes_and_sub_elements( repository, archive_name ):
"""
Get the information about a repository to create and populate an XML tag set. The generated attributes will
be contained within the <repository> tag, while the sub_elements will be tag sets contained within the <repository>
tag set.
"""
attributes = odict()
sub_elements = odict()
attributes[ 'name' ] = str( repository.name )
attributes[ 'type' ] = str( repository.type )
# We have to associate the public username since the user_id will be different between tool sheds.
attributes[ 'username' ] = str( repository.user.username )
# Don't coerce description or long description from unicode to string because the fields are free text.
sub_elements[ 'description' ] = repository.description
sub_elements[ 'long_description' ] = repository.long_description
sub_elements[ 'archive' ] = archive_name
# Keep track of Category associations.
categories = []
for rca in repository.categories:
category = rca.category
categories.append( ( 'category', str( category.name ) ) )
sub_elements[ 'categories' ] = categories
return attributes, sub_elements
def get_repository_ids( app, repo_info_dicts ):
"""Return a list of repository ids associated with each dictionary in the received repo_info_dicts."""
repository_ids = []
for repo_info_dict in repo_info_dicts:
for repository_name, repo_info_tup in repo_info_dict.items():
description, \
repository_clone_url, \
changeset_revision, \
ctx_rev, \
repository_owner, \
repository_dependencies, \
tool_dependencies = \
suc.get_repo_info_tuple_contents( repo_info_tup )
repository = suc.get_repository_by_name_and_owner( app, repository_name, repository_owner )
repository_ids.append( app.security.encode_id( repository.id ) )
return repository_ids
def order_components_for_import( app, primary_repository_id, repository_ids, repo_info_dicts ):
"""
Some repositories may have repository dependencies that must be imported and have metadata set on
them before the dependent repository is imported. This method will inspect the list of repositories
about to be exported and make sure to order them appropriately for proper import. For each repository
about to be exported, if required repositories are not contained in the list of repositories about to
be exported, then they are not considered. Repository dependency definitions that contain circular
dependencies should not result in an infinite loop, but obviously ordering the list will not be handled
for one or more of the repositories that require prior import.
"""
# The received primary_repository_id is the id of the repository being exported, with the received list
# of repository_ids being only the ids of all of its repository dependencies. The primary repository will
# always be last in the returned lists.
ordered_repository_ids = []
ordered_repositories = []
ordered_changeset_revisions = []
# Create a dictionary whose keys are the received repository_ids and whose values are a list of
# repository_ids, each of which is contained in the received list of repository_ids and whose associated
# repository must be imported prior to the repository associated with the repository_id key.
prior_import_required_dict = suc.get_prior_import_or_install_required_dict( app, repository_ids, repo_info_dicts )
processed_repository_ids = []
# Process the list of repository dependencies defined for the repository associated with the received
# primary_repository_id.
while len( processed_repository_ids ) != len( prior_import_required_dict.keys() ):
repository_id = suc.get_next_prior_import_or_install_required_dict_entry( prior_import_required_dict, processed_repository_ids )
if repository_id == primary_repository_id:
# Append the primary_repository_id without processing it since it must be returned last in the order.
# It will be processed below after all dependencies are processed.
processed_repository_ids.append( primary_repository_id )
continue
processed_repository_ids.append( repository_id )
if repository_id not in ordered_repository_ids:
prior_import_required_ids = prior_import_required_dict[ repository_id ]
for prior_import_required_id in prior_import_required_ids:
if prior_import_required_id not in ordered_repository_ids:
# Import the associated repository dependency first.
prior_repo_info_dict = get_repo_info_dict_for_import( prior_import_required_id, repository_ids, repo_info_dicts )
prior_repository, prior_import_changeset_revision = get_components_from_repo_info_dict( app, prior_repo_info_dict )
if prior_repository and prior_import_changeset_revision:
ordered_repository_ids.append( prior_import_required_id )
ordered_repositories.append( prior_repository )
ordered_changeset_revisions.append( prior_import_changeset_revision )
repo_info_dict = get_repo_info_dict_for_import( repository_id, repository_ids, repo_info_dicts )
repository, changeset_revision = get_components_from_repo_info_dict( app, repo_info_dict )
if repository and changeset_revision:
ordered_repository_ids.append( repository_id )
ordered_repositories.append( repository )
ordered_changeset_revisions.append( changeset_revision )
# Process the repository associated with the received primary_repository_id last.
repo_info_dict = get_repo_info_dict_for_import( primary_repository_id, repository_ids, repo_info_dicts )
repository, changeset_revision = get_components_from_repo_info_dict( app, repo_info_dict )
if repository and changeset_revision:
ordered_repository_ids.append( repository_id )
ordered_repositories.append( repository )
ordered_changeset_revisions.append( changeset_revision )
return ordered_repository_ids, ordered_repositories, ordered_changeset_revisions
def remove_protocol_from_tool_shed_url( base_url ):
protocol, base = base_url.split( '://' )
base = base.replace( ':', '_colon_' )
base = base.rstrip( '/' )
return base
-475
View File
@@ -1,475 +0,0 @@
import logging
import os
import shutil
import tarfile
import tempfile
import urllib
from galaxy import util
from tool_shed.dependencies import dependency_manager
from tool_shed.util import commit_util
from tool_shed.util import encoding_util
from tool_shed.util import hg_util
from tool_shed.util import metadata_util
from tool_shed.util import repository_maintenance_util
from tool_shed.util import shed_util_common as suc
from tool_shed.util import xml_util
import tool_shed.repository_types.util as rt_util
log = logging.getLogger( __name__ )
def check_status_and_reset_downloadable( app, import_results_tups ):
"""Check the status of each imported repository and set downloadable to False if errors."""
sa_session = app.model.context.current
flush = False
for import_results_tup in import_results_tups:
ok, name_owner, message = import_results_tup
name, owner = name_owner
if not ok:
repository = suc.get_repository_by_name_and_owner( app, name, owner )
if repository is not None:
# Do not allow the repository to be automatically installed if population resulted in errors.
tip_changeset_revision = repository.tip( app )
repository_metadata = suc.get_repository_metadata_by_changeset_revision( app,
app.security.encode_id( repository.id ),
tip_changeset_revision )
if repository_metadata:
if repository_metadata.downloadable:
repository_metadata.downloadable = False
sa_session.add( repository_metadata )
if not flush:
flush = True
# Do not allow dependent repository revisions to be automatically installed if population
# resulted in errors.
dependent_downloadable_revisions = suc.get_dependent_downloadable_revisions( app, repository_metadata )
for dependent_downloadable_revision in dependent_downloadable_revisions:
if dependent_downloadable_revision.downloadable:
dependent_downloadable_revision.downloadable = False
sa_session.add( dependent_downloadable_revision )
if not flush:
flush = True
if flush:
sa_session.flush()
def create_repository_and_import_archive( app, host, user, repository_archive_dict, import_results_tups ):
"""
Create a new repository in the tool shed and populate it with the contents of a gzip compressed
tar archive that was exported as part or all of the contents of a capsule.
"""
results_message = ''
name = repository_archive_dict.get( 'name', None )
username = repository_archive_dict.get( 'owner', None )
if name is None or username is None:
ok = False
results_message += 'Import failed: required repository name <b>%s</b> or owner <b>%s</b> is missing.' % \
( str( name ), str( username ))
import_results_tups.append( ( ok, ( str( name ), str( username ) ), results_message ) )
else:
status = repository_archive_dict.get( 'status', None )
if status is None:
# The repository does not yet exist in this Tool Shed and the current user is authorized to import
# the current archive file.
type = repository_archive_dict.get( 'type', 'unrestricted' )
description = repository_archive_dict.get( 'description', '' )
long_description = repository_archive_dict.get( 'long_description', '' )
# The owner entry in the repository_archive_dict is the public username of the user associated with
# the exported repository archive.
user = suc.get_user_by_username( app, username )
if user is None:
ok = False
results_message += 'Import failed: repository owner <b>%s</b> does not have an account in this Tool Shed.' % str( username )
import_results_tups.append( ( ok, ( str( name ), str( username ) ), results_message ) )
else:
user_id = user.id
# The categories entry in the repository_archive_dict is a list of category names. If a name does not
# exist in the current Tool Shed, the category will not be created, so it will not be associated with
# the repository.
category_ids = []
category_names = repository_archive_dict.get( 'category_names', [] )
for category_name in category_names:
category = suc.get_category_by_name( app, category_name )
if category is None:
results_message += 'This Tool Shed does not have the category <b>%s</b> so it ' % str( category_name )
results_message += 'will not be associated with this repository.'
else:
category_ids.append( app.security.encode_id( category.id ) )
# Create the repository record in the database.
repository, create_message = repository_maintenance_util.create_repository( app,
name,
type,
description,
long_description,
user_id=user_id,
category_ids=category_ids )
if create_message:
results_message += create_message
# Populate the new repository with the contents of exported repository archive.
results_dict = import_repository_archive( app, host, user, repository, repository_archive_dict )
ok = results_dict.get( 'ok', False )
error_message = results_dict.get( 'error_message', '' )
if error_message:
results_message += error_message
import_results_tups.append( ( ok, ( str( name ), str( username ) ), results_message ) )
else:
# The repository either already exists in this Tool Shed or the current user is not authorized to create it.
ok = True
results_message += 'Import not necessary: repository status for this Tool Shed is: %s.' % str( status )
import_results_tups.append( ( ok, ( str( name ), str( username ) ), results_message ) )
return import_results_tups
def extract_capsule_files( **kwd ):
"""Extract the uploaded capsule archive into a temporary location for inspection, validation and potential import."""
return_dict = {}
tar_archive = kwd.get( 'tar_archive', None )
capsule_file_name = kwd.get( 'capsule_file_name', None )
if tar_archive is not None and capsule_file_name is not None:
return_dict.update( kwd )
extract_directory_path = tempfile.mkdtemp( prefix="tmp-capsule-ecf" )
if capsule_file_name.endswith( '.tar.gz' ):
extract_directory_name = capsule_file_name.replace( '.tar.gz', '' )
elif capsule_file_name.endswith( '.tar' ):
extract_directory_name = capsule_file_name.replace( '.tar', '' )
else:
extract_directory_name = capsule_file_name
file_path = os.path.join( extract_directory_path, extract_directory_name )
return_dict[ 'encoded_file_path' ] = encoding_util.tool_shed_encode( file_path )
tar_archive.extractall( path=file_path )
try:
tar_archive.close()
except Exception, e:
log.exception( "Cannot close tar_archive: %s" % str( e ) )
del return_dict[ 'tar_archive' ]
return return_dict
def get_archives_from_manifest( manifest_file_path ):
"""
Return the list of archive names defined in the capsule manifest. This method sill validate the manifest by ensuring all
<repository> tag sets contain a valid <archive> sub-element.
"""
archives = []
error_message = ''
manifest_tree, error_message = xml_util.parse_xml( manifest_file_path )
if error_message:
return archives, error_message
manifest_root = manifest_tree.getroot()
for elem in manifest_root:
# <repository name="package_lapack_3_4" type="tool_dependency_definition" username="test">
if elem.tag != 'repository':
error_message = 'All level one sub-elements in the manifest.xml file must be <repository> tag sets. '
error_message += 'The tag <b><%s></b> is invalid.' % str( elem.tag )
return [], error_message
archive_file_name = None
for repository_elem in elem:
if repository_elem.tag == 'archive':
# <archive>package_lapack_3_4-9e7a45ad3522.tar.gz</archive>
archive_file_name = repository_elem.text
break
if archive_file_name is None:
error_message = 'The %s tag set is missing a required <archive> sub-element.' % str( elem.tag )
return [], error_message
archives.append( archive_file_name )
return archives, error_message
def get_export_info_dict( export_info_file_path ):
"""Parse the export_info.xml file contained within the capsule and return a dictionary containing its entries."""
export_info_tree, error_message = xml_util.parse_xml( export_info_file_path )
export_info_root = export_info_tree.getroot()
export_info_dict = {}
for elem in export_info_root:
if elem.tag == 'export_time':
export_info_dict[ 'export_time' ] = elem.text
elif elem.tag == 'tool_shed':
export_info_dict[ 'tool_shed' ] = elem.text
elif elem.tag == 'repository_name':
export_info_dict[ 'repository_name' ] = elem.text
elif elem.tag == 'repository_owner':
export_info_dict[ 'repository_owner' ] = elem.text
elif elem.tag == 'changeset_revision':
export_info_dict[ 'changeset_revision' ] = elem.text
elif elem.tag == 'export_repository_dependencies':
if util.asbool( elem.text ):
export_info_dict[ 'export_repository_dependencies' ] = 'Yes'
else:
export_info_dict[ 'export_repository_dependencies' ] = 'No'
return export_info_dict
def get_repository_info_from_manifest( manifest_file_path ):
"""
Parse the capsule manifest and return a list of dictionaries containing information about each exported repository
archive contained within the capsule.
"""
repository_info_dicts = []
manifest_tree, error_message = xml_util.parse_xml( manifest_file_path )
if error_message:
return repository_info_dicts, error_message
manifest_root = manifest_tree.getroot()
for elem in manifest_root:
# <repository name="package_lapack_3_4" type="tool_dependency_definition" username="test">
if elem.tag != 'repository':
error_message = 'All level one sub-elements in the manifest.xml file must be <repository> tag sets. '
error_message += 'The tag <b><%s></b> is invalid.' % str( elem.tag )
return [], error_message
name = elem.get( 'name', None )
owner = elem.get( 'username', None )
type = elem.get( 'type', None )
if name is None or owner is None or type is None:
error_message = 'Missing required name, type, owner attributes from the tag %s' % str( elem.tag )
return [], error_message
repository_info_dict = dict( name=name, owner=owner, type=type )
for repository_elem in elem:
if repository_elem.tag == 'archive':
# <archive>package_lapack_3_4-9e7a45ad3522.tar.gz</archive>
archive_file_name = repository_elem.text
repository_info_dict[ 'archive_file_name' ] = archive_file_name
items = archive_file_name.split( '-' )
changeset_revision = items[ 1 ].rstrip( '.tar.gz' )
repository_info_dict [ 'changeset_revision' ] = changeset_revision
elif repository_elem.tag == 'categories':
category_names = []
for category_elem in repository_elem:
if category_elem.tag == 'category':
category_names.append( category_elem.text )
repository_info_dict[ 'category_names' ] = category_names
elif repository_elem.tag == 'description':
repository_info_dict[ 'description' ] = repository_elem.text
elif repository_elem.tag == 'long_description':
repository_info_dict[ 'long_description' ] = repository_elem.text
repository_info_dicts.append( repository_info_dict )
return repository_info_dicts, error_message
def get_repository_status_from_tool_shed( app, user, user_is_admin, repository_info_dicts ):
"""
For each exported repository archive contained in the capsule, inspect the Tool Shed to see if that repository already
exists or if the current user is authorized to create the repository, and set a status appropriately. If repository
dependencies are included in the capsule, repositories may have various owners. We will keep repositories associated
with owners, so we need to restrict created repositories to those the current user can create. If the current user is
an admin or a member of the IUC, all repositories will be created no matter the owner. Otherwise, only repositories
whose associated owner is the current user will be created.
"""
repository_status_info_dicts = []
for repository_info_dict in repository_info_dicts:
repository = suc.get_repository_by_name_and_owner( app, repository_info_dict[ 'name' ], repository_info_dict[ 'owner' ] )
if repository:
if repository.deleted:
repository_info_dict[ 'status' ] = 'Exists, deleted'
elif repository.deprecated:
repository_info_dict[ 'status' ] = 'Exists, deprecated'
else:
repository_info_dict[ 'status' ] = 'Exists'
else:
# No repository with the specified name and owner currently exists, so make sure the current user can create one.
if user_is_admin:
repository_info_dict[ 'status' ] = None
elif app.security_agent.user_can_import_repository_archive( user, repository_info_dict[ 'owner' ] ):
repository_info_dict[ 'status' ] = None
else:
repository_info_dict[ 'status' ] = 'Not authorized to import'
repository_status_info_dicts.append( repository_info_dict )
return repository_status_info_dicts
def import_repository_archive( app, host, user, repository, repository_archive_dict ):
"""Import a repository archive contained within a repository capsule."""
rdah = dependency_manager.RepositoryDependencyAttributeHandler( app, unpopulate=False )
tdah = dependency_manager.ToolDependencyAttributeHandler( app, unpopulate=False )
archive_file_name = repository_archive_dict.get( 'archive_file_name', None )
capsule_file_name = repository_archive_dict[ 'capsule_file_name' ]
encoded_file_path = repository_archive_dict[ 'encoded_file_path' ]
file_path = encoding_util.tool_shed_decode( encoded_file_path )
results_dict = dict( ok=True, error_message='' )
archive_file_path = os.path.join( file_path, archive_file_name )
archive = tarfile.open( archive_file_path, 'r:*' )
repo_dir = repository.repo_path( app )
repo = hg_util.get_repo_for_repository( app, repository=None, repo_path=repo_dir, create=False )
undesirable_dirs_removed = 0
undesirable_files_removed = 0
ok, error_message = commit_util.check_archive( repository, archive )
if ok:
full_path = os.path.abspath( repo_dir )
filenames_in_archive = []
for tarinfo_obj in archive.getmembers():
# Check files and directories in the archive.
ok = os.path.basename( tarinfo_obj.name ) not in commit_util.UNDESIRABLE_FILES
if ok:
for file_path_item in tarinfo_obj.name.split( '/' ):
if file_path_item in commit_util.UNDESIRABLE_DIRS:
undesirable_dirs_removed += 1
error_message = 'Import failed: invalid file path <b>%s</b> in archive <b>%s</b>' % \
( str( file_path_item ), str( archive_file_name ) )
results_dict[ 'ok' ] = False
results_dict[ 'error_message' ] += error_message
return results_dict
filenames_in_archive.append( tarinfo_obj.name )
else:
undesirable_files_removed += 1
# Extract the uploaded archive to the repository root.
archive.extractall( path=full_path )
archive.close()
for filename in filenames_in_archive:
uploaded_file_name = os.path.join( full_path, filename )
if os.path.split( uploaded_file_name )[ -1 ] == rt_util.REPOSITORY_DEPENDENCY_DEFINITION_FILENAME:
# Inspect the contents of the file to see if toolshed or changeset_revision attributes
# are missing and if so, set them appropriately.
altered, root_elem, error_message = rdah.handle_tag_attributes( uploaded_file_name )
if error_message:
results_dict[ 'ok' ] = False
results_dict[ 'error_message' ] += error_message
if altered:
tmp_filename = xml_util.create_and_write_tmp_file( root_elem )
shutil.move( tmp_filename, uploaded_file_name )
elif os.path.split( uploaded_file_name )[ -1 ] == rt_util.TOOL_DEPENDENCY_DEFINITION_FILENAME:
# Inspect the contents of the file to see if toolshed or changeset_revision
# attributes are missing and if so, set them appropriately.
altered, root_elem, error_message = tdah.handle_tag_attributes( uploaded_file_name )
if error_message:
results_dict[ 'ok' ] = False
results_dict[ 'error_message' ] += error_message
if altered:
tmp_filename = xml_util.create_and_write_tmp_file( root_elem )
shutil.move( tmp_filename, uploaded_file_name )
commit_message = 'Imported from capsule %s' % str( capsule_file_name )
# Send email notification to those that have registered to receive alerts for new repositories in this Tool Shed.
new_repo_alert = True
# Since the repository is new, the following must be False.
remove_repo_files_not_in_tar = False
ok, error_message, files_to_remove, content_alert_str, undesirable_dirs_removed, undesirable_files_removed = \
commit_util.handle_directory_changes( app,
host,
user.username,
repository,
full_path,
filenames_in_archive,
remove_repo_files_not_in_tar,
new_repo_alert,
commit_message,
undesirable_dirs_removed,
undesirable_files_removed )
if error_message:
results_dict[ 'ok' ] = False
results_dict[ 'error_message' ] += error_message
try:
status, error_message = metadata_util.set_repository_metadata_due_to_new_tip( app,
host,
user,
repository,
content_alert_str=content_alert_str )
if error_message:
results_dict[ 'ok' ] = False
results_dict[ 'error_message' ] += error_message
except Exception, e:
log.debug( "Error setting metadata on repository %s created from imported archive %s: %s" % \
( str( repository.name ), str( archive_file_name ), str( e ) ) )
else:
archive.close()
results_dict[ 'ok' ] = False
results_dict[ 'error_message' ] += error_message
return results_dict
def upload_capsule( **kwd ):
"""Upload and prepare an exported repository capsule for validation."""
file_data = kwd.get( 'file_data', '' )
url = kwd.get( 'url', '' )
uploaded_file = None
return_dict = dict( error_message='',
encoded_file_path=None,
status='ok',
tar_archive=None,
uploaded_file=None,
capsule_file_name=None )
if file_data == '' and url == '':
message = 'No files were entered on the import form.'
status = 'error'
elif url:
valid_url = True
try:
stream = urllib.urlopen( url )
except Exception, e:
valid_url = False
message = 'Error importing file via http: %s' % str( e )
status = 'error'
if valid_url:
fd, uploaded_file_name = tempfile.mkstemp()
uploaded_file = open( uploaded_file_name, 'wb' )
while 1:
chunk = stream.read( util.CHUNK_SIZE )
if not chunk:
break
uploaded_file.write( chunk )
uploaded_file.flush()
uploaded_file_filename = url.split( '/' )[ -1 ]
isempty = os.path.getsize( os.path.abspath( uploaded_file_name ) ) == 0
elif file_data not in ( '', None ):
uploaded_file = file_data.file
uploaded_file_name = uploaded_file.name
uploaded_file_filename = os.path.split( file_data.filename )[ -1 ]
isempty = os.path.getsize( os.path.abspath( uploaded_file_name ) ) == 0
if uploaded_file is not None:
if isempty:
uploaded_file.close()
return_dict[ 'error_message' ] = 'Your uploaded capsule file is empty.'
return_dict[ 'status' ] = 'error'
return return_dict
try:
# Open for reading with transparent compression.
tar_archive = tarfile.open( uploaded_file_name, 'r:*' )
except tarfile.ReadError, e:
error_message = 'Error opening file %s: %s' % ( str( uploaded_file_name ), str( e ) )
log.exception( error_message )
return_dict[ 'error_message' ] = error_message
return_dict[ 'status' ] = 'error'
uploaded_file.close()
return return_dict
return_dict[ 'tar_archive' ] = tar_archive
return_dict[ 'capsule_file_name' ] = uploaded_file_filename
uploaded_file.close()
else:
return_dict[ 'error_message' ] = 'No files were entered on the import form.'
return_dict[ 'status' ] = 'error'
return return_dict
return return_dict
def validate_capsule( **kwd ):
"""Inspect the uploaded capsule's manifest and its contained files to ensure it is a valid repository capsule."""
capsule_dict = {}
capsule_dict.update( kwd )
encoded_file_path = capsule_dict.get( 'encoded_file_path', '' )
file_path = encoding_util.tool_shed_decode( encoded_file_path )
# The capsule must contain a valid XML file named export_info.xml.
export_info_file_path = os.path.join( file_path, 'export_info.xml' )
export_info_tree, error_message = xml_util.parse_xml( export_info_file_path )
if error_message:
capsule_dict[ 'error_message' ] = error_message
capsule_dict[ 'status' ] = 'error'
return capsule_dict
# The capsule must contain a valid XML file named manifest.xml.
manifest_file_path = os.path.join( file_path, 'manifest.xml' )
# Validate the capsule manifest by inspecting name, owner, changeset_revision and type information contained within
# each <repository> tag set.
repository_info_dicts, error_message = get_repository_info_from_manifest( manifest_file_path )
if error_message:
capsule_dict[ 'error_message' ] = error_message
capsule_dict[ 'status' ] = 'error'
return capsule_dict
# Validate the capsule manifest by ensuring all <repository> tag sets contain a valid <archive> sub-element.
archives, error_message = get_archives_from_manifest( manifest_file_path )
if error_message:
capsule_dict[ 'error_message' ] = error_message
capsule_dict[ 'status' ] = 'error'
return capsule_dict
# Validate the capsule manifest by ensuring each defined archive file name exists within the capsule.
error_message = verify_archives_in_capsule( file_path, archives )
if error_message:
capsule_dict[ 'error_message' ] = error_message
capsule_dict[ 'status' ] = 'error'
return capsule_dict
capsule_dict[ 'status' ] = 'ok'
return capsule_dict
def verify_archives_in_capsule( file_path, archives ):
"""Inspect the files contained within the capsule and make sure each is defined correctly in the capsule manifest."""
error_message = ''
for archive_file_name in archives:
full_path = os.path.join( file_path, archive_file_name )
if not os.path.exists( full_path ):
error_message = 'The uploaded capsule is invalid because the contained manifest.xml file defines an archive file '
error_message += 'named <b>%s</b> which is not contained within the capsule.' % str( archive_file_name )
break
return error_message
@@ -66,7 +66,7 @@ def create_repo_info_dict( app, repository_clone_url, changeset_revision, ctx_re
1. During the tool shed repository installation process via the tool shed's get_repository_information()
method. In this case both the received repository and repository_metadata will be objects, but
tool_dependencies and repository_dependencies will be None.
2. When getting updates for an install repository where the updates include newly defined repository
2. When getting updates for an installed repository where the updates include newly defined repository
dependency definitions. This scenario is similar to 1. above. The tool shed's get_repository_information()
method is the caller, and both the received repository and repository_metadata will be objects, but
tool_dependencies and repository_dependencies will be None.