diff --git a/eggs.ini b/eggs.ini
index 38692a98526..a198487ecad 100644
--- 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
diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py
index a1b6ed21155..8e6401be6c7 100644
--- 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 )
diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py
index 49aceacfa90..4cf834fee48 100644
--- a/lib/galaxy/model/__init__.py
+++ b/lib/galaxy/model/__init__.py
@@ -829,6 +829,8 @@ class HistoryDatasetAssociationDisplayAtAuthorization( object ):
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 ) ]
-
-
diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py
index 9efb8291c91..b0eda4469e9 100644
--- a/lib/galaxy/model/mapping.py
+++ b/lib/galaxy/model/mapping.py
@@ -889,6 +889,12 @@ UserAction.table = Table( "user_action", metadata,
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, UserAction.table,
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
diff --git a/lib/galaxy/model/migrate/versions/0049_api_keys_table.py b/lib/galaxy/model/migrate/versions/0049_api_keys_table.py
new file mode 100644
index 00000000000..106920b2e5c
--- /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 ) )
diff --git a/lib/galaxy/tools/actions/upload_common.py b/lib/galaxy/tools/actions/upload_common.py
index 6e56fba429a..5d0db288884 100644
--- 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_dataset, state=None ):
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
diff --git a/lib/galaxy/util/__init__.py b/lib/galaxy/util/__init__.py
index 0789dbb6cc7..8f1ab4b7134 100644
--- 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
diff --git a/lib/galaxy/web/__init__.py b/lib/galaxy/web/__init__.py
index e9c2c41a52c..1c9295630e0 100644
--- 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
diff --git a/lib/galaxy/web/api/__init__.py b/lib/galaxy/web/api/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/lib/galaxy/web/api/contents.py b/lib/galaxy/web/api/contents.py
new file mode 100644
index 00000000000..e31fc267355
--- /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
diff --git a/lib/galaxy/web/api/libraries.py b/lib/galaxy/web/api/libraries.py
new file mode 100644
index 00000000000..a5b06989b21
--- /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
diff --git a/lib/galaxy/web/buildapp.py b/lib/galaxy/web/buildapp.py
index 389db165e0c..4cb33c6f83b 100644
--- 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 ):
diff --git a/lib/galaxy/web/controllers/library_common.py b/lib/galaxy/web/controllers/library_common.py
index 0fce00d5380..3fea1124629 100644
--- 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: