Skip to content

Commit 9ecd32c

Browse files
aicambobbai00
andauthored
Fix download operators result (#3241)
This PR is in continuous of [PR 3221](#3221) to implement download logic in backend. ## Export request structure updates In this PR, we update `ResultExportRequest` to include a new variable named `destination`. If its set to `local`, means the user wants to download operators result. Also, `operatorIds` is now plural so with one request, multiple operators can be exported (locally or to dataset). ## Export endpoint updates In this PR, we update `ResultResource` to support `destination=local`. In this regard, we have two cases: - If destination is local and one operator is selected, download one operator result based on export type - If destination is local and multiple operators are selected, download a zip file containing all The export to dataset follows the old logic. ## Export service updates In this PR, we update `ResultExportService` to have two core more new functions: - `exportOperatorResultAsStream`: For local download of a single operator. Streams the data directly. - `exportOperatorsAsZip`: For local download of multiple operators. Zip the output and stream back. ## Demo video https://github.com/user-attachments/assets/b46b33a7-9503-4ed8-99f2-9dc15878e54d --------- Co-authored-by: Jiadong Bai <[email protected]>
1 parent eff3e04 commit 9ecd32c

File tree

12 files changed

+594
-519
lines changed

12 files changed

+594
-519
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package edu.uci.ics.texera.web.model.websocket.request
22

33
case class ResultExportRequest(
4-
exportType: String,
4+
exportType: String, // e.g. "csv", "google_sheet", "arrow", "data"
55
workflowId: Int,
66
workflowName: String,
7-
operatorId: String,
8-
operatorName: String,
7+
operatorIds: List[String], // changed from single operatorId: String -> List of strings
98
datasetIds: List[Int],
10-
rowIndex: Int,
11-
columnIndex: Int,
12-
filename: String
9+
rowIndex: Int, // used by "data" export
10+
columnIndex: Int, // used by "data" export
11+
filename: String, // optional filename override
12+
destination: String // "dataset" or "local"
1313
)
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
package edu.uci.ics.texera.web.model.websocket.response
22

3-
import edu.uci.ics.texera.web.model.websocket.event.TexeraWebSocketEvent
4-
5-
case class ResultExportResponse(status: String, message: String) extends TexeraWebSocketEvent
3+
case class ResultExportResponse(status: String, message: String)

core/amber/src/main/scala/edu/uci/ics/texera/web/resource/ResultResource.scala

+60-6
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import edu.uci.ics.texera.web.service.ResultExportService
99
import io.dropwizard.auth.Auth
1010

1111
import javax.ws.rs._
12-
import javax.ws.rs.core.Response
12+
import javax.ws.rs.core.{MediaType, Response}
13+
import javax.ws.rs.core.Response.Status
1314
import scala.jdk.CollectionConverters._
1415

1516
@Path("/result")
17+
@Produces(Array(MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM))
1618
class ResultResource extends LazyLogging {
1719

1820
@POST
@@ -22,21 +24,73 @@ class ResultResource extends LazyLogging {
2224
@Auth user: SessionUser
2325
): Response = {
2426

27+
if (request.operatorIds.size <= 0)
28+
Response
29+
.status(Response.Status.BAD_REQUEST)
30+
.`type`(MediaType.APPLICATION_JSON)
31+
.entity(Map("error" -> "No operator selected").asJava)
32+
.build()
33+
2534
try {
26-
val resultExportService = new ResultExportService(WorkflowIdentity(request.workflowId))
35+
request.destination match {
36+
case "local" =>
37+
// CASE A: multiple operators => produce ZIP
38+
if (request.operatorIds.size > 1) {
39+
val resultExportService = new ResultExportService(WorkflowIdentity(request.workflowId))
40+
val (zipStream, zipFileNameOpt) =
41+
resultExportService.exportOperatorsAsZip(user.user, request)
42+
43+
if (zipStream == null) {
44+
throw new RuntimeException("Zip stream is null")
45+
}
46+
47+
val finalFileName = zipFileNameOpt.getOrElse("operators.zip")
48+
return Response
49+
.ok(zipStream, "application/zip")
50+
.header("Content-Disposition", "attachment; filename=\"" + finalFileName + "\"")
51+
.build()
52+
}
2753

28-
val exportResponse: ResultExportResponse =
29-
resultExportService.exportResult(user.user, request)
54+
// CASE B: exactly one operator => single file
55+
if (request.operatorIds.size != 1) {
56+
return Response
57+
.status(Response.Status.BAD_REQUEST)
58+
.`type`(MediaType.APPLICATION_JSON)
59+
.entity(Map("error" -> "Local download supports no operator or many.").asJava)
60+
.build()
61+
}
62+
val singleOpId = request.operatorIds.head
3063

31-
Response.ok(exportResponse).build()
64+
val resultExportService = new ResultExportService(WorkflowIdentity(request.workflowId))
65+
val (streamingOutput, fileNameOpt) =
66+
resultExportService.exportOperatorResultAsStream(request, singleOpId)
3267

68+
if (streamingOutput == null) {
69+
return Response
70+
.status(Response.Status.INTERNAL_SERVER_ERROR)
71+
.`type`(MediaType.APPLICATION_JSON)
72+
.entity(Map("error" -> "Failed to export operator").asJava)
73+
.build()
74+
}
75+
76+
val finalFileName = fileNameOpt.getOrElse("download.dat")
77+
Response
78+
.ok(streamingOutput, MediaType.APPLICATION_OCTET_STREAM)
79+
.header("Content-Disposition", "attachment; filename=\"" + finalFileName + "\"")
80+
.build()
81+
case _ =>
82+
// destination = "dataset" by default
83+
val resultExportService = new ResultExportService(WorkflowIdentity(request.workflowId))
84+
val exportResponse = resultExportService.exportResult(user.user, request)
85+
Response.ok(exportResponse).build()
86+
}
3387
} catch {
3488
case ex: Exception =>
3589
Response
3690
.status(Response.Status.INTERNAL_SERVER_ERROR)
91+
.`type`(MediaType.APPLICATION_JSON)
3792
.entity(Map("error" -> ex.getMessage).asJava)
3893
.build()
3994
}
4095
}
41-
4296
}

0 commit comments

Comments
 (0)