Skip to content
Open
889 changes: 495 additions & 394 deletions app/controllers/ProjectEntryController.scala

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions app/services/ZipService.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package services

import java.io.File
import java.nio.file.{Files, Paths}
import java.util.zip.{ZipEntry, ZipOutputStream}
import scala.concurrent.Future
import akka.stream.scaladsl.Source
import akka.util.ByteString
import akka.stream.scaladsl.StreamConverters
import javax.inject.Inject

class ZipService @Inject()() {

def zipProjectAndAssets(projectFilePath: String, assetFolderPath: String): Source[ByteString, _] = {
import scala.concurrent.ExecutionContext.Implicits.global
StreamConverters.asOutputStream().mapMaterializedValue { os =>
Future {
val zipOut = new ZipOutputStream(os)
try {
// Add project file to zip
val projectFile = Paths.get(projectFilePath)
val projectFileEntry = new ZipEntry(projectFile.getFileName.toString)
zipOut.putNextEntry(projectFileEntry)
Files.copy(projectFile, zipOut)
zipOut.closeEntry()

// Add asset folder contents to zip
def addFolderToZip(folder: File, parentFolder: String): Unit = {
val files = folder.listFiles()
if (files != null && files.nonEmpty) {
files.foreach { file =>
val entryName = parentFolder + "/" + file.getName
if (file.isDirectory) {
addFolderToZip(file, entryName)
} else {
zipOut.putNextEntry(new ZipEntry(entryName))
Files.copy(file.toPath, zipOut)
zipOut.closeEntry()
}
}
} else {
zipOut.putNextEntry(new ZipEntry(parentFolder + "/"))
zipOut.closeEntry()
}
}

val assetFolder = new File(assetFolderPath)
addFolderToZip(assetFolder, assetFolder.getName)
} finally {
zipOut.close()
}
}
}
}
}
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ val akkaVersion = "2.8.4"
//messaging persistence and clustering
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-persistence" % akkaVersion,
"com.typesafe.akka" %% "akka-stream" % akkaVersion,
"com.typesafe.akka" %% "akka-persistence-query" % akkaVersion,
"com.github.dnvriend" %% "akka-persistence-jdbc" % "3.5.3",
"com.typesafe.akka" %% "akka-actor-typed" % akkaVersion,
Expand Down
6 changes: 3 additions & 3 deletions frontend/app/ProjectEntryList/ProjectEntryEditComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
updateProjectOpenedStatus,
getSimpleProjectTypeData,
getMissingFiles,
downloadProjectFile,
downloadProject,
} from "./helpers";
import {
SystemNotification,
Expand Down Expand Up @@ -457,7 +457,7 @@ const ProjectEntryEditComponent: React.FC<ProjectEntryEditComponentProps> = (
variant="contained"
onClick={async () => {
try {
await downloadProjectFile(project.id);
await downloadProject(project.id);
} catch (error) {
SystemNotification.open(
SystemNotifcationKind.Error,
Expand All @@ -467,7 +467,7 @@ const ProjectEntryEditComponent: React.FC<ProjectEntryEditComponentProps> = (
}
}}
>
Download&nbsp;Project&nbsp;File
Download&nbsp;Project
</Button>
</Tooltip>
) : null}
Expand Down
45 changes: 26 additions & 19 deletions frontend/app/ProjectEntryList/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,7 @@ export const getMissingFiles = async (id: number): Promise<MissingFiles[]> => {
}
};

export const downloadProjectFile = async (id: number) => {
export const downloadProject = async (id: number) => {
const url = `${deploymentRootPath}${API_PROJECTS}/${id}/fileDownload`;

const token = localStorage.getItem("pluto:access-token");
Expand All @@ -599,24 +599,31 @@ export const downloadProjectFile = async (id: number) => {

let filename = "";

fetch(url, newInit)
.then((response) => {
// @ts-ignore
filename = response.headers
.get("Content-Disposition")
.split('filename="')[1]
.split('";')[0];
try {
const response = await fetch(url, newInit);

if (filename.substr(filename.length - 1)) {
filename = filename.slice(0, -1);
}
if (!response.ok) {
throw new Error(
`HTTP error! status: ${response.status} ${response.statusText}`
);
}

return response.blob();
})
.then((blob) => {
saveAs(blob, filename);
})
.catch((err) => {
console.log(err);
});
// @ts-ignore
filename = response.headers
.get("Content-Disposition")
.split('filename="')[1]
.split('";')[0];

if (filename.substr(filename.length - 1)) {
filename = filename.slice(0, -1);
}

const blob = await response.blob();
saveAs(blob, filename);
} catch (err) {
console.error(err);
// Display the error to the user
// replace this with your notification or alert system
alert(`An error occurred while downloading the project: ${err.message}`);
}
};
9 changes: 9 additions & 0 deletions test/controllers/ProjectEntryControllerSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import play.api.db.slick.DatabaseConfigProvider
import play.api.libs.json.JsValue
import play.api.test.Helpers._
import play.api.test._
import services.ZipService
import slick.jdbc.JdbcProfile

import scala.concurrent.Await
Expand Down Expand Up @@ -248,4 +249,12 @@ class ProjectEntryControllerSpec extends Specification with utils.BuildMyApp wit
resultList.length mustEqual 0
}
}
//
// "ProjectEntryController fileDownload" should {
// "Return 200 OK for a valid file download request" in new WithApplication(buildApp) {
// val response = route(app, FakeRequest(GET, "/api/project/1/fileDownload").withSession("uid" -> "testuser")).get
//
// status(response) mustEqual OK
// }
// }
}
66 changes: 66 additions & 0 deletions test/services/ZipServiceSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package services

import akka.actor.ActorSystem
import akka.stream.Materializer
import org.specs2.mock.Mockito
import org.specs2.mutable.Specification

import scala.concurrent.duration._
import akka.stream.scaladsl._
import akka.util.ByteString
import org.specs2.runner.sbtRun.env.executionContext

import java.nio.file.{Files, Path}
import java.io._
import java.util.zip.ZipInputStream
import scala.concurrent.{Await, ExecutionContext}
import scala.util.Try

class ZipServiceSpec extends Specification with Mockito {

implicit lazy val actorSystem: ActorSystem = ActorSystem("pluto-core-download", defaultExecutionContext = Some(executionContext))
implicit lazy val mat: Materializer = Materializer(actorSystem)

implicit val ec: ExecutionContext = ExecutionContext.Implicits.global

val zipService = new ZipService()

"ZipService" should {

"zip project and assets correctly" in {
// Initialize projectFile and assetDir
val projectFile: Path = Files.createTempFile("project", ".pproj")
val assetDir: Path = Files.createTempDirectory("assets")

// Write some content to the project file and asset directory
Files.write(projectFile, "Hello, project!".getBytes)
val assetFile = Files.createFile(assetDir.resolve("asset.mp4"))
Files.write(assetFile, "Hello, asset!".getBytes)

// Call the method under test
val resultSource: Source[ByteString, _] = zipService.zipProjectAndAssets(projectFile.toString, assetDir.toString)

// Collect the result into a byte array
val resultFuture = resultSource.runFold(ByteString.empty)(_ ++ _)
val resultBytes = Await.result(resultFuture, 5.seconds).toArray

// Check that the result is a valid zip file containing the expected entries
val zipInputStream = new ZipInputStream(new ByteArrayInputStream(resultBytes))

val projectEntry = zipInputStream.getNextEntry
projectEntry.getName must beEqualTo(projectFile.getFileName.toString)
scala.io.Source.fromInputStream(zipInputStream).mkString must beEqualTo("Hello, project!")

val assetEntry = zipInputStream.getNextEntry
assetEntry.getName must beEqualTo(assetDir.getFileName.toString + "/asset.mp4")
scala.io.Source.fromInputStream(zipInputStream).mkString must beEqualTo("Hello, asset!")

// Cleanup
Try(Files.deleteIfExists(projectFile))
Try(Files.deleteIfExists(assetFile))
Try(Files.deleteIfExists(assetDir))

success
}
}
}