Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7bca872
Merge RemoteSourceDescriptorService into DataVaultService
fm3 Oct 7, 2025
188d316
don’t pass redundant baseDir
fm3 Oct 7, 2025
7bf68a3
Merge branch 'master' into refactor-datavault-service
fm3 Oct 13, 2025
f0c484e
Merge branch 'master' into refactor-datavault-service
fm3 Oct 14, 2025
f28cb7a
Merge branch 'master' into refactor-datavault-service
fm3 Oct 27, 2025
dc2fc33
Make allowScalar always explicit. Remove mag paths relative to layer
fm3 Oct 27, 2025
401443d
don’t abort when one realpath didn’t work
fm3 Oct 27, 2025
69c1b80
separate scan from report
fm3 Oct 27, 2025
1f31825
update realpaths by original path; collect failures
fm3 Oct 27, 2025
fab76e3
adapt mag paths when getting datasources from dirs
fm3 Oct 27, 2025
3f4f462
WIP attachments
fm3 Oct 27, 2025
a72b002
Merge branch 'master' into refactor-datavault-service
fm3 Oct 28, 2025
5a13552
Merge branch 'refactor-datavault-service' into realpath-scan
fm3 Oct 28, 2025
1fd5143
evolutions
fm3 Oct 28, 2025
fff7515
changelog
fm3 Oct 28, 2025
35f0ca3
Merge branch 'master' into refactor-datavault-service
fm3 Oct 29, 2025
a361420
Merge branch 'refactor-datavault-service' into realpath-scan
fm3 Oct 29, 2025
da0acb6
Merge branch 'master' into realpath-scan
fm3 Oct 29, 2025
362e3fe
Merge branch 'master' into realpath-scan
fm3 Oct 29, 2025
bbb4f3e
wip also scan attachments
fm3 Oct 29, 2025
76601d6
restructure scan
fm3 Oct 29, 2025
5c74222
log realpath scan failures
fm3 Oct 30, 2025
961de3f
do not resolve mag paths when processing uploaded dataset with dataso…
fm3 Oct 30, 2025
27c12bc
fix typo, add attachment path absolute assertion
fm3 Oct 30, 2025
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
2 changes: 1 addition & 1 deletion app/controllers/WKRemoteDataStoreController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ class WKRemoteDataStoreController @Inject()(
}
}

def updatePaths(name: String, key: String): Action[List[DataSourcePathInfo]] =
def updateRealPaths(name: String, key: String): Action[List[DataSourcePathInfo]] =
Action.async(validateJson[List[DataSourcePathInfo]]) { implicit request =>
dataStoreService.validateAccess(name, key) { _ =>
for {
Expand Down
36 changes: 27 additions & 9 deletions app/models/dataset/Dataset.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import com.scalableminds.webknossos.datastore.models.datasource.{
ThinPlateSplineCorrespondences,
DataLayerAttachments => AttachmentWrapper
}
import com.scalableminds.webknossos.datastore.services.MagPathInfo
import com.scalableminds.webknossos.datastore.services.RealPathInfo
import com.scalableminds.webknossos.schema.Tables._
import controllers.DatasetUpdateParameters

Expand Down Expand Up @@ -824,16 +824,15 @@ class DatasetMagsDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionConte
replaceSequentiallyAsTransaction(clearQuery, insertQueries)
}

def updateMagPathsForDataset(datasetId: ObjectId, magPathInfos: List[MagPathInfo]): Fox[Unit] =
// Note: also see attachments
def updateMagRealPathsForDataset(datasetId: ObjectId, realPathInfos: Seq[RealPathInfo]): Fox[Unit] =
for {
_ <- Fox.successful(())
updateQueries = magPathInfos.map(magPathInfo => {
val magLiteral = s"(${magPathInfo.mag.x}, ${magPathInfo.mag.y}, ${magPathInfo.mag.z})"
updateQueries = realPathInfos.map(realPathInfo => {
q"""UPDATE webknossos.dataset_mags
SET path = ${magPathInfo.path}, realPath = ${magPathInfo.realPath}, hasLocalData = ${magPathInfo.hasLocalData}
WHERE _dataset = $datasetId
AND dataLayerName = ${magPathInfo.layerName}
AND mag = CAST($magLiteral AS webknossos.vector3)""".asUpdate
SET realPath = ${realPathInfo.realPath}, hasLocalData = ${realPathInfo.hasLocalData}
WHERE _dataset = $datasetId
AND path = ${realPathInfo.path}""".asUpdate
})
composedQuery = DBIO.sequence(updateQueries)
_ <- run(
Expand Down Expand Up @@ -1130,7 +1129,8 @@ class DatasetLayerAttachmentsDAO @Inject()(sqlClient: SqlClient)(implicit ec: Ex

def findAllForDatasetAndDataLayerName(datasetId: ObjectId, layerName: String): Fox[AttachmentWrapper] =
for {
rows <- run(q"""SELECT _dataset, layerName, name, path, type, dataFormat, uploadToPathIsPending
rows <- run(
q"""SELECT _dataset, layerName, name, path, realpath, hasLocalData, type, dataFormat, uploadToPathIsPending
FROM webknossos.dataset_layer_attachments
WHERE _dataset = $datasetId
AND layerName = $layerName
Expand Down Expand Up @@ -1170,6 +1170,24 @@ class DatasetLayerAttachmentsDAO @Inject()(sqlClient: SqlClient)(implicit ec: Ex
replaceSequentiallyAsTransaction(clearQuery, insertQueries)
}

// Note: also see mags.
def updateAttachmentRealPathsForDataset(datasetId: ObjectId, realPathInfos: Seq[RealPathInfo]): Fox[Unit] =
for {
_ <- Fox.successful(())
updateQueries = realPathInfos.map(realPathInfo => {
q"""UPDATE webknossos.dataset_layer_attachments
SET realPath = ${realPathInfo.realPath}, hasLocalData = ${realPathInfo.hasLocalData}
WHERE _dataset = $datasetId
AND path = ${realPathInfo.path}""".asUpdate
})
composedQuery = DBIO.sequence(updateQueries)
_ <- run(
composedQuery.transactionally.withTransactionIsolation(Serializable),
retryCount = 50,
retryIfErrorContains = List(transactionSerializationError)
)
} yield ()

def insertPending(datasetId: ObjectId,
layerName: String,
attachmentName: String,
Expand Down
33 changes: 17 additions & 16 deletions app/models/dataset/DatasetService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class DatasetService @Inject()(organizationDAO: OrganizationDAO,
datasetLastUsedTimesDAO: DatasetLastUsedTimesDAO,
datasetDataLayerDAO: DatasetLayerDAO,
datasetMagsDAO: DatasetMagsDAO,
datasetLayerAttachmentsDAO: DatasetLayerAttachmentsDAO,
teamDAO: TeamDAO,
folderDAO: FolderDAO,
multiUserDAO: MultiUserDAO,
Expand Down Expand Up @@ -470,26 +471,26 @@ class DatasetService @Inject()(organizationDAO: OrganizationDAO,
_ <- datasetDAO.updateUploader(dataset._id, Some(_uploader)) ?~> "dataset.uploader.forbidden"
} yield ()

private def updateRealPath(pathInfo: DataSourcePathInfo)(implicit ctx: DBAccessContext): Fox[Unit] =
if (pathInfo.magPathInfos.isEmpty) {
Fox.successful(())
} else {
val dataset = datasetDAO.findOneByDataSourceId(pathInfo.dataSourceId).shiftBox
dataset.flatMap {
case Full(dataset) if !dataset.isVirtual =>
datasetMagsDAO.updateMagPathsForDataset(dataset._id, pathInfo.magPathInfos)
case Full(_) => // Dataset is virtual, no updates from datastore are accepted.
Fox.successful(())
case Empty => // Dataset reported but ignored (non-existing/forbidden org)
Fox.successful(())
case e: EmptyBox =>
Fox.failure("dataset.notFound", e)
}
private def updateRealPathsForDataSource(pathInfo: DataSourcePathInfo)(implicit ctx: DBAccessContext): Fox[Unit] = {
val datasetBox = datasetDAO.findOneByDataSourceId(pathInfo.dataSourceId).shiftBox
datasetBox.flatMap {
case Full(dataset) if !dataset.isVirtual =>
for {
_ <- datasetMagsDAO.updateMagRealPathsForDataset(dataset._id, pathInfo.magPathInfos)
_ <- datasetLayerAttachmentsDAO.updateAttachmentRealPathsForDataset(dataset._id, pathInfo.attachmentPathInfos)
} yield ()
case Full(_) => // Dataset is virtual, no updates from datastore are accepted.
Fox.successful(())
case Empty => // Dataset reported but ignored (non-existing/forbidden org)
Fox.successful(())
case e: EmptyBox =>
Fox.failure("dataset.notFound", e)
}
}

def updateRealPaths(pathInfos: List[DataSourcePathInfo])(implicit ctx: DBAccessContext): Fox[Unit] =
for {
_ <- Fox.serialCombined(pathInfos)(updateRealPath)
_ <- Fox.serialCombined(pathInfos)(updateRealPathsForDataSource)
} yield ()

/**
Expand Down
2 changes: 1 addition & 1 deletion app/models/dataset/DatasetUploadToPathsService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ class DatasetUploadToPathsService @Inject()(datasetService: DatasetService,
} yield layerUpdated

private def addPathToMag(mag: MagLocator, layerPath: UPath): MagLocator =
mag.copy(path = Some(layerPath / mag.mag.toMagLiteral()))
mag.copy(path = Some(layerPath / mag.mag.toMagLiteral(allowScalar = true)))

private def addPathsToAttachments(attachmentsOpt: Option[DataLayerAttachments], layerPath: UPath)(
implicit ec: ExecutionContext): Fox[Option[DataLayerAttachments]] =
Expand Down
2 changes: 1 addition & 1 deletion app/models/dataset/WKRemoteDataStoreClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class WKRemoteDataStoreClient(dataStore: DataStore, rpc: RPC) extends LazyLoggin
s"Thumbnail called for: ${dataset._id}, organization: ${dataset._organization}, directoryName: ${dataset.directoryName}, Layer: $dataLayerName")
rpc(s"${dataStore.url}/data/datasets/${dataset._id}/layers/$dataLayerName/thumbnail.jpg")
.addQueryParam("token", RpcTokenHolder.webknossosToken)
.addQueryParam("mag", mag.toMagLiteral())
.addQueryParam("mag", mag.toMagLiteral(allowScalar = false))
.addQueryParam("x", mag1BoundingBox.topLeft.x)
.addQueryParam("y", mag1BoundingBox.topLeft.y)
.addQueryParam("z", mag1BoundingBox.topLeft.z)
Expand Down
10 changes: 10 additions & 0 deletions conf/evolutions/145-attachment-realpaths.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
START TRANSACTION;

do $$ begin if (select schemaVersion from webknossos.releaseInformation) <> 144 then raise exception 'Previous schema version mismatch'; end if; end; $$ language plpgsql;

ALTER TABLE webknossos.dataset_layer_attachments ADD COLUMN realPath TEXT;
ALTER TABLE webknossos.dataset_layer_attachments ADD COLUMN hasLocalData BOOLEAN NOT NULL DEFAULT false;

UPDATE webknossos.releaseInformation SET schemaVersion = 145;

COMMIT TRANSACTION;
10 changes: 10 additions & 0 deletions conf/evolutions/reversions/145-attachment-realpaths.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
START TRANSACTION;

do $$ begin if (select schemaVersion from webknossos.releaseInformation) <> 145 then raise exception 'Previous schema version mismatch'; end if; end; $$ language plpgsql;

ALTER TABLE webknossos.dataset_layer_attachments DROP COLUMN realPath;
ALTER TABLE webknossos.dataset_layer_attachments DROP COLUMN hasLocalData;

UPDATE webknossos.releaseInformation SET schemaVersion = 144;

COMMIT TRANSACTION;
2 changes: 1 addition & 1 deletion conf/webknossos.latest.routes
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ DELETE /folders/:id
GET /datastores controllers.DataStoreController.list()
PUT /datastores/:name/datasource controllers.WKRemoteDataStoreController.updateOne(name: String, key: String)
PUT /datastores/:name/datasources controllers.WKRemoteDataStoreController.updateAll(name: String, key: String, organizationId: Option[String])
PUT /datastores/:name/datasources/paths controllers.WKRemoteDataStoreController.updatePaths(name: String, key: String)
PUT /datastores/:name/datasources/paths controllers.WKRemoteDataStoreController.updateRealPaths(name: String, key: String)
GET /datastores/:name/datasources/:datasetId/paths controllers.WKRemoteDataStoreController.getPaths(name: String, key: String, datasetId: ObjectId)
GET /datastores/:name/datasources/:datasetId controllers.WKRemoteDataStoreController.getDataSource(name: String, key: String, datasetId: ObjectId)
PUT /datastores/:name/datasources/:datasetId controllers.WKRemoteDataStoreController.updateDataSource(name: String, key: String, datasetId: ObjectId)
Expand Down
20 changes: 11 additions & 9 deletions tools/postgres/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ CREATE TABLE webknossos.releaseInformation (
schemaVersion BIGINT NOT NULL
);

INSERT INTO webknossos.releaseInformation(schemaVersion) values(144);
INSERT INTO webknossos.releaseInformation(schemaVersion) values(145);
COMMIT TRANSACTION;


Expand Down Expand Up @@ -166,14 +166,16 @@ CREATE TABLE webknossos.dataset_layer_additionalAxes(
CREATE TYPE webknossos.LAYER_ATTACHMENT_TYPE AS ENUM ('agglomerate', 'connectome', 'segmentIndex', 'mesh', 'cumsum');
CREATE TYPE webknossos.LAYER_ATTACHMENT_DATAFORMAT AS ENUM ('hdf5', 'zarr3', 'json', 'neuroglancerPrecomputed');
CREATE TABLE webknossos.dataset_layer_attachments(
_dataset TEXT CONSTRAINT _dataset_objectId CHECK (_dataset ~ '^[0-9a-f]{24}$') NOT NULL,
layerName TEXT NOT NULL,
name TEXT NOT NULL,
path TEXT NOT NULL,
type webknossos.LAYER_ATTACHMENT_TYPE NOT NULL,
dataFormat webknossos.LAYER_ATTACHMENT_DATAFORMAT NOT NULL,
uploadToPathIsPending BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY(_dataset, layerName, name, type)
_dataset TEXT CONSTRAINT _dataset_objectId CHECK (_dataset ~ '^[0-9a-f]{24}$') NOT NULL,
layerName TEXT NOT NULL,
name TEXT NOT NULL,
path TEXT NOT NULL,
realPath TEXT,
hasLocalData BOOLEAN NOT NULL DEFAULT FALSE,
type webknossos.LAYER_ATTACHMENT_TYPE NOT NULL,
dataFormat webknossos.LAYER_ATTACHMENT_DATAFORMAT NOT NULL,
uploadToPathIsPending BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY(_dataset, layerName, name, type)
);

CREATE TABLE webknossos.dataset_allowedTeams(
Expand Down
8 changes: 8 additions & 0 deletions unreleased_changes/9019.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
### Added
- Added registering attachment realpaths next to mag realpsths.

### Fixed
- Fixed that scanning realpaths of dataset mags would stop if a single mag didn’t exist.

### Postgres Evolutions
- [145-attachment-realpaths.sql](conf/evolutions/145-attachment-realpaths.sql)
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ case class Vec3Int(x: Int, y: Int, z: Int) {

override def toString: String = s"($x, $y, $z)"

def toMagLiteral(allowScalar: Boolean = false): String =
def toMagLiteral(allowScalar: Boolean): String =
if (allowScalar && isIsotropic) s"$x" else s"$x-$y-$z"

def toUriLiteral: String = s"$x,$y,$z"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import com.google.inject.Inject
import com.google.inject.name.Named
import com.scalableminds.util.accesscontext.TokenContext
import com.scalableminds.util.cache.AlfuCache
import com.scalableminds.util.geometry.Vec3Int
import com.scalableminds.util.objectid.ObjectId
import com.scalableminds.util.tools.{Fox, FoxImplicits}
import com.scalableminds.webknossos.datastore.DataStoreConfig
Expand Down Expand Up @@ -38,16 +37,20 @@ object TracingStoreInfo {
implicit val jsonFormat: OFormat[TracingStoreInfo] = Json.format[TracingStoreInfo]
}

case class DataSourcePathInfo(dataSourceId: DataSourceId, magPathInfos: List[MagPathInfo])
case class DataSourcePathInfo(dataSourceId: DataSourceId,
magPathInfos: Seq[RealPathInfo],
attachmentPathInfos: Seq[RealPathInfo]) {
def nonEmpty: Boolean = magPathInfos.nonEmpty || attachmentPathInfos.nonEmpty
}

object DataSourcePathInfo {
implicit val jsonFormat: OFormat[DataSourcePathInfo] = Json.format[DataSourcePathInfo]
}

case class MagPathInfo(layerName: String, mag: Vec3Int, path: UPath, realPath: UPath, hasLocalData: Boolean)
case class RealPathInfo(path: UPath, realPath: UPath, hasLocalData: Boolean)

object MagPathInfo {
implicit val jsonFormat: OFormat[MagPathInfo] = Json.format[MagPathInfo]
object RealPathInfo {
implicit val jsonFormat: OFormat[RealPathInfo] = Json.format[RealPathInfo]
}

trait RemoteWebknossosClient {
Expand Down Expand Up @@ -108,7 +111,7 @@ class DSRemoteWebknossosClient @Inject()(
.silent
.putJson(dataSources)

def reportRealPaths(dataSourcePaths: List[DataSourcePathInfo]): Fox[_] =
def reportRealPaths(dataSourcePaths: Seq[DataSourcePathInfo]): Fox[_] =
rpc(s"$webknossosUri/api/datastores/$dataStoreName/datasources/paths")
.addQueryParam("key", dataStoreKey)
.silent
Expand Down
Loading
Loading