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

Create new large images for each item in a folder #1572

Merged
merged 13 commits into from
Jul 16, 2024
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
70 changes: 68 additions & 2 deletions girder/girder_large_image/rest/large_image_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@
from girder.api import access
from girder.api.describe import Description, autoDescribeRoute, describeRoute
from girder.api.rest import Resource
from girder.constants import SortDir, TokenScope
from girder.constants import AccessType, SortDir, TokenScope
from girder.exceptions import RestException
from girder.models.file import File
from girder.models.folder import Folder
from girder.models.item import Item
from girder.models.setting import Setting
from large_image import cache_util
from large_image.exceptions import TileGeneralError
from large_image.exceptions import TileGeneralError, TileSourceError

from .. import constants, girder_tilesource
from ..models.image_item import ImageItem
Expand Down Expand Up @@ -255,6 +256,7 @@
self.route('GET', ('histograms',), self.countHistograms)
self.route('DELETE', ('histograms',), self.deleteHistograms)
self.route('DELETE', ('tiles', 'incomplete'), self.deleteIncompleteTiles)
self.route('PUT', ('folder', ':id', 'tiles'), self.createLargeImages)

@describeRoute(
Description('Clear tile source caches to release resources and file handles.'),
Expand Down Expand Up @@ -445,6 +447,70 @@
removed += 1
return removed

@access.user(scope=TokenScope.DATA_WRITE)
@autoDescribeRoute(
Description('Create new large images for all items within a folder.')
.notes('Does not work for items with multiple files and skips over items with '
'existing or unfinished large images.')
.modelParam('id', 'The ID of the folder.', model=Folder, level=AccessType.WRITE,
required=True)
.param('force', 'Whether creation job(s) should be forced for each large image.',
required=False, default=False, dataType='boolean')
.param('localJobs', 'Whether the job(s) created should be local.', required=False,
default=False, dataType='boolean')
.param('recurse', 'Whether child folders should be recursed.', required=False,
default=False, dataType='boolean')
.errorResponse('ID was invalid.')
.errorResponse('Write access was denied for the folder.', 403),
)
def createLargeImages(self, folder, params):
user = self.getCurrentUser()
createJobs = 'always' if self.boolParam('force', params, default=False) else True
return self.createImagesRecurseOption(folder=folder, createJobs=createJobs, user=user,
recurse=params.get('recurse'),
localJobs=params.get('localJobs'))

def createImagesRecurseOption(self, folder, createJobs, user, recurse, localJobs):
result = {'childFoldersRecursed': 0,
'itemsSkipped': 0,
'largeImagesCreated': 0,
'largeImagesRemovedAndRecreated': 0,
'totalItems': 0}
if recurse:
for childFolder in Folder().childFolders(parent=folder, parentType='folder'):
result['childFoldersRecursed'] += 1
childResult = self.createImagesRecurseOption(folder=childFolder,

Check warning on line 482 in girder/girder_large_image/rest/large_image_resource.py

View check run for this annotation

Codecov / codecov/patch

girder/girder_large_image/rest/large_image_resource.py#L481-L482

Added lines #L481 - L482 were not covered by tests
createJobs=createJobs, user=user,
recurse=recurse, localJobs=localJobs)
for key in childResult:
result[key] += childResult[key]

Check warning on line 486 in girder/girder_large_image/rest/large_image_resource.py

View check run for this annotation

Codecov / codecov/patch

girder/girder_large_image/rest/large_image_resource.py#L486

Added line #L486 was not covered by tests
for item in Folder().childItems(folder=folder):
annehaley marked this conversation as resolved.
Show resolved Hide resolved
result['totalItems'] += 1
if item.get('largeImage'):
if item['largeImage'].get('expected'):
result['itemsSkipped'] += 1

Check warning on line 491 in girder/girder_large_image/rest/large_image_resource.py

View check run for this annotation

Codecov / codecov/patch

girder/girder_large_image/rest/large_image_resource.py#L491

Added line #L491 was not covered by tests
else:
try:
ImageItem().getMetadata(item)
result['itemsSkipped'] += 1
continue
except (TileSourceError, KeyError):
previousFileId = item['largeImage'].get('originalId',
item['largeImage']['fileId'])
ImageItem().delete(item)
ImageItem().createImageItem(item, File().load(user=user, id=previousFileId),
createJob=createJobs, localJob=localJobs)
result['largeImagesRemovedAndRecreated'] += 1
else:
files = list(Item().childFiles(item=item, limit=2))
if len(files) == 1:
ImageItem().createImageItem(item, files[0], createJob=createJobs,
localJob=localJobs)
result['largeImagesCreated'] += 1
else:
result['itemsSkipped'] += 1

Check warning on line 511 in girder/girder_large_image/rest/large_image_resource.py

View check run for this annotation

Codecov / codecov/patch

girder/girder_large_image/rest/large_image_resource.py#L511

Added line #L511 was not covered by tests
return result

@describeRoute(
Description('Remove large images from items where the large image job '
'incomplete.')
Expand Down
31 changes: 31 additions & 0 deletions girder/test_girder/test_large_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,37 @@ def testThumbnailFileJob(server, admin, user, fsAssetstore):
Setting().set(constants.PluginSettings.LARGE_IMAGE_MAX_THUMBNAIL_FILES, 0)


@pytest.mark.usefixtures('unbindLargeImage')
@pytest.mark.plugin('large_image')
def testFolderCreateImages(server, admin, user, fsAssetstore):
file = utilities.uploadExternalFile('sample_image.ptif', admin, fsAssetstore)
itemId = file['itemId']
item = Item().load(itemId, user=admin)
folderId = str(item['folderId'])
# Remove the large image from this item
ImageItem().delete(item)
# Ask to make all items in this folder large images
resp = server.request(
method='PUT', path=f'/large_image/folder/{folderId}/tiles', user=admin)
assert utilities.respStatus(resp) == 200
assert resp.json['largeImagesCreated'] == 1
item = Item().load(itemId, user=admin)
# Check that this item became a large image again
assert 'largeImage' in item
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's extend this to test a few more conditions. Continuing within this function:

    # Hitting the endpoint again should skip the item
    resp = server.request(
        method='PUT', path=f'/large_image/folder/{folderId}/tiles', user=admin)
    assert utilities.respStatus(resp) == 200
    assert resp.json['itemsSkipped'] == 1

    # If the item's source isn't working, it should be recreated.
    item['largeImage']['sourceName'] = 'unknown'
    Item().updateItem(item)
    resp = server.request(
        method='PUT', path=f'/large_image/folder/{folderId}/tiles', user=admin)
    assert utilities.respStatus(resp) == 200
    assert resp.json['largeImagesRemovedAndRecreated'] == 1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function modified here.

# Hitting the endpoint again should skip the item
resp = server.request(
method='PUT', path=f'/large_image/folder/{folderId}/tiles', user=admin)
assert utilities.respStatus(resp) == 200
assert resp.json['itemsSkipped'] == 1
# If the item's source isn't working, it should be recreated.
item['largeImage']['sourceName'] = 'unknown'
Item().updateItem(item)
resp = server.request(
method='PUT', path=f'/large_image/folder/{folderId}/tiles', user=admin)
assert utilities.respStatus(resp) == 200
assert resp.json['largeImagesRemovedAndRecreated'] == 1


@pytest.mark.singular()
@pytest.mark.usefixtures('unbindLargeImage')
@pytest.mark.plugin('large_image')
Expand Down