diff --git a/MIGRATIONS.unreleased.md b/MIGRATIONS.unreleased.md index b85db39550f..f8f8b5f88f3 100644 --- a/MIGRATIONS.unreleased.md +++ b/MIGRATIONS.unreleased.md @@ -9,4 +9,7 @@ User-facing changes are documented in the [changelog](CHANGELOG.released.md). [Commits](https://github.com/scalableminds/webknossos/compare/25.05.0...HEAD) ### Postgres Evolutions: +<<<<<<< HEAD + - [131-more-indices-on-users.sql](conf/evolutions/131-more-indices-on-users.sql) +- [132-remove-stored-meshes.sql](conf/evolutions/132-remove-stored-meshes.sql) diff --git a/app/controllers/MeshController.scala b/app/controllers/MeshController.scala deleted file mode 100644 index e45e7f2078b..00000000000 --- a/app/controllers/MeshController.scala +++ /dev/null @@ -1,83 +0,0 @@ -package controllers - -import play.silhouette.api.Silhouette -import com.scalableminds.util.tools.FoxImplicits -import models.annotation.AnnotationDAO -import models.mesh.{MeshDAO, MeshInfo, MeshInfoParameters, MeshService} -import play.api.mvc.{Action, AnyContent, PlayBodyParsers, RawBuffer} -import security.WkEnv -import com.scalableminds.util.objectid.ObjectId -import javax.inject.Inject - -import scala.concurrent.ExecutionContext - -// Note that this wk-side controller deals with user-uploaded meshes stored in postgres -// Not to be confused with the DSMeshController that deals with on-disk meshfiles - -class MeshController @Inject()(meshDAO: MeshDAO, - annotationDAO: AnnotationDAO, - sil: Silhouette[WkEnv], - meshService: MeshService)(implicit ec: ExecutionContext, bodyParsers: PlayBodyParsers) - extends Controller - with FoxImplicits { - - def get(id: ObjectId): Action[AnyContent] = sil.UserAwareAction.async { implicit request => - for { - meshInfo <- meshDAO.findOne(id) ?~> "mesh.notFound" ~> NOT_FOUND - _ <- annotationDAO.findOne(meshInfo._annotation) ?~> "annotation.notFound" ~> NOT_FOUND - meshInfoJs <- meshService.publicWrites(meshInfo) ?~> "mesh.write.failed" - } yield JsonOk(meshInfoJs) - } - - def create: Action[MeshInfoParameters] = sil.SecuredAction.async(validateJson[MeshInfoParameters]) { - implicit request => - val params = request.body - val _id = ObjectId.generate - for { - _ <- annotationDAO.assertUpdateAccess(params.annotationId) ?~> "notAllowed" ~> FORBIDDEN - _ <- meshDAO - .insertOne(MeshInfo(_id, params.annotationId, params.description, params.position)) ?~> "mesh.create.failed" - inserted <- meshDAO.findOne(_id) ?~> "mesh.notFound" - js <- meshService.publicWrites(inserted) ?~> "mesh.write.failed" - } yield Ok(js) - } - - def update(id: ObjectId): Action[MeshInfoParameters] = sil.SecuredAction.async(validateJson[MeshInfoParameters]) { - implicit request => - val params = request.body - for { - meshInfo <- meshDAO.findOne(id) ?~> "mesh.notFound" ~> NOT_FOUND - _ <- annotationDAO.assertUpdateAccess(meshInfo._annotation) ?~> "notAllowed" ~> FORBIDDEN - _ <- annotationDAO.assertUpdateAccess(params.annotationId) ?~> "notAllowed" ~> FORBIDDEN - _ <- meshDAO.updateOne(id, params.annotationId, params.description, params.position) ?~> "mesh.update.failed" - updated <- meshDAO.findOne(id) ?~> "mesh.notFound" - js <- meshService.publicWrites(updated) ?~> "mesh.write.failed" - } yield Ok(js) - } - - def getData(id: ObjectId): Action[AnyContent] = sil.SecuredAction.async { implicit request => - for { - meshInfo <- meshDAO.findOne(id) ?~> "mesh.notFound" ~> NOT_FOUND - _ <- annotationDAO.findOne(meshInfo._annotation) ?~> "annotation.notFound" ~> NOT_FOUND - data <- meshDAO.getData(id) ?~> "mesh.data.get.failed" - } yield Ok(data) - } - - def updateData(id: ObjectId): Action[RawBuffer] = sil.SecuredAction.async(parse.raw) { implicit request => - for { - meshInfo <- meshDAO.findOne(id) ?~> "mesh.notFound" ~> NOT_FOUND - _ <- annotationDAO.assertUpdateAccess(meshInfo._annotation) ?~> "notAllowed" ~> FORBIDDEN - byteString <- request.body.asBytes(maxLength = 1024 * 1024 * 1024).toFox ?~> "mesh.data.read.failed" - _ <- meshDAO.updateData(id, byteString.toArray) ?~> "mesh.data.save.failed" - } yield Ok - } - - def delete(id: ObjectId): Action[AnyContent] = sil.SecuredAction.async { implicit request => - for { - meshInfo <- meshDAO.findOne(id) ?~> "mesh.notFound" ~> NOT_FOUND - _ <- annotationDAO.assertUpdateAccess(meshInfo._annotation) ?~> "notAllowed" ~> FORBIDDEN - _ <- meshDAO.deleteOne(id) ?~> "mesh.delete.failed" - } yield Ok - } - -} diff --git a/app/models/mesh/Mesh.scala b/app/models/mesh/Mesh.scala deleted file mode 100644 index a120c866a88..00000000000 --- a/app/models/mesh/Mesh.scala +++ /dev/null @@ -1,125 +0,0 @@ -package models.mesh - -import com.google.common.io.BaseEncoding -import com.scalableminds.util.accesscontext.DBAccessContext -import com.scalableminds.util.geometry.Vec3Int -import com.scalableminds.util.time.Instant -import com.scalableminds.util.tools.Fox -import com.scalableminds.webknossos.schema.Tables._ - -import javax.inject.Inject -import play.api.libs.json.Json._ -import play.api.libs.json._ -import slick.lifted.Rep -import utils.sql.{SQLDAO, SqlClient, SqlToken} -import com.scalableminds.util.objectid.ObjectId - -import scala.concurrent.ExecutionContext - -case class MeshInfo( - _id: ObjectId, - _annotation: ObjectId, - description: String, - position: Vec3Int, - created: Instant = Instant.now, - isDeleted: Boolean = false -) - -case class MeshInfoParameters( - annotationId: ObjectId, - description: String, - position: Vec3Int, -) -object MeshInfoParameters { - implicit val jsonFormat: OFormat[MeshInfoParameters] = Json.format[MeshInfoParameters] -} - -class MeshService @Inject()()(implicit ec: ExecutionContext) { - def publicWrites(meshInfo: MeshInfo): Fox[JsObject] = - Fox.successful( - Json.obj( - "id" -> meshInfo._id.toString, - "annotationId" -> meshInfo._annotation.toString, - "description" -> meshInfo.description, - "position" -> meshInfo.position - )) - - def compactWrites(meshInfo: MeshInfo): Fox[JsObject] = - Fox.successful( - Json.obj( - "id" -> meshInfo._id.toString, - "description" -> meshInfo.description, - "position" -> meshInfo.position - )) -} - -class MeshDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionContext) - extends SQLDAO[MeshInfo, MeshesRow, Meshes](sqlClient) { - protected val collection = Meshes - - protected def idColumn(x: Meshes): Rep[String] = x._Id - - protected def isDeletedColumn(x: Meshes): Rep[Boolean] = x.isdeleted - - private val infoColumns = SqlToken.raw((columnsList diff Seq("data")).mkString(", ")) - private type InfoTuple = (ObjectId, ObjectId, String, String, Instant, Boolean) - - override protected def parse(r: MeshesRow): Fox[MeshInfo] = - Fox.failure("not implemented, use parseInfo or get the data directly") - - private def parseInfo(r: InfoTuple): Fox[MeshInfo] = - for { - position <- Vec3Int.fromList(parseArrayLiteral(r._4).map(_.toInt)).toFox ?~> "could not parse mesh position" - } yield { - MeshInfo( - r._1, //_id - r._2, //_annotation - r._3, // description - position, - r._5, //created - r._6 //isDeleted - ) - } - - override def findOne(id: ObjectId)(implicit ctx: DBAccessContext): Fox[MeshInfo] = - for { - accessQuery <- readAccessQuery - rows <- run(q"SELECT $infoColumns FROM $existingCollectionName WHERE _id = $id AND $accessQuery".as[InfoTuple]) - r <- rows.headOption.toFox - parsed <- parseInfo(r) - } yield parsed - - def insertOne(m: MeshInfo): Fox[Unit] = - for { - _ <- run(q"""INSERT INTO webknossos.meshes(_id, _annotation, description, position, created, isDeleted) - VALUES(${m._id}, ${m._annotation}, ${m.description}, ${m.position}, ${m.created}, ${m.isDeleted}) - """.asUpdate) - } yield () - - def updateOne(id: ObjectId, annotationId: ObjectId, description: String, position: Vec3Int)( - implicit ctx: DBAccessContext): Fox[Unit] = - for { - _ <- assertUpdateAccess(id) - _ <- run(q"""UPDATE webknossos.meshes - SET - _annotation = $annotationId, - description = $description, - position = $position - WHERE _id = $id""".asUpdate) - } yield () - - def getData(id: ObjectId)(implicit ctx: DBAccessContext): Fox[Array[Byte]] = - for { - accessQuery <- readAccessQuery - rows <- run(q"SELECT data FROM webknossos.meshes WHERE _id = $id AND $accessQuery".as[Option[String]]) - r <- rows.headOption.flatten.toFox - binary = BaseEncoding.base64().decode(r) - } yield binary - - def updateData(id: ObjectId, data: Array[Byte])(implicit ctx: DBAccessContext): Fox[Unit] = - for { - _ <- assertUpdateAccess(id) - _ <- run(q"UPDATE webknossos.meshes SET data = ${BaseEncoding.base64().encode(data)} WHERE _id = $id".asUpdate) - } yield () - -} diff --git a/conf/evolutions/132-remove-stored-meshes.sql b/conf/evolutions/132-remove-stored-meshes.sql new file mode 100644 index 00000000000..14e26d037c7 --- /dev/null +++ b/conf/evolutions/132-remove-stored-meshes.sql @@ -0,0 +1,6 @@ +do $$ begin ASSERT (select schemaVersion from webknossos.releaseInformation) = 131, 'Previous schema version mismatch'; end; $$ LANGUAGE plpgsql; + +DROP VIEW webknossos.meshes_; +DROP TABLE webknossos.meshes; + +UPDATE webknossos.releaseInformation SET schemaVersion = 132; diff --git a/conf/evolutions/reversions/132-remove-stored-meshes.sql b/conf/evolutions/reversions/132-remove-stored-meshes.sql new file mode 100644 index 00000000000..90a70295fda --- /dev/null +++ b/conf/evolutions/reversions/132-remove-stored-meshes.sql @@ -0,0 +1,19 @@ +do $$ begin ASSERT (select schemaVersion from webknossos.releaseInformation) = 132, 'Previous schema version mismatch'; end; $$ LANGUAGE plpgsql; + + +CREATE TABLE webknossos.meshes( + _id TEXT CONSTRAINT _id_objectId CHECK (_id ~ '^[0-9a-f]{24}$') PRIMARY KEY, + _annotation TEXT CONSTRAINT _annotation_objectId CHECK (_annotation ~ '^[0-9a-f]{24}$') NOT NULL, + description TEXT NOT NULL DEFAULT '', + position webknossos.VECTOR3 NOT NULL, + data TEXT, + created TIMESTAMPTZ NOT NULL DEFAULT NOW(), + isDeleted BOOLEAN NOT NULL DEFAULT false +); + +CREATE VIEW webknossos.meshes_ AS SELECT * FROM webknossos.meshes WHERE NOT isDeleted; + +ALTER TABLE webknossos.meshes + ADD CONSTRAINT annotation_ref FOREIGN KEY(_annotation) REFERENCES webknossos.annotations(_id) DEFERRABLE; + +UPDATE webknossos.releaseInformation SET schemaVersion = 131; diff --git a/conf/messages b/conf/messages index 14b09b98796..b7aab81dbd6 100644 --- a/conf/messages +++ b/conf/messages @@ -260,19 +260,9 @@ annotation.editableMapping.getAgglomerateGraph.failed=Could not look up an agglo annotation.editableMapping.getAgglomerateIdsForSegments.failed=Could not look up agglomerate ids for requested segments. annotation.duplicate.failed=Failed to duplicate annotation -mesh.notFound=Mesh could not be found -mesh.write.failed=Failed to convert mesh info to json -mesh.create.failed=Failed to save mesh info -mesh.update.failed=Failed to update mesh info -mesh.data.get.failed=Failed to load mesh raw data -mesh.data.read.failed=Failed to convert mesh raw data for saving -mesh.data.save.failed=Failed to save mesh raw data -mesh.delete.failed=Failed to delete mesh -mesh.file.load.failed=Failed to load mesh for segment {0} mesh.file.listChunks.failed=Failed to load chunk list for segment {0} from mesh file “{1}” mesh.file.loadChunk.failed=Failed to load mesh chunk for segment mesh.file.open.failed=Failed to open mesh file for reading -mesh.file.readData.failed=Failed to read data from mesh file mesh.file.readEncoding.failed=Failed to read encoding from mesh file task.create.noTasks=Zero tasks were requested diff --git a/conf/webknossos.latest.routes b/conf/webknossos.latest.routes index 245cd6c8d05..5ede27c1b0f 100644 --- a/conf/webknossos.latest.routes +++ b/conf/webknossos.latest.routes @@ -177,14 +177,6 @@ PUT /zarrPrivateLinks/:id DELETE /zarrPrivateLinks/:id controllers.AnnotationPrivateLinkController.delete(id: ObjectId) GET /zarrPrivateLinks/:id controllers.AnnotationPrivateLinkController.get(id: ObjectId) -# Meshes -POST /meshes controllers.MeshController.create() -PUT /meshes/:id controllers.MeshController.update(id: ObjectId) -DELETE /meshes/:id controllers.MeshController.delete(id: ObjectId) -GET /meshes/:id controllers.MeshController.get(id: ObjectId) -PUT /meshes/:id/data controllers.MeshController.updateData(id: ObjectId) -GET /meshes/:id/data controllers.MeshController.getData(id: ObjectId) - # Tasks POST /tasks controllers.TaskController.create() POST /tasks/createFromFiles controllers.TaskController.createFromFiles() diff --git a/tools/postgres/schema.sql b/tools/postgres/schema.sql index a89f8a8c5ca..1fd002c73ed 100644 --- a/tools/postgres/schema.sql +++ b/tools/postgres/schema.sql @@ -21,7 +21,7 @@ CREATE TABLE webknossos.releaseInformation ( schemaVersion BIGINT NOT NULL ); -INSERT INTO webknossos.releaseInformation(schemaVersion) values(131); +INSERT INTO webknossos.releaseInformation(schemaVersion) values(132); COMMIT TRANSACTION; @@ -82,16 +82,6 @@ CREATE TABLE webknossos.annotation_mutexes( expiry TIMESTAMP NOT NULL ); -CREATE TABLE webknossos.meshes( - _id TEXT CONSTRAINT _id_objectId CHECK (_id ~ '^[0-9a-f]{24}$') PRIMARY KEY, - _annotation TEXT CONSTRAINT _annotation_objectId CHECK (_annotation ~ '^[0-9a-f]{24}$') NOT NULL, - description TEXT NOT NULL DEFAULT '', - position webknossos.VECTOR3 NOT NULL, - data TEXT, - created TIMESTAMPTZ NOT NULL DEFAULT NOW(), - isDeleted BOOLEAN NOT NULL DEFAULT FALSE -); - CREATE TABLE webknossos.publications( _id TEXT PRIMARY KEY, publicationDate TIMESTAMPTZ, @@ -740,7 +730,6 @@ CREATE TABLE webknossos.analyticsEvents( CREATE VIEW webknossos.annotations_ AS SELECT * FROM webknossos.annotations WHERE NOT isDeleted; -CREATE VIEW webknossos.meshes_ AS SELECT * FROM webknossos.meshes WHERE NOT isDeleted; CREATE VIEW webknossos.publications_ AS SELECT * FROM webknossos.publications WHERE NOT isDeleted; CREATE VIEW webknossos.datasets_ AS SELECT * FROM webknossos.datasets WHERE NOT isDeleted; CREATE VIEW webknossos.dataStores_ AS SELECT * FROM webknossos.dataStores WHERE NOT isDeleted; @@ -823,8 +812,6 @@ ALTER TABLE webknossos.annotation_contributors ALTER TABLE webknossos.annotation_mutexes ADD CONSTRAINT annotation_ref FOREIGN KEY(_annotation) REFERENCES webknossos.annotations(_id) ON DELETE CASCADE DEFERRABLE, ADD CONSTRAINT user_ref FOREIGN KEY(_user) REFERENCES webknossos.users(_id) ON DELETE CASCADE DEFERRABLE; -ALTER TABLE webknossos.meshes - ADD CONSTRAINT annotation_ref FOREIGN KEY(_annotation) REFERENCES webknossos.annotations(_id) DEFERRABLE; ALTER TABLE webknossos.datasets ADD CONSTRAINT organization_ref FOREIGN KEY(_organization) REFERENCES webknossos.organizations(_id) DEFERRABLE, ADD CONSTRAINT dataStore_ref FOREIGN KEY(_dataStore) REFERENCES webknossos.dataStores(name) DEFERRABLE,