Esempio n. 1
0
class LabelResource(Resource):

    def __init__(self):
        super().__init__()
        self.resourceName = 'label'

        self.ann_file_name = "annotation.json"

        self.coll_m = Collection()
        self.file_m = File()
        self.folder_m = Folder()
        self.item_m = Item()
        self.upload_m = Upload()
        self.asset_m = Assetstore()

        self.setupRoutes()

    def setupRoutes(self):
        self.route('GET', (), handler=self.getLabelList)
        self.route('GET', (':label_id',), self.getLabel)
        self.route('GET', ('meta',), self.getLabelMeta)
        self.route('GET', ('create',), self.createLabelFile)
        self.route('GET', ('by_name',), self.getLabelByName)
        self.route('POST', (), self.postLabel)

    # ############# PUBLIC METHODS ##################

    @access.public
    @autoDescribeRoute(
        Description('Get label list').param('assign_id', 'assignment folder id'))
    @rest.rawResponse
    @trace
    def getLabelList(self, assign_id):
        files = self.folder_m.fileList(assign_id, user=self.getCurrentUser(), data=False,
                                       includeMetadata=True, mimeFilter=['application/json'])
        files = list(files)
        cherrypy.response.headers["Content-Type"] = "application/json"
        return dumps(files)

    @access.public
    @autoDescribeRoute(
        Description('Create a new label file inside the label folder if it doesnt exist')
            .param('assign_id', 'the parent folder id')
            .param('name', 'image name for which we are creating this label file'))
    @rest.rawResponse
    @trace
    def createLabelFile(self, assign_id, name):
        p_folder = self.folder_m.load(id=assign_id,
                                      user=self.getCurrentUser(),
                                      level=AccessType.WRITE)

        label_folder = find_folder(p_folder=p_folder,
                                   name=name,
                                   user=self.getCurrentUser(),
                                   desc="Label Folder",
                                   create=True)

        file = find_file(p_folder=label_folder,
                         name=self.ann_file_name,
                         user=self.getCurrentUser(),
                         assetstore=self.asset_m.getCurrent(),
                         create=False)

        if not file:
            file = create_new_file(p_folder=label_folder,
                                   name=self.ann_file_name,
                                   user=self.getCurrentUser(),
                                   assetstore=self.asset_m.getCurrent())

            config_file = self.__find_config(assign_id)

            if not config_file:
                printFail("No config file found")
                return errorMessage("No config file found")
            else:
                res = copy_file(src_file=config_file,
                                dest_file=file,
                                user=self.getCurrentUser())
                return dumps({
                    "label_id": res['fileId']
                })

        return dumps({
            "label_id": file['_id']
        })

    @access.public
    @autoDescribeRoute(
        Description('Get labels by file_name')
            .param('name', 'label file name')
            .param('assign_id', 'the assignment id'))
    @rest.rawResponse
    @trace
    def getLabelByName(self, name, assign_id):
        p_folder = self.folder_m.load(assign_id,
                                      user=self.getCurrentUser(),
                                      level=AccessType.READ)

        label_folder = find_folder(p_folder=p_folder,
                                   name=name,
                                   user=self.getCurrentUser(),
                                   desc="Label Folder",
                                   create=True)

        # this file is created in <assign_folder>/<label_folder>/assignment.json
        file = find_file(p_folder=label_folder,
                         name=self.ann_file_name,
                         user=self.getCurrentUser(),
                         assetstore=self.asset_m.getCurrent(),
                         create=False)

        cherrypy.response.headers["Content-Type"] = "application/json"
        if file:
            return self.file_m.download(file)
        else:
            return dumps({})

    @access.public
    @autoDescribeRoute(
        Description('Get label file by id')
            .param('label_id', 'label file id'))
    @rest.rawResponse
    @trace
    def getLabel(self, label_id):
        file = self.file_m.load(label_id, level=AccessType.READ, user=self.getCurrentUser())
        printOk2(file)
        cherrypy.response.headers["Content-Type"] = "application/json"
        return self.file_m.download(file)

    @access.public
    @autoDescribeRoute(
        Description('Get label meta data by id')
            .param('label_id', 'label file id'))
    @trace
    def getLabelMeta(self, label_id):
        file = self.file_m.load(label_id, level=AccessType.READ, user=self.getCurrentUser())
        cherrypy.response.headers["Content-Type"] = "application/json"
        return dumps(file)

    @access.public
    @autoDescribeRoute(
        Description('Post to label file by id')
            .param('label_id', 'label file id')
            .param('labels', 'labels to be updated'))
    @rest.rawResponse
    @trace
    def postLabel(self, label_id, labels):
        file = self.file_m.load(label_id, level=AccessType.WRITE, user=self.getCurrentUser())
        cherrypy.response.headers["Content-Type"] = "application/json"
        params = {'labels': json.loads(labels)}
        data = json.dumps(params, indent=2, sort_keys=True)
        upload = writeData(self.getCurrentUser(), file, data)
        printOk2(file)
        printOk(upload)
        return dumps(upload)

    # ############# PRIVATE METHODS ##################

    def __create_new_file(self, folder, file_name):
        item = self.item_m.createItem(file_name,
                                      creator=self.getCurrentUser(),
                                      folder=folder,
                                      description='label file',
                                      reuseExisting=False)

        file = self.file_m.createFile(size=0,
                                      item=item,
                                      name=file_name,
                                      creator=self.getCurrentUser(),
                                      assetstore=self.asset_m.getCurrent(),
                                      mimeType="application/json")
        return file

    @staticmethod
    def __get_owner_id(folder):
        aclList = Folder().getFullAccessList(folder)
        for acl in aclList['users']:
            if acl['level'] == AccessType.ADMIN:
                return str(acl['id'])
        return None

    def __get_config_folder(self, label_folder_id):
        label_folder = Folder().load(label_folder_id,
                                     user=self.getCurrentUser(),
                                     level=AccessType.READ)
        ownerId = self.__get_owner_id(label_folder)
        config_folder = self.folder_m.load(label_folder['meta'][ownerId], level=AccessType.READ,
                                           user=self.getCurrentUser())
        return config_folder

    def __find_config(self, folder_id):
        folder = self.__get_config_folder(folder_id)
        printOk2("Config folder {}".format(folder))
        files = self.folder_m.fileList(folder, self.getCurrentUser(), data=False)
        for file_path, file in files:
            printOk(file)
            if file['name'] == "config.json":
                return file

    def __findFolder(self, p_folder, name, desc="", create=False):
        """
        Find folder by name. If not found create the folder
        :param p_folder: parent folder
        :param name: name of the folder you want to find inside the parent folder
        :return: folder doc
        """
        folder = list(self.folder_m.find({'folderId': p_folder['_id'], 'name': name}).limit(1))
        if not folder:
            # check if you are allowed to create, else return nothing
            if create:
                folder = self.folder_m.createFolder(parent=p_folder,
                                                    name=name,
                                                    creator=self.getCurrentUser(),
                                                    description=desc,
                                                    reuseExisting=True)
            else:
                return None

        return folder

    def __findFile(self, folder, file_name):
        item = list(self.item_m.find({'folderId': folder['_id'], 'name': file_name}).limit(1))
        if not item:
            return None

        item = item[0]
        file = list(self.file_m.find({'itemId': item['_id']}).limit(1))

        if not file:
            return None

        return file[0]
Esempio n. 2
0
class File(Resource):
    """
    API Endpoint for files. Includes utilities for uploading and downloading
    them.
    """
    def __init__(self):
        super(File, self).__init__()
        self._model = FileModel()

        self.resourceName = 'file'
        self.route('DELETE', (':id',), self.deleteFile)
        self.route('DELETE', ('upload', ':id'), self.cancelUpload)
        self.route('GET', ('offset',), self.requestOffset)
        self.route('GET', (':id',), self.getFile)
        self.route('GET', (':id', 'download'), self.download)
        self.route('GET', (':id', 'download', ':name'), self.downloadWithName)
        self.route('POST', (), self.initUpload)
        self.route('POST', ('chunk',), self.readChunk)
        self.route('POST', ('completion',), self.finalizeUpload)
        self.route('POST', (':id', 'copy'), self.copy)
        self.route('PUT', (':id',), self.updateFile)
        self.route('PUT', (':id', 'contents'), self.updateFileContents)
        self.route('PUT', (':id', 'move'), self.moveFileToAssetstore)

    @access.public(scope=TokenScope.DATA_READ)
    @filtermodel(model=FileModel)
    @autoDescribeRoute(
        Description('Get a file\'s information.')
        .modelParam('id', model=FileModel, level=AccessType.READ)
        .errorResponse()
        .errorResponse('Read access was denied on the file.', 403)
    )
    def getFile(self, file):
        return file

    @access.user(scope=TokenScope.DATA_WRITE)
    @autoDescribeRoute(
        Description('Start a new upload or create an empty or link file.')
        .notes('Use POST /file/chunk to send the contents of the file.  '
               'The data for the first chunk of the file can be included with '
               'this query by sending it as the body of the request using an '
               'appropriate content-type and with the other parameters as '
               'part of the query string.  If the entire file is uploaded via '
               'this call, the resulting file is returned.')
        .responseClass('Upload')
        .param('parentType', 'Type being uploaded into.', enum=['folder', 'item'])
        .param('parentId', 'The ID of the parent.')
        .param('name', 'Name of the file being created.')
        .param('size', 'Size in bytes of the file.', dataType='integer', required=False)
        .param('mimeType', 'The MIME type of the file.', required=False)
        .param('linkUrl', 'If this is a link file, pass its URL instead '
               'of size and mimeType using this parameter.', required=False)
        .param('reference', 'If included, this information is passed to the '
               'data.process event when the upload is complete.',
               required=False)
        .param('assetstoreId', 'Direct the upload to a specific assetstore (admin-only).',
               required=False)
        .errorResponse()
        .errorResponse('Write access was denied on the parent folder.', 403)
        .errorResponse('Failed to create upload.', 500)
    )
    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.', 'girder.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)

    @access.user(scope=TokenScope.DATA_WRITE)
    @autoDescribeRoute(
        Description('Finalize an upload explicitly if necessary.')
        .notes('This is only required in certain non-standard upload '
               'behaviors. Clients should know which behavior models require '
               'the finalize step to be called in their behavior handlers.')
        .modelParam('uploadId', paramType='formData', model=Upload)
        .errorResponse(('ID was invalid.',
                        'The upload does not require finalization.',
                        'Not enough bytes have been uploaded.'))
        .errorResponse('You are not the user who initiated the upload.', 403)
    )
    def finalizeUpload(self, upload):
        user = self.getCurrentUser()

        if upload['userId'] != user['_id']:
            raise AccessException('You did not initiate this upload.')

        # If we don't have as much data as we were told would be uploaded and
        # the upload hasn't specified it has an alternate behavior, refuse to
        # complete the upload.
        if upload['received'] != upload['size'] and 'behavior' not in upload:
            raise RestException(
                'Server has only received %s bytes, but the file should be %s bytes.' %
                (upload['received'], upload['size']))

        file = Upload().finalizeUpload(upload)
        extraKeys = file.get('additionalFinalizeKeys', ())
        return self._model.filter(file, user, additionalKeys=extraKeys)

    @access.user(scope=TokenScope.DATA_WRITE)
    @autoDescribeRoute(
        Description('Request required offset before resuming an upload.')
        .modelParam('uploadId', paramType='formData', model=Upload)
        .errorResponse("The ID was invalid, or the offset did not match the server's record.")
    )
    def requestOffset(self, upload):
        """
        This should be called when resuming an interrupted upload. It will
        report the offset into the upload that should be used to resume.
        :param uploadId: The _id of the temp upload record being resumed.
        :returns: The offset in bytes that the client should use.
        """
        offset = Upload().requestOffset(upload)

        if isinstance(offset, six.integer_types):
            upload['received'] = offset
            Upload().save(upload)
            return {'offset': offset}
        else:
            return offset

    @access.user(scope=TokenScope.DATA_WRITE)
    @autoDescribeRoute(
        Description('Upload a chunk of a file.')
        .notes('The data for the chunk should be sent as the body of the '
               'request using an appropriate content-type and with the other '
               'parameters as part of the query string.')
        .modelParam('uploadId', paramType='formData', model=Upload)
        .param('offset', 'Offset of the chunk in the file.', dataType='integer',
               paramType='query', required=False, default=0)
        .errorResponse(('ID was invalid.',
                        'Received too many bytes.',
                        'Chunk is smaller than the minimum size.'))
        .errorResponse('You are not the user who initiated the upload.', 403)
        .errorResponse('Failed to store upload.', 500)
    )
    def readChunk(self, upload, offset, params):
        """
        After the temporary upload record has been created (see initUpload),
        the bytes themselves should be passed up in ordered chunks. The user
        must remain logged in when passing each chunk, to authenticate that
        the writer of the chunk is the same as the person who initiated the
        upload. The passed offset is a verification mechanism for ensuring the
        server and client agree on the number of bytes sent/received.
        """
        if 'chunk' in params:
            # If we see the undocumented "chunk" query string parameter, then we abort trying to
            # read the body, use the query string value as chunk, and pass it through to
            # Upload().handleChunk. This case is used by the direct S3 upload process.
            chunk = params['chunk']
        else:
            chunk = RequestBodyStream(cherrypy.request.body)
        user = self.getCurrentUser()

        if upload['userId'] != user['_id']:
            raise AccessException('You did not initiate this upload.')

        if upload['received'] != offset:
            raise RestException(
                'Server has received %s bytes, but client sent offset %s.' % (
                    upload['received'], offset))
        try:
            return Upload().handleChunk(upload, chunk, filter=True, user=user)
        except IOError as exc:
            if exc.errno == errno.EACCES:
                raise Exception('Failed to store upload.')
            raise

    @access.public(scope=TokenScope.DATA_READ, cookie=True)
    @autoDescribeRoute(
        Description('Download a file.')
        .notes('This endpoint also accepts the HTTP "Range" header for partial '
               'file downloads.')
        .modelParam('id', model=FileModel, level=AccessType.READ)
        .param('offset', 'Start downloading at this offset in bytes within '
               'the file.', dataType='integer', required=False, default=0)
        .param('endByte', 'If you only wish to download part of the file, '
               'pass this as the index of the last byte to download. Unlike '
               'the HTTP Range header, the endByte parameter is non-inclusive, '
               'so you should set it to the index of the byte one past the '
               'final byte you wish to receive.', dataType='integer',
               required=False)
        .param('contentDisposition', 'Specify the Content-Disposition response '
               'header disposition-type value.', required=False,
               enum=['inline', 'attachment'], default='attachment')
        .param('extraParameters', 'Arbitrary data to send along with the download request.',
               required=False)
        .errorResponse('ID was invalid.')
        .errorResponse('Read access was denied on the parent folder.', 403)
    )
    def download(self, file, offset, endByte, contentDisposition, extraParameters):
        """
        Defers to the underlying assetstore adapter to stream a file out.
        Requires read permission on the folder that contains the file's item.
        """
        rangeHeader = cherrypy.lib.httputil.get_ranges(
            cherrypy.request.headers.get('Range'), file.get('size', 0))

        # The HTTP Range header takes precedence over query params
        if rangeHeader and len(rangeHeader):
            # Currently we only support a single range.
            offset, endByte = rangeHeader[0]

        return self._model.download(
            file, offset, endByte=endByte, contentDisposition=contentDisposition,
            extraParameters=extraParameters)

    @access.public(scope=TokenScope.DATA_READ, cookie=True)
    @describeRoute(
        Description('Download a file.')
        .param('id', 'The ID of the file.', paramType='path')
        .param('name', 'The name of the file.  This is ignored.',
               paramType='path')
        .param('offset', 'Start downloading at this offset in bytes within '
               'the file.', dataType='integer', required=False)
        .notes('The name parameter doesn\'t alter the download.  Some '
               'download clients save files based on the last part of a path, '
               'and specifying the name satisfies those clients.')
        .errorResponse('ID was invalid.')
        .errorResponse('Read access was denied on the parent folder.', 403)
    )
    def downloadWithName(self, id, name, params):
        return self.download(id=id, params=params)

    @access.user(scope=TokenScope.DATA_WRITE)
    @autoDescribeRoute(
        Description('Delete a file by ID.')
        .modelParam('id', model=FileModel, level=AccessType.WRITE)
        .errorResponse('ID was invalid.')
        .errorResponse('Write access was denied on the parent folder.', 403)
    )
    def deleteFile(self, file):
        self._model.remove(file)

    @access.user(scope=TokenScope.DATA_WRITE)
    @autoDescribeRoute(
        Description('Cancel a partially completed upload.')
        .modelParam('id', model=Upload)
        .errorResponse('ID was invalid.')
        .errorResponse('You lack permission to cancel this upload.', 403)
    )
    def cancelUpload(self, upload):
        user = self.getCurrentUser()

        if upload['userId'] != user['_id'] and not user['admin']:
            raise AccessException('You did not initiate this upload.')

        Upload().cancelUpload(upload)
        return {'message': 'Upload canceled.'}

    @access.user(scope=TokenScope.DATA_WRITE)
    @filtermodel(model=FileModel)
    @autoDescribeRoute(
        Description('Change file metadata such as name or MIME type.')
        .modelParam('id', model=FileModel, level=AccessType.WRITE)
        .param('name', 'The name to set on the file.', required=False, strip=True)
        .param('mimeType', 'The MIME type of the file.', required=False, strip=True)
        .errorResponse('ID was invalid.')
        .errorResponse('Write access was denied on the parent folder.', 403)
    )
    def updateFile(self, file, name, mimeType):
        if name is not None:
            file['name'] = name
        if mimeType is not None:
            file['mimeType'] = mimeType

        return self._model.updateFile(file)

    @access.user(scope=TokenScope.DATA_WRITE)
    @autoDescribeRoute(
        Description('Change the contents of an existing file.')
        .modelParam('id', model=FileModel, level=AccessType.WRITE)
        .param('size', 'Size in bytes of the new file.', dataType='integer')
        .param('reference', 'If included, this information is passed to the '
               'data.process event when the upload is complete.', required=False)
        .param('assetstoreId', 'Direct the upload to a specific assetstore (admin-only).',
               required=False)
        .notes('After calling this, send the chunks just like you would with a '
               'normal file upload.')
    )
    def updateFileContents(self, file, size, reference, assetstoreId):
        user = self.getCurrentUser()

        assetstore = None
        if assetstoreId:
            self.requireAdmin(
                user, message='You must be an admin to select a destination assetstore.')
            assetstore = Assetstore().load(assetstoreId)
        # Create a new upload record into the existing file
        upload = Upload().createUploadToFile(
            file=file, user=user, size=size, reference=reference, assetstore=assetstore)

        if upload['size'] > 0:
            return upload
        else:
            return self._model.filter(Upload().finalizeUpload(upload), user)

    @access.admin(scope=TokenScope.DATA_WRITE)
    @filtermodel(model=FileModel)
    @autoDescribeRoute(
        Description('Move a file to a different assetstore.')
        .modelParam('id', model=FileModel, level=AccessType.WRITE)
        .modelParam('assetstoreId', 'The destination assetstore.', paramType='formData',
                    model=Assetstore)
        .param('progress', 'Controls whether progress notifications will be sent.',
               dataType='boolean', default=False, required=False)
    )
    def moveFileToAssetstore(self, file, assetstore, progress):
        user = self.getCurrentUser()
        title = 'Moving file "%s" to assetstore "%s"' % (file['name'], assetstore['name'])

        with ProgressContext(progress, user=user, title=title, total=file['size']) as ctx:
            return Upload().moveFileToAssetstore(
                file=file, user=user, assetstore=assetstore, progress=ctx)

    @access.user(scope=TokenScope.DATA_WRITE)
    @filtermodel(model=FileModel)
    @autoDescribeRoute(
        Description('Copy a file.')
        .modelParam('id', model=FileModel, level=AccessType.READ)
        .modelParam('itemId', description='The ID of the item to copy the file to.',
                    level=AccessType.WRITE, paramType='formData', model=Item)
    )
    def copy(self, file, item):
        return self._model.copyFile(file, self.getCurrentUser(), item=item)
Esempio n. 3
0
class File(Resource):
    """
    API Endpoint for files. Includes utilities for uploading and downloading
    them.
    """
    def __init__(self):
        super(File, self).__init__()
        self._model = FileModel()

        self.resourceName = 'file'
        self.route('DELETE', (':id',), self.deleteFile)
        self.route('DELETE', ('upload', ':id'), self.cancelUpload)
        self.route('GET', ('offset',), self.requestOffset)
        self.route('GET', (':id',), self.getFile)
        self.route('GET', (':id', 'download'), self.download)
        self.route('GET', (':id', 'download', ':name'), self.downloadWithName)
        self.route('POST', (), self.initUpload)
        self.route('POST', ('chunk',), self.readChunk)
        self.route('POST', ('completion',), self.finalizeUpload)
        self.route('POST', (':id', 'copy'), self.copy)
        self.route('PUT', (':id',), self.updateFile)
        self.route('PUT', (':id', 'contents'), self.updateFileContents)
        self.route('PUT', (':id', 'move'), self.moveFileToAssetstore)

    @access.public(scope=TokenScope.DATA_READ)
    @filtermodel(model=FileModel)
    @autoDescribeRoute(
        Description('Get a file\'s information.')
        .modelParam('id', model=FileModel, level=AccessType.READ)
        .errorResponse()
        .errorResponse('Read access was denied on the file.', 403)
    )
    def getFile(self, file):
        return file

    @access.user(scope=TokenScope.DATA_WRITE)
    @autoDescribeRoute(
        Description('Start a new upload or create an empty or link file.')
        .notes('Use POST /file/chunk to send the contents of the file.  '
               'The data for the first chunk of the file can be included with '
               'this query by sending it as the body of the request using an '
               'appropriate content-type and with the other parameters as '
               'part of the query string.  If the entire file is uploaded via '
               'this call, the resulting file is returned.')
        .responseClass('Upload')
        .param('parentType', 'Type being uploaded into.', enum=['folder', 'item'])
        .param('parentId', 'The ID of the parent.')
        .param('name', 'Name of the file being created.')
        .param('size', 'Size in bytes of the file.', dataType='integer', required=False)
        .param('mimeType', 'The MIME type of the file.', required=False)
        .param('linkUrl', 'If this is a link file, pass its URL instead '
               'of size and mimeType using this parameter.', required=False)
        .param('reference', 'If included, this information is passed to the '
               'data.process event when the upload is complete.',
               required=False)
        .param('assetstoreId', 'Direct the upload to a specific assetstore (admin-only).',
               required=False)
        .errorResponse()
        .errorResponse('Write access was denied on the parent folder.', 403)
        .errorResponse('Failed to create upload.', 500)
    )
    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 = self.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.', 'girder.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)

    @access.user(scope=TokenScope.DATA_WRITE)
    @autoDescribeRoute(
        Description('Finalize an upload explicitly if necessary.')
        .notes('This is only required in certain non-standard upload '
               'behaviors. Clients should know which behavior models require '
               'the finalize step to be called in their behavior handlers.')
        .modelParam('uploadId', paramType='formData', model=Upload)
        .errorResponse(('ID was invalid.',
                        'The upload does not require finalization.',
                        'Not enough bytes have been uploaded.'))
        .errorResponse('You are not the user who initiated the upload.', 403)
    )
    def finalizeUpload(self, upload):
        user = self.getCurrentUser()

        if upload['userId'] != user['_id']:
            raise AccessException('You did not initiate this upload.')

        # If we don't have as much data as we were told would be uploaded and
        # the upload hasn't specified it has an alternate behavior, refuse to
        # complete the upload.
        if upload['received'] != upload['size'] and 'behavior' not in upload:
            raise RestException(
                'Server has only received %s bytes, but the file should be %s bytes.' %
                (upload['received'], upload['size']))

        file = Upload().finalizeUpload(upload)
        extraKeys = file.get('additionalFinalizeKeys', ())
        return self._model.filter(file, user, additionalKeys=extraKeys)

    @access.user(scope=TokenScope.DATA_WRITE)
    @autoDescribeRoute(
        Description('Request required offset before resuming an upload.')
        .modelParam('uploadId', paramType='formData', model=Upload)
        .errorResponse("The ID was invalid, or the offset did not match the server's record.")
    )
    def requestOffset(self, upload):
        """
        This should be called when resuming an interrupted upload. It will
        report the offset into the upload that should be used to resume.
        :param uploadId: The _id of the temp upload record being resumed.
        :returns: The offset in bytes that the client should use.
        """
        offset = Upload().requestOffset(upload)

        if isinstance(offset, six.integer_types):
            upload['received'] = offset
            Upload().save(upload)
            return {'offset': offset}
        else:
            return offset

    @access.user(scope=TokenScope.DATA_WRITE)
    @autoDescribeRoute(
        Description('Upload a chunk of a file.')
        .notes('The data for the chunk should be sent as the body of the '
               'request using an appropriate content-type and with the other '
               'parameters as part of the query string.  Alternately, the '
               'data can be sent as a file in the "chunk" field in multipart '
               'form data.  Multipart uploads are much less efficient and '
               'their use is deprecated.')
        .modelParam('uploadId', paramType='formData', model=Upload)
        .param('offset', 'Offset of the chunk in the file.', dataType='integer',
               paramType='formData')
        .errorResponse(('ID was invalid.',
                        'Received too many bytes.',
                        'Chunk is smaller than the minimum size.'))
        .errorResponse('You are not the user who initiated the upload.', 403)
        .errorResponse('Failed to store upload.', 500)
    )
    def readChunk(self, upload, offset, params):
        """
        After the temporary upload record has been created (see initUpload),
        the bytes themselves should be passed up in ordered chunks. The user
        must remain logged in when passing each chunk, to authenticate that
        the writer of the chunk is the same as the person who initiated the
        upload. The passed offset is a verification mechanism for ensuring the
        server and client agree on the number of bytes sent/received.

        This method accepts both the legacy multipart content encoding, as
        well as passing offset and uploadId as query parameters and passing
        the chunk as the body, which is the recommended method.

        .. deprecated :: 2.2.0
        """
        if 'chunk' in params:
            chunk = params['chunk']
            if isinstance(chunk, cherrypy._cpreqbody.Part):
                # Seek is the only obvious way to get the length of the part
                chunk.file.seek(0, os.SEEK_END)
                size = chunk.file.tell()
                chunk.file.seek(0, os.SEEK_SET)
                chunk = RequestBodyStream(chunk.file, size=size)
        else:
            chunk = RequestBodyStream(cherrypy.request.body)

        user = self.getCurrentUser()

        if upload['userId'] != user['_id']:
            raise AccessException('You did not initiate this upload.')

        if upload['received'] != offset:
            raise RestException(
                'Server has received %s bytes, but client sent offset %s.' % (
                    upload['received'], offset))
        try:
            return Upload().handleChunk(upload, chunk, filter=True, user=user)
        except IOError as exc:
            if exc.errno == errno.EACCES:
                raise Exception('Failed to store upload.')
            raise

    @access.cookie
    @access.public(scope=TokenScope.DATA_READ)
    @autoDescribeRoute(
        Description('Download a file.')
        .notes('This endpoint also accepts the HTTP "Range" header for partial '
               'file downloads.')
        .modelParam('id', model=FileModel, level=AccessType.READ)
        .param('offset', 'Start downloading at this offset in bytes within '
               'the file.', dataType='integer', required=False, default=0)
        .param('endByte', 'If you only wish to download part of the file, '
               'pass this as the index of the last byte to download. Unlike '
               'the HTTP Range header, the endByte parameter is non-inclusive, '
               'so you should set it to the index of the byte one past the '
               'final byte you wish to receive.', dataType='integer',
               required=False)
        .param('contentDisposition', 'Specify the Content-Disposition response '
               'header disposition-type value.', required=False,
               enum=['inline', 'attachment'], default='attachment')
        .param('extraParameters', 'Arbitrary data to send along with the download request.',
               required=False)
        .errorResponse('ID was invalid.')
        .errorResponse('Read access was denied on the parent folder.', 403)
    )
    def download(self, file, offset, endByte, contentDisposition, extraParameters):
        """
        Defers to the underlying assetstore adapter to stream a file out.
        Requires read permission on the folder that contains the file's item.
        """
        rangeHeader = cherrypy.lib.httputil.get_ranges(
            cherrypy.request.headers.get('Range'), file.get('size', 0))

        # The HTTP Range header takes precedence over query params
        if rangeHeader and len(rangeHeader):
            # Currently we only support a single range.
            offset, endByte = rangeHeader[0]

        return self._model.download(
            file, offset, endByte=endByte, contentDisposition=contentDisposition,
            extraParameters=extraParameters)

    @access.cookie
    @access.public(scope=TokenScope.DATA_READ)
    @describeRoute(
        Description('Download a file.')
        .param('id', 'The ID of the file.', paramType='path')
        .param('name', 'The name of the file.  This is ignored.',
               paramType='path')
        .param('offset', 'Start downloading at this offset in bytes within '
               'the file.', dataType='integer', required=False)
        .notes('The name parameter doesn\'t alter the download.  Some '
               'download clients save files based on the last part of a path, '
               'and specifying the name satisfies those clients.')
        .errorResponse('ID was invalid.')
        .errorResponse('Read access was denied on the parent folder.', 403)
    )
    def downloadWithName(self, id, name, params):
        return self.download(id=id, params=params)

    @access.user(scope=TokenScope.DATA_WRITE)
    @autoDescribeRoute(
        Description('Delete a file by ID.')
        .modelParam('id', model=FileModel, level=AccessType.WRITE)
        .errorResponse('ID was invalid.')
        .errorResponse('Write access was denied on the parent folder.', 403)
    )
    def deleteFile(self, file):
        self._model.remove(file)

    @access.user(scope=TokenScope.DATA_WRITE)
    @autoDescribeRoute(
        Description('Cancel a partially completed upload.')
        .modelParam('id', model=Upload)
        .errorResponse('ID was invalid.')
        .errorResponse('You lack permission to cancel this upload.', 403)
    )
    def cancelUpload(self, upload):
        user = self.getCurrentUser()

        if upload['userId'] != user['_id'] and not user['admin']:
            raise AccessException('You did not initiate this upload.')

        Upload().cancelUpload(upload)
        return {'message': 'Upload canceled.'}

    @access.user(scope=TokenScope.DATA_WRITE)
    @filtermodel(model=FileModel)
    @autoDescribeRoute(
        Description('Change file metadata such as name or MIME type.')
        .modelParam('id', model=FileModel, level=AccessType.WRITE)
        .param('name', 'The name to set on the file.', required=False, strip=True)
        .param('mimeType', 'The MIME type of the file.', required=False, strip=True)
        .errorResponse('ID was invalid.')
        .errorResponse('Write access was denied on the parent folder.', 403)
    )
    def updateFile(self, file, name, mimeType):
        if name is not None:
            file['name'] = name
        if mimeType is not None:
            file['mimeType'] = mimeType

        return self._model.updateFile(file)

    @access.user(scope=TokenScope.DATA_WRITE)
    @autoDescribeRoute(
        Description('Change the contents of an existing file.')
        .modelParam('id', model=FileModel, level=AccessType.WRITE)
        .param('size', 'Size in bytes of the new file.', dataType='integer')
        .param('reference', 'If included, this information is passed to the '
               'data.process event when the upload is complete.', required=False)
        .param('assetstoreId', 'Direct the upload to a specific assetstore (admin-only).',
               required=False)
        .notes('After calling this, send the chunks just like you would with a '
               'normal file upload.')
    )
    def updateFileContents(self, file, size, reference, assetstoreId):
        user = self.getCurrentUser()

        assetstore = None
        if assetstoreId:
            self.requireAdmin(
                user, message='You must be an admin to select a destination assetstore.')
            assetstore = Assetstore().load(assetstoreId)
        # Create a new upload record into the existing file
        upload = Upload().createUploadToFile(
            file=file, user=user, size=size, reference=reference, assetstore=assetstore)

        if upload['size'] > 0:
            return upload
        else:
            return self._model.filter(Upload().finalizeUpload(upload), user)

    @access.admin(scope=TokenScope.DATA_WRITE)
    @filtermodel(model=FileModel)
    @autoDescribeRoute(
        Description('Move a file to a different assetstore.')
        .modelParam('id', model=FileModel, level=AccessType.WRITE)
        .modelParam('assetstoreId', 'The destination assetstore.', paramType='formData',
                    model=Assetstore)
        .param('progress', 'Controls whether progress notifications will be sent.',
               dataType='boolean', default=False, required=False)
    )
    def moveFileToAssetstore(self, file, assetstore, progress):
        user = self.getCurrentUser()
        title = 'Moving file "%s" to assetstore "%s"' % (file['name'], assetstore['name'])

        with ProgressContext(progress, user=user, title=title, total=file['size']) as ctx:
            return Upload().moveFileToAssetstore(
                file=file, user=user, assetstore=assetstore, progress=ctx)

    @access.user(scope=TokenScope.DATA_WRITE)
    @filtermodel(model=FileModel)
    @autoDescribeRoute(
        Description('Copy a file.')
        .modelParam('id', model=FileModel, level=AccessType.READ)
        .modelParam('itemId', description='The ID of the item to copy the file to.',
                    level=AccessType.WRITE, paramType='formData', model=Item)
    )
    def copy(self, file, item):
        return self._model.copyFile(file, self.getCurrentUser(), item=item)
Esempio n. 4
0
class LabelResource(Resource):

    def __init__(self):
        super().__init__()
        self.resourceName = 'label'

        self.coll_m = Collection()
        self.file_m = File()
        self.folder_m = Folder()
        self.item_m = Item()
        self.upload_m = Upload()
        self.asset_m = Assetstore()

        self.setupRoutes()

    def setupRoutes(self):
        self.route('GET', (), handler=self.getLabelList)
        self.route('GET', (':label_id',), self.getLabel)
        self.route('GET', ('meta',), self.getLabelMeta)
        self.route('GET', ('create',), self.createLabelFile)
        self.route('GET', ('by_name',), self.getLabelByName)
        self.route('POST', (), self.postLabel)

    def createNewFile(self, folder, file_name):
        item = self.item_m.createItem(file_name,
                                      creator=self.getCurrentUser(),
                                      folder=folder,
                                      description='label file',
                                      reuseExisting=False)

        file = self.file_m.createFile(size=0,
                                      item=item,
                                      name=file_name,
                                      creator=self.getCurrentUser(),
                                      assetstore=self.asset_m.getCurrent(),
                                      mimeType="application/json")
        return file

    def copy(self, srcFile, destFile):
        upload = self.upload_m.createUploadToFile(destFile, self.getCurrentUser(), srcFile['size'])
        self.upload_m.handleChunk(upload=upload,
                                  chunk=RequestBodyStream(self.file_m.open(srcFile), size=destFile['size']),
                                  user=self.getCurrentUser())
        return upload

    @access.public
    @autoDescribeRoute(
        Description('Get label list'))
    @rest.rawResponse
    def getLabelList(self):
        printOk('getLabelsList() was called!')

        try:
            collection = list(self.coll_m.list(user=self.getCurrentUser(), offset=0, limit=1))[0]
            files = self.coll_m.fileList(collection, user=self.getCurrentUser(), data=False,
                                         includeMetadata=True, mimeFilter=['application/json'])
            files = list(files)
            cherrypy.response.headers["Content-Type"] = "application/json"
            return dumps(files)

        except:
            printFail(traceback.print_exc)

    @staticmethod
    def getOwnerId(folder):
        aclList = Folder().getFullAccessList(folder)
        for acl in aclList['users']:
            if acl['level'] == AccessType.ADMIN:
                return str(acl['id'])
        return None

    def getConfigFolder(self, label_folder_id):
        label_folder = Folder().load(label_folder_id,
                                     user=self.getCurrentUser(),
                                     level=AccessType.READ)
        ownerId = self.getOwnerId(label_folder)
        config_folder = self.folder_m.load(label_folder['meta'][ownerId], level=AccessType.READ,
                                           user=self.getCurrentUser())
        return config_folder

    def findConfig(self, folder_id):
        folder = self.getConfigFolder(folder_id)
        printOk2("Config folder {}".format(folder))
        files = self.folder_m.fileList(folder, self.getCurrentUser(), data=False)
        for file_path, file in files:
            printOk(file)
            if file['name'] == "config.json":
                return file

    def __findFile(self, folder, file_name):
        item = list(self.item_m.find({'folderId': folder['_id'], 'name': file_name}).limit(1))
        if not item:
            return None

        item = item[0]
        file = list(self.file_m.find({'itemId': item['_id']}).limit(1))

        if not file:
            return None

        return file[0]

    @access.public
    @autoDescribeRoute(
        Description('Create a new label file if it doesnt exist')
            .param('file_name', 'label file name').param('folder_id', 'the parent folder id'))
    @rest.rawResponse
    def createLabelFile(self, file_name, folder_id):
        try:
            folder = self.folder_m.load(folder_id, user=self.getCurrentUser(), level=AccessType.WRITE)
            file = self.__findFile(folder, file_name)
            if not file:
                file = self.createNewFile(folder, file_name)
                config_file = self.findConfig(folder_id)
                if not config_file:
                    printFail("No config file found")
                    return errorMessage("No config file found")
                else:
                    res = self.copy(config_file, file)
                    return dumps({
                        "label_id": res['fileId']
                    })

            return dumps({
                "label_id": file['_id']
            })
        except:
            printFail(traceback.print_exc)
            cherrypy.response.status = 500

    @access.public
    @autoDescribeRoute(
        Description('Get labels by file_name')
            .param('file_name', 'label file name').param('folder_id', 'the parent folder id'))
    @rest.rawResponse
    def getLabelByName(self, file_name, folder_id):
        try:
            folder = self.folder_m.load(folder_id, user=self.getCurrentUser(), level=AccessType.READ)
            file = self.__findFile(folder, file_name)
            cherrypy.response.headers["Content-Type"] = "application/json"
            if file:
                return self.file_m.download(file)
            else:
                return dumps({})
        except:
            printFail(traceback.print_exc)
            cherrypy.response.status = 500

    @access.public
    @autoDescribeRoute(
        Description('Get label by id')
            .param('label_id', 'label file id'))
    @rest.rawResponse
    def getLabel(self, label_id):
        try:
            file = self.file_m.load(label_id, level=AccessType.READ, user=self.getCurrentUser())
            printOk2(file)
            cherrypy.response.headers["Content-Type"] = "application/json"
            return self.file_m.download(file)
        except:
            # Unknown slug
            printFail(traceback.print_exc)
            cherrypy.response.status = 404

    @access.public
    @autoDescribeRoute(
        Description('Get label by id')
            .param('label_id', 'label file id'))
    def getLabelMeta(self, label_id):
        try:
            file = self.file_m.load(label_id, level=AccessType.READ, user=self.getCurrentUser())
            cherrypy.response.headers["Content-Type"] = "application/json"
            return dumps(file)
        except:
            # Unknown slug
            printFail(traceback.print_exc)
            cherrypy.response.status = 404

    @access.public
    @autoDescribeRoute(
        Description('Post label by id')
            .param('label_id', 'label file id'))
    @rest.rawResponse
    def postLabel(self, label_id, params):
        try:
            file = self.file_m.load(label_id, level=AccessType.WRITE, user=self.getCurrentUser())
            cherrypy.response.headers["Content-Type"] = "application/json"
            params['labels'] = json.loads(params['labels'])
            data = json.dumps(params, indent=2, sort_keys=True)
            upload = writeData(self.getCurrentUser(), file, data)
            printOk2(file)
            printOk(upload)
            return dumps(upload)

        except:
            # Unknown slug
            printFail(traceback.print_exc)
            cherrypy.response.status = 404

    @access.public
    @autoDescribeRoute(
        Description('Post label by id')
            .param('label_id', 'label file id'))
    @rest.rawResponse
    def strokeToOutline(self, strokes):
        pass