All Downloads are FREE. Search and download functionalities are using the official Maven repository.

geotrellis.store.s3.S3AttributeStore.scala Maven / Gradle / Ivy

/*
 * Copyright 2016 Azavea
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package geotrellis.store.s3

import geotrellis.store._

import software.amazon.awssdk.core.sync.RequestBody
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model._
import org.apache.commons.io.IOUtils
import _root_.io.circe._
import _root_.io.circe.syntax._
import _root_.io.circe.parser._
import cats.syntax.either._

import scala.collection.JavaConverters._
import java.nio.charset.Charset

/**
 * Stores and retrieves layer attributes in an S3 bucket in JSON format
 *
 * @param bucket    S3 bucket to use for attribute store
 * @param prefix    path in the bucket for given LayerId
 */
class S3AttributeStore(
  val bucket: String,
  val prefix: String,
  s3Client: => S3Client = S3ClientProducer.get()
) extends BlobLayerAttributeStore {
  import S3AttributeStore._

  @transient lazy val client = s3Client

  /** NOTE:
   * S3 is eventually consistent, therefore it is possible to write an attribute and fail to read it
   * immediately afterwards. It is not clear if this is a practical concern.
   * It could be remedied by some kind of time-out cache for both read/write in this class.
   */

  def path(parts: String*) =
    parts
      .filter(_.nonEmpty)
      .map { s => if(s.endsWith("/")) s.dropRight(1) else s }
      .mkString("/")

  def attributePath(id: LayerId, attributeName: String): String =
    path(prefix, "_attributes", s"${attributeName}${SEP}${id.name}${SEP}${id.zoom}.json")

  def attributePrefix(attributeName: String): String =
    path(prefix, "_attributes", s"${attributeName}${SEP}")

  private def readKey[T: Decoder](key: String): (LayerId, T) = {
    val getRequest = GetObjectRequest.builder()
      .bucket(bucket)
      .key(key)
      .build()
    val s3objStream = s3Client.getObject(getRequest)
    val json =
      try {
        IOUtils.toString(s3objStream, Charset.forName("UTF-8"))
      } finally {
        s3objStream.close()
      }

    parse(json).flatMap(_.as[(LayerId, T)]).valueOr(throw _)
  }

  def read[T: Decoder](layerId: LayerId, attributeName: String): T =
    try {
      readKey[T](attributePath(layerId, attributeName))._2
    } catch {
      case e: S3Exception =>
        throw new AttributeNotFoundError(attributeName, layerId).initCause(e)
    }

  def readAll[T: Decoder](attributeName: String): Map[LayerId, T] = {
    val listRequest = ListObjectsV2Request.builder()
      .bucket(bucket)
      .prefix(attributePrefix(attributeName))
      .build()
    s3Client
      .listObjectsV2Paginator(listRequest)
      .contents
      .asScala
      .map{ s3obj =>
        try {
          readKey[T](s3obj.key)
        } catch {
          case e: S3Exception =>
            throw new LayerIOError(s"Unable to list $attributeName attributes from $bucket/${s3obj.key}").initCause(e)
        }
      }
      .toMap
  }

  def write[T: Encoder](layerId: LayerId, attributeName: String, value: T): Unit = {
    val key = attributePath(layerId, attributeName)
    val str = (layerId, value).asJson.noSpaces
    val putRequest = PutObjectRequest.builder()
      .bucket(bucket)
      .key(key)
      .build()
    val requestBody = RequestBody.fromBytes(str.getBytes("UTF-8"))
    s3Client.putObject(putRequest, requestBody)
    //AmazonServiceException possible
  }

  def layerExists(layerId: LayerId): Boolean = {
    val listRequest = ListObjectsV2Request.builder()
      .bucket(bucket)
      .prefix(path(prefix, "_attributes"))
      .build()
    s3Client
      .listObjectsV2Paginator(listRequest)
      .contents
      .asScala
      .exists(_.key.endsWith(s"${AttributeStore.Fields.metadata}${SEP}${layerId.name}${SEP}${layerId.zoom}.json"))
  }

  def delete(layerId: LayerId, attributeName: String): Unit = {
    val deleteRequest = DeleteObjectRequest.builder()
      .bucket(bucket)
      .key(attributePath(layerId, attributeName))
      .build()
    s3Client.deleteObject(deleteRequest)
    clearCache(layerId, attributeName)
  }

  private def layerKeys(layerId: LayerId): Seq[String] = {
    val listRequest = ListObjectsV2Request.builder()
      .bucket(bucket)
      .prefix(path(prefix, "_attributes"))
      .build()
    s3Client
      .listObjectsV2Paginator(listRequest)
      .contents
      .asScala
      .map { _.key }
      .filter { _.contains(s"${SEP}${layerId.name}${SEP}${layerId.zoom}.json") }
      .toSeq
  }

  def delete(layerId: LayerId): Unit = {
    val identifiers =
      layerKeys(layerId).map { key => ObjectIdentifier.builder().key(key).build() }
    val deleteDefinition = Delete.builder()
      .objects(identifiers:_*)
      .build()
    val deleteRequest = DeleteObjectsRequest.builder()
      .bucket(bucket)
      .delete(deleteDefinition)
      .build()
    s3Client.deleteObjects(deleteRequest)
    clearCache(layerId)
  }

  def layerIds: Seq[LayerId] = {
    val listRequest = ListObjectsV2Request.builder()
      .bucket(bucket)
      .prefix(path(prefix, "_attributes/metadata"))
      .build()
    s3Client
      .listObjectsV2Paginator(listRequest)
      .contents
      .asScala
      .map { s3obj =>
        val List(zoomStr, name) = new java.io.File(s3obj.key).getName.split(SEP).reverse.take(2).toList
        LayerId(name, zoomStr.replace(".json", "").toInt)
      }
      .toSeq
      .distinct
  }

  def availableAttributes(layerId: LayerId): Seq[String] = {
    layerKeys(layerId).map { key =>
      new java.io.File(key).getName.split(SEP).head
    }
  }

  override def availableZoomLevels(layerName: String): Seq[Int] = {
    val listRequest = ListObjectsV2Request.builder()
      .bucket(bucket)
      .prefix(path(prefix, s"_attributes/metadata${SEP}${layerName}"))
      .build()
    s3Client
      .listObjectsV2Paginator(listRequest)
      .contents
      .asScala
      .flatMap { s3obj =>
        val List(zoomStr, foundName) = new java.io.File(s3obj.key).getName.split(SEP).reverse.take(2).toList
        if (foundName == layerName)
          Some(zoomStr.replace(".json", "").toInt)
        else
          None
      }
      .toSeq
      .distinct
  }
}

object S3AttributeStore {
  final val SEP = "__"

  def apply(bucket: String, root: String, s3Client: => S3Client = S3ClientProducer.get()) =
    new S3AttributeStore(bucket, Option(root).getOrElse(""), s3Client)

  def apply(bucket: String, s3Client: => S3Client): S3AttributeStore =
    apply(bucket, "", s3Client)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy