# HG changeset patch -- Bitbucket.org # Project galaxy-dist # URL http://bitbucket.org/galaxy/galaxy-dist/overview # User Nate Coraor <nate@bx.psu.edu> # Date 1277226628 14400 # Node ID 464734ed09ad4b36afc3409b5d975732b1c6eb21 # Parent 4b4385fbf52982ee3bf6394c5cb269e7bf3ab912 Initial implementation of the Galaxy Web API. Disabled unless 'enable_api = True' in config file. You should not enable the API on production sites as this code is brand new and may contain serious bugs and security flaws! Implemented: * Display libraries * Display library info * Display library contents * Display library content info * Create library folders * Upload datasets to a library from a server directory or with a path paste. * Basic example scripts in scripts/api/ Framework changes that were made to support this: * API Key interface in User Preferences. * New api_keys database table for storing users' API Keys. * New API-specific route mapper in webapp. * API controllers in galaxy.web.api * Return handling in reused library_common methods. * expose_api decorator for API controller methods validates key and ensures valid JSON format. * UniverseWebTransaction renamed to GalaxyWebTransaction and subclassed for GalaxyWebUITransaction and GalaxyWebAPITransaction. Things that need to be done next: * Documentation! * Refactor reused code from library_common and other controllers into an even-more-generic location and format. The main changes are that the Web UI returns redirects and rendered templates, whereas the API returns various HTTP status codes and JSON. * Implement more functionality. * The request and response format should be considered alpha and are subject to change. They will be standardized as the API matures. Hints to get started can be found in scripts/api/README --- /dev/null +++ b/lib/galaxy/web/api/libraries.py @@ -0,0 +1,62 @@ +""" +API operations on 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 LibrariesController( BaseController ): + + @web.expose_api + def index( self, trans, **kwd ): + """ + GET /api/libraries + Displays a collection (list) of libraries. + """ + query = trans.sa_session.query( trans.app.model.Library ).filter( trans.app.model.Library.table.c.deleted == False ) + current_user_role_ids = [ role.id for role in trans.get_current_user_roles() ] + library_access_action = trans.app.security_agent.permitted_actions.LIBRARY_ACCESS.action + restricted_library_ids = [ lp.library_id for lp in trans.sa_session.query( trans.model.LibraryPermissions ) \ + .filter( trans.model.LibraryPermissions.table.c.action == library_access_action ) \ + .distinct() ] + accessible_restricted_library_ids = [ lp.library_id for lp in trans.sa_session.query( trans.model.LibraryPermissions ) \ + .filter( and_( trans.model.LibraryPermissions.table.c.action == library_access_action, + trans.model.LibraryPermissions.table.c.role_id.in_( current_user_role_ids ) ) ) ] + query = query.filter( or_( not_( trans.model.Library.table.c.id.in_( restricted_library_ids ) ), + trans.model.Library.table.c.id.in_( accessible_restricted_library_ids ) ) ) + rval = [] + for library in query: + item = library.get_api_value() + item['url'] = url_for( 'library', id=trans.security.encode_id( library.id ) ) + item['id'] = trans.security.encode_id( item['id'] ) + rval.append( item ) + return rval + + @web.expose_api + def show( self, trans, id, **kwd ): + """ + GET /api/libraries/{encoded_library_id} + Displays information about a library. + """ + library_id = id + params = util.Params( kwd ) + 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( trans.get_current_user_roles(), library ) ): + trans.response.status = 400 + return "Invalid library id ( %s ) specified." % str( library_id ) + item = library.get_api_value( view='element' ) + item['contents_url'] = url_for( 'contents', library_id=library_id ) + return item --- /dev/null +++ b/scripts/api/common.py @@ -0,0 +1,94 @@ +import os, sys, urllib, urllib2 + +new_path = [ os.path.join( os.path.dirname( __file__ ), '..', '..', 'lib' ) ] +new_path.extend( sys.path[1:] ) +sys.path = new_path + +from galaxy import eggs +import pkg_resources + +pkg_resources.require( "simplejson" ) +import simplejson + +def make_url( api_key, url, args=None ): + # Adds the API Key to the URL if it's not already there. + if args is None: + args = [] + argsep = '&' + if '?' not in url: + argsep = '?' + if '?key=' not in url and '&key=' not in url: + args.insert( 0, ( 'key', api_key ) ) + return url + argsep + '&'.join( [ '='.join( t ) for t in args ] ) + +def get( api_key, url ): + # Do the actual GET. + url = make_url( api_key, url ) + return simplejson.loads( urllib2.urlopen( url ).read() ) + +def post( api_key, url, data ): + # Do the actual POST. + url = make_url( api_key, url ) + req = urllib2.Request( url, headers = { 'Content-Type': 'application/json' }, data = simplejson.dumps( data ) ) + return simplejson.loads( urllib2.urlopen( req ).read() ) + +def display( api_key, url ): + # Sends an API GET request and acts as a generic formatter for the JSON response. + try: + r = get( api_key, url ) + except urllib2.HTTPError, e: + print e + print e.read( 1024 ) # Only return the first 1K of errors. + sys.exit( 1 ) + if type( r ) == unicode: + print 'error: %s' % r + return None + elif type( r ) == list: + # Response is a collection as defined in the REST style. + print 'Collection Members' + print '------------------' + for i in r: + # All collection members should have a name and url in the response. + print i.pop( 'url' ) + print ' name: %s' % i.pop( 'name' ) + for k, v in i.items(): + print ' %s: %s' % ( k, v ) + print '' + print '%d elements in collection' % len( r ) + elif type( r ) == dict: + # Response is an element as defined in the REST style. + print 'Member Information' + print '------------------' + for k, v in r.items(): + print '%s: %s' % ( k, v ) + else: + print 'response is unknown type: %s' % type( r ) + +def submit( api_key, url, data ): + # Sends an API POST request and acts as a generic formatter for the JSON response. + # 'data' will become the JSON payload read by Galaxy. + try: + r = post( api_key, url, data ) + except urllib2.HTTPError, e: + print e + print e.read( 1024 ) + sys.exit( 1 ) + print 'Response' + print '--------' + if type( r ) == list: + # Currently the only implemented responses are lists of dicts, because + # submission creates some number of collection elements. + for i in r: + if type( i ) == dict: + if 'url' in i: + print i.pop( 'url' ) + else: + print '----' + if 'name' in i: + print ' name: %s' % i.pop( 'name' ) + for k, v in i.items(): + print ' %s: %s' % ( k, v ) + else: + print i + else: + print r --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -30,6 +30,7 @@ from babel.support import Translations pkg_resources.require( "SQLAlchemy >= 0.4" ) from sqlalchemy import and_ +from sqlalchemy.orm.exc import NoResultFound pkg_resources.require( "pexpect" ) pkg_resources.require( "amqplib" ) @@ -87,6 +88,43 @@ def require_login( verb="perform this ac return decorator return argcatcher +def expose_api( func ): + def decorator( self, trans, *args, **kwargs ): + def error( environ, start_response ): + start_response( error_status, [('Content-type', 'text/plain')] ) + return error_message + error_status = '403 Forbidden' + if 'key' not in kwargs: + error_message = 'No API key provided with request, please consult the API documentation.' + return error + try: + provided_key = trans.sa_session.query( trans.app.model.APIKeys ).filter( trans.app.model.APIKeys.table.c.key == kwargs['key'] ).one() + except NoResultFound: + error_message = 'Provided API key is not valid.' + return error + newest_key = provided_key.user.api_keys[0] + if newest_key.key != provided_key.key: + error_message = 'Provided API key has expired.' + return error + if trans.request.body: + try: + payload = util.recursively_stringify_dictionary_keys( simplejson.loads( trans.request.body ) ) + kwargs['payload'] = payload + except ValueError: + error_status = '400 Bad Request' + error_message = 'Your request did not appear to be valid JSON, please consult the API documentation' + return error + trans.response.set_content_type( "application/json" ) + trans.set_user( provided_key.user ) + if trans.debug: + return simplejson.dumps( func( self, trans, *args, **kwargs ), indent=4, sort_keys=True ) + else: + return simplejson.dumps( func( self, trans, *args, **kwargs ) ) + if not hasattr(func, '_orig'): + decorator._orig = func + decorator.exposed = True + return decorator + def require_admin( func ): def decorator( self, trans, *args, **kwargs ): admin_users = trans.app.config.get( "admin_users", "" ).split( "," ) @@ -119,7 +157,7 @@ def form( *args, **kwargs ): class WebApplication( base.WebApplication ): def __init__( self, galaxy_app, session_cookie='galaxysession' ): base.WebApplication.__init__( self ) - self.set_transaction_factory( lambda e: UniverseWebTransaction( e, galaxy_app, self, session_cookie ) ) + self.set_transaction_factory( lambda e: self.transaction_chooser( e, galaxy_app, session_cookie ) ) # Mako support self.mako_template_lookup = mako.lookup.TemplateLookup( directories = [ galaxy_app.config.template_path ] , @@ -135,21 +173,21 @@ class WebApplication( base.WebApplicatio if isinstance( body, FormBuilder ): body = trans.show_form( body ) return base.WebApplication.make_body_iterable( self, trans, body ) + def transaction_chooser( self, environ, galaxy_app, session_cookie ): + if 'is_api_request' in environ: + return GalaxyWebAPITransaction( environ, galaxy_app, self ) + else: + return GalaxyWebUITransaction( environ, galaxy_app, self, session_cookie ) -class UniverseWebTransaction( base.DefaultWebTransaction ): +class GalaxyWebTransaction( base.DefaultWebTransaction ): """ - Encapsulates web transaction specific state for the Universe application + Encapsulates web transaction specific state for the Galaxy application (specifically the user's "cookie" session and history) """ - def __init__( self, environ, app, webapp, session_cookie ): + def __init__( self, environ, app, webapp ): self.app = app self.webapp = webapp self.security = webapp.security - # FIXME: the following 3 attributes are not currently used - # Remove them if they are not going to be... - self.__user = NOT_SET - self.__history = NOT_SET - self.__galaxy_session = NOT_SET base.DefaultWebTransaction.__init__( self, environ ) self.setup_i18n() self.sa_session.expunge_all() @@ -158,13 +196,6 @@ class UniverseWebTransaction( base.Defau # that the current history should not be used for parameter values # and such). self.workflow_building_mode = False - # Always have a valid galaxy session - self.__ensure_valid_session( session_cookie ) - # Prevent deleted users from accessing Galaxy - if self.app.config.use_remote_user and self.galaxy_session.user.deleted: - self.response.send_redirect( url_for( '/static/user_disabled.html' ) ) - if self.app.config.require_login: - self.__ensure_logged_in_user( environ ) def setup_i18n( self ): if 'HTTP_ACCEPT_LANGUAGE' in self.environ: # locales looks something like: ['en', 'en-us;q=0.7', 'ja;q=0.3'] @@ -252,12 +283,7 @@ class UniverseWebTransaction( base.Defau tstamp = time.localtime ( time.time() + 3600 * 24 * age ) self.response.cookies[name]['expires'] = time.strftime( '%a, %d-%b-%Y %H:%M:%S GMT', tstamp ) self.response.cookies[name]['version'] = version - #@property - #def galaxy_session( self ): - # if not self.__galaxy_session: - # self.__ensure_valid_session() - # return self.__galaxy_session - def __ensure_valid_session( self, session_cookie ): + def _ensure_valid_session( self, session_cookie ): """ Ensure that a valid Galaxy session exists and is available as trans.session (part of initialization) @@ -344,7 +370,7 @@ class UniverseWebTransaction( base.Defau # If the old session was invalid, get a new history with our new session if invalidate_existing_session: self.new_history() - def __ensure_logged_in_user( self, environ ): + def _ensure_logged_in_user( self, environ ): allowed_paths = ( url_for( controller='root', action='index' ), url_for( controller='root', action='tool_menu' ), @@ -537,15 +563,6 @@ class UniverseWebTransaction( base.Defau self.sa_session.add_all( ( self.galaxy_session, history ) ) self.sa_session.flush() return history - def get_user( self ): - """Return the current user if logged in or None.""" - return self.galaxy_session.user - def set_user( self, user ): - """Set the current user.""" - self.galaxy_session.user = user - self.sa_session.add( self.galaxy_session ) - self.sa_session.flush() - user = property( get_user, set_user ) def get_current_user_roles( self ): user = self.get_user() if user: @@ -735,6 +752,53 @@ class FormInput( object ): self.help = help self.use_label = use_label +class GalaxyWebAPITransaction( GalaxyWebTransaction ): + def __init__( self, environ, app, webapp ): + GalaxyWebTransaction.__init__( self, environ, app, webapp ) + self.__user = None + self._ensure_valid_session( None ) + def _ensure_valid_session( self, session_cookie ): + self.galaxy_session = Bunch() + self.galaxy_session.history = self.galaxy_session.current_history = Bunch() + self.galaxy_session.history.genome_build = None + self.galaxy_session.is_api = True + def get_user( self ): + """Return the current user (the expose_api decorator ensures that it is set).""" + return self.__user + def set_user( self, user ): + """Compatibility method""" + self.__user = user + user = property( get_user, set_user ) + @property + def db_builds( self ): + dbnames = [] + if 'dbkeys' in self.user.preferences: + user_keys = from_json_string( self.user.preferences['dbkeys'] ) + for key, chrom_dict in user_keys.iteritems(): + dbnames.append((key, "%s (%s) [Custom]" % (chrom_dict['name'], key) )) + dbnames.extend( util.dbnames ) + return dbnames + +class GalaxyWebUITransaction( GalaxyWebTransaction ): + def __init__( self, environ, app, webapp, session_cookie ): + GalaxyWebTransaction.__init__( self, environ, app, webapp ) + # Always have a valid galaxy session + self._ensure_valid_session( session_cookie ) + # Prevent deleted users from accessing Galaxy + if self.app.config.use_remote_user and self.galaxy_session.user.deleted: + self.response.send_redirect( url_for( '/static/user_disabled.html' ) ) + if self.app.config.require_login: + self._ensure_logged_in_user( environ ) + def get_user( self ): + """Return the current user if logged in or None.""" + return self.galaxy_session.user + def set_user( self, user ): + """Set the current user.""" + self.galaxy_session.user = user + self.sa_session.add( self.galaxy_session ) + self.sa_session.flush() + user = property( get_user, set_user ) + class SelectInput( FormInput ): """ A select form input. """ def __init__( self, name, label, value=None, options=[], error=None, help=None, use_label=True ): --- a/lib/galaxy/web/buildapp.py +++ b/lib/galaxy/web/buildapp.py @@ -48,6 +48,27 @@ def add_controllers( webapp, app ): if isclass( T ) and T is not BaseController and issubclass( T, BaseController ): webapp.add_controller( name, T( app ) ) +def add_api_controllers( webapp, app ): + from galaxy.web.base.controller import BaseController + from galaxy.web.base.controller import ControllerUnavailable + import galaxy.web.api + controller_dir = galaxy.web.api.__path__[0] + for fname in os.listdir( controller_dir ): + if not( fname.startswith( "_" ) ) and fname.endswith( ".py" ): + name = fname[:-3] + module_name = "galaxy.web.api." + name + try: + module = __import__( module_name ) + except ControllerUnavailable, exc: + log.debug("%s could not be loaded: %s" % (module_name, str(exc))) + continue + for comp in module_name.split( "." )[1:]: + module = getattr( module, comp ) + for key in dir( module ): + T = getattr( module, key ) + if isclass( T ) and T is not BaseController and issubclass( T, BaseController ): + webapp.add_api_controller( name, T( app ) ) + def app_factory( global_conf, **kwargs ): """ Return a wsgi application serving the root object @@ -80,6 +101,11 @@ def app_factory( global_conf, **kwargs ) webapp.add_route( '/u/:username/h/:slug', controller='history', action='display_by_username_and_slug' ) webapp.add_route( '/u/:username/w/:slug', controller='workflow', action='display_by_username_and_slug' ) webapp.add_route( '/u/:username/v/:slug', controller='visualization', action='display_by_username_and_slug' ) + # If enabled, add the web API + if asbool( kwargs.get( 'enable_api', False ) ): + add_api_controllers( webapp, app ) + webapp.api_mapper.resource( 'content', 'contents', path_prefix='/api/libraries/:library_id', parent_resources=dict( member_name='library', collection_name='libraries' ) ) + webapp.api_mapper.resource( 'library', 'libraries', path_prefix='/api' ) webapp.finalize_config() # Wrap the webapp in some useful middleware if kwargs.get( 'middleware', True ): --- /dev/null +++ b/scripts/api/README @@ -0,0 +1,106 @@ +This is not documentation. These are hints and examples to get you started +until the documentation is written. + +Set these options in universe_wsgi.ini and start the server: + +enable_api = True +admin_users = you@example.org +library_import_dir = /path/to/some/directory + +In the directory you specified for 'library_import_dir', create some +subdirectories, and put (or symlink) files to import into Galaxy into those +subdirectories. + +In Galaxy, create an account that matches the address you put in 'admin_users', +then browse to that user's preferences and generate a new API Key. Copy the +key to your clipboard. Create a new library (doing this via the API is not yet +implemented). Then take your API Key and use the scripts in scripts/api/ to do +things: + +% ./display.py my_key http://localhost:4096/api/libraries +Collection Members +------------------ +/api/libraries/f3f73e481f432006 + name: api_test + id: f3f73e481f432006 + +% ./display.py my_key http://localhost:4096/api/libraries/f3f73e481f432006 +Member Information +------------------ +synopsys: None +contents_url: /api/libraries/f3f73e481f432006/contents +description: API Test Library +name: api_test + +% ./display.py my_key http://localhost:4096/api/libraries/f3f73e481f432006/contents +Collection Members +------------------ +/api/libraries/f3f73e481f432006/contents/28202595c0d2591f61ddda595d2c3670 + name: / + type: folder + id: 28202595c0d2591f61ddda595d2c3670 + +% ./library_create_folder.py my_key http://localhost:4096/api/libraries/f3f73e481f432006/contents 28202595c0d2591f61ddda595d2c3670 api_test_folder1 'API Test Folder 1' +Response +-------- +/api/libraries/f3f73e481f432006/contents/28202595c0d2591fa4f9089d2303fd89 + name: api_test_folder1 + id: 28202595c0d2591fa4f9089d2303fd89 + +% ./library_upload_from_import_dir.py my_key http://localhost:4096/api/libraries/f3f73e481f432006/contents 28202595c0d2591fa4f9089d2303fd89 bed bed hg19 +Response +-------- +/api/libraries/f3f73e481f432006/contents/e9ef7fdb2db87d7b + name: 2.bed + id: e9ef7fdb2db87d7b +/api/libraries/f3f73e481f432006/contents/3b7f6a31f80a5018 + name: 3.bed + id: 3b7f6a31f80a5018 + +% ./display.py my_key http://localhost:4096/api/libraries/f3f73e481f432006/contents +Collection Members +------------------ +/api/libraries/f3f73e481f432006/contents/28202595c0d2591f61ddda595d2c3670 + name: / + type: folder + id: 28202595c0d2591f61ddda595d2c3670 +/api/libraries/f3f73e481f432006/contents/28202595c0d2591fa4f9089d2303fd89 + name: /api_test_folder1 + type: folder + id: 28202595c0d2591fa4f9089d2303fd89 +/api/libraries/f3f73e481f432006/contents/e9ef7fdb2db87d7b + name: /api_test_folder1/2.bed + type: file + id: e9ef7fdb2db87d7b +/api/libraries/f3f73e481f432006/contents/3b7f6a31f80a5018 + name: /api_test_folder1/3.bed + type: file + id: 3b7f6a31f80a5018 + +% ./display.py my_key http://localhost:4096/api/libraries/f3f73e481f432006/contents/e9ef7fdb2db87d... +Member Information +------------------ +misc_blurb: 68 regions +metadata_endCol: 3 +data_type: bed +metadata_columns: 6 +metadata_nameCol: 4 +uploaded_by: nate@... +metadata_strandCol: 6 +name: 2.bed +genome_build: hg19 +metadata_comment_lines: None +metadata_startCol: 2 +metadata_chromCol: 1 +file_size: 4272 +metadata_data_lines: 68 +message: +metadata_dbkey: hg19 +misc_info: uploaded bed file +date_uploaded: 2010-06-22T17:01:51.266119 +metadata_column_types: str, int, int, str, int, str + +Other parameters are valid when uploading, they are the same parameters as are +used in the web form, like 'link_data_only' and etc. + +The request and response format should be considered alpha and are subject to change. --- a/lib/galaxy/web/framework/base.py +++ b/lib/galaxy/web/framework/base.py @@ -45,7 +45,13 @@ class WebApplication( object ): and `__call__` to handle a request (WSGI style). """ self.controllers = dict() + self.api_controllers = dict() self.mapper = routes.Mapper() + # FIXME: The following two options are deprecated and should be + # removed. Consult the Routes documentation. + self.mapper.minimization = True + self.mapper.explicit = False + self.api_mapper = routes.Mapper() self.transaction_factory = DefaultWebTransaction def add_controller( self, controller_name, controller ): """ @@ -56,6 +62,10 @@ class WebApplication( object ): log.debug( "Enabling '%s' controller, class: %s", controller_name, controller.__class__.__name__ ) self.controllers[ controller_name ] = controller + def add_api_controller( self, controller_name, controller ): + log.debug( "Enabling '%s' API controller, class: %s", + controller_name, controller.__class__.__name__ ) + self.api_controllers[ controller_name ] = controller def add_route( self, route, **kwargs ): """ Add a route to match a URL with a method. Accepts all keyword @@ -80,6 +90,7 @@ class WebApplication( object ): """ # Create/compile the regular expressions for route mapping self.mapper.create_regs( self.controllers.keys() ) + self.api_mapper.create_regs( self.api_controllers.keys() ) def __call__( self, environ, start_response ): """ Call interface as specified by WSGI. Wraps the environment in user @@ -88,12 +99,20 @@ class WebApplication( object ): """ # Map url using routes path_info = environ.get( 'PATH_INFO', '' ) - map = self.mapper.match( path_info ) + map = self.mapper.match( path_info, environ ) + if map is None: + environ[ 'is_api_request' ] = True + map = self.api_mapper.match( path_info, environ ) + mapper = self.api_mapper + controllers = self.api_controllers + else: + mapper = self.mapper + controllers = self.controllers if map == None: raise httpexceptions.HTTPNotFound( "No route for " + path_info ) # Setup routes rc = routes.request_config() - rc.mapper = self.mapper + rc.mapper = mapper rc.mapper_dict = map rc.environ = environ # Setup the transaction @@ -101,7 +120,7 @@ class WebApplication( object ): rc.redirect = trans.response.send_redirect # Get the controller class controller_name = map.pop( 'controller', None ) - controller = self.controllers.get( controller_name, None ) + controller = controllers.get( controller_name, None ) if controller_name is None: raise httpexceptions.HTTPNotFound( "No controller for " + path_info ) # Resolve action method on controller @@ -112,7 +131,7 @@ class WebApplication( object ): if method is None: raise httpexceptions.HTTPNotFound( "No action for " + path_info ) # Is the method exposed - if not getattr( method, 'exposed', False ): + if not getattr( method, 'exposed', False ): raise httpexceptions.HTTPNotFound( "Action not exposed for " + path_info ) # Is the method callable if not callable( method ): --- a/lib/galaxy/web/security/__init__.py +++ b/lib/galaxy/web/security/__init__.py @@ -43,6 +43,8 @@ class SecurityHelper( object ): return self.id_cipher.encrypt( s ).encode( 'hex' ) def decode_id( self, obj_id ): return int( self.id_cipher.decrypt( obj_id.decode( 'hex' ) ).lstrip( "!" ) ) + def decode_string_id( self, obj_id ): + return self.id_cipher.decrypt( obj_id.decode( 'hex' ) ).lstrip( "!" ) def encode_guid( self, session_key ): # Session keys are strings # Pad to a multiple of 8 with leading "!" --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -39,6 +39,8 @@ class Configuration( object ): self.file_path = resolve_path( kwargs.get( "file_path", "database/files" ), self.root ) self.new_file_path = resolve_path( kwargs.get( "new_file_path", "database/tmp" ), self.root ) self.cookie_path = kwargs.get( "cookie_path", "/" ) + # web API + self.enable_api = string_as_bool( kwargs.get( 'enable_api', False ) ) # dataset Track files self.track_store_path = kwargs.get( "track_store_path", "${extra_files_path}/tracks") self.tool_path = resolve_path( kwargs.get( "tool_path", "tools" ), self.root ) --- /dev/null +++ b/lib/galaxy/model/migrate/versions/0049_api_keys_table.py @@ -0,0 +1,38 @@ +""" +Migration script to add the api_keys table. +""" + +from sqlalchemy import * +from migrate import * +from migrate.changeset import * +from galaxy.model.custom_types import * + +import datetime +now = datetime.datetime.utcnow + +import logging +log = logging.getLogger( __name__ ) + +metadata = MetaData( migrate_engine ) + +APIKeys_table = Table( "api_keys", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "key", TrimmedString( 32 ), index=True, unique=True ) ) + +def upgrade(): + print __doc__ + metadata.reflect() + try: + APIKeys_table.create() + except Exception, e: + log.debug( "Creating api_keys table failed: %s" % str( e ) ) + +def downgrade(): + # Load existing tables + metadata.reflect() + try: + APIKeys_table.drop() + except Exception, e: + log.debug( "Dropping api_keys table failed: %s" % str( e ) ) --- a/lib/galaxy/tools/actions/upload_common.py +++ b/lib/galaxy/tools/actions/upload_common.py @@ -124,7 +124,7 @@ def new_history_upload( trans, uploaded_ return hda def new_library_upload( trans, cntrller, uploaded_dataset, library_bunch, state=None ): current_user_roles = trans.get_current_user_roles() - if not ( cntrller in [ 'library_admin' ] or trans.app.security_agent.can_add_library_item( current_user_roles, library_bunch.folder ) ): + if not ( ( trans.user_is_admin() and cntrller in [ 'library_admin', 'api' ] ) or trans.app.security_agent.can_add_library_item( current_user_roles, library_bunch.folder ) ): # This doesn't have to be pretty - the only time this should happen is if someone's being malicious. raise Exception( "User is not authorized to add datasets to this library." ) folder = library_bunch.folder --- /dev/null +++ b/lib/galaxy/web/api/contents.py @@ -0,0 +1,154 @@ +""" +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: + 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: + 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( '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( '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( 'content', library_id=library_id, id=encoded_id ) ) ) + return rval --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -829,6 +829,8 @@ class HistoryDatasetAssociationDisplayAt class Library( object ): permitted_actions = get_permitted_actions( filter='LIBRARY' ) + api_collection_visible_keys = ( 'id', 'name' ) + api_element_visible_keys = ( 'name', 'description', 'synopsys' ) def __init__( self, name=None, description=None, synopsis=None, root_folder=None ): self.name = name or "Unnamed library" self.description = description @@ -870,8 +872,21 @@ class Library( object ): if isinstance( name, str ): name = unicode( name, 'utf-8' ) return name + def get_api_value( self, view='collection' ): + rval = {} + try: + visible_keys = self.__getattribute__( 'api_' + view + '_visible_keys' ) + except AttributeError: + raise Exception( 'Unknown API view: %s' % view ) + for key in visible_keys: + try: + rval[key] = self.__getattribute__( key ) + except AttributeError: + rval[key] = None + return rval class LibraryFolder( object ): + api_element_visible_keys = ( 'name', 'description', 'item_count', 'genome_build' ) def __init__( self, name=None, description=None, item_count=0, order_id=None ): self.name = name or "Unnamed folder" self.description = description @@ -961,6 +976,18 @@ class LibraryFolder( object ): if isinstance( name, str ): name = unicode( name, 'utf-8' ) return name + def get_api_value( self, view='collection' ): + rval = {} + try: + visible_keys = self.__getattribute__( 'api_' + view + '_visible_keys' ) + except AttributeError: + raise Exception( 'Unknown API view: %s' % view ) + for key in visible_keys: + try: + rval[key] = self.__getattribute__( key ) + except AttributeError: + rval[key] = None + return rval @property def parent_library( self ): f = self @@ -1011,6 +1038,28 @@ class LibraryDataset( object ): if not purged and self.purged: raise Exception( "Cannot unpurge once purged" ) purged = property( get_purged, set_purged ) + def get_api_value( self, view='collection' ): + # Since this class is a proxy to rather complex attributes we want to + # display in other objects, we can't use the simpler method used by + # other model classes. + ldda = self.library_dataset_dataset_association + rval = dict( name = ldda.name, + uploaded_by = ldda.user.email, + message = ldda.message, + date_uploaded = ldda.create_time.isoformat(), + file_size = int( ldda.get_size() ), + data_type = ldda.ext, + genome_build = ldda.dbkey, + misc_info = ldda.info, + misc_blurb = ldda.blurb ) + for name, spec in ldda.metadata.spec.items(): + val = ldda.metadata.get( name ) + if isinstance( val, MetadataFile ): + val = val.file_name + elif isinstance( val, list ): + val = ', '.join( val ) + rval['metadata_' + name] = val + return rval class LibraryDatasetDatasetAssociation( DatasetInstance ): def __init__( self, @@ -1734,6 +1783,9 @@ class UserAction( object ): self.params = params self.context = context +class APIKeys( object ): + pass + ## ---- Utility methods ------------------------------------------------------- def directory_hash_id( id ): @@ -1748,5 +1800,3 @@ def directory_hash_id( id ): padded = padded[:-3] # Break into chunks of three return [ padded[i*3:(i+1)*3] for i in range( len( padded ) // 3 ) ] - - --- a/lib/galaxy/util/__init__.py +++ b/lib/galaxy/util/__init__.py @@ -417,6 +417,14 @@ def stringify_dictionary_keys( in_dict ) out_dict[ str( key ) ] = value return out_dict +def recursively_stringify_dictionary_keys( d ): + if isinstance(d, dict): + return dict([(k.encode('utf-8'), recursively_stringify_dictionary_keys(v)) for k,v in d.iteritems()]) + elif isinstance(d, list): + return [recursively_stringify_dictionary_keys(x) for x in d] + else: + return d + def mkstemp_ln( src, prefix='mkstemp_ln_' ): """ From tempfile._mkstemp_inner, generate a hard link in the same dir with a --- /dev/null +++ b/scripts/api/library_upload_from_import_dir.py @@ -0,0 +1,19 @@ +#!/usr/bin/python + +import os, sys +sys.path.insert( 0, os.path.dirname( __file__ ) ) +from common import submit + +try: + data = {} + data[ 'folder_id' ] = sys.argv[3] + data[ 'file_type' ] = sys.argv[4] + data[ 'server_dir' ] = sys.argv[5] + data[ 'dbkey' ] = sys.argv[6] + data[ 'upload_option' ] = 'upload_directory' + data[ 'create_type' ] = 'file' +except IndexError: + print 'usage: %s key url folder_id file_type server_dir dbkey' % os.path.basename( sys.argv[0] ) + sys.exit( 1 ) + +submit( sys.argv[1], sys.argv[2], data ) --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -889,6 +889,12 @@ UserAction.table = Table( "user_action", Column( "context", Unicode( 512 ) ), Column( "params", Unicode( 1024 ) ) ) +APIKeys.table = Table( "api_keys", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "key", TrimmedString( 32 ), index=True, unique=True ) ) + # With the tables defined we can define the mappers and setup the # relationships between the model objects. @@ -1067,8 +1073,9 @@ assign_mapper( context, User, User.table _preferences=relation( UserPreference, backref="user", collection_class=attribute_mapped_collection('name')), # addresses=relation( UserAddress, # primaryjoin=( User.table.c.id == UserAddress.table.c.user_id ) ) - values=relation( FormValues, - primaryjoin=( User.table.c.form_values_id == FormValues.table.c.id ) ), + values=relation( FormValues, + primaryjoin=( User.table.c.form_values_id == FormValues.table.c.id ) ), + api_keys=relation( APIKeys, backref="user", order_by=desc( APIKeys.table.c.create_time ) ), ) ) # Set up proxy so that this syntax is possible: @@ -1485,6 +1492,9 @@ assign_mapper( context, UserAction, User properties = dict( user=relation( User.mapper ) ) ) +assign_mapper( context, APIKeys, APIKeys.table, + properties = {} ) + def db_next_hid( self ): """ Override __next_hid to generate from the database in a concurrency --- a/lib/galaxy/web/controllers/library_common.py +++ b/lib/galaxy/web/controllers/library_common.py @@ -280,7 +280,7 @@ class LibraryCommon( BaseController ): status = params.get( 'status', 'done' ) show_deleted = util.string_as_bool( params.get( 'show_deleted', False ) ) use_panels = util.string_as_bool( params.get( 'use_panels', False ) ) - is_admin = trans.user_is_admin() and cntrller == 'library_admin' + is_admin = trans.user_is_admin() and cntrller in ( 'library_admin', 'api' ) current_user_roles = trans.get_current_user_roles() try: parent_folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( trans.security.decode_id( parent_id ) ) @@ -294,6 +294,8 @@ class LibraryCommon( BaseController ): # its parent library, or if they are not able to see the folder's contents. if not parent_folder or ( not is_admin and not trans.app.security_agent.can_access_library_item( current_user_roles, parent_folder, trans.user ) ): message = "Invalid parent folder id ( %s ) specified." % str( parent_id ) + if cntrller == 'api': + return 400, message # This doesn't give away the library's existence since # browse_library will simply punt to browse_libraries if the # user-supplied id is invalid or inaccessible. @@ -309,6 +311,8 @@ class LibraryCommon( BaseController ): if not ( is_admin or trans.app.security_agent.can_add_library_item( current_user_roles, parent_folder ) ): message = "You are not authorized to create a folder in parent folder '%s'." % parent_folder.name # Redirect to the real parent library since we know we have access to it. + if cntrller == 'api': + return 403, message return trans.response.send_redirect( web.url_for( controller='library_common', action='browse_library', cntrller=cntrller, @@ -317,7 +321,7 @@ class LibraryCommon( BaseController ): show_deleted=show_deleted, message=util.sanitize_text( message ), status='error' ) ) - if params.get( 'new_folder_button', False ): + if params.get( 'new_folder_button', False ) or cntrller == 'api': new_folder = trans.app.model.LibraryFolder( name=util.restore_text( params.name ), description=util.restore_text( params.description ) ) # We are associating the last used genome build with folders, so we will always @@ -329,6 +333,9 @@ class LibraryCommon( BaseController ): trans.sa_session.flush() # New folders default to having the same permissions as their parent folder trans.app.security_agent.copy_library_permissions( parent_folder, new_folder ) + # If we're creating in the API, we're done + if cntrller == 'api': + return 200, dict( created=new_folder ) # If we have an inheritable template, redirect to the folder_info page so information # can be filled in immediately. widgets = [] @@ -654,7 +661,7 @@ class LibraryCommon( BaseController ): show_deleted=show_deleted, message=util.sanitize_text( message ), status='error' ) ) - if ( trans.user_is_admin() and cntrller == 'library_admin' ): + if is_admin: # Get all associated hdas and lddas that use the same disk file. associated_hdas = trans.sa_session.query( trans.model.HistoryDatasetAssociation ) \ .filter( and_( trans.model.HistoryDatasetAssociation.deleted == False, @@ -848,7 +855,7 @@ class LibraryCommon( BaseController ): last_used_build = dbkey[0] else: last_used_build = dbkey - is_admin = trans.user_is_admin() and cntrller == 'library_admin' + is_admin = trans.user_is_admin() and cntrller in ( 'library_admin', 'api' ) current_user_roles = trans.get_current_user_roles() if replace_id not in [ None, 'None' ]: try: @@ -859,6 +866,8 @@ class LibraryCommon( BaseController ): # its parent library, or if they are not able to view the dataset itself. if not replace_dataset or ( not is_admin and not trans.app.security_agent.can_access_library_item( current_user_roles, replace_dataset, trans.user ) ): message = "Invalid library dataset id ( %s ) to replace specified." % replace_id + if cntrller == 'api': + return 400, message return trans.response.send_redirect( web.url_for( controller='library_common', action='browse_library', cntrller=cntrller, @@ -870,6 +879,8 @@ class LibraryCommon( BaseController ): # Deny access if the user is not an admin and does not have the LIBRARY_MODIFY permission. if not ( is_admin or trans.app.security_agent.can_modify_library_item( current_user_roles, replace_dataset ) ): message = "You are not authorized to replace library dataset '%s'." % replace_dataset.name + if cntrller == 'api': + return 403, message return trans.response.send_redirect( web.url_for( controller='library_common', action='browse_library', cntrller=cntrller, @@ -896,6 +907,8 @@ class LibraryCommon( BaseController ): # its parent library, or if they are not able to see the folder's contents. if not folder or ( not is_admin and not trans.app.security_agent.can_access_library_item( current_user_roles, folder, trans.user ) ): message = "Invalid parent folder id ( %s ) specified." % str( folder_id ) + if cntrller == 'api': + return 400, message return trans.response.send_redirect( web.url_for( controller='library_common', action='browse_library', cntrller=cntrller, @@ -907,6 +920,8 @@ class LibraryCommon( BaseController ): # Deny access if the user is not an admin and does not have the LIBRARY_ADD permission. if not ( is_admin or trans.app.security_agent.can_add_library_item( current_user_roles, folder ) ): message = "You are not authorized to create a library dataset in parent folder '%s'." % folder.name + if cntrller == 'api': + return 403, message return trans.response.send_redirect( web.url_for( controller='library_common', action='browse_library', cntrller=cntrller, @@ -918,7 +933,7 @@ class LibraryCommon( BaseController ): library = folder.parent_library if folder and last_used_build in [ 'None', None, '?' ]: last_used_build = folder.genome_build - if params.get( 'runtool_btn', False ) or params.get( 'ajax_upload', False ): + if params.get( 'runtool_btn', False ) or params.get( 'ajax_upload', False ) or cntrller == 'api': # Check to see if the user selected roles to associate with the DATASET_ACCESS permission # on the dataset that would cause accessibility issues. roles = params.get( 'roles', False ) @@ -931,7 +946,8 @@ class LibraryCommon( BaseController ): permissions, in_roles, error, message = \ trans.app.security_agent.derive_roles_from_access( trans, library.id, cntrller, library=True, **vars ) if error: - status = 'error' + if cntrller == 'api': + return 400, message trans.response.send_redirect( web.url_for( controller='library_common', action='upload_library_dataset', cntrller=cntrller, @@ -961,6 +977,11 @@ class LibraryCommon( BaseController ): replace_dataset=replace_dataset, **kwd ) if created_outputs_dict: + if cntrller == 'api': + # created_outputs_dict can only ever be a string if cntrller == 'api' + if type( created_outputs_dict ) == str: + return 400, created_outputs_dict + return 200, created_outputs_dict total_added = len( created_outputs_dict.keys() ) ldda_id_list = [ str( v.id ) for k, v in created_outputs_dict.items() ] created_ldda_ids=",".join( ldda_id_list ) @@ -1001,6 +1022,9 @@ class LibraryCommon( BaseController ): created_ldda_ids = '' message = "Upload failed" status='error' + if cntrller == 'api': + return 400, message + response_code = 400 trans.response.send_redirect( web.url_for( controller='library_common', action='browse_library', cntrller=cntrller, @@ -1068,7 +1092,6 @@ class LibraryCommon( BaseController ): # Library-specific params params = util.Params( kwd ) # is this filetoolparam safe? show_deleted = util.string_as_bool( params.get( 'show_deleted', False ) ) - library_bunch = upload_common.handle_library_params( trans, params, folder_id, replace_dataset ) message = util.restore_text( params.get( 'message', '' ) ) status = params.get( 'status', 'done' ) server_dir = util.restore_text( params.get( 'server_dir', '' ) ) @@ -1077,11 +1100,11 @@ class LibraryCommon( BaseController ): else: replace_id = None upload_option = params.get( 'upload_option', 'upload_file' ) - err_redirect = False + response_code = 200 if upload_option == 'upload_directory': if server_dir in [ None, 'None', '' ]: - err_redirect = True - if cntrller == 'library_admin': + response_code = 400 + if cntrller == 'library_admin' or ( cntrller == 'api' and trans.user_is_admin ): import_dir = trans.app.config.library_import_dir import_dir_desc = 'library_import_dir' full_dir = os.path.join( import_dir, server_dir ) @@ -1095,21 +1118,35 @@ class LibraryCommon( BaseController ): if import_dir: message = 'Select a directory' else: + response_code = 403 message = '"%s" is not defined in the Galaxy configuration file' % import_dir_desc - # Proceed with (mostly) regular upload processing - precreated_datasets = upload_common.get_precreated_datasets( trans, tool_params, trans.app.model.LibraryDatasetDatasetAssociation, controller=cntrller ) - if upload_option == 'upload_file': - tool_params = upload_common.persist_uploads( tool_params ) - uploaded_datasets = upload_common.get_uploaded_datasets( trans, cntrller, tool_params, precreated_datasets, dataset_upload_inputs, library_bunch=library_bunch ) - elif upload_option == 'upload_directory': - uploaded_datasets, err_redirect, message = self.get_server_dir_uploaded_datasets( trans, cntrller, params, full_dir, import_dir_desc, library_bunch, err_redirect, message ) elif upload_option == 'upload_paths': - uploaded_datasets, err_redirect, message = self.get_path_paste_uploaded_datasets( trans, cntrller, params, library_bunch, err_redirect, message ) - upload_common.cleanup_unused_precreated_datasets( precreated_datasets ) - if upload_option == 'upload_file' and not uploaded_datasets: - message = 'Select a file, enter a URL or enter text' - err_redirect = True - if err_redirect: + if not trans.app.config.allow_library_path_paste: + response_code = 403 + message = '"allow_library_path_paste" is not defined in the Galaxy configuration file' + # Some error handling should be added to this method. + try: + library_bunch = upload_common.handle_library_params( trans, params, folder_id, replace_dataset ) + except: + response_code = 500 + message = "Unable to parse upload parameters, please report this error." + # Proceed with (mostly) regular upload processing if we're still errorless + if response_code == 200: + precreated_datasets = upload_common.get_precreated_datasets( trans, tool_params, trans.app.model.LibraryDatasetDatasetAssociation, controller=cntrller ) + if upload_option == 'upload_file': + tool_params = upload_common.persist_uploads( tool_params ) + uploaded_datasets = upload_common.get_uploaded_datasets( trans, cntrller, tool_params, precreated_datasets, dataset_upload_inputs, library_bunch=library_bunch ) + elif upload_option == 'upload_directory': + uploaded_datasets, response_code, message = self.get_server_dir_uploaded_datasets( trans, cntrller, params, full_dir, import_dir_desc, library_bunch, response_code, message ) + elif upload_option == 'upload_paths': + uploaded_datasets, response_code, message = self.get_path_paste_uploaded_datasets( trans, cntrller, params, library_bunch, response_code, message ) + upload_common.cleanup_unused_precreated_datasets( precreated_datasets ) + if upload_option == 'upload_file' and not uploaded_datasets: + response_code = 400 + message = 'Select a file, enter a URL or enter text' + if response_code != 200: + if cntrller == 'api': + return ( response_code, message ) trans.response.send_redirect( web.url_for( controller='library_common', action='upload_library_dataset', cntrller=cntrller, @@ -1145,7 +1182,7 @@ class LibraryCommon( BaseController ): trans.sa_session.add_all( ( uploaded_dataset.data, uploaded_dataset.data.dataset ) ) trans.sa_session.flush() return uploaded_dataset - def get_server_dir_uploaded_datasets( self, trans, cntrller, params, full_dir, import_dir_desc, library_bunch, err_redirect, message ): + def get_server_dir_uploaded_datasets( self, trans, cntrller, params, full_dir, import_dir_desc, library_bunch, response_code, message ): files = [] try: for entry in os.listdir( full_dir ): @@ -1173,22 +1210,22 @@ class LibraryCommon( BaseController ): files.append( path ) except Exception, e: message = "Unable to get file list for configured %s, error: %s" % ( import_dir_desc, str( e ) ) - err_redirect = True - return None, err_redirect, message + response_code = 500 + return None, response_code, message if not files: message = "The directory '%s' contains no valid files" % full_dir - err_redirect = True - return None, err_redirect, message + response_code = 400 + return None, response_code, message uploaded_datasets = [] for file in files: name = os.path.basename( file ) uploaded_datasets.append( self.make_library_uploaded_dataset( trans, cntrller, params, name, file, 'server_dir', library_bunch ) ) - return uploaded_datasets, None, None - def get_path_paste_uploaded_datasets( self, trans, cntrller, params, library_bunch, err_redirect, message ): + return uploaded_datasets, 200, None + def get_path_paste_uploaded_datasets( self, trans, cntrller, params, library_bunch, response_code, message ): if params.get( 'filesystem_paths', '' ) == '': message = "No paths entered in the upload form" - err_redirect = True - return None, err_redirect, message + response_code = 400 + return None, response_code, message preserve_dirs = True if params.get( 'dont_preserve_dirs', False ): preserve_dirs = False @@ -1222,8 +1259,8 @@ class LibraryCommon( BaseController ): in_folder ) ) if bad_paths: message = "Invalid paths:<br><ul><li>%s</li></ul>" % "</li><li>".join( bad_paths ) - err_redirect = True - return None, err_redirect, message + response_code = 400 + return None, response_code, message return uploaded_datasets, None, None @web.expose def add_history_datasets_to_library( self, trans, cntrller, library_id, folder_id, hda_ids='', **kwd ): --- a/eggs.ini +++ b/eggs.ini @@ -41,7 +41,7 @@ Paste = 1.6 PasteDeploy = 1.3.3 PasteScript = 1.7.3 pexpect = 2.4 -Routes = 1.11 +Routes = 1.12.3 simplejson = 1.5 SQLAlchemy = 0.5.6 sqlalchemy_migrate = 0.5.4 --- /dev/null +++ b/scripts/api/library_create_folder.py @@ -0,0 +1,20 @@ +#!/usr/bin/python + +import os, sys +sys.path.insert( 0, os.path.dirname( __file__ ) ) +from common import submit + +try: + data = {} + data[ 'folder_id' ] = sys.argv[3] + data[ 'name' ] = sys.argv[4] + data[ 'create_type' ] = 'folder' +except IndexError: + print 'usage: %s key url folder_id name [description]' % os.path.basename( sys.argv[0] ) + sys.exit( 1 ) +try: + data[ 'description' ] = sys.argv[5] +except IndexError: + data[ 'description' ] = '' + +submit( sys.argv[1], sys.argv[2], data ) --- a/lib/galaxy/web/controllers/user.py +++ b/lib/galaxy/web/controllers/user.py @@ -979,3 +979,25 @@ class User( BaseController ): lines_skipped=lines_skipped ) + @web.expose + def new_api_key( self, trans, **kwd ): + params = util.Params( kwd ) + message = util.restore_text( params.get( 'message', '' ) ) + status = params.get( 'status', 'done' ) + admin_view = util.string_as_bool( params.get( 'admin_view', False ) ) + error = '' + user = trans.sa_session.query( trans.app.model.User ).get( int( params.get( 'user_id', None ) ) ) + if params.get( 'new_api_key_button', None ) == 'Generate a new key now': + new_key = trans.app.model.APIKeys() + new_key.user_id = user.id + new_key.key = trans.app.security.get_new_guid() + trans.sa_session.add( new_key ) + trans.sa_session.flush() + message = "Generated a new web API key" + status = "done" + return trans.response.send_redirect( web.url_for( controller='user', + action='show_info', + admin_view=admin_view, + user_id=user.id, + message=message, + status=status ) ) --- a/templates/webapps/galaxy/user/info.mako +++ b/templates/webapps/galaxy/user/info.mako @@ -66,8 +66,9 @@ <p></p> %endif +<p/> + <div class="toolForm"> - <p></p><form name="user_info" id="user_info" action="${h.url_for( controller='user', action='new_address', user_id=user.id, admin_view=admin_view )}" method="post" ><div class="toolFormTitle">User Addresses</div><div class="toolFormBody"> @@ -118,5 +119,37 @@ </div></div></form> - <p></p></div> + +<p/> + +%if trans.app.config.enable_api: + <div class="toolForm"> + <form name="user_info" id="user_info" action="${h.url_for( controller='user', action='new_api_key', user_id=user.id, admin_view=admin_view )}" method="post" > + <div class="toolFormTitle">Web API Key</div> + <div class="toolFormBody"> + <div class="form-row"> + <label>Current API key:</label> + %if user.api_keys: + ${user.api_keys[0].key} + %else: + none set + %endif + </div> + <div class="form-row"> + <input type="submit" name="new_api_key_button" value="Generate a new key now"/> + %if user.api_keys: + (invalidates old key) + %endif + <div class="toolParamHelp" style="clear: both;"> + An API key will allow you to access Galaxy via its web + API (documentation forthcoming). Please note that + <strong>this key acts as an alternate means to access + your account, and should be treated with the same care + as your login password</strong>. + </div> + </div> + </div> + </form> + </div> +%endif --- a/lib/galaxy/web/__init__.py +++ b/lib/galaxy/web/__init__.py @@ -2,6 +2,6 @@ The Galaxy web application. """ -from framework import expose, json, json_pretty, require_login, require_admin, url_for, error, form, FormBuilder +from framework import expose, json, json_pretty, require_login, require_admin, url_for, error, form, FormBuilder, expose_api from framework.base import httpexceptions --- a/universe_wsgi.ini.sample +++ b/universe_wsgi.ini.sample @@ -112,6 +112,9 @@ gbrowse_display_sites = wormbase,tair,mo # Define your GeneTrack servers in tool-data/shared/genetrack/genetrack_sites.txt #genetrack_display_sites = +# Enable the (experimental! beta!) Web API. Documentation forthcoming. +#enable_api = False + # Serving static files (needed if running standalone) static_enabled = True static_cache_time = 360 --- /dev/null +++ b/scripts/api/display.py @@ -0,0 +1,11 @@ +#!/usr/bin/python + +import os, sys +sys.path.insert( 0, os.path.dirname( __file__ ) ) +from common import display + +try: + display( *sys.argv[1:3] ) +except TypeError: + print 'usage: %s key url' % os.path.basename( sys.argv[0] ) + sys.exit( 1 )