Skip to content

Commit 59b2e10

Browse files
Merge branch 'master' into reduce-save-requests
2 parents 7a84f0a + d1f1ee2 commit 59b2e10

37 files changed

+566
-207
lines changed

CHANGELOG.unreleased.md

+5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
1111
[Commits](https://github.com/scalableminds/webknossos/compare/22.02.0...HEAD)
1212

1313
### Added
14+
- Added the option to make a segment's ID active via the right-click context menu in the segments list. [#5935](https://github.com/scalableminds/webknossos/pull/6006)
1415
- Added a button next to the histogram which adapts the contrast and brightness to the currently visible data. [#5961](https://github.com/scalableminds/webknossos/pull/5961)
1516
- Running uploads can now be cancelled. [#5958](https://github.com/scalableminds/webknossos/pull/5958)
1617

@@ -21,11 +22,15 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
2122
- Changed that webKnossos no longer tries to reach a save state where all updates are sent to the backend to be in sync with the frontend when the save is triggered by a timeout. [#5999](https://github.com/scalableminds/webknossos/pull/5999)
2223
- When changing which layers are visible in an annotation, this setting is persisted in the annotation, so when you share it, viewers will see the same visibility configuration. [#5967](https://github.com/scalableminds/webknossos/pull/5967)
2324
- Downloading public annotations is now also allowed without being authenticated. [#6001](https://github.com/scalableminds/webknossos/pull/6001)
25+
- Downloaded volume annotation layers no longer produce zero-byte zipfiles but rather a valid header-only zip file with no contents. [#6022](https://github.com/scalableminds/webknossos/pull/6022)
26+
- Changed a number of API routes from GET to POST to avoid unwanted side effects. [#6023](https://github.com/scalableminds/webknossos/pull/6023)
27+
- Removed unused datastore route `checkInbox` (use `checkInboxBlocking` instead). [#6023](https://github.com/scalableminds/webknossos/pull/6023)
2428

2529
### Fixed
2630
- Fixed volume-related bugs which could corrupt the volume data in certain scenarios. [#5955](https://github.com/scalableminds/webknossos/pull/5955)
2731
- Fixed the placeholder resolution computation for anisotropic layers with missing base resolutions. [#5983](https://github.com/scalableminds/webknossos/pull/5983)
2832
- Fixed a bug where ad-hoc meshes were computed for a mapping, although it was disabled. [#5982](https://github.com/scalableminds/webknossos/pull/5982)
33+
- Fixed a bug where volume annotation downloads would sometimes contain truncated zips. [#6009](https://github.com/scalableminds/webknossos/pull/6009)
2934

3035

3136
### Removed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@ yarn run lint
179179
# Format frontend code
180180
yarn run pretty
181181

182+
# Format backend code
183+
yarn pretty-backend
184+
182185
# Frontend type checking
183186
yarn flow
184187

app/controllers/AnnotationController.scala

+18-11
Original file line numberDiff line numberDiff line change
@@ -355,17 +355,24 @@ class AnnotationController @Inject()(
355355
} yield JsonOk(Messages("annotation.edit.success"))
356356
}
357357

358-
@ApiOperation(hidden = true, value = "")
359-
def annotationsForTask(taskId: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
360-
for {
361-
taskIdValidated <- ObjectId.parse(taskId)
362-
task <- taskDAO.findOne(taskIdValidated) ?~> "task.notFound" ~> NOT_FOUND
363-
project <- projectDAO.findOne(task._project)
364-
_ <- Fox.assertTrue(userService.isTeamManagerOrAdminOf(request.identity, project._team))
365-
annotations <- annotationService.annotationsFor(task._id) ?~> "task.annotation.failed"
366-
jsons <- Fox.serialSequence(annotations)(a => annotationService.publicWrites(a, Some(request.identity)))
367-
} yield Ok(JsArray(jsons.flatten))
368-
}
358+
@ApiOperation(value = "Information about all annotations for a specific task", nickname = "annotationInfosByTaskId")
359+
@ApiResponses(
360+
Array(
361+
new ApiResponse(code = 200,
362+
message = "JSON list of objects containing information about the selected annotations."),
363+
new ApiResponse(code = 400, message = badRequestLabel)
364+
))
365+
def annotationsForTask(@ApiParam(value = "The id of the task") taskId: String): Action[AnyContent] =
366+
sil.SecuredAction.async { implicit request =>
367+
for {
368+
taskIdValidated <- ObjectId.parse(taskId)
369+
task <- taskDAO.findOne(taskIdValidated) ?~> "task.notFound" ~> NOT_FOUND
370+
project <- projectDAO.findOne(task._project)
371+
_ <- Fox.assertTrue(userService.isTeamManagerOrAdminOf(request.identity, project._team))
372+
annotations <- annotationService.annotationsFor(task._id) ?~> "task.annotation.failed"
373+
jsons <- Fox.serialSequence(annotations)(a => annotationService.publicWrites(a, Some(request.identity)))
374+
} yield Ok(JsArray(jsons.flatten))
375+
}
369376

370377
@ApiOperation(hidden = true, value = "")
371378
def cancel(typ: String, id: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>

app/controllers/AnnotationIOController.scala

+71-51
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
package controllers
22

3-
import java.io.File
3+
import java.io.{BufferedOutputStream, File, FileOutputStream}
44

55
import akka.actor.ActorSystem
66
import akka.stream.Materializer
7-
import akka.stream.scaladsl._
87
import com.mohiva.play.silhouette.api.Silhouette
98
import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContext}
109
import com.scalableminds.util.io.{NamedEnumeratorStream, ZipIO}
@@ -30,9 +29,7 @@ import models.task._
3029
import models.user._
3130
import oxalis.security.WkEnv
3231
import play.api.i18n.{Messages, MessagesProvider}
33-
import play.api.libs.Files.TemporaryFile
34-
import play.api.libs.iteratee.Enumerator
35-
import play.api.libs.iteratee.streams.IterateeStreams
32+
import play.api.libs.Files.{TemporaryFile, TemporaryFileCreator}
3633
import play.api.libs.json.Json
3734
import play.api.mvc.{Action, AnyContent, MultipartFormData}
3835
import utils.ObjectId
@@ -51,6 +48,7 @@ class AnnotationIOController @Inject()(
5148
taskDAO: TaskDAO,
5249
taskTypeDAO: TaskTypeDAO,
5350
tracingStoreService: TracingStoreService,
51+
temporaryFileCreator: TemporaryFileCreator,
5452
annotationService: AnnotationService,
5553
analyticsService: AnalyticsService,
5654
sil: Silhouette[WkEnv],
@@ -223,8 +221,11 @@ Expects:
223221
@ApiOperation(value = "Download an annotation as NML/ZIP", nickname = "annotationDownload")
224222
@ApiResponses(
225223
Array(
226-
new ApiResponse(code = 200,
227-
message = "NML or Zip file containing skeleton and/or volume data of this annotation."),
224+
new ApiResponse(
225+
code = 200,
226+
message =
227+
"NML or Zip file containing skeleton and/or volume data of this annotation. In case of Compound annotations, multiple such annotations wrapped in another zip"
228+
),
228229
new ApiResponse(code = 400, message = badRequestLabel)
229230
))
230231
def download(
@@ -268,30 +269,34 @@ Expects:
268269
volumeVersion: Option[Long],
269270
skipVolumeData: Boolean)(implicit ctx: DBAccessContext) = {
270271

271-
def skeletonToDownloadStream(dataSet: DataSet, annotation: Annotation, name: String, organizationName: String) =
272+
def skeletonToTemporaryFile(dataSet: DataSet,
273+
annotation: Annotation,
274+
organizationName: String): Fox[TemporaryFile] =
272275
for {
273276
tracingStoreClient <- tracingStoreService.clientFor(dataSet)
274277
fetchedAnnotationLayers <- Fox.serialCombined(annotation.skeletonAnnotationLayers)(
275278
tracingStoreClient.getSkeletonTracing(_, skeletonVersion))
276279
user <- userService.findOneById(annotation._user, useCache = true)
277280
taskOpt <- Fox.runOptional(annotation._task)(taskDAO.findOne)
278-
} yield {
279-
(nmlWriter.toNmlStream(fetchedAnnotationLayers,
280-
Some(annotation),
281-
dataSet.scale,
282-
None,
283-
organizationName,
284-
Some(user),
285-
taskOpt),
286-
name + ".nml")
287-
}
281+
nmlStream = nmlWriter.toNmlStream(fetchedAnnotationLayers,
282+
Some(annotation),
283+
dataSet.scale,
284+
None,
285+
organizationName,
286+
Some(user),
287+
taskOpt)
288+
nmlTemporaryFile = temporaryFileCreator.create()
289+
temporaryFileStream = new BufferedOutputStream(new FileOutputStream(nmlTemporaryFile))
290+
_ <- NamedEnumeratorStream("", nmlStream).writeTo(temporaryFileStream)
291+
_ = temporaryFileStream.close()
292+
} yield nmlTemporaryFile
288293

289-
def volumeOrHybridToDownloadStream(dataSet: DataSet,
290-
annotation: Annotation,
291-
name: String,
292-
organizationName: String): Fox[(Enumerator[Array[Byte]], String)] =
294+
def volumeOrHybridToTemporaryFile(dataset: DataSet,
295+
annotation: Annotation,
296+
name: String,
297+
organizationName: String): Fox[TemporaryFile] =
293298
for {
294-
tracingStoreClient <- tracingStoreService.clientFor(dataSet)
299+
tracingStoreClient <- tracingStoreService.clientFor(dataset)
295300
fetchedVolumeLayers: List[FetchedAnnotationLayer] <- Fox.serialCombined(annotation.volumeAnnotationLayers) {
296301
volumeAnnotationLayer =>
297302
tracingStoreClient.getVolumeTracing(volumeAnnotationLayer, volumeVersion, skipVolumeData)
@@ -302,46 +307,61 @@ Expects:
302307
}
303308
user <- userService.findOneById(annotation._user, useCache = true)
304309
taskOpt <- Fox.runOptional(annotation._task)(taskDAO.findOne)
305-
} yield {
306-
val nmlStream = NamedEnumeratorStream(
307-
name + ".nml",
308-
nmlWriter.toNmlStream(fetchedSkeletonLayers ::: fetchedVolumeLayers,
309-
Some(annotation),
310-
dataSet.scale,
311-
None,
312-
organizationName,
313-
Some(user),
314-
taskOpt)
315-
)
316-
val dataStreams: List[NamedEnumeratorStream] =
317-
fetchedVolumeLayers.zipWithIndex.flatMap {
318-
case (volumeLayer, index) => volumeLayer.namedVolumeDataEnumerator(index, fetchedVolumeLayers.length == 1)
319-
}
320-
(Enumerator.outputStream { outputStream =>
321-
ZipIO.zip(
322-
nmlStream :: dataStreams,
323-
outputStream
324-
)
325-
}, name + ".zip")
326-
}
310+
nmlStream = nmlWriter.toNmlStream(fetchedSkeletonLayers ::: fetchedVolumeLayers,
311+
Some(annotation),
312+
dataset.scale,
313+
None,
314+
organizationName,
315+
Some(user),
316+
taskOpt)
317+
temporaryFile = temporaryFileCreator.create()
318+
zipper = ZipIO.startZip(new BufferedOutputStream(new FileOutputStream(new File(temporaryFile.path.toString))))
319+
_ <- zipper.addFileFromEnumerator(name + ".nml", nmlStream)
320+
_ = fetchedVolumeLayers.zipWithIndex.map {
321+
case (volumeLayer, index) =>
322+
volumeLayer.volumeDataOpt.foreach { volumeData =>
323+
val dataZipName = volumeLayer.volumeDataZipName(index, fetchedSkeletonLayers.length == 1)
324+
zipper.addFileFromBytes(dataZipName, volumeData)
325+
}
326+
}
327+
_ = zipper.close()
328+
} yield temporaryFile
329+
330+
def annotationToTemporaryFile(dataSet: DataSet,
331+
annotation: Annotation,
332+
name: String,
333+
organizationName: String): Fox[TemporaryFile] =
334+
if (annotation.tracingType == TracingType.skeleton)
335+
skeletonToTemporaryFile(dataSet, annotation, organizationName)
336+
else
337+
volumeOrHybridToTemporaryFile(dataSet, annotation, name, organizationName)
338+
339+
def exportExtensionForAnnotation(annotation: Annotation): String =
340+
if (annotation.tracingType == TracingType.skeleton)
341+
".nml"
342+
else
343+
".zip"
327344

328-
def tracingToDownloadStream(dataSet: DataSet, annotation: Annotation, name: String, organizationName: String) =
345+
def exportMimeTypeForAnnotation(annotation: Annotation): String =
329346
if (annotation.tracingType == TracingType.skeleton)
330-
skeletonToDownloadStream(dataSet, annotation, name, organizationName)
347+
"application/xml"
331348
else
332-
volumeOrHybridToDownloadStream(dataSet, annotation, name, organizationName)
349+
"application/zip"
333350

334351
for {
335352
annotation <- provider.provideAnnotation(typ, annotationId, issuingUser) ~> NOT_FOUND
336353
restrictions <- provider.restrictionsFor(typ, annotationId)
337354
name <- provider.nameFor(annotation) ?~> "annotation.name.impossible"
355+
fileExtension = exportExtensionForAnnotation(annotation)
356+
fileName = name + fileExtension
357+
mimeType = exportMimeTypeForAnnotation(annotation)
338358
_ <- restrictions.allowDownload(issuingUser) ?~> "annotation.download.notAllowed" ~> FORBIDDEN
339359
dataSet <- dataSetDAO.findOne(annotation._dataSet)(GlobalAccessContext) ?~> "dataSet.notFoundForAnnotation" ~> NOT_FOUND
340360
organization <- organizationDAO.findOne(dataSet._organization)(GlobalAccessContext) ?~> "organization.notFound" ~> NOT_FOUND
341-
(downloadStream, fileName) <- tracingToDownloadStream(dataSet, annotation, name, organization.name)
361+
temporaryFile <- annotationToTemporaryFile(dataSet, annotation, name, organization.name)
342362
} yield {
343-
Ok.chunked(Source.fromPublisher(IterateeStreams.enumeratorToPublisher(downloadStream)))
344-
.as(if (fileName.toLowerCase.endsWith(".zip")) "application/zip" else "application/xml")
363+
Ok.sendFile(temporaryFile, inline = false)
364+
.as(mimeType)
345365
.withHeaders(CONTENT_DISPOSITION ->
346366
s"attachment;filename=${'"'}$fileName${'"'}")
347367
}

0 commit comments

Comments
 (0)