def _matchRoute(self, method, path): """ Helper function that attempts to match the requested ``method`` and ``path`` with a registered route specification. :param method: The requested HTTP method, in lowercase. :type method: str :param path: The requested path. :type path: tuple[str] :returns: A tuple of ``(route, handler, wildcards)``, where ``route`` is the registered `list` of route components, ``handler`` is the route handler `function`, and ``wildcards`` is a `dict` of kwargs that should be passed to the underlying handler, based on the wildcard tokens of the route. :raises: `GirderException`, when no routes are defined on this resource. :raises: `RestException`, when no route can be matched. """ if not self._routes: raise GirderException('No routes defined for resource') for route, handler in self._routes[method][len(path)]: wildcards = {} for routeComponent, pathComponent in six.moves.zip(route, path): if routeComponent[0] == ':': # Wildcard token wildcards[routeComponent[1:]] = pathComponent elif routeComponent != pathComponent: # Exact match token break else: return route, handler, wildcards raise RestException('No matching route for "%s %s"' % (method.upper(), '/'.join(path)))
def discardPartialUploads(self, uploadId, userId, parentId, assetstoreId, minimumAge, includeUntracked): filters = {} if uploadId is not None: filters['uploadId'] = uploadId if userId is not None: filters['userId'] = userId if assetstoreId is not None: filters['assetstoreId'] = assetstoreId if parentId is not None: filters['parentId'] = parentId if minimumAge is not None: filters['minimumAge'] = minimumAge uploadList = list(Upload().list(filters=filters)) # Move the results to list that isn't a cursor so we don't have to have # the cursor sitting around while we work on the data. for upload in uploadList: try: Upload().cancelUpload(upload) except OSError as exc: if exc.errno == errno.EACCES: raise GirderException( 'Failed to delete upload.', 'girderformindlogger.api.v1.system.delete-upload-failed' ) raise if includeUntracked: uploadList += Upload().untrackedUploads('delete', assetstoreId) return uploadList
def getUploadAssetstore(self, event): """ Handle the model.upload.assetstore event. This event passes a dictionary consisting of a model type and resource document. If the base document has an assetstore policy, then set an assetstore key of this dictionary to an assetstore document that should be used or prevent the default action if no appropriate assetstores are allowed. :param event: event record. """ model, resource = self._getBaseResource(event.info['model'], event.info['resource']) if resource is None: return policy = resource[QUOTA_FIELD] assetstore = self._checkAssetstore( policy.get('preferredAssetstore', None)) if assetstore is False: assetstore = self._checkAssetstore( policy.get('fallbackAssetstore', None)) if assetstore is not False: logger.info( 'preferredAssetstore not available for %s %s, ' 'using fallbackAssetstore', model, resource['_id']) if assetstore is False: raise GirderException('Required assetstore is unavailable') if assetstore: event.addResponse(assetstore)
def removeRoute(self, method, route, resource=None): """ Remove a route from the handler and documentation. :param method: The HTTP method, e.g. 'GET', 'POST', 'PUT' :type method: str :param route: The route, as a list of path params relative to the resource root. Elements of this list starting with ':' are assumed to be wildcards. :type route: tuple[str] :param resource: the name of the resource at the root of this route. """ self._ensureInit() nLengthRoutes = self._routes[method.lower()][len(route)] for i, (registeredRoute, registeredHandler) in enumerate(nLengthRoutes): if registeredRoute == route: handler = registeredHandler del nLengthRoutes[i] break else: raise GirderException('No such route: %s %s' % (method, '/'.join(route))) # Remove the api doc if resource is None: resource = getattr(self, 'resourceName', handler.__module__.rsplit('.', 1)[-1]) if getattr(handler, 'description', None) is not None: docs.removeRouteDocs( resource=resource, route=route, method=method, info=handler.description.asDict(), handler=handler)
def getApiUrl(url=None, preferReferer=False): """ In a request thread, call this to get the path to the root of the REST API. The returned path does *not* end in a forward slash. :param url: URL from which to extract the base URL. If not specified, uses the server root system setting. If that is not specified, uses `cherrypy.url()` :param preferReferer: if no url is specified, this is true, and this is in a cherrypy request that has a referer header that contains the api string, use that referer as the url. """ apiStr = config.getConfig()['server']['api_root'] if not url: if preferReferer and apiStr in cherrypy.request.headers.get('referer', ''): url = cherrypy.request.headers['referer'] else: root = Setting().get(SettingKey.SERVER_ROOT) if root: return posixpath.join(root, apiStr.lstrip('/')) url = url or cherrypy.url() idx = url.find(apiStr) if idx < 0: raise GirderException('Could not determine API root in %s.' % url) return url[:idx + len(apiStr)]
def moveFileToAssetstore(self, file, user, assetstore, progress=noProgress): """ Move a file from whatever assetstore it is located in to a different assetstore. This is done by downloading and re-uploading the file. :param file: the file to move. :param user: the user that is authorizing the move. :param assetstore: the destination assetstore. :param progress: optional progress context. :returns: the original file if it is not moved, or the newly 'uploaded' file if it is. """ from girderformindlogger.models.file import File if file['assetstoreId'] == assetstore['_id']: return file # Allow an event to cancel the move. This could be done, for instance, # on files that could change dynamically. event = events.trigger('model.upload.movefile', { 'file': file, 'assetstore': assetstore }) if event.defaultPrevented: raise GirderException( 'The file %s could not be moved to assetstore %s' % (file['_id'], assetstore['_id'])) # Create a new upload record into the existing file upload = self.createUploadToFile(file=file, user=user, size=int(file['size']), assetstore=assetstore) if file['size'] == 0: return File().filter(self.finalizeUpload(upload), user) # Uploads need to be chunked for some assetstores chunkSize = self._getChunkSize() chunk = None for data in File().download(file, headers=False)(): if chunk is not None: chunk += data else: chunk = data if len(chunk) >= chunkSize: upload = self.handleChunk( upload, RequestBodyStream(six.BytesIO(chunk), len(chunk))) progress.update(increment=len(chunk)) chunk = None if chunk is not None: upload = self.handleChunk( upload, RequestBodyStream(six.BytesIO(chunk), len(chunk))) progress.update(increment=len(chunk)) return upload
def createItem(self, name, creator, folder, description='', reuseExisting=False, validate=True): """ Create a new item. The creator will be given admin access to it. :param name: The name of the item. :type name: str :param description: Description for the item. :type description: str :param folder: The parent folder of the item. :param creator: User document representing the creator of the item. :type creator: dict :param reuseExisting: If an item with the given name already exists under the given folder, return that item rather than creating a new one. :type reuseExisting: bool :returns: The item document that was created. """ if reuseExisting: existing = self.findOne({'folderId': folder['_id'], 'name': name}) if existing: return existing now = datetime.datetime.utcnow() if not isinstance(creator, dict) or '_id' not in creator: # Internal error -- this shouldn't be called without a user. raise GirderException( 'Creator must be a user.', 'girderformindlogger.models.item.creator-not-user') if 'baseParentType' not in folder: pathFromRoot = self.parentsToRoot({'folderId': folder['_id']}, creator, force=True) folder['baseParentType'] = pathFromRoot[0]['type'] folder['baseParentId'] = pathFromRoot[0]['object']['_id'] return self.save( { 'name': self._validateString(name), 'description': self._validateString(description), 'folderId': ObjectId(folder['_id']), 'creatorId': creator['_id'], 'baseParentType': folder['baseParentType'], 'baseParentId': folder['baseParentId'], 'created': now, 'updated': now, 'size': 0, 'meta': {} }, validate=validate)
def createScreen(self, name, creator, activity=None, description='', readOnly=False): """ Create a new screen. :param name: The name of the item. :type name: str :param description: Description for the screen. :type description: str :param activity: The parent activity of the screen. :param creator: User document representing the creator of the screen. :type creator: dict :param readOnly: A ready-to-use screen :type readOnly: bool :returns: The screen item document that was created. """ try: activity = ActivityModel().load(activity.get('_id', activity), level=AccessType.WRITE, user=creator) except: raise ValidationException( 'Invalid Activity ID or inadequate access rights', 'activity') now = datetime.datetime.utcnow() if not isinstance(creator, dict) or '_id' not in creator: # Internal error -- this shouldn't be called without a user. raise GirderException( 'Creator must be a user.', 'girderformindlogger.models.item.creator-not-user') if 'baseParentType' not in activity: pathFromRoot = self.parentsToRoot({'folderId': activity['_id']}, creator, force=True) activity['baseParentType'] = pathFromRoot[0]['type'] activity['baseParentId'] = pathFromRoot[0]['object']['_id'] return self.save({ 'name': self._validateString(name), 'description': self._validateString(description), 'folderId': ObjectId(activity['_id']), 'creatorId': creator['_id'], 'baseParentType': activity['baseParentType'], 'baseParentId': activity['baseParentId'], 'created': now, 'updated': now, 'size': 0, 'readOnly': readOnly })
def getCurrent(self): """ Returns the current assetstore. If none exists, this will raise a 500 exception. """ current = self.findOne({'current': True}) if current is None: raise GirderException( 'No current assetstore is set.', 'girderformindlogger.model.assetstore.no-current-assetstore') return current
def addSearchMode(mode, handler): """ Register a search mode. New searches made for the registered mode will call the handler function. The handler function must take parameters: `query`, `types`, `user`, `level`, `limit`, `offset`, and return the search results. :param mode: A search mode identifier. :type mode: str :param handler: A search mode handler function. :type handler: function """ if _allowedSearchMode.get(mode) is not None: raise GirderException('A search mode %r already exists.' % mode) _allowedSearchMode[mode] = handler
def _loadPlugins(info, names=None): """Load plugins with the given app info object. If `names` is None, all installed plugins will be loaded. If `names` is a list, then only those plugins in the provided list will be loaded. """ if names is None: names = allPlugins() for name in names: pluginObject = getPlugin(name) if pluginObject is None: raise GirderException('Plugin %s is not installed' % name) pluginObject.load(info)
def read(self, size=None): """ Read *size* bytes from the file data. :param size: The number of bytes to read from the current position. The actual number returned could be less than this if the end of the file is reached. An empty response indicates that the file has been completely consumed. If None or negative, read to the end of the file. :type size: int :rtype: bytes """ if size is None or size < 0: size = self._file['size'] - self._pos if size > self._maximumReadSize: raise GirderException('Read exceeds maximum allowed size.') data = six.BytesIO() length = 0 for chunk in itertools.chain(self._prev, self._stream): chunkLen = len(chunk) if chunkLen == 0: break if length + chunkLen <= size: data.write(chunk) self._prev = [] length += chunkLen if length == size: break else: chunkLen = min(size - length, chunkLen) data.write(chunk[:chunkLen]) self._prev = [chunk[chunkLen:]] length += chunkLen break self._pos += length return data.getvalue()
def downloadFile(self, file, offset=0, headers=True, endByte=None, contentDisposition=None, extraParameters=None, **kwargs): """ Returns a generator function that will be used to stream the file from disk to the response. """ if endByte is None or endByte > file['size']: endByte = file['size'] path = self.fullPath(file) if not os.path.isfile(path): raise GirderException( 'File %s does not exist.' % path, 'girderformindlogger.utility.filesystem_assetstore_adapter.' 'file-does-not-exist') if headers: setResponseHeader('Accept-Ranges', 'bytes') self.setContentHeaders(file, offset, endByte, contentDisposition) def stream(): bytesRead = offset with open(path, 'rb') as f: if offset > 0: f.seek(offset) while True: readLen = min(BUF_SIZE, endByte - bytesRead) if readLen <= 0: break data = f.read(readLen) bytesRead += readLen if not data: break yield data return stream
def validate(self, doc, allowRename=False): """ Validate the name and description of the folder, ensure that it is associated with a valid parent and that it has a unique name. :param doc: the folder document to validate. :param allowRename: if True and a folder or item exists with the same name, rename the folder so that it is unique. :returns: `the validated folder document` """ from girderformindlogger.models.item import Item doc['name'] = doc['name'].strip() doc['lowerName'] = doc['name'].lower() doc['description'] = doc['description'].strip() if not doc['name']: raise ValidationException('Folder name must not be empty.', 'name') if not doc['parentCollection'] in ('folder', 'user', 'collection'): # Internal error; this shouldn't happen raise GirderException('Invalid folder parent type: %s.' % doc['parentCollection'], 'girderformindlogger.models.folder.invalid-parent-type') name = doc['name'] # If the folder already exists with the current name, don't check. # Although we don't want duplicate names, they can occur when there are # simultaneous uploads, and also because Mongo has no guaranteed # multi-collection uniqueness constraints. If this occurs, and we are # changing a non-name property, don't validate the name (since that may # fail). If the name is being changed, validate that it is probably # unique. checkName = '_id' not in doc or not self.findOne({'_id': doc['_id'], 'name': name}) n = 0 itemModel = Item() while checkName: q = { 'parentId': doc['parentId'], 'name': name, 'parentCollection': doc['parentCollection'] } if '_id' in doc: q['_id'] = {'$ne': doc['_id']} dupFolder = self.findOne(q, fields=['_id']) if doc['parentCollection'] == 'folder': q = { 'folderId': doc['parentId'], 'name': name } dupItem = itemModel.findOne(q, fields=['_id']) else: dupItem = None if dupItem is None and dupFolder is None: doc['name'] = name break if not allowRename: if dupFolder: raise ValidationException('A folder with that name ' 'already exists here.', 'name') raise ValidationException('An item with that name already ' 'exists here.', 'name') n += 1 name = '%s (%d)' % (doc['name'], n) return doc
def initUpload(self, parentType, parentId, name, size, mimeType, linkUrl, reference, assetstoreId): """ Before any bytes of the actual file are sent, a request should be made to initialize the upload. This creates the temporary record of the forthcoming upload that will be passed in chunks to the readChunk method. If you pass a "linkUrl" parameter, it will make a link file in the designated parent. """ user = self.getCurrentUser() parent = ModelImporter.model(parentType).load(id=parentId, user=user, level=AccessType.WRITE, exc=True) if linkUrl is not None: return self._model.filter( self._model.createLinkFile(url=linkUrl, parent=parent, name=name, parentType=parentType, creator=user, size=size, mimeType=mimeType), user) else: self.requireParams({'size': size}) assetstore = None if assetstoreId: self.requireAdmin( user, message= 'You must be an admin to select a destination assetstore.') assetstore = Assetstore().load(assetstoreId) chunk = None if size > 0 and cherrypy.request.headers.get('Content-Length'): ct = cherrypy.request.body.content_type.value if (ct not in cherrypy.request.body.processors and ct.split( '/', 1)[0] not in cherrypy.request.body.processors): chunk = RequestBodyStream(cherrypy.request.body) if chunk is not None and chunk.getSize() <= 0: chunk = None try: # TODO: This can be made more efficient by adding # save=chunk is None # to the createUpload call parameters. However, since this is # a breaking change, that should be deferred until a major # version upgrade. upload = Upload().createUpload(user=user, name=name, parentType=parentType, parent=parent, size=size, mimeType=mimeType, reference=reference, assetstore=assetstore) except OSError as exc: if exc.errno == errno.EACCES: raise GirderException( 'Failed to create upload.', 'girderformindlogger.api.v1.file.create-upload-failed') raise if upload['size'] > 0: if chunk: return Upload().handleChunk(upload, chunk, filter=True, user=user) return upload else: return self._model.filter(Upload().finalizeUpload(upload), user)