[galaxy-commits] 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.

Bitbucket commits-noreply at bitbucket.org
Fri Aug 26 13:55:17 EDT 2011


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.


More information about the galaxy-commits mailing list