diff --git a/daikoku/app/controllers/AssetsController.scala b/daikoku/app/controllers/AssetsController.scala index 077b3373b..f9db3c490 100644 --- a/daikoku/app/controllers/AssetsController.scala +++ b/daikoku/app/controllers/AssetsController.scala @@ -317,6 +317,7 @@ class TeamAssetsController( ) } case None => + println("HEHE") NotFound(Json.obj("error" -> "Asset not found!")) } } diff --git a/daikoku/app/services/AssetsService.scala b/daikoku/app/services/AssetsService.scala index cabbf6e0f..4056f52cc 100644 --- a/daikoku/app/services/AssetsService.scala +++ b/daikoku/app/services/AssetsService.scala @@ -1,14 +1,13 @@ package fr.maif.otoroshi.daikoku.services import fr.maif.otoroshi.daikoku.actions.ApiActionContext -import fr.maif.otoroshi.daikoku.audit.AuditTrailEvent -import fr.maif.otoroshi.daikoku.ctrls.CmsApiActionContext import fr.maif.otoroshi.daikoku.domain.{Asset, AssetId} import fr.maif.otoroshi.daikoku.env.Env import fr.maif.otoroshi.daikoku.logger.AppLogger import fr.maif.otoroshi.daikoku.services.NormalizeSupport.normalize import fr.maif.otoroshi.daikoku.utils.IdGenerator import fr.maif.otoroshi.daikoku.utils.StringImplicits.BetterString +import org.apache.pekko.http.scaladsl.model.{ContentType, ContentTypes, HttpResponse} import org.apache.pekko.http.scaladsl.util.FastFuture import org.apache.pekko.stream.connectors.s3.ObjectMetadata import org.apache.pekko.stream.scaladsl.{Sink, Source} @@ -153,6 +152,10 @@ class AssetsService { .flatMap(slug => if (slug.isEmpty) None else Some(slug)) val assetId = AssetId(IdGenerator.uuid) + println(contentType) + + Future.successful(Ok(Json.obj())) + ctx.tenant.bucketSettings match { case None => FastFuture.successful( @@ -281,28 +284,29 @@ class AssetsService { def listAssets[T](ctx: ApiActionContext[T])(implicit env: Env) = { implicit val ec = env.defaultExecutionContext - ctx.request.getQueryString("teamId") match { - case Some(teamId) => - ctx.tenant.bucketSettings match { - case None => - FastFuture.successful( - NotFound(Json.obj("error" -> "No bucket config found !")) - ) - case Some(cfg) => - env.assetsStore.listTenantAssets(ctx.tenant.id)(cfg).map { res => - Ok(JsArray(res.map(_.asJson))) - } - } + ctx.tenant.bucketSettings match { case None => - ctx.tenant.bucketSettings match { - case None => - FastFuture.successful( - NotFound(Json.obj("error" -> "No bucket config found !")) - ) - case Some(cfg) => - env.assetsStore.listTenantAssets(ctx.tenant.id)(cfg).map { res => - Ok(JsArray(res.map(_.asJson))) - } + FastFuture.successful( + NotFound(Json.obj("error" -> "No bucket config found !")) + ) + case Some(cfg) => + for { + slugs <- env.dataStore.assetRepo + .forTenant(ctx.tenant) + .findWithProjection(Json.obj(), Json.obj("slug" -> true, "_id" -> true)) + .map(items => items.foldLeft(Map.empty[String, Option[String]]) { case (acc, item) => + acc + ((item \ "_id").as[String] -> (item \ "slug").asOpt[String]) + }) + assets <- env.assetsStore.listTenantAssets(ctx.tenant.id)(cfg) + } yield { + Ok(JsArray(assets.map(item => { + val id = item.content.key.split("/").last + + (slugs.get(id) match { + case Some(slug) => item.copy(slug = slug) + case None => item + }).asJson + }))) } } } @@ -382,6 +386,7 @@ class AssetsService { ) = { implicit val ec = env.defaultExecutionContext + ctx.tenant.bucketSettings match { case None => FastFuture.successful( @@ -415,28 +420,38 @@ class AssetsService { case Some(_) if download => env.assetsStore .getTenantAsset(ctx.tenant.id, AssetId(assetId))(cfg) - .map { - case None => - NotFound(Json.obj("error" -> "Asset not found!")) - case Some((source, meta)) => - val filename = meta.metadata - .filter(_.name().startsWith("x-amz-meta-")) - .find(_.name() == "x-amz-meta-filename") - .map(_.value()) - .getOrElse("asset.txt") - - Ok.sendEntity( - HttpEntity.Streamed( - source, - None, - meta.contentType - .map(Some.apply) - .getOrElse(Some("application/octet-stream")) - ) - ) - .withHeaders( - "Content-Disposition" -> s"""attachment; filename="$filename"""" - ) + .map { case (metadata, data, s3Source) => + +// case None => +// NotFound(Json.obj("error" -> "Asset not found!")) +// case Some((source, meta)) => +// val filename = meta.metadata +// .filter(_.name().startsWith("x-amz-meta-")) +// .find(_.name() == "x-amz-meta-filename") +// .map(_.value()) +// .getOrElse("asset.txt") + +// Ok.send( + val entity = org.apache.pekko.http.scaladsl.model.HttpEntity( + metadata.contentType + .flatMap(ContentType.parse(_).toOption) + .getOrElse(ContentTypes.`application/octet-stream`), + metadata.contentLength, + s3Source) + + Ok.sendEntity(HttpEntity.Streamed(entity, None, None)) + +// ) +// HttpEntity.Streamed( +// Source.single(source), +// None, +// meta.contentType +// .map(Some.apply) +// .getOrElse(Some("application/octet-stream")) +// ) +// .withHeaders( +// "Content-Disposition" -> s"""attachment; filename="$filename"""" +// ) } case Some(url) => env.wsClient diff --git a/daikoku/app/utils/s3.scala b/daikoku/app/utils/s3.scala index 2508a548b..a1125a63c 100644 --- a/daikoku/app/utils/s3.scala +++ b/daikoku/app/utils/s3.scala @@ -3,14 +3,11 @@ package fr.maif.otoroshi.daikoku.utils import com.amazonaws.auth.{AWSStaticCredentialsProvider, BasicAWSCredentials} import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration import com.amazonaws.services.s3.AmazonS3ClientBuilder +import com.amazonaws.services.s3.model.PutObjectRequest import com.amazonaws.{ClientConfiguration, HttpMethod, SdkClientException} import fr.maif.otoroshi.daikoku.domain._ import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.http.scaladsl.model.{ - ContentType, - ContentTypes, - HttpHeader -} +import org.apache.pekko.http.scaladsl.model.{ContentType, ContentTypes, HttpHeader} import org.apache.pekko.stream.Materializer import org.apache.pekko.stream.connectors.s3.headers.CannedAcl import org.apache.pekko.stream.connectors.s3.scaladsl.S3 @@ -35,7 +32,8 @@ class BadFileContentFromContentType() case class S3ListItem( content: ListBucketResultContents, - objectMetadata: ObjectMetadata + objectMetadata: ObjectMetadata, + slug: Option[String] = None ) { def asJson: JsValue = Json.obj( @@ -57,7 +55,8 @@ case class S3ListItem( objectMetadata.metadata .filter(_.name().startsWith("x-amz-meta-")) .map(h => (h.name().replace("x-amz-meta-", ""), JsString(h.value()))) - ) + ), + "slug" -> slug ) } @@ -121,7 +120,9 @@ class AssetsDataStore(actorSystem: ActorSystem)(implicit override def getRegion: Region = Region.of(conf.region) }, listBucketApiVersion = ApiVersion.ListBucketVersion2 - ).withEndpointUrl(conf.endpoint) + ) + .withEndpointUrl(conf.endpoint) + .withAccessStyle(AccessStyle.PathAccessStyle) S3Attributes.settings(settings) } @@ -140,6 +141,7 @@ class AssetsDataStore(actorSystem: ActorSystem)(implicit val ctype = ContentType .parse(contentType) .getOrElse(ContentTypes.`application/octet-stream`) + val meta = MetaHeaders( Map( "filename" -> name, @@ -151,6 +153,14 @@ class AssetsDataStore(actorSystem: ActorSystem)(implicit "content-type" -> ctype.value ) ) + + lazy val opts = new ClientConfiguration() + lazy val endpointConfiguration = + new EndpointConfiguration(conf.endpoint, conf.region) + lazy val credentialsProvider = new AWSStaticCredentialsProvider( + new BasicAWSCredentials(conf.access, conf.secret) + ) + val sink = S3 .multipartUpload( bucket = conf.bucket, @@ -278,7 +288,7 @@ class AssetsDataStore(actorSystem: ActorSystem)(implicit contentType = ctype, metaHeaders = meta, cannedAcl = CannedAcl.Private, // CannedAcl.PublicRead - chunkingParallelism = 1 + chunkingParallelism = 1, ) .withAttributes(s3ClientSettingsAttrs) content.toMat(sink)(Keep.right).run() @@ -312,17 +322,24 @@ class AssetsDataStore(actorSystem: ActorSystem)(implicit val attrs = s3ClientSettingsAttrs S3.listBucket(conf.bucket, Some(s"/${tenant.value}/tenant-assets")) .mapAsync(1) { content => - val none: Option[ObjectMetadata] = None - S3.getObjectMetadata(conf.bucket, content.key) - .withAttributes(attrs) - .runFold(none)((_, opt) => opt) - .map { - case None => - S3ListItem( - content, - ObjectMetadata(collection.immutable.Seq.empty[HttpHeader]) +// S3.getObjectMetadata(conf.bucket, content.key) +// .withAttributes(s3ClientSettingsAttrs) +// .toMat(Sink.head)(Keep.both) +// .run() +// ._2 +// .map(meta => S3ListItem(content, meta.getOrElse(ObjectMetadata(Seq.empty)))) + val (metadataFuture, _dataFuture) = S3.getObject( + conf.bucket, + content.key ) - case Some(meta) => S3ListItem(content, meta) + .withAttributes(s3ClientSettingsAttrs) + .toMat(Sink.head)(Keep.both) + .run() + + for { + metadata <- metadataFuture + } yield { + S3ListItem(content, metadata) } } .withAttributes(attrs) @@ -343,11 +360,45 @@ class AssetsDataStore(actorSystem: ActorSystem)(implicit def getTenantAsset(tenant: TenantId, asset: AssetId)(implicit conf: S3Configuration - ): Future[Option[(Source[ByteString, NotUsed], ObjectMetadata)]] = { - val none: Option[(Source[ByteString, NotUsed], ObjectMetadata)] = None - S3.getObject(conf.bucket, s"/${tenant.value}/tenant-assets/${asset.value}") - .withAttributes(s3ClientSettingsAttrs) - .runFold(none)((opt, _) => opt) + ): Future[(ObjectMetadata, ByteString, Source[ByteString, Future[ObjectMetadata]])] = { +// val none: Option[(Source[ByteString, NotUsed], ObjectMetadata)] = None + +// val s3Source: Source[ByteString, Future[ObjectMetadata]] = S3.getObject(conf.bucket, s"/${tenant.value}/tenant-assets/${asset.value}") +// s3Source + val s3Source: Source[ByteString, Future[ObjectMetadata]] = + S3.getObject(conf.bucket, s"/${tenant.value}/tenant-assets/${asset.value}") + + val (metadataFuture, dataFuture) = + s3Source.toMat(Sink.head)(Keep.both).run() + + for { + m <- metadataFuture + d <- dataFuture + } yield { + (m, d, s3Source) + } + +// val (meta, data) = S3.getObject(conf.bucket, s"/${tenant.value}/tenant-assets/${asset.value}") +// .withAttributes(s3ClientSettingsAttrs) +// .toMat(Sink.head)(Keep.both) +// .run() +// +// for { +// m <- meta +// d <- data +// } yield { +// (d, m) +// } + +// S3.getObject(conf.bucket, s"/${tenant.value}/tenant-assets/${asset.value}") +// .withAttributes(s3ClientSettingsAttrs) +// .runFold(none) { case (meta, data) => +// +// println(meta) +// println(data) +// +// meta +// } } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/daikoku/javascript/src/components/backoffice/assets/AssetsList.tsx b/daikoku/javascript/src/components/backoffice/assets/AssetsList.tsx index bdece5c38..4f15fe141 100644 --- a/daikoku/javascript/src/components/backoffice/assets/AssetsList.tsx +++ b/daikoku/javascript/src/components/backoffice/assets/AssetsList.tsx @@ -219,6 +219,10 @@ export const AssetsList = ({ header: translate('Title'), meta: { style: { textAlign: 'left' } }, }), + columnHelper.accessor(row => row.slug || '--', { + header: translate('Slug'), + meta: { style: { textAlign: 'left' } }, + }), columnHelper.accessor(row => row.meta.desc || '--', { header: translate('Description'), meta: { style: { textAlign: 'left' } }, diff --git a/daikoku/javascript/src/types/tenant.ts b/daikoku/javascript/src/types/tenant.ts index 292e25ad4..d17fe2b2c 100644 --- a/daikoku/javascript/src/types/tenant.ts +++ b/daikoku/javascript/src/types/tenant.ts @@ -235,6 +235,7 @@ export interface IAsset { contentType: string; meta: { [key: string]: string }; link: string; + slug?: string; } export interface ITenantAdministration {