tanishqgandhi1908 commented on code in PR #6872:
URL: https://github.com/apache/texera/pull/6872#discussion_r3856009404
##########
file-service/src/main/scala/org/apache/texera/service/resource/ModelResource.scala:
##########
@@ -408,4 +460,341 @@ class ModelResource extends LazyLogging {
): DashboardModel = {
withTransaction(context)(ctx => getDashboardModel(ctx, mid, None))
}
+
+ //
===========================================================================
+ // Versioning
+ //
===========================================================================
+
+ @POST
+ @RolesAllowed(Array("REGULAR", "ADMIN"))
+ @Path("/{mid}/version/create")
+ @Consumes(Array(MediaType.TEXT_PLAIN))
+ def createModelVersion(
+ versionName: String,
+ @PathParam("mid") mid: Integer,
+ @Auth user: SessionUser
+ ): DashboardModelVersion = {
+ val uid = user.getUid
+ withTransaction(context) { ctx =>
+ if (!userHasWriteAccess(ctx, mid, uid)) {
+ throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
+ }
+
+ val model = getModelByID(ctx, mid)
+ val modelName = model.getName
+ val repositoryName = model.getRepositoryName
+
+ // Check if there are any changes in LakeFS before creating a new version
+ val diffs = withLakeFSErrorHandling {
+ LakeFSStorageClient.retrieveUncommittedObjects(repoName =
repositoryName)
+ }
+
+ if (diffs.isEmpty) {
+ throw new WebApplicationException(
+ "No changes detected in model. Version creation aborted.",
+ Response.Status.BAD_REQUEST
+ )
+ }
+
+ // Generate a new version name
+ val versionCount = ctx
+ .selectCount()
+ .from(MODEL_VERSION)
+ .where(MODEL_VERSION.MID.eq(mid))
+ .fetchOne(0, classOf[Int])
+
+ val sanitizedVersionName =
Option(versionName).filter(_.nonEmpty).getOrElse("")
+ val newVersionName = if (sanitizedVersionName.isEmpty) {
+ s"v${versionCount + 1}"
+ } else {
+ s"v${versionCount + 1} - $sanitizedVersionName"
+ }
+
+ // Create a commit in LakeFS
+ val commit = withLakeFSErrorHandling {
+ LakeFSStorageClient.createCommit(
+ repoName = repositoryName,
+ branch = "main",
+ commitMessage = s"Created model version: $newVersionName"
+ )
+ }
+
+ if (commit == null || commit.getId == null) {
+ throw new WebApplicationException(
+ "Failed to create commit in LakeFS. Version creation aborted.",
+ Response.Status.INTERNAL_SERVER_ERROR
+ )
+ }
+
+ // Create a new model version entry in the database
+ val modelVersion = new ModelVersion()
+ modelVersion.setMid(mid)
+ modelVersion.setCreatorUid(uid)
+ modelVersion.setName(newVersionName)
+ modelVersion.setVersionHash(commit.getId) // Store LakeFS version hash
+
+ val insertedVersion = ctx
+ .insertInto(MODEL_VERSION)
+ .set(ctx.newRecord(MODEL_VERSION, modelVersion))
+ .returning()
+ .fetchOne()
+ .into(classOf[ModelVersion])
+
+ // Retrieve committed file structure
+ val fileNodes = withLakeFSErrorHandling {
+ LakeFSStorageClient.retrieveObjectsOfVersion(repositoryName,
commit.getId)
+ }
+
+ DashboardModelVersion(
+ insertedVersion,
+ LakeFSFileNode
+ .fromLakeFSRepositoryCommittedObjects(
+ ResourceType.Model,
+ Map((user.getEmail, modelName, newVersionName) -> fileNodes)
+ )
+ )
+ }
+ }
+
+ @GET
+ @RolesAllowed(Array("REGULAR", "ADMIN"))
+ @Path("/{mid}/version/list")
+ def getModelVersionList(
+ @PathParam("mid") mid: Integer,
+ @Auth user: SessionUser
+ ): List[ModelVersion] = {
+ val uid = user.getUid
+ withTransaction(context)(ctx => {
+ val model = getModelByID(ctx, mid)
+ if (!userHasReadAccess(ctx, model.getMid, uid)) {
+ throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
+ }
+ fetchModelVersions(ctx, model.getMid)
+ })
+ }
+
+ @GET
+ @RolesAllowed(Array("REGULAR", "ADMIN"))
+ @Path("/{mid}/version/latest")
+ def retrieveLatestModelVersion(
+ @PathParam("mid") mid: Integer,
+ @Auth user: SessionUser
+ ): DashboardModelVersion = {
+ val uid = user.getUid
+ withTransaction(context)(ctx => {
+ if (!userHasReadAccess(ctx, mid, uid)) {
+ throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
+ }
+ val model = getModelByID(ctx, mid)
+ val latestVersion = getLatestModelVersion(ctx, mid).getOrElse(
+ throw new NotFoundException(ERR_MODEL_VERSION_NOT_FOUND_MESSAGE)
+ )
+ DashboardModelVersion(latestVersion, versionRootFileNodes(ctx, mid,
latestVersion))
+ })
+ }
+
+ @GET
+ @RolesAllowed(Array("REGULAR", "ADMIN"))
+ @Path("/{mid}/version/{mvid}/rootFileNodes")
+ def retrieveModelVersionRootFileNodes(
+ @PathParam("mid") mid: Integer,
+ @PathParam("mvid") mvid: Integer,
+ @Auth user: SessionUser
+ ): ModelVersionRootFileNodesResponse = {
+ val uid = user.getUid
+ withTransaction(context)(ctx => fetchModelVersionRootFileNodes(ctx, mid,
mvid, Some(uid)))
+ }
+
+ //
===========================================================================
+ // File upload (one-shot + session-based multipart)
+ //
===========================================================================
+
+ @POST
+ @RolesAllowed(Array("REGULAR", "ADMIN"))
+ @Path("/{mid}/upload")
+ @Consumes(Array(MediaType.APPLICATION_OCTET_STREAM))
+ def uploadOneFileToModel(
+ @PathParam("mid") mid: Integer,
+ @QueryParam("filePath") encodedFilePath: String,
+ @QueryParam("message") message: String,
+ fileStream: InputStream,
+ @Context headers: HttpHeaders,
+ @Auth user: SessionUser
+ ): Response = {
+ ResourceUploadService.uploadOneFile(
+ ResourceStorage.Model,
+ mid,
+ encodedFilePath,
+ fileStream,
+ headers,
+ user.getUid
+ )
+ }
+
+ @DELETE
+ @RolesAllowed(Array("REGULAR", "ADMIN"))
+ @Path("/{mid}/file")
+ @Consumes(Array(MediaType.APPLICATION_JSON))
+ def deleteModelFile(
+ @PathParam("mid") mid: Integer,
+ @QueryParam("filePath") encodedFilePath: String,
+ @Auth user: SessionUser
+ ): Response = {
+ ResourceUploadService.deleteStagedFile(
+ ResourceStorage.Model,
+ mid,
+ encodedFilePath,
+ user.getUid
+ )
+ }
+
+ @POST
+ @RolesAllowed(Array("REGULAR", "ADMIN"))
+ @Path("/multipart-upload")
+ @Consumes(Array(MediaType.APPLICATION_JSON))
+ def multipartUpload(
+ @QueryParam("type") operationType: String,
+ @QueryParam("ownerEmail") ownerEmail: String,
+ @QueryParam("modelName") modelName: String,
+ @QueryParam("filePath") filePath: String,
+ @QueryParam("fileSizeBytes") fileSizeBytes: Optional[java.lang.Long],
+ @QueryParam("partSizeBytes") partSizeBytes: Optional[java.lang.Long],
+ @QueryParam("restart") restart: Optional[java.lang.Boolean],
+ @Auth user: SessionUser
+ ): Response = {
+ val uid = user.getUid
+ val model: Model = getModelBy(ownerEmail, modelName)
+
+ operationType.toLowerCase match {
+ case "list" => listMultipartUploads(model.getMid, uid)
+ case "init" =>
+ initMultipartUpload(model.getMid, filePath, fileSizeBytes,
partSizeBytes, restart, uid)
+ case "finish" => finishMultipartUpload(model.getMid, filePath, uid)
+ case "abort" => abortMultipartUpload(model.getMid, filePath, uid)
+ case _ =>
+ throw new BadRequestException("Invalid type parameter. Use 'init',
'finish', or 'abort'.")
+ }
+ }
+
+ @POST
+ @RolesAllowed(Array("REGULAR", "ADMIN"))
+ @Consumes(Array(MediaType.APPLICATION_OCTET_STREAM))
+ @Path("/multipart-upload/part")
+ def uploadPart(
+ @QueryParam("ownerEmail") modelOwnerEmail: String,
+ @QueryParam("modelName") modelName: String,
+ @QueryParam("filePath") encodedFilePath: String,
+ @QueryParam("partNumber") partNumber: Int,
+ partStream: InputStream,
+ @Context headers: HttpHeaders,
+ @Auth user: SessionUser
+ ): Response = {
+ val model = getModelBy(modelOwnerEmail, modelName)
+ ResourceUploadService.uploadPart(
+ ResourceStorage.Model,
+ model.getMid,
+ user.getUid,
+ encodedFilePath,
+ partNumber,
+ partStream,
+ headers
+ )
+ }
+
+ //
===========================================================================
+ // Private helpers
+ //
===========================================================================
+
+ private def fetchModelVersions(ctx: DSLContext, mid: Integer):
List[ModelVersion] = {
+ ctx
+ .selectFrom(MODEL_VERSION)
+ .where(MODEL_VERSION.MID.eq(mid))
+ .orderBy(MODEL_VERSION.CREATION_TIME.desc())
+ .fetchInto(classOf[ModelVersion])
+ .asScala
+ .toList
+ }
+
+ /**
+ * Builds the file-tree children of a single model version, drilling into
the
+ * owner/model/version nesting produced by LakeFSFileNode.
+ */
+ private def versionRootFileNodes(
+ ctx: DSLContext,
+ mid: Integer,
+ modelVersion: ModelVersion
+ ): List[LakeFSFileNode] = {
+ val model = getModelByID(ctx, mid)
+ ResourceUploadService
+ .versionRootFileNodes(
+ ResourceType.Model,
+ getOwner(ctx, mid).getEmail,
+ model.getName,
+ modelVersion.getName,
+ model.getRepositoryName,
+ modelVersion.getVersionHash
+ )
+ ._1
+ }
+
+ private def fetchModelVersionRootFileNodes(
+ ctx: DSLContext,
+ mid: Integer,
+ mvid: Integer,
+ uid: Option[Integer]
+ ): ModelVersionRootFileNodesResponse = {
+ val model = getDashboardModel(ctx, mid, uid)
+ val modelVersion = getModelVersionByID(ctx, mvid)
Review Comment:
Added `.and(MODEL_VERSION.MID.eq(mid))` to the lookup, so it is a 404
before any path is built. Covered by a test that asks for model B's version
through model A.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]