Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
e8b3124
Disable lower environment.
tonytw1 May 7, 2026
8a8cc9e
Push raw s3 client usages to the S3 trait. S3 client field now private
tonytw1 Dec 15, 2024
7510815
Push s3 end point to all s3 users. Bucket specific end points.
tonytw1 Nov 29, 2024
64d1d13
Setting up for GCP and AWS clients by introducing s3 end point along …
tonytw1 May 23, 2026
29aa7fa
Image projector is bucket S3 endpoint aware.
tonytw1 Feb 24, 2026
27ac5c8
Image bucket takes end point config from config.
tonytw1 Nov 29, 2024
7307530
Ingest and rejected buckets have configurable end points.
tonytw1 Jan 17, 2025
bc2bab1
Thumbs bucket has configurable end point.
tonytw1 Jan 24, 2025
d791aa6
Configure a GCP S3 client and selectively use it based on the bucket …
tonytw1 Nov 29, 2024
542539a
v2 sigs on all AWS buckets for better caching.
tonytw1 Nov 30, 2024
11217d4
Setup a local Minio S3 client.
tonytw1 May 23, 2026
c30b533
Local S3.
tonytw1 Jan 23, 2025
ba390f6
Local S3.
tonytw1 Jan 23, 2025
f9d262c
Local S3 urls to keys.
tonytw1 Jan 24, 2025
88a1179
Local S3 - host.
tonytw1 Jan 24, 2025
3fea9ae
Kahuna UI can connect to ingest bucket endpoint.
tonytw1 Jan 25, 2025
94076e5
Noisy log.
tonytw1 Apr 2, 2025
caed555
Noisy log Redact debug.
tonytw1 Apr 2, 2025
b53273d
GCP S3 interop does not support S3 bulkDelete; reimplement as single …
tonytw1 Apr 13, 2025
eb1da7e
Logging tidy up; storeImage
tonytw1 Apr 28, 2025
bcb6526
Logging tidy up; thumbnail bucket.
tonytw1 Apr 28, 2025
bd0a1dc
Clean up; unused unit assignment.
tonytw1 Apr 28, 2025
0727f02
Logging; lower to debug.
tonytw1 Apr 28, 2025
f698542
Logging; bucket name.
tonytw1 Apr 28, 2025
8a80abf
Logging; bucket name.
tonytw1 Apr 28, 2025
6a40195
Inject S3 into Crops from CropperComponents.
tonytw1 Feb 7, 2026
80c0a7d
S3Object takes a full S3Bucket as it's bucket parameter. endpoint par…
tonytw1 May 23, 2026
db3418d
S3 objectUrl for bucket and key can move to the bucket (which knows i…
tonytw1 Sep 14, 2025
d73027d
S3KeyFromURL move to S3Bucket as it know's it's own URL scheme.
tonytw1 Sep 14, 2025
a3aab45
Clean up; further isolating S3Client as the cloudfront concern.
tonytw1 Sep 14, 2025
24e0936
Refactor; extracting the is path style URLs bucket decision.
tonytw1 Sep 14, 2025
cd04a16
S3 path based URLs decision moves to bucket property and config.
tonytw1 Sep 14, 2025
1a13d05
Extract bucket base URL function.
tonytw1 Dec 13, 2025
ae581bc
Crops bucket can be on non AWS bucket.
tonytw1 Jan 25, 2026
46b4bfa
S3 Client moves onto the S3 Bucket.
tonytw1 May 23, 2026
198f4f4
Overly verbose fileMarkers included the all the bucket client's fields.
tonytw1 Feb 21, 2026
4a0aa01
[gcp-storage-buckets] Crops bucket references from media-api for down…
tonytw1 Feb 9, 2026
209d0ef
[gcp-storage-buckets] uploadToQuarantineEnabled unused
tonytw1 Feb 25, 2026
8a3d232
Rebase - revert to origin.
tonytw1 May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.gu.mediaservice.lib

import org.apache.pekko.actor.{Cancellable, Scheduler}
import com.gu.mediaservice.lib.aws.S3
import com.gu.mediaservice.lib.aws.{S3, S3Bucket}
import com.gu.mediaservice.lib.config.CommonConfig
import com.gu.mediaservice.lib.logging.GridLogging
import org.joda.time.DateTime
Expand All @@ -14,7 +14,7 @@ import scala.concurrent.duration._
import scala.util.control.NonFatal


abstract class BaseStore[TStoreKey, TStoreVal](bucket: String, config: CommonConfig)(implicit ec: ExecutionContext)
abstract class BaseStore[TStoreKey, TStoreVal](bucket: S3Bucket, config: CommonConfig)(implicit ec: ExecutionContext)
extends GridLogging {

val s3 = new S3(config)
Expand All @@ -25,15 +25,14 @@ abstract class BaseStore[TStoreKey, TStoreVal](bucket: String, config: CommonCon
protected def getS3Object(key: String): Option[String] = s3.getObjectAsString(bucket, key)

protected def getLatestS3Stream: Option[InputStream] = {
val objects = s3.client
.listObjects(bucket).getObjectSummaries.asScala
val objects = s3.listObjects(bucket).getObjectSummaries.asScala
.filterNot(_.getKey == "AMAZON_SES_SETUP_NOTIFICATION")

if (objects.nonEmpty) {
val obj = objects.maxBy(_.getLastModified)
logger.info(s"Latest key ${obj.getKey} in bucket $bucket")

val stream = s3.client.getObject(bucket, obj.getKey).getObjectContent
val stream = s3.getObject(bucket, obj).getObjectContent
Some(stream)
} else {
logger.error(s"Bucket $bucket is empty")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.gu.mediaservice.lib

import com.amazonaws.services.s3.model.{DeleteObjectsRequest, MultiObjectDeleteException}
import com.amazonaws.services.s3.model.MultiObjectDeleteException

import java.io.File
import com.gu.mediaservice.lib.config.CommonConfig
import com.gu.mediaservice.lib.aws.S3Object
import com.gu.mediaservice.lib.aws.{S3Bucket, S3Object}
import com.gu.mediaservice.lib.logging.LogMarker
import com.gu.mediaservice.model.{Instance, MimeType, Png}
import com.typesafe.scalalogging.StrictLogging
Expand All @@ -21,7 +21,7 @@ object ImageIngestOperations {
private def snippetForId(id: String) = id.take(6).mkString("/") + "/" + id
}

class ImageIngestOperations(imageBucket: String, thumbnailBucket: String, config: CommonConfig, isVersionedS3: Boolean = false)
class ImageIngestOperations(imageBucket: S3Bucket, thumbnailBucket: S3Bucket, config: CommonConfig, isVersionedS3: Boolean = false)
extends S3ImageStorage(config) with StrictLogging {

import ImageIngestOperations.{fileKeyFromId, optimisedPngKeyFromId}
Expand All @@ -44,7 +44,7 @@ class ImageIngestOperations(imageBucket: String, thumbnailBucket: String, config
private def storeThumbnailImage(storableImage: StorableThumbImage)
(implicit logMarker: LogMarker): Future[S3Object] = {
val instanceSpecificKey = instanceAwareThumbnailImageKey(storableImage)
logger.info(s"Storing thumbnail to instance specific key: $thumbnailBucket / $instanceSpecificKey")
logger.info(s"Storing thumbnail to instance specific key: ${thumbnailBucket.bucket} / $instanceSpecificKey")
storeImage(thumbnailBucket, instanceSpecificKey, storableImage.file, Some(storableImage.mimeType),
overwrite = true)
}
Expand All @@ -57,26 +57,45 @@ class ImageIngestOperations(imageBucket: String, thumbnailBucket: String, config
overwrite = true)
}

private def bulkDelete(bucket: String, keys: List[String]): Future[Map[String, Boolean]] = keys match {
private def bulkDelete(bucket: S3Bucket, keys: List[String]): Future[Map[String, Boolean]] = keys match {
case Nil => Future.successful(Map.empty)
case _ => Future {
try {
logger.info(s"Creating S3 bulkDelete request for $bucket / keys: " + keys.mkString(","))
client.deleteObjects(
new DeleteObjectsRequest(bucket).withKeys(keys: _*)
)
keys.map { key =>
key -> true
}.toMap
} catch {
case partialFailure: MultiObjectDeleteException =>
logger.warn(s"Partial failure when deleting images from $bucket: ${partialFailure.getMessage} ${partialFailure.getErrors}")
val errorKeys = partialFailure.getErrors.asScala.map(_.getKey).toSet
case _ =>
val bulkDeleteImplemented = bucket.endpoint != "storage.googleapis.com"
if (bulkDeleteImplemented) {
Future {
try {
logger.info(s"Bulk deleting S3 objects from ${bucket.bucket}: " + keys.mkString(","))
deleteObjects(bucket, keys)
keys.map { key =>
key -> true
}.toMap
} catch {
case partialFailure: MultiObjectDeleteException =>
logger.warn(s"Partial failure when deleting images from $bucket: ${partialFailure.getMessage} ${partialFailure.getErrors}")
val errorKeys = partialFailure.getErrors.asScala.map(_.getKey).toSet
keys.map { key =>
key -> !errorKeys.contains(key)
}.toMap
}
}

} else {
Future.sequence {
keys.map { key =>
key -> !errorKeys.contains(key)
}.toMap
Future {
logger.info(s"Deleting S3 objects from ${bucket.bucket}: " + key)
try {
deleteObject(bucket, key)
(key, true)
} catch {
case e: Exception =>
logger.debug(s"Failure when deleting images from $bucket: $key, ${e.getMessage}")
(key, false)
}
}
}
}.map(_.toMap)
}
}
}

def deleteOriginal(id: String)(implicit logMarker: LogMarker, instance: Instance): Future[Unit] = if(isVersionedS3) deleteVersionedImage(imageBucket, fileKeyFromId(id)) else deleteImage(imageBucket, fileKeyFromId(id))
Expand All @@ -87,7 +106,7 @@ class ImageIngestOperations(imageBucket: String, thumbnailBucket: String, config
def deletePNGs(ids: Set[String])(implicit instance: Instance) = bulkDelete(imageBucket, ids.map(id => optimisedPngKeyFromId(id)).toList)

def doesOriginalExist(id: String)(implicit instance: Instance): Boolean =
client.doesObjectExist(imageBucket, fileKeyFromId(id))
doesObjectExist(imageBucket, fileKeyFromId(id))

private def instanceAwareOriginalImageKey(storableImage: StorableOriginalImage) = {
fileKeyFromId(storableImage.id)(storableImage.instance)
Expand All @@ -107,7 +126,7 @@ sealed trait ImageWrapper {
val instance: Instance
}
sealed trait StorableImage extends ImageWrapper {
def toProjectedS3Object(thumbBucket: String): S3Object = S3Object(
def toProjectedS3Object(thumbBucket: S3Bucket): S3Object = S3Object(
thumbBucket,
ImageIngestOperations.fileKeyFromId(id)(instance),
file,
Expand All @@ -119,7 +138,7 @@ sealed trait StorableImage extends ImageWrapper {

case class StorableThumbImage(id: String, file: File, mimeType: MimeType, meta: Map[String, String] = Map.empty, instance: Instance) extends StorableImage
case class StorableOriginalImage(id: String, file: File, mimeType: MimeType, lastModified: DateTime, meta: Map[String, String] = Map.empty, instance: Instance) extends StorableImage {
override def toProjectedS3Object(thumbBucket: String): S3Object = S3Object(
override def toProjectedS3Object(thumbBucket: S3Bucket): S3Object = S3Object(
thumbBucket,
ImageIngestOperations.fileKeyFromId(id)(instance),
file,
Expand All @@ -129,7 +148,7 @@ case class StorableOriginalImage(id: String, file: File, mimeType: MimeType, las
)
}
case class StorableOptimisedImage(id: String, file: File, mimeType: MimeType, meta: Map[String, String] = Map.empty, instance: Instance) extends StorableImage {
override def toProjectedS3Object(thumbBucket: String): S3Object = S3Object(
override def toProjectedS3Object(thumbBucket: S3Bucket): S3Object = S3Object(
thumbBucket,
ImageIngestOperations.optimisedPngKeyFromId(id)(instance),
file,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ package com.gu.mediaservice.lib

import java.io.File
import com.gu.mediaservice.lib.config.CommonConfig
import com.gu.mediaservice.lib.aws.S3Object
import com.gu.mediaservice.lib.aws.{S3Bucket, S3Object}
import com.gu.mediaservice.lib.logging.LogMarker
import com.gu.mediaservice.model.{Instance, MimeType}

import scala.concurrent.Future

class ImageQuarantineOperations(quarantineBucket: String, config: CommonConfig, isVersionedS3: Boolean = false)
class ImageQuarantineOperations(quarantineBucket: S3Bucket, config: CommonConfig, isVersionedS3: Boolean = false)
extends S3ImageStorage(config) {

def storeQuarantineImage(id: String, file: File, mimeType: Option[MimeType], meta: Map[String, String] = Map.empty)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ package com.gu.mediaservice.lib

import java.util.concurrent.Executors
import java.io.File

import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.duration._
import scala.language.postfixOps
import com.gu.mediaservice.lib.aws.S3Object
import com.gu.mediaservice.lib.aws.{S3Bucket, S3Object}
import com.gu.mediaservice.lib.logging.LogMarker
import com.gu.mediaservice.model.MimeType

Expand Down Expand Up @@ -37,9 +36,9 @@ trait ImageStorage {
/** Store a copy of the given file and return the URI of that copy.
* The file can safely be deleted afterwards.
*/
def storeImage(bucket: String, id: String, file: File, mimeType: Option[MimeType],
def storeImage(bucket: S3Bucket, id: String, file: File, mimeType: Option[MimeType],
meta: Map[String, String] = Map.empty, overwrite: Boolean)
(implicit logMarker: LogMarker): Future[S3Object]

def deleteImage(bucket: String, id: String)(implicit logMarker: LogMarker): Future[Unit]
def deleteImage(bucket: S3Bucket, id: String)(implicit logMarker: LogMarker): Future[Unit]
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.gu.mediaservice.lib

import com.gu.mediaservice.lib.aws.S3
import com.gu.mediaservice.lib.aws.{S3, S3Bucket}
import com.gu.mediaservice.lib.config.CommonConfig
import com.gu.mediaservice.lib.logging.{GridLogging, LogMarker}
import com.gu.mediaservice.model.MimeType
Expand All @@ -14,10 +14,10 @@ import scala.concurrent.Future
class S3ImageStorage(config: CommonConfig) extends S3(config) with ImageStorage with GridLogging {

private val cacheSetting = Some(cacheForever)
def storeImage(bucket: String, id: String, file: File, mimeType: Option[MimeType],
def storeImage(bucket: S3Bucket, id: String, file: File, mimeType: Option[MimeType],
meta: Map[String, String] = Map.empty, overwrite: Boolean)
(implicit logMarker: LogMarker) = {
logger.info(logMarker, s"bucket: $bucket, id: $id, meta: $meta")
logger.info(logMarker, s"storeImage to bucket: ${bucket.bucket}, id: $id, meta: $meta")
val eventualObject = if (overwrite) {
store(bucket, id, file, mimeType, meta, cacheSetting)
} else {
Expand All @@ -27,21 +27,21 @@ class S3ImageStorage(config: CommonConfig) extends S3(config) with ImageStorage
eventualObject
}

def deleteImage(bucket: String, id: String)(implicit logMarker: LogMarker) = Future {
client.deleteObject(bucket, id)
def deleteImage(bucket: S3Bucket, id: String)(implicit logMarker: LogMarker) = Future {
deleteObject(bucket, id)
logger.info(logMarker, s"Deleted image $id from bucket $bucket")
}

def deleteVersionedImage(bucket: String, id: String)(implicit logMarker: LogMarker) = Future {
val objectVersion = client.getObjectMetadata(bucket, id).getVersionId
client.deleteVersion(bucket, id, objectVersion)
def deleteVersionedImage(bucket: S3Bucket, id: String)(implicit logMarker: LogMarker) = Future {
val objectVersion = getObjectMetadata(bucket, id).getVersionId
deleteVersion(bucket, id, objectVersion)
logger.info(logMarker, s"Deleted image $id from bucket $bucket (version: $objectVersion)")
}

def deleteFolder(bucket: String, id: String)(implicit logMarker: LogMarker) = Future {
val files = client.listObjects(bucket, id).getObjectSummaries.asScala
def deleteFolder(bucket: S3Bucket, id: String)(implicit logMarker: LogMarker) = Future {
val files = listObjects(bucket, id).getObjectSummaries.asScala
logger.info(s"Found ${files.size} files to delete in folder $id")
files.foreach(file => client.deleteObject(bucket, file.getKey))
files.foreach(file => deleteObject(bucket, file.getKey))
logger.info(logMarker, s"Deleting images in folder $id from bucket $bucket")
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package com.gu.mediaservice.lib.auth

import com.gu.mediaservice.lib.BaseStore
import com.gu.mediaservice.lib.aws.S3Bucket
import com.gu.mediaservice.lib.config.CommonConfig
import com.gu.mediaservice.model.Instance

import scala.jdk.CollectionConverters._
import scala.concurrent.ExecutionContext

class KeyStore(bucket: String, config: CommonConfig)(implicit ec: ExecutionContext)
class KeyStore(bucket: S3Bucket, config: CommonConfig)(implicit ec: ExecutionContext)
extends BaseStore[String, ApiAccessor](bucket, config)(ec) {

def lookupIdentity(key: String)(implicit instance: Instance): Option[ApiAccessor] = store.get().get(instance.id + "/" + key)
Expand All @@ -17,7 +17,6 @@ class KeyStore(bucket: String, config: CommonConfig)(implicit ec: ExecutionConte
}

private def fetchAll: Map[String, ApiAccessor] = {
val keys = s3.client.listObjects(bucket).getObjectSummaries.asScala.map(_.getKey)
keys.flatMap(k => getS3Object(k).map(k -> ApiAccessor(_))).toMap
s3.listObjectKeys(bucket).flatMap(k => getS3Object(k).map(k -> ApiAccessor(_))).toMap
}
}
Loading
Loading