Skip to content

Commit

Permalink
Remove Tar archiving
Browse files Browse the repository at this point in the history
  • Loading branch information
dantb committed Sep 20, 2023
1 parent b0f70f9 commit 66a53bb
Show file tree
Hide file tree
Showing 13 changed files with 110 additions and 323 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import monix.execution.Scheduler

import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import akka.stream.alpakka.file.ArchiveMetadata

/**
* Archive download functionality.
Expand All @@ -55,10 +56,9 @@ trait ArchiveDownload {
* @param caller
* the caller to be used for checking for access
*/
def apply[M](
def apply(
value: ArchiveValue,
project: ProjectRef,
format: ArchiveFormat[M],
ignoreNotFound: Boolean
)(implicit caller: Caller, scheduler: Scheduler): IO[ArchiveRejection, AkkaSource]

Expand Down Expand Up @@ -96,18 +96,17 @@ object ArchiveDownload {
private val printer = Printer.spaces2.copy(dropNullValues = true)
private val sourcePrinter = Printer.spaces2.copy(dropNullValues = false)

override def apply[M](
override def apply(
value: ArchiveValue,
project: ProjectRef,
format: ArchiveFormat[M],
ignoreNotFound: Boolean
)(implicit caller: Caller, scheduler: Scheduler): IO[ArchiveRejection, AkkaSource] = {
for {
references <- value.resources.toList.traverse(toFullReference)
_ <- checkResourcePermissions(references, project)
contentStream <- resolveReferencesAsStream(references, project, ignoreNotFound, format)
contentStream <- resolveReferencesAsStream(references, project, ignoreNotFound)
} yield {
Source.fromGraph(StreamConverter(contentStream)).via(format.writeFlow)
Source.fromGraph(StreamConverter(contentStream)).via(Zip.writeFlow)
}
}

Expand All @@ -124,34 +123,29 @@ object ArchiveDownload {
}
}

private def resolveReferencesAsStream[M](
private def resolveReferencesAsStream(
references: List[FullArchiveReference],
project: ProjectRef,
ignoreNotFound: Boolean,
format: ArchiveFormat[M]
)(implicit caller: Caller): IO[ArchiveRejection, Stream[Task, (M, AkkaSource)]] = {
ignoreNotFound: Boolean
)(implicit caller: Caller): IO[ArchiveRejection, Stream[Task, (ArchiveMetadata, AkkaSource)]] = {
references
.traverseFilter {
case ref: FileReference => fileEntry(ref, project, format, ignoreNotFound)
case ref: ResourceReference => resourceEntry(ref, project, format, ignoreNotFound)
case ref: FileReference => fileEntry(ref, project, ignoreNotFound)
case ref: ResourceReference => resourceEntry(ref, project, ignoreNotFound)
}
.map(sortWith(format))
.map(sortWith)
.map(asStream)
}

private def sortWith[M](
format: ArchiveFormat[M]
)(list: List[(M, Task[AkkaSource])]): List[(M, Task[AkkaSource])] = {
list.sortBy { case (entry, _) =>
entry
}(format.ordering)
}
private def sortWith(list: List[(ArchiveMetadata, Task[AkkaSource])]): List[(ArchiveMetadata, Task[AkkaSource])] =
list.sortBy { case (entry, _) => entry }(Zip.ordering)

private def asStream[M](list: List[(M, Task[AkkaSource])]) = {
Stream.iterable(list).evalMap[Task, (M, AkkaSource)] { case (metadata, source) =>
private def asStream(
list: List[(ArchiveMetadata, Task[AkkaSource])]
): Stream[Task, (ArchiveMetadata, AkkaSource)] =
Stream.iterable(list).evalMap { case (metadata, source) =>
source.map(metadata -> _)
}
}

private def checkResourcePermissions(
refs: List[FullArchiveReference],
Expand All @@ -166,14 +160,13 @@ object ArchiveDownload {
)
.void

private def fileEntry[Metadata](
private def fileEntry(
ref: FileReference,
project: ProjectRef,
format: ArchiveFormat[Metadata],
ignoreNotFound: Boolean
)(implicit
caller: Caller
): IO[ArchiveRejection, Option[(Metadata, Task[AkkaSource])]] = {
): IO[ArchiveRejection, Option[(ArchiveMetadata, Task[AkkaSource])]] = {
val refProject = ref.project.getOrElse(project)
// the required permissions are checked for each file content fetch
val entry = fetchFileContent(ref.ref, refProject, caller)
Expand All @@ -184,21 +177,19 @@ object ArchiveDownload {
case FileRejection.AuthorizationFailed(addr, perm) => AuthorizationFailed(addr, perm)
case other => WrappedFileRejection(other)
}
.flatMap { case FileResponse(fileMetadata, content) =>
IO.fromEither(
pathOf(ref, project, format, fileMetadata.filename).map { path =>
val archiveMetadata = format.metadata(path, fileMetadata.bytes)
val contentTask: Task[AkkaSource] = content
.tapError(response =>
UIO.delay(
logger
.error(s"Error streaming file '${fileMetadata.filename}' for archive: ${response.value.value}")
)
)
.mapError(response => ArchiveDownloadError(fileMetadata.filename, response))
Some((archiveMetadata, contentTask))
}
)
.map { case FileResponse(fileMetadata, content) =>
val path = pathOf(ref, project, fileMetadata.filename)
val archiveMetadata = Zip.metadata(path)
val contentTask: Task[AkkaSource] = content
.tapError(response =>
UIO.delay(
logger
.error(s"Error streaming file '${fileMetadata.filename}' for archive: ${response.value.value}")
)
)
.mapError(response => ArchiveDownloadError(fileMetadata.filename, response))
Some((archiveMetadata, contentTask))

}
if (ignoreNotFound) entry.onErrorRecover { case _: ResourceNotFound => None }
else entry
Expand All @@ -207,27 +198,21 @@ object ArchiveDownload {
private def pathOf(
ref: FileReference,
project: ProjectRef,
format: ArchiveFormat[_],
filename: String
): Either[FilenameTooLong, String] =
ref.path.map { p => Right(p.value.toString) }.getOrElse {
): String =
ref.path.map(_.value.toString).getOrElse {
val p = ref.project.getOrElse(project)
Either.cond(
format != ArchiveFormat.Tar || filename.length < 100,
s"$p/file/$filename",
FilenameTooLong(ref.ref.original, p, filename)
)
s"$p/file/$filename"
}

private def resourceEntry[Metadata](
private def resourceEntry(
ref: ResourceReference,
project: ProjectRef,
format: ArchiveFormat[Metadata],
ignoreNotFound: Boolean
): IO[ArchiveRejection, Option[(Metadata, Task[AkkaSource])]] = {
): IO[ArchiveRejection, Option[(ArchiveMetadata, Task[AkkaSource])]] = {
val archiveEntry = resourceRefToByteString(ref, project).map { content =>
val path = pathOf(ref, project)
val metadata = format.metadata(path, content.length.toLong)
val metadata = Zip.metadata(path)
Some((metadata, Task.pure(Source.single(content))))
}
if (ignoreNotFound) archiveEntry.onErrorHandle { _: ResourceNotFound => None }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,12 @@ class Archives(
def download(
id: IdSegment,
project: ProjectRef,
format: ArchiveFormat[_],
ignoreNotFound: Boolean
)(implicit caller: Caller, scheduler: Scheduler): IO[ArchiveRejection, AkkaSource] =
(for {
resource <- fetch(id, project)
value = resource.value
source <- archiveDownload(value.value, project, format, ignoreNotFound)
source <- archiveDownload(value.value, project, ignoreNotFound)
} yield source).span("downloadArchive")

private def eval(cmd: CreateArchive): IO[ArchiveRejection, ArchiveResource] =
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,6 @@ object ArchiveRejection {
)).mkString("\n")
)

final case class FilenameTooLong(id: Iri, project: ProjectRef, fileName: String)
extends ArchiveRejection(
s"File '$id' in project '$project' has a file name '$fileName' exceeding the 100 character limit for a tar file."
)

/**
* Rejection returned when an archive doesn't exist.
*
Expand Down Expand Up @@ -201,7 +196,6 @@ object ArchiveRejection {
HttpResponseFields {
case ResourceAlreadyExists(_, _) => StatusCodes.Conflict
case InvalidResourceCollection(_, _, _) => StatusCodes.BadRequest
case FilenameTooLong(_, _, _) => StatusCodes.BadRequest
case ArchiveNotFound(_, _) => StatusCodes.NotFound
case InvalidArchiveId(_) => StatusCodes.BadRequest
case ProjectContextRejection(rejection) => rejection.status
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package ch.epfl.bluebrain.nexus.delta.plugins.archive.model

import akka.NotUsed
import akka.http.scaladsl.model.{ContentType, HttpRequest, MediaTypes}
import akka.stream.alpakka.file.scaladsl.Archive
import akka.stream.alpakka.file.ArchiveMetadata
import akka.stream.scaladsl.{Flow, Source}
import akka.util.ByteString
import ch.epfl.bluebrain.nexus.delta.sdk.utils.HeadersUtils

/**
* Zip archive format
*
* @see
* https://en.wikipedia.org/wiki/ZIP_(file_format)#Limits for the limitations
*/
object Zip {
type WriteFlow[Metadata] = Flow[(Metadata, Source[ByteString, _]), ByteString, NotUsed]

def apply(req: HttpRequest): Option[Zip.type] =
if (HeadersUtils.matches(req.headers, Zip.contentType.mediaType)) Some(Zip) else None

def contentType: ContentType = MediaTypes.`application/zip`

def fileExtension: String = "zip"

def metadata(filename: String): ArchiveMetadata =
ArchiveMetadata.create(filename)

def writeFlow: WriteFlow[ArchiveMetadata] = Archive.zip()

def ordering: Ordering[ArchiveMetadata] = Ordering.by(md => md.filePath)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import akka.http.scaladsl.model.StatusCodes.{Created, SeeOther}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.{Directive1, Route}
import ch.epfl.bluebrain.nexus.delta.plugins.archive.Archives
import ch.epfl.bluebrain.nexus.delta.plugins.archive.model.{permissions, ArchiveFormat}
import ch.epfl.bluebrain.nexus.delta.plugins.archive.model.permissions
import ch.epfl.bluebrain.nexus.delta.plugins.archive.model.Zip
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution
import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering
import ch.epfl.bluebrain.nexus.delta.sdk.AkkaSource
Expand Down Expand Up @@ -76,14 +77,14 @@ class ArchiveRoutes(
(get & pathEndOrSingleSlash) {
authorizeFor(ref, permissions.read).apply {
archiveResponse {
case Some(format) =>
case Some(_) =>
parameter("ignoreNotFound".as[Boolean] ? false) { ignoreNotFound =>
val response = archives.download(id, ref, format, ignoreNotFound).map { source =>
sourceToFileResponse(source, format)
val response = archives.download(id, ref, ignoreNotFound).map { source =>
sourceToFileResponse(source)
}
emit(response)
}
case None => emit(archives.fetch(id, ref))
case None => emit(archives.fetch(id, ref))
}
}
}
Expand All @@ -96,9 +97,9 @@ class ArchiveRoutes(
}
}

private def sourceToFileResponse(source: AkkaSource, format: ArchiveFormat[_]): FileResponse =
FileResponse(s"archive.${format.fileExtension}", format.contentType, 0L, source)
private def sourceToFileResponse(source: AkkaSource): FileResponse =
FileResponse(s"archive.${Zip.fileExtension}", Zip.contentType, 0L, source)

private def archiveResponse: Directive1[Option[ArchiveFormat[_]]] =
extractRequest.map(ArchiveFormat(_))
private def archiveResponse: Directive1[Option[Zip.type]] =
extractRequest.map(Zip(_))
}
Loading

0 comments on commit 66a53bb

Please sign in to comment.