Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an endpoint to make it easier to replace thumbnails #1253

Merged
merged 1 commit into from
Aug 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Adjust the bounds on non geospatial leaflet maps ([#1249](../../pull/1249))
- Allow hiding metadata on item pages ([#1250](../../pull/1250))
- Allow caching rounded histograms ([#1252](../../pull/1252))
- Add an endpoint to make it easier to replace thumbnails ([#1253](../../pull/1253))

### Bug Fixes
- Guard against ICC profiles that won't parse ([#1245](../../pull/1245))
Expand Down
74 changes: 70 additions & 4 deletions girder/girder_large_image/rest/tiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#############################################################################

import hashlib
import io
import math
import os
import pathlib
Expand All @@ -33,6 +34,7 @@
from girder.exceptions import RestException
from girder.models.file import File
from girder.models.item import Item
from girder.models.upload import Upload
from girder.utility.progress import setResponseTimeLimit
from large_image.cache_util import strhash
from large_image.constants import TileInputUnits, TileOutputMimeTypes
Expand Down Expand Up @@ -150,6 +152,7 @@ def __init__(self, apiRoot):
apiRoot.item.route('GET', (':itemId', 'tiles', 'thumbnail'), self.getTilesThumbnail)
apiRoot.item.route('GET', (':itemId', 'tiles', 'thumbnails'), self.listTilesThumbnails)
apiRoot.item.route('DELETE', (':itemId', 'tiles', 'thumbnails'), self.deleteTilesThumbnails)
apiRoot.item.route('POST', (':itemId', 'tiles', 'thumbnails'), self.addTilesThumbnails)
apiRoot.item.route('GET', (':itemId', 'tiles', 'region'), self.getTilesRegion)
apiRoot.item.route('GET', (':itemId', 'tiles', 'tile_frames'), self.tileFrames)
apiRoot.item.route('GET', (':itemId', 'tiles', 'tile_frames', 'quad_info'),
Expand Down Expand Up @@ -1429,11 +1432,74 @@ def listTilesThumbnails(self, item):
@autoDescribeRoute(
Description('Delete thumbnail and data files associated with a large_image item.')
.modelParam('itemId', model=Item, level=AccessType.READ)
.param('keep', 'Number of thumbnails to keep.', dataType='integer',
required=False, default=10000)
.param('keep', 'Number of thumbnails to keep. Ignored if a key is '
'specified.', dataType='integer', required=False,
default=10000)
.param('key', 'A specific key to delete', required=False)
.param('thumbnail', 'If a key is specified, true if the key is a '
'thumbnail; false if the key is a data record',
dataType='boolean', required=False)
.errorResponse('ID was invalid.')
.errorResponse('Read access was denied for the item.', 403)
)
@access.admin(scope=TokenScope.DATA_WRITE)
def deleteTilesThumbnails(self, item, keep):
return self.imageItemModel.removeThumbnailFiles(item, keep=keep or 0)
def deleteTilesThumbnails(self, item, keep, key=None, thumbnail=True):
if not key:
return self.imageItemModel.removeThumbnailFiles(item, keep=keep or 0)
thumbnail = str(thumbnail).lower() != 'false'
query = {
'attachedToType': 'item',
'attachedToId': item['_id'],
'isLargeImageThumbnail' if thumbnail is not False else 'isLargeImageData': True,
'thumbnailKey': key,
}
file = File().findOne(query)
if file:
File().remove(file)
return [file]

@autoDescribeRoute(
Description('Associate or replace a thumbnail or data file with a large_image items.')
.responseClass('File')
.modelParam('itemId', model=Item, level=AccessType.WRITE)
.param('key', 'A specific key to delete', required=True)
.param('thumbnail', 'If a key is specified, true if the key is a '
'thumbnail; false if the key is a data record',
dataType='boolean', required=False)
.param('mimeType', 'The MIME type of the file.', required=False)
.param('data', 'An image or data block to associated with the large_image item.',
paramType='body', dataType='binary')
.consumes('application/octet-stream')
.errorResponse('ID was invalid.')
.errorResponse('Read access was denied for the item.', 403)
)
@access.user(scope=TokenScope.DATA_WRITE)
def addTilesThumbnails(self, item, key, mimeType, thumbnail=False, data=None):
user = self.getCurrentUser()
thumbnail = str(thumbnail).lower() != 'false'
query = {
'attachedToType': 'item',
'attachedToId': item['_id'],
'isLargeImageThumbnail' if thumbnail is not False else 'isLargeImageData': True,
'thumbnailKey': key,
}
file = File().findOne(query)
if file:
File().remove(file)
data = cherrypy.request.body.read()
try:
import magic
mimeType = magic.from_buffer(data, mime=True) or mimeType
except Exception:
pass
mimeType = mimeType or 'application/octet-stream'
datafile = Upload().uploadFromFile(
io.BytesIO(data), size=len(data),
name='_largeImageThumbnail', parentType='item', parent=item,
user=user, mimeType=mimeType, attachParent=True)
datafile.update({
'isLargeImageThumbnail' if thumbnail is not False else 'isLargeImageData': True,
'thumbnailKey': key,
})
datafile = File().save(datafile)
return datafile
27 changes: 27 additions & 0 deletions girder/test_girder/test_tiles_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1499,3 +1499,30 @@ def testThumbnailMaintenance(server, admin, fsAssetstore):
resp = server.request(path='/item/%s/tiles/thumbnails' % itemId, user=admin)
assert utilities.respStatus(resp) == 200
assert len(resp.json) == 0

# Get a thumbnail
resp = server.request(path='/item/%s/tiles/thumbnail' % itemId,
user=admin, isJson=False)
assert utilities.respStatus(resp) == 200
thumb = utilities.getBody(resp, text=False)
resp = server.request(path='/item/%s/tiles/thumbnails' % itemId, user=admin)
assert utilities.respStatus(resp) == 200
# Ask to delete it specifically
key = resp.json[0]['thumbnailKey']
resp = server.request(
path='/item/%s/tiles/thumbnails' % itemId, method='DELETE', user=admin,
params={'key': key})
assert utilities.respStatus(resp) == 200
assert len(resp.json) == 1
# It should be gone
resp = server.request(path='/item/%s/tiles/thumbnails' % itemId, user=admin)
assert utilities.respStatus(resp) == 200
assert len(resp.json) == 0
# Add it back
resp = server.request(
path='/item/%s/tiles/thumbnails' % itemId, method='POST', user=admin,
params={'key': key}, body=thumb, type='application/octet-stream')

resp = server.request(path='/item/%s/tiles/thumbnails' % itemId, user=admin)
assert utilities.respStatus(resp) == 200
assert len(resp.json) == 1