Skip to content

Commit 87e4cb4

Browse files
frcrothfm3
andauthored
Remove datastore from communication about virtual datasets (#8848)
Creation of virtual remote datasets still involves the datastore, because it does the exploring (to handle local file paths). ### Steps to test: - Create a virtual remote dataset - Delete a virtual remote dataset - Delete a non-virtual dataset ### Issues: - fixes #8814 ------ - [x] Needs datastore update after deployment --------- Co-authored-by: Florian M <[email protected]>
1 parent 2d6943d commit 87e4cb4

File tree

15 files changed

+147
-97
lines changed

15 files changed

+147
-97
lines changed

app/controllers/DatasetController.scala

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import com.scalableminds.util.objectid.ObjectId
77
import com.scalableminds.util.time.Instant
88
import com.scalableminds.util.tools.{Fox, TristateOptionJsonHelper}
99
import com.scalableminds.webknossos.datastore.models.AdditionalCoordinate
10-
import com.scalableminds.webknossos.datastore.models.datasource.ElementClass
10+
import com.scalableminds.webknossos.datastore.models.datasource.{DataSource, ElementClass}
1111
import mail.{MailchimpClient, MailchimpTag}
1212
import models.analytics.{AnalyticsService, ChangeDatasetSettingsEvent, OpenDatasetEvent}
1313
import models.dataset._
@@ -71,6 +71,12 @@ object SegmentAnythingMaskParameters {
7171
implicit val jsonFormat: Format[SegmentAnythingMaskParameters] = Json.format[SegmentAnythingMaskParameters]
7272
}
7373

74+
case class DataSourceRegistrationInfo(dataSource: DataSource, folderId: Option[String], dataStoreName: String)
75+
76+
object DataSourceRegistrationInfo {
77+
implicit val jsonFormat: OFormat[DataSourceRegistrationInfo] = Json.format[DataSourceRegistrationInfo]
78+
}
79+
7480
class DatasetController @Inject()(userService: UserService,
7581
userDAO: UserDAO,
7682
datasetService: DatasetService,
@@ -153,6 +159,27 @@ class DatasetController @Inject()(userService: UserService,
153159
} yield Ok
154160
}
155161

162+
def addVirtualDataset(name: String): Action[DataSourceRegistrationInfo] =
163+
sil.SecuredAction.async(validateJson[DataSourceRegistrationInfo]) { implicit request =>
164+
for {
165+
dataStore <- dataStoreDAO.findOneByName(request.body.dataStoreName) ?~> Messages(
166+
"datastore.notFound",
167+
request.body.dataStoreName) ~> NOT_FOUND
168+
user = request.identity
169+
isTeamManagerOrAdmin <- userService.isTeamManagerOrAdminOfOrg(user, user._organization)
170+
_ <- Fox.fromBool(isTeamManagerOrAdmin || user.isDatasetManager) ~> FORBIDDEN
171+
_ <- Fox.fromBool(request.body.dataSource.dataLayers.nonEmpty) ?~> "dataset.explore.zeroLayers"
172+
_ <- datasetService.validatePaths(request.body.dataSource.allExplicitPaths, dataStore) ?~> "dataSource.add.pathsNotAllowed"
173+
dataset <- datasetService.createVirtualDataset(
174+
name,
175+
dataStore,
176+
request.body.dataSource,
177+
request.body.folderId,
178+
user
179+
)
180+
} yield Ok(Json.obj("newDatasetId" -> dataset._id))
181+
}
182+
156183
// List all accessible datasets (list of json objects, one per dataset)
157184
def list(
158185
// Optional filtering: If true, list only active datasets, if false, list only inactive datasets
@@ -491,6 +518,17 @@ class DatasetController @Inject()(userService: UserService,
491518
}
492519
}
493520

521+
def deleteOnDisk(datasetId: ObjectId): Action[AnyContent] =
522+
sil.SecuredAction.async { implicit request =>
523+
for {
524+
dataset <- datasetDAO.findOne(datasetId) ?~> notFoundMessage(datasetId.toString) ~> NOT_FOUND
525+
_ <- Fox.fromBool(conf.Features.allowDeleteDatasets) ?~> "dataset.delete.disabled"
526+
_ <- Fox.assertTrue(datasetService.isEditableBy(dataset, Some(request.identity))) ?~> "notAllowed" ~> FORBIDDEN
527+
_ <- Fox.fromBool(request.identity.isAdminOf(dataset._organization)) ~> FORBIDDEN
528+
_ <- datasetService.deleteVirtualOrDiskDataset(dataset)
529+
} yield Ok
530+
}
531+
494532
def compose(): Action[ComposeRequest] =
495533
sil.SecuredAction.async(validateJson[ComposeRequest]) { implicit request =>
496534
for {

app/controllers/WKRemoteDataStoreController.scala

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import com.scalableminds.webknossos.datastore.helpers.{LayerMagLinkInfo, MagLink
99
import com.scalableminds.webknossos.datastore.models.UnfinishedUpload
1010
import com.scalableminds.webknossos.datastore.models.datasource.{AbstractDataLayer, DataSource, DataSourceId}
1111
import com.scalableminds.webknossos.datastore.models.datasource.inbox.{InboxDataSourceLike => InboxDataSource}
12-
import com.scalableminds.webknossos.datastore.services.{DataSourcePathInfo, DataSourceRegistrationInfo, DataStoreStatus}
12+
import com.scalableminds.webknossos.datastore.services.{DataSourcePathInfo, DataStoreStatus}
1313
import com.scalableminds.webknossos.datastore.services.uploading.{
1414
LinkedLayerIdentifier,
1515
ReserveAdditionalInformation,
@@ -226,6 +226,9 @@ class WKRemoteDataStoreController @Inject()(
226226
}
227227
}
228228

229+
/**
230+
* Called by the datastore after a dataset has been deleted on disk.
231+
*/
229232
def deleteDataset(name: String, key: String): Action[DataSourceId] = Action.async(validateJson[DataSourceId]) {
230233
implicit request =>
231234
dataStoreService.validateAccess(name, key) { _ =>
@@ -245,17 +248,6 @@ class WKRemoteDataStoreController @Inject()(
245248
}
246249
}
247250

248-
def deleteVirtualDataset(name: String, key: String): Action[ObjectId] =
249-
Action.async(validateJson[ObjectId]) { implicit request =>
250-
dataStoreService.validateAccess(name, key) { _ =>
251-
for {
252-
dataset <- datasetDAO.findOne(request.body)(GlobalAccessContext) ~> NOT_FOUND
253-
_ <- Fox.fromBool(dataset.isVirtual) ?~> "dataset.delete.notVirtual" ~> FORBIDDEN
254-
_ <- datasetDAO.deleteDataset(dataset._id, onlyMarkAsDeleted = true)
255-
} yield Ok
256-
}
257-
}
258-
259251
def findDatasetId(name: String,
260252
key: String,
261253
datasetDirectoryName: String,
@@ -316,7 +308,6 @@ class WKRemoteDataStoreController @Inject()(
316308
_ <- Fox.fromBool(organization._id == user._organization) ?~> "notAllowed" ~> FORBIDDEN
317309
dataset <- datasetService.createVirtualDataset(
318310
directoryName,
319-
organizationId,
320311
dataStore,
321312
request.body.dataSource,
322313
request.body.folderId,

app/models/dataset/ComposeService.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ class ComposeService @Inject()(datasetDAO: DatasetDAO, dataStoreDAO: DataStoreDA
4444
dataSource <- createDatasource(composeRequest, composeRequest.newDatasetName, composeRequest.organizationId)
4545
dataStore <- dataStoreDAO.findOneWithUploadsAllowed
4646
dataset <- datasetService.createVirtualDataset(composeRequest.newDatasetName,
47-
composeRequest.organizationId,
4847
dataStore,
4948
dataSource,
5049
Some(composeRequest.targetFolderId.toString),

app/models/dataset/DatasetService.scala

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import models.team._
3939
import models.user.{User, UserService}
4040
import com.scalableminds.util.tools.Box.tryo
4141
import com.scalableminds.util.tools.{Empty, EmptyBox, Full}
42+
import com.scalableminds.webknossos.datastore.controllers.PathValidationResult
4243
import play.api.libs.json.{JsObject, Json}
4344
import security.RandomIDGenerator
4445
import utils.WkConf
@@ -101,17 +102,16 @@ class DatasetService @Inject()(organizationDAO: OrganizationDAO,
101102
}
102103

103104
def createVirtualDataset(datasetName: String,
104-
organizationId: String,
105105
dataStore: DataStore,
106106
dataSource: DataSource,
107107
folderId: Option[String],
108108
user: User): Fox[Dataset] =
109109
for {
110110
_ <- assertValidDatasetName(datasetName)
111-
isDatasetNameAlreadyTaken <- datasetDAO.doesDatasetDirectoryExistInOrganization(datasetName, organizationId)(
111+
isDatasetNameAlreadyTaken <- datasetDAO.doesDatasetDirectoryExistInOrganization(datasetName, user._organization)(
112112
GlobalAccessContext)
113113
_ <- Fox.fromBool(!isDatasetNameAlreadyTaken) ?~> "dataset.name.alreadyTaken"
114-
organization <- organizationDAO.findOne(organizationId)(GlobalAccessContext) ?~> "organization.notFound"
114+
organization <- organizationDAO.findOne(user._organization)(GlobalAccessContext) ?~> "organization.notFound"
115115
folderId <- ObjectId.fromString(folderId.getOrElse(organization._rootFolder.toString)) ?~> "dataset.upload.folderId.invalid"
116116
_ <- folderDAO.assertUpdateAccess(folderId)(AuthorizedAccessContext(user)) ?~> "folder.noWriteAccess"
117117
newDatasetId = ObjectId.generate
@@ -155,7 +155,7 @@ class DatasetService @Inject()(organizationDAO: OrganizationDAO,
155155

156156
val dataSourceHash = if (dataSource.isUsable) Some(dataSource.hashCode()) else None
157157
for {
158-
organization <- organizationDAO.findOne(dataSource.id.organizationId)
158+
organization <- organizationDAO.findOne(dataSource.id.organizationId) ?~> "organization.notFound"
159159
organizationRootFolder <- folderDAO.findOne(organization._rootFolder)
160160
dataset = Dataset(
161161
datasetId,
@@ -657,6 +657,31 @@ class DatasetService @Inject()(organizationDAO: OrganizationDAO,
657657
})
658658
} yield magInfosAndLinkedMags
659659

660+
def validatePaths(paths: Seq[String], dataStore: DataStore): Fox[Unit] =
661+
for {
662+
_ <- Fox.successful(())
663+
client = new WKRemoteDataStoreClient(dataStore, rpc)
664+
pathValidationResults <- client.validatePaths(paths)
665+
_ <- Fox.serialCombined(pathValidationResults)({
666+
case PathValidationResult(_, true) => Fox.successful(())
667+
case PathValidationResult(path, false) => Fox.failure(s"Path validation failed for path: $path")
668+
})
669+
} yield ()
670+
671+
def deleteVirtualOrDiskDataset(dataset: Dataset)(implicit ctx: DBAccessContext): Fox[Unit] =
672+
for {
673+
_ <- if (dataset.isVirtual) {
674+
// At this point, we should also free space in S3 once implemented.
675+
// Right now, we can just mark the dataset as deleted in the database.
676+
datasetDAO.deleteDataset(dataset._id, onlyMarkAsDeleted = true)
677+
} else {
678+
for {
679+
datastoreClient <- clientFor(dataset)
680+
_ <- datastoreClient.deleteOnDisk(dataset._id)
681+
} yield ()
682+
} ?~> "dataset.delete.failed"
683+
} yield ()
684+
660685
def publicWrites(dataset: Dataset,
661686
requestingUserOpt: Option[User],
662687
organization: Option[Organization] = None,

app/models/dataset/WKRemoteDataStoreClient.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.scalableminds.util.cache.AlfuCache
44
import com.scalableminds.util.geometry.{BoundingBox, Vec3Int}
55
import com.scalableminds.util.objectid.ObjectId
66
import com.scalableminds.util.tools.Fox
7+
import com.scalableminds.webknossos.datastore.controllers.PathValidationResult
78
import com.scalableminds.webknossos.datastore.explore.{
89
ExploreRemoteDatasetRequest,
910
ExploreRemoteDatasetResponse,
@@ -98,11 +99,23 @@ class WKRemoteDataStoreClient(dataStore: DataStore, rpc: RPC) extends LazyLoggin
9899
.postJsonWithJsonResponse[ExploreRemoteDatasetRequest, ExploreRemoteDatasetResponse](
99100
ExploreRemoteDatasetRequest(layerParameters, organizationId))
100101

102+
def validatePaths(paths: Seq[String]): Fox[List[PathValidationResult]] =
103+
rpc(s"${dataStore.url}/data/datasets/validatePaths")
104+
.addQueryString("token" -> RpcTokenHolder.webknossosToken)
105+
.postJsonWithJsonResponse[Seq[String], List[PathValidationResult]](paths)
106+
101107
def updateDatasetInDSCache(datasetId: String): Fox[Unit] =
102108
for {
103109
_ <- rpc(s"${dataStore.url}/data/datasets/$datasetId")
104110
.addQueryString("token" -> RpcTokenHolder.webknossosToken)
105111
.delete()
106112
} yield ()
107113

114+
def deleteOnDisk(datasetId: ObjectId): Fox[Unit] =
115+
for {
116+
_ <- rpc(s"${dataStore.url}/data/datasets/$datasetId/deleteOnDisk")
117+
.addQueryString("token" -> RpcTokenHolder.webknossosToken)
118+
.delete()
119+
} yield ()
120+
108121
}

app/models/dataset/explore/WKExploreRemoteLayerService.scala

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,9 @@ class WKExploreRemoteLayerService @Inject()(credentialService: CredentialService
111111
folderId: Option[ObjectId])(implicit ctx: DBAccessContext): Fox[Unit] =
112112
for {
113113
dataStore <- dataStoreDAO.findOneWithUploadsAllowed
114-
organizationId = user._organization
115114
_ <- datasetService.assertValidDatasetName(datasetName)
116115
_ <- datasetService.createVirtualDataset(
117116
datasetName,
118-
organizationId,
119117
dataStore,
120118
dataSource,
121119
folderId.map(_.toString),

conf/webknossos.latest.routes

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ GET /datasets
8888
POST /datasets controllers.DatasetController.create(typ: String)
8989
POST /datasets/exploreRemote controllers.DatasetController.exploreRemoteDataset()
9090
POST /datasets/exploreAndAddRemote controllers.DatasetController.exploreAndAddRemoteDataset()
91+
POST /datasets/addVirtualDataset/:name controllers.DatasetController.addVirtualDataset(name: String)
9192
GET /datasets/disambiguate/:datasetName/toNew controllers.DatasetController.getOrganizationForDataset(datasetName: String, sharingToken: Option[String])
9293
GET /datasets/disambiguate/:organizationId/:datasetName/toId controllers.DatasetController.getDatasetIdFromNameAndOrganization(datasetName: String, organizationId: String, sharingToken: Option[String])
9394
GET /datasets/:datasetId/health controllers.DatasetController.health(datasetId: ObjectId, sharingToken: Option[String])
@@ -102,6 +103,7 @@ POST /datasets/:datasetId/layers/:layer/segmentAnythingMask
102103
PUT /datasets/:datasetId/clearThumbnailCache controllers.DatasetController.removeFromThumbnailCache(datasetId: ObjectId)
103104
GET /datasets/:datasetName/isValidNewName controllers.DatasetController.isValidNewName(datasetName: String)
104105
GET /datasets/:datasetId controllers.DatasetController.read(datasetId: ObjectId, sharingToken: Option[String])
106+
DELETE /datasets/:datasetId/deleteOnDisk controllers.DatasetController.deleteOnDisk(datasetId: ObjectId)
105107
POST /datasets/compose controllers.DatasetController.compose()
106108

107109
# Folders
@@ -120,14 +122,12 @@ PUT /datastores/:name/datasources
120122
PUT /datastores/:name/datasources/paths controllers.WKRemoteDataStoreController.updatePaths(name: String, key: String)
121123
GET /datastores/:name/datasources/:datasetId/paths controllers.WKRemoteDataStoreController.getPaths(name: String, key: String, datasetId: ObjectId)
122124
GET /datastores/:name/datasources/:datasetId controllers.WKRemoteDataStoreController.getDataSource(name: String, key: String, datasetId: ObjectId)
123-
POST /datastores/:name/datasources/:organizationId/:directoryName controllers.WKRemoteDataStoreController.registerDataSource(name: String, key: String, organizationId: String, directoryName: String, token: String)
124125
PUT /datastores/:name/datasources/:datasetId controllers.WKRemoteDataStoreController.updateDataSource(name: String, key: String, datasetId: ObjectId, allowNewPaths: Boolean)
125126
PATCH /datastores/:name/status controllers.WKRemoteDataStoreController.statusUpdate(name: String, key: String)
126127
POST /datastores/:name/reserveUpload controllers.WKRemoteDataStoreController.reserveDatasetUpload(name: String, key: String, token: String)
127128
GET /datastores/:name/getUnfinishedUploadsForUser controllers.WKRemoteDataStoreController.getUnfinishedUploadsForUser(name: String, key: String, token: String, organizationName: String)
128129
POST /datastores/:name/reportDatasetUpload controllers.WKRemoteDataStoreController.reportDatasetUpload(name: String, key: String, token: String, datasetDirectoryName: String, datasetSizeBytes: Long, needsConversion: Boolean, viaAddRoute: Boolean)
129130
POST /datastores/:name/deleteDataset controllers.WKRemoteDataStoreController.deleteDataset(name: String, key: String)
130-
POST /datastores/:name/deleteVirtualDataset controllers.WKRemoteDataStoreController.deleteVirtualDataset(name: String, key: String)
131131
GET /datastores/:name/findDatasetId controllers.WKRemoteDataStoreController.findDatasetId(name: String, key: String, datasetDirectoryName: String, organizationId: String)
132132
GET /datastores/:name/jobExportProperties controllers.WKRemoteDataStoreController.jobExportProperties(name: String, key: String, jobId: ObjectId)
133133
GET /datastores/:name/findCredential controllers.WKRemoteDataStoreController.findCredential(name: String, key: String, credentialId: ObjectId)

frontend/javascripts/admin/dataset/dataset_add_remote_view.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,11 @@ function DatasetAddRemoteView(props: Props) {
146146
if (nameValidationResult) {
147147
throw new Error(nameValidationResult);
148148
}
149+
const dataSourceJson = JSON.parse(dataSourceJsonStr);
149150
const { newDatasetId } = await storeRemoteDataset(
150-
datastoreToUse.url,
151+
datastoreToUse.name,
151152
datasetName,
152-
activeUser.organization,
153-
dataSourceJsonStr,
153+
dataSourceJson,
154154
targetFolderId,
155155
);
156156
onAdded(newDatasetId, datasetName);

frontend/javascripts/admin/rest_api.ts

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,27 +1256,29 @@ export async function exploreRemoteDataset(
12561256
return { dataSource, report };
12571257
}
12581258

1259+
type StoreRemoteDatasetArgs = {
1260+
dataStoreName: string;
1261+
dataSource: APIDataSource;
1262+
folderId?: string | null;
1263+
};
1264+
12591265
export async function storeRemoteDataset(
1260-
datastoreUrl: string,
1266+
dataStoreName: string,
12611267
datasetName: string,
1262-
organizationId: string,
1263-
datasource: string,
1268+
dataSource: APIDataSource,
12641269
folderId: string | null,
12651270
): Promise<NewDatasetReply> {
1266-
return doWithToken((token) => {
1267-
const params = new URLSearchParams();
1268-
params.set("token", token);
1269-
if (folderId) {
1270-
params.set("folderId", folderId);
1271-
}
1271+
const payload: StoreRemoteDatasetArgs = {
1272+
dataSource,
1273+
dataStoreName: dataStoreName,
1274+
};
1275+
if (folderId) {
1276+
payload["folderId"] = folderId;
1277+
}
12721278

1273-
return Request.sendJSONReceiveJSON(
1274-
`${datastoreUrl}/data/datasets/${organizationId}/${datasetName}?${params}`,
1275-
{
1276-
method: "POST",
1277-
data: datasource,
1278-
},
1279-
);
1279+
return Request.sendJSONReceiveJSON(`/api/datasets/addVirtualDataset/${datasetName}`, {
1280+
method: "POST",
1281+
data: payload,
12801282
});
12811283
}
12821284

@@ -1343,13 +1345,10 @@ export async function triggerDatasetClearCache(
13431345
});
13441346
}
13451347

1346-
export async function deleteDatasetOnDisk(datastoreHost: string, datasetId: string): Promise<void> {
1347-
await doWithToken((token) =>
1348-
Request.triggerRequest(`/data/datasets/${datasetId}/deleteOnDisk?token=${token}`, {
1349-
host: datastoreHost,
1350-
method: "DELETE",
1351-
}),
1352-
);
1348+
export async function deleteDatasetOnDisk(datasetId: string): Promise<void> {
1349+
await Request.triggerRequest(`/api/datasets/${datasetId}/deleteOnDisk`, {
1350+
method: "DELETE",
1351+
});
13531352
}
13541353

13551354
export async function triggerDatasetClearThumbnailCache(datasetId: string): Promise<void> {

frontend/javascripts/dashboard/advanced_dataset/dataset_action_view.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ function DatasetActionView(props: Props) {
163163
return;
164164
}
165165

166-
await deleteDatasetOnDisk(dataset.dataStore.url, dataset.id);
166+
await deleteDatasetOnDisk(dataset.id);
167167

168168
Toast.success(
169169
messages["dataset.delete_success"]({

0 commit comments

Comments
 (0)