commit/galaxy-central: natefoo: API enhancements: Rename the 'contents' controller to 'library_contents', move (some) sanity and security checks to galaxy.web.api.util, add some enhancements to the histories code and some sample scripts for histories.
1 new changeset in galaxy-central: http://bitbucket.org/galaxy/galaxy-central/changeset/b35bf6a8c11f/ changeset: b35bf6a8c11f user: natefoo date: 2011-08-26 19:55:08 summary: API enhancements: Rename the 'contents' controller to 'library_contents', move (some) sanity and security checks to galaxy.web.api.util, add some enhancements to the histories code and some sample scripts for histories. affected #: 9 files (2.6 KB) --- a/lib/galaxy/web/api/contents.py Fri Aug 26 10:39:34 2011 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,154 +0,0 @@ -""" -API operations on the contents of a library. -""" -import logging, os, string, shutil, urllib, re, socket -from cgi import escape, FieldStorage -from galaxy import util, datatypes, jobs, web, util -from galaxy.web.base.controller import * -from galaxy.util.sanitize_html import sanitize_html -from galaxy.model.orm import * - -log = logging.getLogger( __name__ ) - -class ContentsController( BaseController ): - - @web.expose_api - def index( self, trans, library_id, **kwd ): - """ - GET /api/libraries/{encoded_library_id}/contents - Displays a collection (list) of library contents (files and folders). - """ - rval = [] - current_user_roles = trans.get_current_user_roles() - def traverse( folder ): - admin = trans.user_is_admin() - rval = [] - for subfolder in folder.active_folders: - if not admin: - can_access, folder_ids = trans.app.security_agent.check_folder_contents( trans.user, current_user_roles, subfolder ) - if (admin or can_access) and not subfolder.deleted: - subfolder.api_path = folder.api_path + '/' + subfolder.name - subfolder.api_type = 'folder' - rval.append( subfolder ) - 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 ) - if (admin or can_access) and not ld.deleted: - ld.api_path = folder.api_path + '/' + ld.name - ld.api_type = 'file' - rval.append( ld ) - return rval - try: - decoded_library_id = trans.security.decode_id( library_id ) - except TypeError: - trans.response.status = 400 - return "Malformed library id ( %s ) specified, unable to decode." % str( library_id ) - try: - library = trans.sa_session.query( trans.app.model.Library ).get( decoded_library_id ) - except: - library = None - if not library or not ( trans.user_is_admin() or trans.app.security_agent.can_access_library( current_user_roles, library ) ): - trans.response.status = 400 - return "Invalid library id ( %s ) specified." % str( library_id ) - encoded_id = trans.security.encode_id( 'folder.%s' % library.root_folder.id ) - rval.append( dict( id = encoded_id, - type = 'folder', - name = '/', - url = url_for( 'library_content', library_id=library_id, id=encoded_id ) ) ) - library.root_folder.api_path = '' - for content in traverse( library.root_folder ): - encoded_id = trans.security.encode_id( '%s.%s' % ( content.api_type, content.id ) ) - rval.append( dict( id = encoded_id, - type = content.api_type, - name = content.api_path, - url = url_for( 'library_content', library_id=library_id, id=encoded_id, ) ) ) - return rval - - @web.expose_api - def show( self, trans, id, library_id, **kwd ): - """ - GET /api/libraries/{encoded_library_id}/contents/{encoded_content_type_and_id} - Displays information about a library content (file or folder). - """ - content_id = id - try: - decoded_type_and_id = trans.security.decode_string_id( content_id ) - content_type, decoded_content_id = decoded_type_and_id.split( '.' ) - except: - trans.response.status = 400 - return "Malformed content id ( %s ) specified, unable to decode." % str( content_id ) - if content_type == 'folder': - model_class = trans.app.model.LibraryFolder - elif content_type == 'file': - model_class = trans.app.model.LibraryDataset - else: - trans.response.status = 400 - return "Invalid type ( %s ) specified." % str( content_type ) - try: - content = trans.sa_session.query( model_class ).get( decoded_content_id ) - except: - content = None - if not content or ( not trans.user_is_admin() and not trans.app.security_agent.can_access_library_item( trans.get_current_user_roles(), content, trans.user ) ): - trans.response.status = 400 - return "Invalid %s id ( %s ) specified." % ( content_type, str( content_id ) ) - return content.get_api_value( view='element' ) - - @web.expose_api - def create( self, trans, library_id, payload, **kwd ): - """ - POST /api/libraries/{encoded_library_id}/contents - Creates a new library content item (file or folder). - """ - create_type = None - if 'create_type' not in payload: - trans.response.status = 400 - return "Missing required 'create_type' parameter. Please consult the API documentation for help." - else: - create_type = payload.pop( 'create_type' ) - if create_type not in ( 'file', 'folder' ): - trans.response.status = 400 - return "Invalid value for 'create_type' parameter ( %s ) specified. Please consult the API documentation for help." % create_type - try: - content_id = str( payload.pop( 'folder_id' ) ) - decoded_type_and_id = trans.security.decode_string_id( content_id ) - parent_type, decoded_parent_id = decoded_type_and_id.split( '.' ) - assert parent_type in ( 'folder', 'file' ) - except: - trans.response.status = 400 - return "Malformed parent id ( %s ) specified, unable to decode." % content_id - # "content" can be either a folder or a file, but the parent of new contents can only be folders. - if parent_type == 'file': - trans.response.status = 400 - try: - # With admins or people who can access the dataset provided as the parent, be descriptive. - dataset = trans.sa_session.query( trans.app.model.LibraryDataset ).get( decoded_parent_id ).library_dataset_dataset_association.dataset - assert trans.user_is_admin() or trans.app.security_agent.can_access_dataset( trans.get_current_user_roles(), dataset ) - return "The parent id ( %s ) points to a file, not a folder." % content_id - except: - # If you can't access the parent we don't want to reveal its existence. - return "Invalid parent folder id ( %s ) specified." % content_id - # The rest of the security happens in the library_common controller. - folder_id = trans.security.encode_id( decoded_parent_id ) - # Now create the desired content object, either file or folder. - if create_type == 'file': - status, output = trans.webapp.controllers['library_common'].upload_library_dataset( trans, 'api', library_id, folder_id, **payload ) - elif create_type == 'folder': - status, output = trans.webapp.controllers['library_common'].create_folder( trans, 'api', folder_id, library_id, **payload ) - if status != 200: - trans.response.status = status - # We don't want to reveal the encoded folder_id since it's invalid - # in the API context. Instead, return the content_id originally - # supplied by the client. - output = output.replace( folder_id, content_id ) - return output - else: - rval = [] - for k, v in output.items(): - if type( v ) == trans.app.model.LibraryDatasetDatasetAssociation: - v = v.library_dataset - encoded_id = trans.security.encode_id( create_type + '.' + str( v.id ) ) - rval.append( dict( id = encoded_id, - name = v.name, - url = url_for( 'library_content', library_id=library_id, id=encoded_id ) ) ) - return rval --- a/lib/galaxy/web/api/histories.py Fri Aug 26 10:39:34 2011 -0400 +++ b/lib/galaxy/web/api/histories.py Fri Aug 26 13:55:08 2011 -0400 @@ -7,9 +7,9 @@ from galaxy.web.base.controller import * from galaxy.util.sanitize_html import sanitize_html from galaxy.model.orm import * -from galaxy.model import Dataset import galaxy.datatypes from galaxy.util.bunch import Bunch +from galaxy.web.api.util import * log = logging.getLogger( __name__ ) @@ -21,21 +21,26 @@ GET /api/histories Displays a collection (list) of histories. """ + rval = [] + try: query = trans.sa_session.query( trans.app.model.History ).filter_by( user=trans.user, deleted=False ).order_by( desc(trans.app.model.History.table.c.update_time)).all() except Exception, e: - log.debug("Error in history API: %s" % str(e)) + rval = "Error in history API" + log.error( rval + ": %s" % str(e) ) + trans.response.status = 500 - rval = [] - try: - for history in query: - item = history.get_api_value(value_mapper={'id':trans.security.encode_id}) - item['url'] = url_for( 'history', id=trans.security.encode_id( history.id ) ) - # item['id'] = trans.security.encode_id( item['id'] ) - rval.append( item ) - except Exception, e: - log.debug("Error in history API at constructing return list: %s" % str(e)) + if not rval: + try: + for history in query: + item = history.get_api_value(value_mapper={'id':trans.security.encode_id}) + item['url'] = url_for( 'history', id=trans.security.encode_id( history.id ) ) + rval.append( item ) + except Exception, e: + rval = "Error in history API at constructing return list" + log.error( rval + ": %s" % str(e) ) + trans.response.status = 500 return rval @web.expose_api @@ -49,42 +54,29 @@ def traverse( datasets ): rval = {} - states = Dataset.states + states = trans.app.model.Dataset.states for key, state in states.items(): rval[state] = 0 - #log.debug("History API: Init rval %s" % rval) for dataset in datasets: item = dataset.get_api_value( view='element' ) - #log.debug("History API: Set rval %s" % item['state']) if not item['deleted']: rval[item['state']] = rval[item['state']] + 1 - return rval + return rval try: - decoded_history_id = trans.security.decode_id( history_id ) - except TypeError: - trans.response.status = 400 - return "Malformed history id ( %s ) specified, unable to decode." % str( history_id ) - try: - history = trans.sa_session.query(trans.app.model.History).get(decoded_history_id) - if history.user != trans.user and not trans.user_is_admin(): - if trans.sa_session.query(trans.app.model.HistoryUserShareAssociation).filter_by(user=trans.user, history=history).count() == 0: - trans.response.status = 400 - return("History is not owned by or shared with current user") - except: - trans.response.status = 400 - return "That history does not exist." + history = get_history_for_access( trans, history_id ) + except Exception, e: + return str( e ) try: item = history.get_api_value(view='element', value_mapper={'id':trans.security.encode_id}) num_sets = len( [hda.id for hda in history.datasets if not hda.deleted] ) - states = Dataset.states + states = trans.app.model.Dataset.states state = states.ERROR - if num_sets == 0: + if num_sets == 0: state = states.NEW else: summary = traverse(history.datasets) - #log.debug("History API: Status summary %s" % summary) if summary[states.ERROR] > 0 or summary[states.FAILED_METADATA] > 0: state = states.ERROR elif summary[states.RUNNING] > 0 or summary[states.SETTING_METADATA] > 0: @@ -93,13 +85,13 @@ state = states.QUEUED elif summary[states.OK] == num_sets: state = states.OK - #item['user'] = item['user'].username item['contents_url'] = url_for( 'history_contents', history_id=history_id ) - #item['datasets'] = len( item['datasets'] ) item['state'] = state - #log.debug("History API: State %s for %d datasets" % (state, num_sets)) + item['state_details'] = summary except Exception, e: - log.debug("Error in history API at showing history detail: %s" % str(e)) + item = "Error in history API at showing history detail" + log.error(item + ": %s" % str(e)) + trans.response.status = 500 return item @web.expose_api @@ -109,13 +101,16 @@ Creates a new history. """ params = util.Params( payload ) - hist_name = util.restore_text( params.get( 'name', None ) ) + hist_name = None + if payload.get( 'name', None ): + hist_name = util.restore_text( payload['name'] ) new_history = trans.app.model.History( user=trans.user, name=hist_name ) trans.sa_session.add( new_history ) trans.sa_session.flush() item = new_history.get_api_value(view='element', value_mapper={'id':trans.security.encode_id}) return item + @web.expose_api def delete( self, trans, id, **kwd ): """ @@ -123,33 +118,31 @@ Deletes a history """ history_id = id - params = util.Params( kwd ) + # a request body is optional here + purge = False + if kwd.get( 'payload', None ): + purge = util.string_as_bool( kwd['payload'].get( 'purge', False ) ) try: - decoded_history_id = trans.security.decode_id( history_id ) - except TypeError: - trans.response.status = 400 - return "Malformed history id ( %s ) specified, unable to decode." % str( history_id ) - try: - history = trans.sa_session.query(trans.app.model.History).get(decoded_history_id) - if history.user != trans.user and not trans.user_is_admin(): - if trans.sa_session.query(trans.app.model.HistoryUserShareAssociation).filter_by(user=trans.user, history=history).count() == 0: - trans.response.status = 400 - return("History is not owned by or shared with current user") - except: - trans.response.status = 400 - return "That history does not exist." + history = get_history_for_modification( trans, history_id ) + except Exception, e: + return str( e ) + history.deleted = True - # If deleting the current history, make a new current. - if history == trans.get_history(): - trans.new_history() - if trans.app.config.allow_user_dataset_purge: + if purge and trans.app.config.allow_user_dataset_purge: for hda in history.datasets: + if hda.purged: + continue hda.purged = True trans.sa_session.add( hda ) + trans.sa_session.flush() if hda.dataset.user_can_purge: try: hda.dataset.full_delete() trans.sa_session.add( hda.dataset ) except: - trans.sa_session.flush() + pass + trans.sa_session.flush() + + trans.sa_session.flush() + return 'OK' --- a/lib/galaxy/web/api/history_contents.py Fri Aug 26 10:39:34 2011 -0400 +++ b/lib/galaxy/web/api/history_contents.py Fri Aug 26 13:55:08 2011 -0400 @@ -7,6 +7,7 @@ from galaxy.web.base.controller import * from galaxy.util.sanitize_html import sanitize_html from galaxy.model.orm import * +from galaxy.web.api.util import * import pkg_resources pkg_resources.require( "Routes" ) @@ -23,63 +24,36 @@ Displays a collection (list) of history contents """ try: - decoded_history_id = trans.security.decode_id( history_id ) - except TypeError: - trans.response.status = 400 - return "Malformed history id ( %s ) specified, unable to decode." % str( history_id ) - try: - history = trans.sa_session.query(trans.app.model.History).get(decoded_history_id) - if history.user != trans.user and not trans.user_is_admin(): - if trans.sa_session.query(trans.app.model.HistoryUserShareAssociation).filter_by(user=trans.user, history=history).count() == 0: - trans.response.status = 400 - return("History is not owned by or shared with current user") - except: - trans.response.status = 400 - return "That history does not exist." + history = get_history_for_access( trans, history_id ) + except Exception, e: + return str( e ) rval = [] try: for dataset in history.datasets: api_type = "file" encoded_id = trans.security.encode_id( '%s.%s' % (api_type, dataset.id) ) - #log.debug("History dataset %s" % str(encoded_id)) rval.append( dict( id = encoded_id, type = api_type, name = dataset.name, url = url_for( 'history_content', history_id=history_id, id=encoded_id, ) ) ) except Exception, e: - log.debug("Error in history API at listing contents: %s" % str(e)) + rval = "Error in history API at listing contents" + log.error( rval + ": %s" % str(e) ) + trans.response.status = 500 return rval @web.expose_api def show( self, trans, id, history_id, **kwd ): """ GET /api/histories/{encoded_history_id}/contents/{encoded_content_type_and_id} - Displays information about a history content dataset. + Displays information about a history content (dataset). """ - #log.debug("Entering Content API for history dataset with %s" % str(history_id)) try: - content_id = id - try: - decoded_type_and_id = trans.security.decode_string_id( content_id ) - content_type, decoded_content_id = decoded_type_and_id.split( '.' ) - except: - trans.response.status = 400 - return "Malformed content id ( %s ) specified, unable to decode." % str( content_id ) - if content_type == 'file': - model_class = trans.app.model.HistoryDatasetAssociation - else: - trans.response.status = 400 - return "Invalid type ( %s ) specified." % str( content_type ) - try: - content = trans.sa_session.query( model_class ).get( decoded_content_id ) - except: - trans.response.status = 400 - return "Invalid %s id ( %s ) specified." % ( content_type, str( content_id ) ) - if content.history.user != trans.user and not trans.user_is_admin(): - if trans.sa_session.query(trans.app.model.HistoryUserShareAssociation).filter_by(user=trans.user, history=history).count() == 0: - trans.response.status = 400 - return("History is not owned by or shared with current user") + content = get_history_content_for_access( trans, content_id ) + except Exception, e: + return str( e ) + try: item = content.get_api_value( view='element' ) if not item['deleted']: # Problem: Method url_for cannot use the dataset controller @@ -87,23 +61,41 @@ url = routes.URLGenerator(trans.webapp.mapper, trans.environ) # http://routes.groovie.org/generating.html # url_for is being phased out, so new applications should use url - item['download_url'] = url(controller='dataset', action='display', dataset_id=trans.security.encode_id(decoded_content_id), to_ext=content.ext) + item['download_url'] = url(controller='dataset', action='display', dataset_id=trans.security.encode_id(content.id), to_ext=content.ext) except Exception, e: - log.debug("Error in history API at listing dataset: %s" % str(e)) + item = "Error in history API at listing dataset" + log.error( item + ": %s" % str(e) ) + trans.response.status = 500 return item @web.expose_api def create( self, trans, history_id, payload, **kwd ): """ POST /api/libraries/{encoded_history_id}/contents - Creates a new history content item. """ + Creates a new history content item (file, aka HistoryDatasetAssociation). + """ params = util.Params( payload ) - history_id = util.restore_text( params.get( 'history_id', None ) ) - ldda_id = util.restore_text( params.get( 'ldda_id', None ) ) - add_to_history = True - decoded_history_id = trans.security.decode_id( history_id ) - ld_t, ld_id = trans.security.decode_string_id(ldda_id).split('.') - history = trans.sa_session.query(trans.app.model.History).get(decoded_history_id) - ldda = trans.sa_session.query(self.app.model.LibraryDatasetDatasetAssociation).get(ld_id) - hda = ldda.to_history_dataset_association(history, add_to_history=add_to_history) - history.add_dataset(hda) + from_ld_id = payload.get( 'from_ld_id', None ) + + try: + history = get_history_for_modification( trans, history_id ) + except Exception, e: + return str( e ) + + if from_ld_id: + try: + ld = get_library_content_for_access( trans, from_ld_id ) + assert type( ld ) is trans.app.model.LibraryDataset, "Library content id ( %s ) is not a dataset" % from_ld_id + except AssertionError, e: + trans.response.status = 400 + return str( e ) + except Exception, e: + return str( e ) + hda = ld.library_dataset_dataset_association.to_history_dataset_association( history, add_to_history=True ) + history.add_dataset( hda ) + trans.sa_session.flush() + return hda.get_api_value() + else: + # TODO: implement other "upload" methods here. + trans.response.status = 403 + return "Not implemented." --- a/lib/galaxy/web/buildapp.py Fri Aug 26 10:39:34 2011 -0400 +++ b/lib/galaxy/web/buildapp.py Fri Aug 26 13:55:08 2011 -0400 @@ -108,7 +108,7 @@ add_api_controllers( webapp, app ) webapp.api_mapper.resource( 'content', 'contents', - controller='contents', + controller='library_contents', name_prefix='library_', path_prefix='/api/libraries/:library_id', parent_resources=dict( member_name='library', collection_name='libraries' ) ) 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.
participants (1)
-
Bitbucket