1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/ef514ac8de1d/ Changeset: ef514ac8de1d User: carlfeberhard Date: 2013-06-07 22:06:22 Summary: API: libaray_contents.create: add support for copying an HDA; scripts/api: add copy_hda_to_library_folder.py, rename upload_to_history.py Affected #: 5 files diff -r 46e48f501767de58afb05cb4b133d8dfa54319e1 -r ef514ac8de1df7e49e4dfbb0ca7a399449693873 lib/galaxy/web/base/controller.py --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -5,32 +5,43 @@ import operator import os import re +import urllib +from gettext import gettext + import pkg_resources -import urllib pkg_resources.require("SQLAlchemy >= 0.4") +from sqlalchemy import func, and_, select + pkg_resources.require( "Routes" ) import routes -from sqlalchemy import func, and_, select -from paste.httpexceptions import HTTPBadRequest, HTTPInternalServerError, HTTPNotImplemented, HTTPRequestRangeNotSatisfiable +from paste.httpexceptions import HTTPBadRequest, HTTPInternalServerError +from paste.httpexceptions import HTTPNotImplemented, HTTPRequestRangeNotSatisfiable +from galaxy.exceptions import ItemAccessibilityException, ItemDeletionException, ItemOwnershipException +from galaxy.exceptions import MessageException -from galaxy import util, web, model -from gettext import gettext -from galaxy.datatypes.interval import ChromatinInteractions -from galaxy.exceptions import ItemAccessibilityException, ItemDeletionException, ItemOwnershipException, MessageException -from galaxy.security.validate_user_input import validate_publicname -from galaxy.util.sanitize_html import sanitize_html -from galaxy.visualization.genome.visual_analytics import get_tool_def +from galaxy import web +from galaxy import model +from galaxy import security +from galaxy import util + from galaxy.web import error, url_for from galaxy.web.form_builder import AddressField, CheckboxField, SelectField, TextArea, TextField from galaxy.web.form_builder import build_select_field, HistoryField, PasswordField, WorkflowField, WorkflowMappingField from galaxy.workflow.modules import module_factory from galaxy.model.orm import eagerload, eagerload_all +from galaxy.security.validate_user_input import validate_publicname +from galaxy.util.sanitize_html import sanitize_html + +from galaxy.datatypes.interval import ChromatinInteractions from galaxy.datatypes.data import Text +from galaxy.visualization.genome.visual_analytics import get_tool_def + from galaxy.datatypes.display_applications import util as da_util from galaxy.datatypes.metadata import FileParameter + log = logging.getLogger( __name__ ) # States for passing messages @@ -742,13 +753,105 @@ class UsesLibraryMixinItems( SharableItemSecurityMixin ): def get_library_folder( self, trans, id, check_ownership=False, check_accessible=True ): - return self.get_object( trans, id, 'LibraryFolder', check_ownership=False, check_accessible=check_accessible ) + return self.get_object( trans, id, 'LibraryFolder', + check_ownership=False, check_accessible=check_accessible ) def get_library_dataset_dataset_association( self, trans, id, check_ownership=False, check_accessible=True ): - return self.get_object( trans, id, 'LibraryDatasetDatasetAssociation', check_ownership=False, check_accessible=check_accessible ) + return self.get_object( trans, id, 'LibraryDatasetDatasetAssociation', + check_ownership=False, check_accessible=check_accessible ) def get_library_dataset( self, trans, id, check_ownership=False, check_accessible=True ): - return self.get_object( trans, id, 'LibraryDataset', check_ownership=False, check_accessible=check_accessible ) + return self.get_object( trans, id, 'LibraryDataset', + check_ownership=False, check_accessible=check_accessible ) + + #TODO: it makes no sense that I can get roles from a user but not user.is_admin() + #def can_user_add_to_library_item( self, trans, user, item ): + # if not user: return False + # return ( ( user.is_admin() ) + # or ( trans.app.security_agent.can_add_library_item( user.all_roles(), item ) ) ) + + def can_current_user_add_to_library_item( self, trans, item ): + if not trans.user: return False + return ( ( trans.user_is_admin() ) + or ( trans.app.security_agent.can_add_library_item( trans.get_current_user_roles(), item ) ) ) + + def copy_hda_to_library_folder( self, trans, hda, library_folder, roles=None, ldda_message='' ): + #PRECONDITION: permissions for this action on hda and library_folder have been checked + roles = roles or [] + + # this code was extracted from library_common.add_history_datasets_to_library + #TODO: refactor library_common.add_history_datasets_to_library to use this for each hda to copy + + # create the new ldda and apply the folder perms to it + ldda = hda.to_library_dataset_dataset_association( trans, target_folder=library_folder, + roles=roles, ldda_message=ldda_message ) + self._apply_library_folder_permissions_to_ldda( trans, library_folder, ldda ) + self._apply_hda_permissions_to_ldda( trans, hda, ldda ) + #TODO:?? not really clear on how permissions are being traded here + # seems like hda -> ldda permissions should be set in to_library_dataset_dataset_association + # then they get reset in _apply_library_folder_permissions_to_ldda + # then finally, re-applies hda -> ldda for missing actions in _apply_hda_permissions_to_ldda?? + return ldda + + def _apply_library_folder_permissions_to_ldda( self, trans, library_folder, ldda ): + """ + Copy actions/roles from library folder to an ldda (and it's library_dataset). + """ + #PRECONDITION: permissions for this action on library_folder and ldda have been checked + security_agent = trans.app.security_agent + security_agent.copy_library_permissions( trans, library_folder, ldda ) + security_agent.copy_library_permissions( trans, library_folder, ldda.library_dataset ) + return security_agent.get_permissions( ldda ) + + def _apply_hda_permissions_to_ldda( self, trans, hda, ldda ): + """ + Copy actions/roles from hda to ldda.library_dataset (and then ldda) if ldda + doesn't already have roles for the given action. + """ + #PRECONDITION: permissions for this action on hda and ldda have been checked + # Make sure to apply any defined dataset permissions, allowing the permissions inherited from the + # library_dataset to over-ride the same permissions on the dataset, if they exist. + security_agent = trans.app.security_agent + dataset_permissions_dict = security_agent.get_permissions( hda.dataset ) + library_dataset = ldda.library_dataset + library_dataset_actions = [ permission.action for permission in library_dataset.actions ] + + # except that: if DATASET_MANAGE_PERMISSIONS exists in the hda.dataset permissions, + # we need to instead apply those roles to the LIBRARY_MANAGE permission to the library dataset + dataset_manage_permissions_action = security_agent.get_action( 'DATASET_MANAGE_PERMISSIONS' ).action + library_manage_permissions_action = security_agent.get_action( 'LIBRARY_MANAGE' ).action + #TODO: test this and remove if in loop below + #TODO: doesn't handle action.action + #if dataset_manage_permissions_action in dataset_permissions_dict: + # managing_roles = dataset_permissions_dict.pop( dataset_manage_permissions_action ) + # dataset_permissions_dict[ library_manage_permissions_action ] = managing_roles + + flush_needed = False + for action, dataset_permissions_roles in dataset_permissions_dict.items(): + if isinstance( action, security.Action ): + action = action.action + + # alter : DATASET_MANAGE_PERMISSIONS -> LIBRARY_MANAGE (see above) + if action == dataset_manage_permissions_action: + action = library_manage_permissions_action + + #TODO: generalize to util.update_dict_without_overwrite + # add the hda actions & roles to the library_dataset + #NOTE: only apply an hda perm if it's NOT set in the library_dataset perms (don't overwrite) + if action not in library_dataset_actions: + for role in dataset_permissions_roles: + ldps = trans.model.LibraryDatasetPermissions( action, library_dataset, role ) + ldps = [ ldps ] if not isinstance( ldps, list ) else ldps + for ldp in ldps: + trans.sa_session.add( ldp ) + flush_needed = True + + if flush_needed: + trans.sa_session.flush() + + # finally, apply the new library_dataset to it's associated ldda (must be the same) + security_agent.copy_library_permissions( trans, library_dataset, ldda ) + return security_agent.get_permissions( ldda ) class UsesVisualizationMixin( UsesHistoryDatasetAssociationMixin, UsesLibraryMixinItems ): diff -r 46e48f501767de58afb05cb4b133d8dfa54319e1 -r ef514ac8de1df7e49e4dfbb0ca7a399449693873 lib/galaxy/webapps/galaxy/api/library_contents.py --- a/lib/galaxy/webapps/galaxy/api/library_contents.py +++ b/lib/galaxy/webapps/galaxy/api/library_contents.py @@ -2,13 +2,18 @@ API operations on the contents of a library. """ import logging + from galaxy import web from galaxy.model import ExtendedMetadata, ExtendedMetadataIndex -from galaxy.web.base.controller import BaseAPIController, HTTPBadRequest, url_for, UsesLibraryMixin, UsesLibraryMixinItems +from galaxy.web.base.controller import BaseAPIController, UsesLibraryMixin, UsesLibraryMixinItems +from galaxy.web.base.controller import UsesHistoryDatasetAssociationMixin +from galaxy.web.base.controller import HTTPBadRequest, url_for +from galaxy import util log = logging.getLogger( __name__ ) -class LibraryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibraryMixinItems ): +class LibraryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibraryMixinItems, + UsesHistoryDatasetAssociationMixin ): @web.expose_api # TODO: Add parameter to only get top level of datasets/subfolders. @@ -32,7 +37,8 @@ rval.extend( traverse( subfolder ) ) for ld in folder.datasets: if not admin: - can_access = trans.app.security_agent.can_access_dataset( current_user_roles, ld.library_dataset_dataset_association.dataset ) + can_access = trans.app.security_agent.can_access_dataset( + current_user_roles, ld.library_dataset_dataset_association.dataset ) if (admin or can_access) and not ld.deleted: log.debug( "type(folder): %s" % type( folder ) ) log.debug( "type(api_path): %s; folder.api_path: %s" % ( type(folder.api_path), folder.api_path ) ) @@ -99,6 +105,7 @@ if create_type not in ( 'file', 'folder' ): trans.response.status = 400 return "Invalid value for 'create_type' parameter ( %s ) specified." % create_type + if 'folder_id' not in payload: trans.response.status = 400 return "Missing requred 'folder_id' parameter." @@ -113,6 +120,12 @@ # The rest of the security happens in the library_common controller. real_folder_id = trans.security.encode_id( parent.id ) + # are we copying an HDA to the library folder? + # we'll need the id and any message to attach, then branch to that private function + from_hda_id, ldda_message = ( payload.pop( 'from_hda_id', None ), payload.pop( 'ldda_message', '' ) ) + if create_type == 'file' and from_hda_id: + return self._copy_hda_to_library_folder( trans, from_hda_id, library_id, real_folder_id, ldda_message ) + #check for extended metadata, store it and pop it out of the param #otherwise sanitize_param will have a fit ex_meta_payload = None @@ -180,6 +193,48 @@ #for cross type comparisions, ie "True" == True yield prefix, ("%s" % (meta)).encode("utf8", errors='replace') + def _copy_hda_to_library_folder( self, trans, from_hda_id, library_id, folder_id, ldda_message='' ): + """ + Copies hda `from_hda_id` to library folder `library_folder_id` optionally + adding `ldda_message` to the new ldda's `message`. + + `library_contents.create` will branch to this if called with 'from_hda_id' + in it's payload. + """ + log.debug( '_copy_hda_to_library_folder: %s' %( str(( from_hda_id, library_id, folder_id, ldda_message )) ) ) + #PRECONDITION: folder_id has already been altered to remove the folder prefix ('F') + #TODO: allow name and other, editable ldda attrs? + if ldda_message: + ldda_message = util.sanitize_html.sanitize_html( ldda_message, 'utf-8' ) + + rval = {} + try: + # check permissions on (all three?) resources: hda, library, folder + #TODO: do we really need the library?? + hda = self.get_dataset( trans, from_hda_id, check_ownership=True, check_accessible=True, check_state=True ) + library = self.get_library( trans, library_id, check_accessible=True ) + folder = self.get_library_folder( trans, folder_id, check_accessible=True ) + + if not self.can_current_user_add_to_library_item( trans, folder ): + trans.response.status = 403 + return { 'error' : 'user has no permission to add to library folder (%s)' %( folder_id ) } + + ldda = self.copy_hda_to_library_folder( trans, hda, folder, ldda_message=ldda_message ) + rval = ldda.get_api_value() + + except Exception, exc: + #TODO: grrr... + if 'not accessible to the current user' in str( exc ): + trans.response.status = 403 + return { 'error' : str( exc ) } + else: + log.exception( exc ) + trans.response.status = 500 + return { 'error' : str( exc ) } + + return rval + + @web.expose_api def update( self, trans, id, library_id, payload, **kwd ): """ diff -r 46e48f501767de58afb05cb4b133d8dfa54319e1 -r ef514ac8de1df7e49e4dfbb0ca7a399449693873 scripts/api/copy_hda_to_library_folder.py --- /dev/null +++ b/scripts/api/copy_hda_to_library_folder.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +usage = "USAGE: copy_hda_to_library_folder.py <base url><api key><hda id><library id><folder id> [ message ]" + +import os, sys +sys.path.insert( 0, os.path.dirname( __file__ ) ) +from common import submit + +# ----------------------------------------------------------------------------- +def copy_hda_to_library_folder( base_url, key, hda_id, library_id, folder_id, message='' ): + url = 'http://%s/api/libraries/%s/contents' %( base_url, library_id ) + payload = { + 'folder_id' : folder_id, + 'create_type' : 'file', + 'from_hda_id' : hda_id, + } + if message: + payload.update( dict( ldda_message=message ) ) + + return submit( key, url, payload ) + + +# ----------------------------------------------------------------------------- +if __name__ == '__main__': + num_args = len( sys.argv ) + if num_args < 6: + print >> sys.stderr, usage + sys.exit( 1 ) + + ( base_url, key, hda_id, library_id, folder_id ) = sys.argv[1:6] + + message = '' + if num_args >= 7: + message = sys.argv[6] + + print >> sys.stderr, base_url, key, hda_id, library_id, folder_id, message + returned = copy_hda_to_library_folder( base_url, key, hda_id, library_id, folder_id, message ) diff -r 46e48f501767de58afb05cb4b133d8dfa54319e1 -r ef514ac8de1df7e49e4dfbb0ca7a399449693873 scripts/api/history_upload.py --- a/scripts/api/history_upload.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python -""" -Upload a file to the desired history. -""" -_USAGE = ( "history_upload.py <api key><galaxy base url><history id><filepath to upload>\n" - + " (where galaxy base url is just the root url where your Galaxy is served; e.g. 'localhost:8080')" ) - -import os, sys, json, pprint -#sys.path.insert( 0, os.path.dirname( __file__ ) ) - -try: - import requests -except ImportError, imp_err: - log.error( "Could not import the requests module. See http://docs.python-requests.org/en/latest/ or " - + "install with 'pip install requests'" ) - raise - -def upload_file( full_url, api_key, history_id, filepath, **kwargs ): - - payload = { - 'key' : api_key, - 'tool_id' : 'upload1', - 'history_id' : history_id, - } - inputs = { - 'files_0|NAME' : kwargs.get( 'filename', os.path.basename( filepath ) ), - 'files_0|type' : 'upload_dataset', - #TODO: the following doesn't work with tools.py - #'dbkey' : kwargs.get( 'dbkey', '?' ), - 'dbkey' : '?', - 'file_type' : kwargs.get( 'file_type', 'auto' ), - 'ajax_upload' : u'true', - 'async_datasets': '1', - } - payload[ 'inputs' ] = json.dumps( inputs ) - - response = None - with open( filepath, 'rb' ) as file_to_upload: - files = { 'files_0|file_data' : file_to_upload } - response = requests.post( full_url, data=payload, files=files ) - return response - -if __name__ == '__main__': - - if len( sys.argv ) < 5: - print _USAGE - sys.exit( 1 ) - - api_key, base_url, history_id, filepath = sys.argv[1:5] - full_url = base_url + '/api/tools' - kwargs = dict([ kwarg.split('=', 1) for kwarg in sys.argv[5:]]) - - response = upload_file( full_url, api_key, history_id, filepath, **kwargs ) - print >> sys.stderr, response - print response.content diff -r 46e48f501767de58afb05cb4b133d8dfa54319e1 -r ef514ac8de1df7e49e4dfbb0ca7a399449693873 scripts/api/upload_to_history.py --- /dev/null +++ b/scripts/api/upload_to_history.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +""" +Upload a file to the desired history. +""" +_USAGE = ( "history_upload.py <api key><galaxy base url><history id><filepath to upload>\n" + + " (where galaxy base url is just the root url where your Galaxy is served; e.g. 'localhost:8080')" ) + +import os, sys, json, pprint +#sys.path.insert( 0, os.path.dirname( __file__ ) ) + +try: + import requests +except ImportError, imp_err: + log.error( "Could not import the requests module. See http://docs.python-requests.org/en/latest/ or " + + "install with 'pip install requests'" ) + raise + + +# ----------------------------------------------------------------------------- +def upload_file( full_url, api_key, history_id, filepath, **kwargs ): + full_url = base_url + '/api/tools' + + payload = { + 'key' : api_key, + 'tool_id' : 'upload1', + 'history_id' : history_id, + } + inputs = { + 'files_0|NAME' : kwargs.get( 'filename', os.path.basename( filepath ) ), + 'files_0|type' : 'upload_dataset', + #TODO: the following doesn't work with tools.py + #'dbkey' : kwargs.get( 'dbkey', '?' ), + 'dbkey' : '?', + 'file_type' : kwargs.get( 'file_type', 'auto' ), + 'ajax_upload' : u'true', + 'async_datasets': '1', + } + payload[ 'inputs' ] = json.dumps( inputs ) + + response = None + with open( filepath, 'rb' ) as file_to_upload: + files = { 'files_0|file_data' : file_to_upload } + response = requests.post( full_url, data=payload, files=files ) + return response + + +# ----------------------------------------------------------------------------- +if __name__ == '__main__': + + if len( sys.argv ) < 5: + print _USAGE + sys.exit( 1 ) + + api_key, base_url, history_id, filepath = sys.argv[1:5] + kwargs = dict([ kwarg.split('=', 1) for kwarg in sys.argv[5:]]) + + response = upload_file( base_url, api_key, history_id, filepath, **kwargs ) + print >> sys.stderr, response + print response.content Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.