From f11e65e612114d248b20ae0cdbfaf85876b277f5 Mon Sep 17 00:00:00 2001 From: radekg Date: Thu, 21 Jun 2018 02:10:04 +0200 Subject: [PATCH] Support object tagging, no versions. --- .../s3mock/provider/FileProvider.scala | 36 ++++++++++- .../s3mock/provider/InMemoryProvider.scala | 42 ++++++++++++- .../io/findify/s3mock/provider/Provider.scala | 7 ++- .../provider/tags/InMemoryTagStore.scala | 27 ++++++++ .../s3mock/provider/tags/TagStore.scala | 10 +++ .../s3mock/response/GetObjectTagging.scala | 20 ++++++ .../findify/s3mock/route/DeleteObject.scala | 36 +++++++---- .../io/findify/s3mock/route/GetObject.scala | 61 ++++++++++--------- .../io/findify/s3mock/route/PutObject.scala | 58 ++++++++++++++++-- .../io/findify/s3mock/GetPutObjectTest.scala | 1 + .../io/findify/s3mock/ObjectTaggingTest.scala | 45 ++++++++++++++ 11 files changed, 291 insertions(+), 52 deletions(-) create mode 100644 src/main/scala/io/findify/s3mock/provider/tags/InMemoryTagStore.scala create mode 100644 src/main/scala/io/findify/s3mock/provider/tags/TagStore.scala create mode 100644 src/main/scala/io/findify/s3mock/response/GetObjectTagging.scala create mode 100644 src/test/scala/io/findify/s3mock/ObjectTaggingTest.scala diff --git a/src/main/scala/io/findify/s3mock/provider/FileProvider.scala b/src/main/scala/io/findify/s3mock/provider/FileProvider.scala index 99c67a2..120aa1d 100644 --- a/src/main/scala/io/findify/s3mock/provider/FileProvider.scala +++ b/src/main/scala/io/findify/s3mock/provider/FileProvider.scala @@ -1,14 +1,15 @@ package io.findify.s3mock.provider +import java.io.{FileInputStream, File ⇒ JFile} import java.util.UUID -import java.io.{FileInputStream, File => JFile} import akka.http.scaladsl.model.DateTime import better.files.File import better.files.File.OpenOptions -import com.amazonaws.services.s3.model.ObjectMetadata +import com.amazonaws.services.s3.model.{ObjectMetadata, Tag} import com.typesafe.scalalogging.LazyLogging import io.findify.s3mock.error.{NoSuchBucketException, NoSuchKeyException} import io.findify.s3mock.provider.metadata.{MapMetadataStore, MetadataStore} +import io.findify.s3mock.provider.tags.InMemoryTagStore import io.findify.s3mock.request.{CompleteMultipartUpload, CreateBucketConfiguration} import io.findify.s3mock.response._ import org.apache.commons.codec.digest.DigestUtils @@ -23,6 +24,7 @@ class FileProvider(dir:String) extends Provider with LazyLogging { if (!workDir.exists) workDir.createDirectories() private val meta = new MapMetadataStore(dir) + private val tagStore = new InMemoryTagStore override def metadataStore: MetadataStore = meta @@ -166,6 +168,36 @@ class FileProvider(dir:String) extends Provider with LazyLogging { metadataStore.remove(bucket) } + override def deleteObjectTagging(bucket:String, key:String): Unit = { + val bucketFile = File(s"$dir/$bucket") + val file = File(s"$dir/$bucket/$key") + logger.debug(s"reading object for s://$bucket/$key") + if (!bucketFile.exists) throw NoSuchBucketException(bucket) + if (!file.exists) throw NoSuchKeyException(bucket, key) + if (file.isDirectory) throw NoSuchKeyException(bucket, key) + tagStore.delete(bucket, key) + } + + override def getObjectTagging(bucket:String, key:String): GetObjectTagging = { + val bucketFile = File(s"$dir/$bucket") + val file = File(s"$dir/$bucket/$key") + logger.debug(s"reading object for s://$bucket/$key") + if (!bucketFile.exists) throw NoSuchBucketException(bucket) + if (!file.exists) throw NoSuchKeyException(bucket, key) + if (file.isDirectory) throw NoSuchKeyException(bucket, key) + GetObjectTagging(tagStore.get(bucket, key).getOrElse(List.empty)) + } + + override def setObjectTagging(bucket:String, key:String, tags: List[Tag]): Unit = { + val bucketFile = File(s"$dir/$bucket") + val file = File(s"$dir/$bucket/$key") + logger.debug(s"reading object for s://$bucket/$key") + if (!bucketFile.exists) throw NoSuchBucketException(bucket) + if (!file.exists) throw NoSuchKeyException(bucket, key) + if (file.isDirectory) throw NoSuchKeyException(bucket, key) + tagStore.set(bucket, key, tags) + } + /** Replace the os separator with a '/' */ private def fromOs(path: String): String = { path.replace(JFile.separatorChar, '/') diff --git a/src/main/scala/io/findify/s3mock/provider/InMemoryProvider.scala b/src/main/scala/io/findify/s3mock/provider/InMemoryProvider.scala index a355802..12f4c8e 100644 --- a/src/main/scala/io/findify/s3mock/provider/InMemoryProvider.scala +++ b/src/main/scala/io/findify/s3mock/provider/InMemoryProvider.scala @@ -4,10 +4,11 @@ import java.time.Instant import java.util.{Date, UUID} import akka.http.scaladsl.model.DateTime -import com.amazonaws.services.s3.model.ObjectMetadata +import com.amazonaws.services.s3.model.{ObjectMetadata, Tag} import com.typesafe.scalalogging.LazyLogging import io.findify.s3mock.error.{NoSuchBucketException, NoSuchKeyException} import io.findify.s3mock.provider.metadata.{InMemoryMetadataStore, MetadataStore} +import io.findify.s3mock.provider.tags.InMemoryTagStore import io.findify.s3mock.request.{CompleteMultipartUpload, CreateBucketConfiguration} import io.findify.s3mock.response._ import org.apache.commons.codec.digest.DigestUtils @@ -17,9 +18,11 @@ import scala.collection.mutable import scala.util.Random class InMemoryProvider extends Provider with LazyLogging { + private val mdStore = new InMemoryMetadataStore private val bucketDataStore = new TrieMap[String, BucketContents] private val multipartTempStore = new TrieMap[String, mutable.SortedSet[MultipartChunk]] + private val tagStore = new InMemoryTagStore private case class BucketContents(creationTime: DateTime, keysInBucket: mutable.Map[String, KeyContents]) @@ -186,4 +189,41 @@ class InMemoryProvider extends Provider with LazyLogging { case None => throw NoSuchBucketException(bucket) } } + + override def deleteObjectTagging(bucket: String, key: String): Unit = { + bucketDataStore.get(bucket) match { + case Some(bucketContent) => bucketContent.keysInBucket.get(key) match { + case Some(keyContent) => + logger.debug(s"removing tags for s://$bucket/$key") + tagStore.delete(bucket, key) + case None => throw NoSuchKeyException(bucket, key) + } + case None => throw NoSuchBucketException(bucket) + } + } + + override def getObjectTagging(bucket: String, key: String): GetObjectTagging = { + bucketDataStore.get(bucket) match { + case Some(bucketContent) => bucketContent.keysInBucket.get(key) match { + case Some(keyContent) => + logger.debug(s"reading tags for s://$bucket/$key") + GetObjectTagging(tagStore.get(bucket, key).getOrElse(List.empty)) + case None => throw NoSuchKeyException(bucket, key) + } + case None => throw NoSuchBucketException(bucket) + } + } + + override def setObjectTagging(bucket: String, key: String, tags: List[Tag]): Unit = { + bucketDataStore.get(bucket) match { + case Some(bucketContent) => bucketContent.keysInBucket.get(key) match { + case Some(keyContent) => + logger.debug(s"setting tags for s://$bucket/$key") + tagStore.set(bucket, key, tags) + case None => throw NoSuchKeyException(bucket, key) + } + case None => throw NoSuchBucketException(bucket) + } + } + } diff --git a/src/main/scala/io/findify/s3mock/provider/Provider.scala b/src/main/scala/io/findify/s3mock/provider/Provider.scala index 4dc97c7..91b7964 100644 --- a/src/main/scala/io/findify/s3mock/provider/Provider.scala +++ b/src/main/scala/io/findify/s3mock/provider/Provider.scala @@ -1,6 +1,6 @@ package io.findify.s3mock.provider -import com.amazonaws.services.s3.model.ObjectMetadata +import com.amazonaws.services.s3.model.{CompleteMultipartUploadResult ⇒ _, CopyObjectResult ⇒ _, InitiateMultipartUploadResult ⇒ _, _} import io.findify.s3mock.provider.metadata.MetadataStore import io.findify.s3mock.request.{CompleteMultipartUpload, CreateBucketConfiguration} import io.findify.s3mock.response._ @@ -25,6 +25,11 @@ trait Provider { def deleteBucket(bucket:String):Unit def copyObject(sourceBucket: String, sourceKey: String, destBucket: String, destKey: String, newMeta: Option[ObjectMetadata] = None): CopyObjectResult def copyObjectMultipart(sourceBucket: String, sourceKey: String, destBucket: String, destKey: String, partNumber:Int, uploadId:String, fromByte: Int, toByte:Int, meta: Option[ObjectMetadata] = None): CopyObjectResult + + def deleteObjectTagging(bucket:String, key:String): Unit + def getObjectTagging(bucket:String, key:String): GetObjectTagging + def setObjectTagging(bucket:String, key:String, tags: List[Tag]): Unit + } diff --git a/src/main/scala/io/findify/s3mock/provider/tags/InMemoryTagStore.scala b/src/main/scala/io/findify/s3mock/provider/tags/InMemoryTagStore.scala new file mode 100644 index 0000000..71bcb68 --- /dev/null +++ b/src/main/scala/io/findify/s3mock/provider/tags/InMemoryTagStore.scala @@ -0,0 +1,27 @@ +package io.findify.s3mock.provider.tags + +import com.amazonaws.services.s3.model.{ObjectMetadata, Tag} + +import scala.collection.concurrent.TrieMap +import scala.collection.mutable + +class InMemoryTagStore extends TagStore { + + private val bucketTags = new TrieMap[String, mutable.Map[String, List[Tag]]] + + override def delete(bucket: String, key: String): Unit = { + val currentTags = bucketTags.get(bucket) + currentTags.flatMap(_.remove(key)) + } + + override def get(bucket: String, key: String): Option[List[Tag]] = + bucketTags.get(bucket).flatMap(_.get(key)) + + override def set(bucket: String, key: String, tags: List[Tag]): Unit = { + val currentTags = bucketTags.getOrElseUpdate(bucket, new TrieMap[String, List[Tag]]()) + currentTags.put(key, tags) + } + + override def remove(bucket: String): Unit = bucketTags.remove(bucket) + +} diff --git a/src/main/scala/io/findify/s3mock/provider/tags/TagStore.scala b/src/main/scala/io/findify/s3mock/provider/tags/TagStore.scala new file mode 100644 index 0000000..06029d2 --- /dev/null +++ b/src/main/scala/io/findify/s3mock/provider/tags/TagStore.scala @@ -0,0 +1,10 @@ +package io.findify.s3mock.provider.tags + +import com.amazonaws.services.s3.model.Tag + +trait TagStore { + def delete(bucket: String, key: String): Unit + def get(bucket: String, key: String): Option[List[Tag]] + def remove(bucket: String): Unit + def set(bucket: String, key: String, tags: List[Tag]): Unit +} diff --git a/src/main/scala/io/findify/s3mock/response/GetObjectTagging.scala b/src/main/scala/io/findify/s3mock/response/GetObjectTagging.scala new file mode 100644 index 0000000..75c7c0e --- /dev/null +++ b/src/main/scala/io/findify/s3mock/response/GetObjectTagging.scala @@ -0,0 +1,20 @@ +package io.findify.s3mock.response + +import com.amazonaws.services.s3.model.Tag + +case class GetObjectTagging(tags: List[Tag]) { + def toXML = + + + { + tags.map(tag => + + {tag.getKey} + {tag.getValue} + + ) + } + + +} + diff --git a/src/main/scala/io/findify/s3mock/route/DeleteObject.scala b/src/main/scala/io/findify/s3mock/route/DeleteObject.scala index dcc9213..5a4da06 100644 --- a/src/main/scala/io/findify/s3mock/route/DeleteObject.scala +++ b/src/main/scala/io/findify/s3mock/route/DeleteObject.scala @@ -13,19 +13,29 @@ import scala.util.{Failure, Success, Try} */ case class DeleteObject(implicit provider: Provider) extends LazyLogging { def route(bucket:String, path:String) = delete { - complete { - Try(provider.deleteObject(bucket, path)) match { - case Success(_) => - logger.info(s"deleted object $bucket/$path") - HttpResponse(StatusCodes.NoContent) - case Failure(NoSuchKeyException(_, _)) => - logger.info(s"cannot delete object $bucket/$path: no such key") - HttpResponse(StatusCodes.NotFound) - case Failure(ex) => - logger.error(s"cannot delete object $bucket/$path", ex) - HttpResponse(StatusCodes.NotFound) + parameter('tagging?) { (tagging) ⇒ + tagging match { + case Some(_) ⇒ + complete { + handleTry(bucket, path, Try(provider.deleteObjectTagging(bucket, path))) + } + case None ⇒ + complete { + handleTry(bucket, path, Try(provider.deleteObject(bucket, path))) + } } - } } -} + + private def handleTry[A](bucket: String, path: String, t: Try[A]) = t match { + case Success(_) => + logger.info(s"deleted object $bucket/$path") + HttpResponse(StatusCodes.NoContent) + case Failure(NoSuchKeyException(_, _)) => + logger.info(s"cannot delete object $bucket/$path: no such key") + HttpResponse(StatusCodes.NotFound) + case Failure(ex) => + logger.error(s"cannot delete object $bucket/$path", ex) + HttpResponse(StatusCodes.NotFound) + } +} \ No newline at end of file diff --git a/src/main/scala/io/findify/s3mock/route/GetObject.scala b/src/main/scala/io/findify/s3mock/route/GetObject.scala index 1b15881..aedd017 100644 --- a/src/main/scala/io/findify/s3mock/route/GetObject.scala +++ b/src/main/scala/io/findify/s3mock/route/GetObject.scala @@ -14,6 +14,7 @@ import com.amazonaws.util.DateUtils import com.typesafe.scalalogging.LazyLogging import io.findify.s3mock.error.{InternalErrorException, NoSuchBucketException, NoSuchKeyException} import io.findify.s3mock.provider.{GetObjectData, Provider} +import io.findify.s3mock.response.GetObjectTagging import scala.collection.JavaConverters._ import scala.util.{Failure, Success, Try} @@ -38,7 +39,7 @@ case class GetObject(implicit provider: Provider) extends LazyLogging { } if (params.contains("tagging")) { - handleTaggingRequest(meta) + handleTaggingRequest(provider.getObjectTagging(bucket, path), meta) } else { HttpResponse( status = StatusCodes.OK, @@ -77,44 +78,46 @@ case class GetObject(implicit provider: Provider) extends LazyLogging { - protected def handleTaggingRequest(meta: ObjectMetadata): HttpResponse = { + protected def handleTaggingRequest(tagsMeta: GetObjectTagging, meta: ObjectMetadata): HttpResponse = { var root = var tagset = var w = new StringWriter() - if (meta.getRawMetadata.containsKey("x-amz-tagging")){ - var doc = - - - { - meta.getRawMetadata.get("x-amz-tagging").asInstanceOf[String].split("&").map( - (rawTag: String) => { - rawTag.split("=", 2).map( - (part: String) => URLDecoder.decode(part, "UTF-8") - ) - }).map( - (kv: Array[String]) => - - {kv(0)} - {kv(1)} - ) + var doc = + + + { + tagsMeta.tags.map(tag => + + {tag.getKey} + {tag.getValue} + + ) + } + { + if (meta.getRawMetadata.containsKey("x-amz-tagging")){ + meta.getRawMetadata.get("x-amz-tagging").asInstanceOf[String].split("&").map( + (rawTag: String) => { + rawTag.split("=", 2).map( + (part: String) => URLDecoder.decode(part, "UTF-8") + ) + }).map( + (kv: Array[String]) => + + {kv(0)} + {kv(1)} + ) } - - + } + + + xml.XML.write(w, doc, "UTF-8", true, null) - xml.XML.write(w, doc, "UTF-8", true, null) - } else { - var doc = - xml.XML.write(w, doc, "UTF-8", true, null) - } - - meta.setContentType("application/xml; charset=utf-8") HttpResponse( status = StatusCodes.OK, - entity = w.toString, - headers = `Last-Modified`(DateTime(1970, 1, 1)) :: metadataToHeaderList(meta) + entity = w.toString ) } diff --git a/src/main/scala/io/findify/s3mock/route/PutObject.scala b/src/main/scala/io/findify/s3mock/route/PutObject.scala index 0eb2534..1ea93e4 100644 --- a/src/main/scala/io/findify/s3mock/route/PutObject.scala +++ b/src/main/scala/io/findify/s3mock/route/PutObject.scala @@ -1,11 +1,13 @@ package io.findify.s3mock.route +import java.nio.charset.StandardCharsets + import akka.http.scaladsl.model.{HttpRequest, HttpResponse, StatusCodes} import akka.http.scaladsl.server.Directives._ import akka.stream.Materializer import akka.stream.scaladsl.Sink import akka.util.ByteString -import com.amazonaws.services.s3.model.ObjectMetadata +import com.amazonaws.services.s3.model.{ObjectMetadata, Tag} import com.typesafe.scalalogging.LazyLogging import io.findify.s3mock.S3ChunkedProtocolStage import io.findify.s3mock.error.{InternalErrorException, NoSuchBucketException} @@ -13,19 +15,31 @@ import io.findify.s3mock.provider.Provider import org.apache.commons.codec.digest.DigestUtils import scala.util.{Failure, Success, Try} +import scala.xml.XML /** * Created by shutty on 8/20/16. */ case class PutObject(implicit provider:Provider, mat:Materializer) extends LazyLogging { def route(bucket:String, path:String) = put { - extractRequest { request => - headerValueByName("authorization") { auth => - completeSigned(bucket, path) - } ~ completePlain(bucket, path) + parameter('tagging?) { (tagging) ⇒ + tagging match { + case Some(_) ⇒ completePutTags(bucket, path) + case None ⇒ + extractRequest { request => + headerValueByName("authorization") { auth => + completeSigned(bucket, path) + } ~ completePlain(bucket, path) + } + } } } ~ post { - completePlain(bucket, path) + parameter('tagging?) { (tagging) ⇒ + tagging match { + case Some(_) ⇒ completePutTags(bucket, path) + case None ⇒ completePlain(bucket, path) + } + } } @@ -85,6 +99,38 @@ case class PutObject(implicit provider:Provider, mat:Materializer) extends LazyL } } + def completePutTags(bucket:String, path:String) = extractRequest { request ⇒ + complete { + logger.info(s"put object $bucket/$path (unsigned)") + val result = request.entity.dataBytes + .fold(ByteString(""))(_ ++ _) + .map(data => { + + val tags = (for { + tag ← XML.loadString(new String(data.toArray, StandardCharsets.UTF_8)) \ "TagSet" \ "Tag" + key ← tag \ "Key" + value ← tag \ "Value" + } yield new Tag(key.text, value.text)).toList + + Try(provider.setObjectTagging(bucket, path, tags)) match { + case Success(()) => HttpResponse(StatusCodes.OK) + case Failure(e: NoSuchBucketException) => + HttpResponse( + StatusCodes.NotFound, + entity = e.toXML.toString() + ) + case Failure(t) => + HttpResponse( + StatusCodes.InternalServerError, + entity = InternalErrorException(t).toXML.toString() + ) + } + + }).runWith(Sink.head[HttpResponse]) + result + } + } + private def populateObjectMetadata(request: HttpRequest, bytes: Array[Byte]): ObjectMetadata = { val metadata = MetadataUtil.populateObjectMetadata(request) metadata.setContentMD5(DigestUtils.md5Hex(bytes)) diff --git a/src/test/scala/io/findify/s3mock/GetPutObjectTest.scala b/src/test/scala/io/findify/s3mock/GetPutObjectTest.scala index d751a8e..c70a20a 100644 --- a/src/test/scala/io/findify/s3mock/GetPutObjectTest.scala +++ b/src/test/scala/io/findify/s3mock/GetPutObjectTest.scala @@ -70,6 +70,7 @@ class GetPutObjectTest extends S3MockTest { tagMap.get("key1") shouldBe "val1" tagMap.get("key=&interesting") shouldBe "value=something&stragne" } + it should "be OK with retrieving tags for un-tagged objects" in { s3.putObject("tbucket", "taggedobj", "some-content") var tagging = s3.getObjectTagging(new GetObjectTaggingRequest("tbucket", "taggedobj")).getTagSet diff --git a/src/test/scala/io/findify/s3mock/ObjectTaggingTest.scala b/src/test/scala/io/findify/s3mock/ObjectTaggingTest.scala new file mode 100644 index 0000000..828a2af --- /dev/null +++ b/src/test/scala/io/findify/s3mock/ObjectTaggingTest.scala @@ -0,0 +1,45 @@ +package io.findify.s3mock + +import java.io.ByteArrayInputStream +import java.util + +import com.amazonaws.services.s3.model._ + +class ObjectTaggingTest extends S3MockTest { + override def behaviour(fixture: => Fixture): Unit = { + val s3 = fixture.client + it should "handle PUT / GET / DELETE tagging requests" in { + + s3.createBucket("tbucket") + s3.putObject( + new PutObjectRequest("tbucket", "taggedobj", new ByteArrayInputStream("content".getBytes("UTF-8")), new ObjectMetadata) + ) + + import scala.collection.JavaConverters._ + s3.setObjectTagging( + new SetObjectTaggingRequest( + "tbucket", + "taggedobj", + new ObjectTagging(List(new Tag("key1", "val1"), new Tag("key=&interesting", "value=something&stragne")).asJava))) + + var tagging1 = s3.getObjectTagging(new GetObjectTaggingRequest("tbucket", "taggedobj")).getTagSet.asScala + var tagMap1 = new util.HashMap[String, String]() + for (tag <- tagging1) { + tagMap1.put(tag.getKey, tag.getValue) + } + tagMap1.size() shouldBe 2 + tagMap1.get("key1") shouldBe "val1" + tagMap1.get("key=&interesting") shouldBe "value=something&stragne" + + s3.deleteObjectTagging(new DeleteObjectTaggingRequest("tbucket", "taggedobj")) + + var tagging2 = s3.getObjectTagging(new GetObjectTaggingRequest("tbucket", "taggedobj")).getTagSet.asScala + var tagMap2 = new util.HashMap[String, String]() + for (tag <- tagging2) { + tagMap2.put(tag.getKey, tag.getValue) + } + tagMap2.size() shouldBe 0 + + } + } +}