xuang7 commented on code in PR #6447:
URL: https://github.com/apache/texera/pull/6447#discussion_r3626225070


##########
file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala:
##########
@@ -1361,6 +1305,187 @@ class DatasetResource extends LazyLogging {
     }
   }
 
+  @POST
+  @RolesAllowed(Array("REGULAR", "ADMIN"))
+  @Path("/{did}/drive-export")
+  @Consumes(Array(MediaType.APPLICATION_JSON))
+  def exportDatasetToDrive(
+      @PathParam("did") did: Integer,
+      @QueryParam("dvid") dvid: Integer,
+      @QueryParam("latest") latest: java.lang.Boolean,
+      request: DriveExportRequest,
+      @Auth user: SessionUser
+  ): Response = {
+    if 
(!request.sessionUri.startsWith("https://www.googleapis.com/upload/drive/";)) {
+      throw new BadRequestException("Invalid session URI")
+    }
+    withTransaction(context) { ctx =>
+      if ((dvid != null && latest != null) || (dvid == null && latest == 
null)) {
+        throw new BadRequestException("Specify exactly one: dvid=<ID> OR 
latest=true")
+      }
+
+      val uid = user.getUid
+      if (!userHasReadAccess(ctx, did, uid)) {
+        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
+      }
+
+      val dataset = getDatasetByID(ctx, did)
+      if (!userOwnDataset(ctx, did, uid) && !dataset.getIsDownloadable) {
+        throw new ForbiddenException("Dataset download is not allowed")
+      }
+
+      val datasetVersion = if (dvid != null) {
+        getDatasetVersionByID(ctx, dvid)
+      } else {
+        getLatestDatasetVersion(ctx, did).getOrElse(
+          throw new NotFoundException(ERR_DATASET_VERSION_NOT_FOUND_MESSAGE)
+        )
+      }
+
+      val repositoryName = dataset.getRepositoryName
+      val versionHash = datasetVersion.getVersionHash
+      val objects = withLakeFSErrorHandling(
+        s"listing files of version '$versionHash' of dataset 
'${dataset.getName}'"
+      ) {
+        LakeFSStorageClient.retrieveObjectsOfVersion(repositoryName, 
versionHash)
+      }
+
+      if (objects.isEmpty) {
+        throw new NotFoundException(s"No objects found in version 
$versionHash")
+      }
+
+      val totalBytes = objects.map(_.getSizeBytes.longValue()).sum
+      // TODO: remove this limit once chunked resumable upload to Drive is 
implemented
+      if (totalBytes > DRIVE_EXPORT_MAX_DATASET_BYTES) {
+        throw new BadRequestException("Dataset version exceeds the 500 MB 
export limit.")
+      }
+      if (totalBytes > DRIVE_EXPORT_MAX_FILE_BYTES) {
+        throw new ForbiddenException("Dataset version exceeds the 5 TB Google 
Drive export limit.")
+      }
+
+      // Build the zip in memory
+      val baos = new ByteArrayOutputStream()
+      val zipOut = new java.util.zip.ZipOutputStream(baos)
+      try {
+        objects.foreach { obj =>
+          val filePath = obj.getPath
+          val file = withLakeFSErrorHandling(s"downloading file '$filePath' 
for Drive export") {
+            LakeFSStorageClient.getFileFromRepo(repositoryName, versionHash, 
filePath)
+          }
+          zipOut.putNextEntry(new java.util.zip.ZipEntry(filePath))
+          Files.copy(Paths.get(file.toURI), zipOut)
+          zipOut.closeEntry()
+        }
+      } finally {
+        zipOut.close()
+      }
+
+      val zipBytes = baos.toByteArray
+      val sessionUri = request.sessionUri
+
+      // PUT the zip to the Google Drive session URI
+      val conn = new 
URL(sessionUri).openConnection().asInstanceOf[HttpURLConnection]
+      try {
+        conn.setDoOutput(true)
+        conn.setRequestMethod("PUT")
+        conn.setRequestProperty("Content-Type", "application/zip")
+        conn.setFixedLengthStreamingMode(zipBytes.length)
+        val out = conn.getOutputStream
+        out.write(zipBytes)
+        out.close()
+
+        val code = conn.getResponseCode
+        if (code < 200 || code >= 300) {
+          val body =
+            Option(conn.getErrorStream).map(s => new 
String(s.readAllBytes())).getOrElse("")
+          if (body.contains("storageQuotaExceeded")) {
+            throw new ForbiddenException("Google Drive storage quota 
exceeded.")
+          }
+          throw new WebApplicationException(
+            s"Google Drive upload failed with HTTP $code",
+            Response.Status.BAD_GATEWAY
+          )
+        }
+      } finally {
+        conn.disconnect()
+      }
+
+      Response.ok(Map("message" -> "Dataset exported to Google Drive")).build()
+    }
+  }
+
+  @POST
+  @RolesAllowed(Array("REGULAR", "ADMIN"))
+  @Path("/drive-export/file")
+  @Consumes(Array(MediaType.APPLICATION_JSON))
+  def exportFileToDrive(
+      request: DriveFileExportRequest,
+      @Auth user: SessionUser
+  ): Response = {
+    if 
(!request.sessionUri.startsWith("https://www.googleapis.com/upload/drive/";)) {
+      throw new BadRequestException("Invalid session URI")
+    }
+    val uid = user.getUid
+    resolveDatasetAndPath(request.filePath, null, null, uid) match {

Review Comment:
   resolveDatasetAndPath only checks read access ("download restrictions 
handled per endpoint" per its comment), and this endpoint never applies the 
owner-or-downloadable check that exportDatasetToDrive does at line 1333. Any 
user with read access to a non-downloadable dataset can export its files to 
their own Drive.



##########
file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala:
##########
@@ -1361,6 +1305,187 @@ class DatasetResource extends LazyLogging {
     }
   }
 
+  @POST
+  @RolesAllowed(Array("REGULAR", "ADMIN"))
+  @Path("/{did}/drive-export")
+  @Consumes(Array(MediaType.APPLICATION_JSON))
+  def exportDatasetToDrive(
+      @PathParam("did") did: Integer,
+      @QueryParam("dvid") dvid: Integer,
+      @QueryParam("latest") latest: java.lang.Boolean,
+      request: DriveExportRequest,
+      @Auth user: SessionUser
+  ): Response = {
+    if 
(!request.sessionUri.startsWith("https://www.googleapis.com/upload/drive/";)) {
+      throw new BadRequestException("Invalid session URI")
+    }
+    withTransaction(context) { ctx =>

Review Comment:
   The LakeFS downloads, in-memory zip, and Drive PUT all run inside 
withTransaction, holding one of the pool's 10 DB connections for the whole 
transfer (unbounded, since no read timeout is set). Note getDatasetVersionZip 
avoids this: its downloads run via StreamingOutput after the transaction 
commits.



##########
common/config/src/main/resources/user-system.conf:
##########
@@ -27,6 +27,12 @@ user-sys {
     clientId = ""
     clientId = ${?USER_SYS_GOOGLE_CLIENT_ID}
 
+    clientSecret = ""
+    clientSecret = ${?USER_SYS_GOOGLE_CLIENT_SECRET}

Review Comment:
   ClientSecret is added but referenced nowhere in this PR. If the reworked 
frontend PR no longer does a server-side exchange, please drop this entry.



-- 
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]

Reply via email to