В этой статье я немного отклонюсь от предыдущей, чтобы шаг за шагом рассмотреть аспект взаимодействия с AWS S3 с помощью Clojure.

Думаю, AWS S3 не нуждается в представлении. На всякий случай, вот краткое изложение того, что такое S3, от самой Amazon:

Amazon Simple Storage Service (Amazon S3) — это сервис хранения объектов, который предлагает лучшую в отрасли масштабируемость, доступность данных, безопасность и производительность. Вы можете использовать Amazon S3 для хранения и извлечения любого объема данных в любое время и из любого места.

AWS S3, вероятно, является наиболее часто используемым сервисом хранения для пользователей AWS с вариантами использования, варьирующимися от таких, как мобильные/веб-приложения, до больших данных, машинного обучения и многого другого.

Предпосылки

  1. Leiningen — Убедитесь, что он установлен и находится в PATH. Необходим для работы с проектами в Clojure, что упрощает их создание и настройку с необходимыми зависимостями. Также необходим для запуска Clojure REPL. Если у вас его нет, вы можете пройти процесс быстрой установки здесь — https://leiningen.org/#install
  2. AWS account — создать пользователя в IAM с Programmatic Access и с применением политики разрешений AmazonS3FullAccess. Запишите Access Key ID и Secret Access Key для пользователя. Если вам нужна помощь в этом, вы можете обратиться к шагам, подробно описанным здесь — Создание пользователя AWS IAM.

3. Наконец, не помешает некоторое знакомство с кодом Clojure.

Давайте приступим.

В Clojure есть две мощные библиотеки, которые помогают нам взаимодействовать с AWS S3.

а. Амазоника

б. aws-апи

Здесь я буду работать с библиотекой aws-api через примеры кода. Библиотеку amazonica мы обсудим в другой статье.

Кложур проект

Как всегда, давайте начнем с создания проекта Clojure для работы.

Откройте терминал и введите, как показано ниже:

lein new app aws-s3-demo

Это создаст папку с именем aws-s3-demo в каталоге, из которого вы запустили указанную выше команду. Перейдите в каталог aws-s3-demo и просмотрите список его содержимого. Должна получиться структура папок, как показано ниже:

.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── doc
│   └── intro.md
├── project.clj
├── resources
├── src
│   └── aws_s3_demo
│       └── core.clj
└── test
    └── aws_s3_demo
        └── core_test.clj

Давайте теперь настроим зависимости. Откройте файл project.clj, расположенный в aws-s3-demo (корневой каталог проекта), и обновите раздел dependencies. Окончательный файл project.clj должен выглядеть следующим образом:

(defproject aws-s3-demo "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[org.clojure/clojure "1.10.3"]
                 [com.cognitect.aws/api "0.8.596"]
                 [com.cognitect.aws/endpoints "1.1.12.307"]
                 [com.cognitect.aws/s3 "822.2.1145.0"]]
  :main ^:skip-aot aws-s3-demo.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all
                       :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}})

Те, что выделены жирным шрифтом выше, — это библиотеки Clojure AWS (aws-api). Чтобы убедиться, что зависимости загружены, давайте запустим:

lein deps

Настроив проект и его зависимости, давайте начнем изучение.

Работа с библиотекой aws-api

Убедитесь, что вы находитесь в корневом каталоге проекта (aws-s3-demo) и запустите REPL с помощью следующей команды:

lein repl

Должно появиться такое приглашение:

nREPL server started on port 60598 on host 127.0.0.1 - nrepl://127.0.0.1:60598
REPL-y 0.5.1, nREPL 0.8.3
Clojure 1.10.3
Java HotSpot(TM) 64-Bit Server VM 16.0.2+7-67
    Docs: (doc function-name-here)
          (find-doc "part-of-name-here")
  Source: (source function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
    Exit: Control+D or (exit) or (quit)
 Results: Stored in vars *1, *2, *3, an exception in *e
aws-s3-demo.core=>

В командной строке давайте начнем с require библиотек:

(require '[cognitect.aws.client.api :as aws]
         '[cognitect.aws.credentials :as credentials])

Рабочий процесс, которому нужно следовать, довольно прост:

Create a client for a specific AWS service → invoke an Operation on the service ( with optional request parameters )

Давайте продолжим и создадим client для сервиса s3 на AWS.

(def s3-client (aws/client {:api                  :s3
                     :region               "us-east-1"
                     :credentials-provider (credentials/basic-credentials-provider
                     {:access-key-id     <your_access_key_id>
                      :secret-access-key <your_secret_access_key>})}))
;; create a s3 client
(def s3-client (aws/client {:api           :s3
                            :region               "us-east-1"
                            :credentials-provider    
                            (credentials/basic-credentials-provider
                            {:access-key-id "<your_access_key_id>"
                             :secret-access-key "<your_secret_access_key>"})}))

Давайте разберем его:

  1. Мы пытаемся создать объект s3-client, который возвращается вызовом метода aws/client в библиотеке. В этот метод вы передаете map информации.
  2. Эта карта содержит информацию о том, для какого AWS service вы хотите создать клиент, указанный ключом :api (здесь написано s3)
  3. Ключ :region указывает конечную точку/регион AWS, в котором вы хотите, чтобы клиентские операции отражались.
  4. :credentials-provider использует метод из библиотеки для указания access-key и secret-key (убедитесь, что вы заменили текст <your_access_key_id> и <your_secret_access_key> соответствующими значениями для вашего пользователя AWS)

У нас есть готовый клиент S3. Давайте использовать его.

Примечание. Чтобы уточнить, текст, показанный после ;;, является результатом, а не частью кода, который нужно ввести в REPL

Давайте попробуем перечислить текущие корзины S3. Введите ниже в REPL:

(-> s3-client
      (aws/invoke {:op :ListBuckets})
      (:Buckets))
;; []

Здесь я использую макрос thread-first в Clojure, чтобы лучше представить поток. (Макросы thread-first и thread-last — это очень мощные конструкции в Clojure, которые позволяют создавать понятный и идиоматический код)

Что мы делаем выше:

  1. возьмите s3-client, который мы создали
  2. используйте метод aws/invoke для выполнения операции, указанной картой на клиенте. Карта здесь указывает операцию ( :op ) с именем :ListBuckets, поскольку это то, что мы хотим.
  3. из результата, возвращаемого aws/invoke (который снова является картой), мы извлекаем значение, связанное с ключом :Buckets

Это возвращает пустой вектор [], как и сейчас, поскольку мы еще не создали ни одной корзины S3 (если у пользователя AWS, с которым вы работаете, уже есть несколько корзин, они появятся).

Давайте теперь создадим корзину S3. Введите ниже:

(def bucket-name "clojures3demo")
(-> s3-client
      (aws/invoke {:op :CreateBucket :request {:Bucket bucket-name}}))
;; {:Location "/clojures3demo"}

Здесь снова, как и раньше, мы:

  1. определить имя для ведра — bucket-name
  2. используйте s3-client, который мы создали
  3. передать карту в aws/invoke с указанием операции :CreateBucket над ней с дополнительным параметром карты :request с указанием имени корзины, которое мы определили
  4. В случае успеха возвращает карту

давайте проверим, действительно ли ведро было создано, перечислив ведра, как и раньше.

(-> s3-client
      (aws/invoke {:op :ListBuckets})
      (:Buckets))
;; [{:Name "clojures3demo", :CreationDate #inst "2022-10-06T06:59:43.000-00:00"}]

Мы видим, что он возвращает детали только что созданного ведра, круто!!

От пустых ведер мало толку :-) Давайте загрузим в него какой-нибудь контент.

(-> s3-client
    (aws/invoke {:op :PutObject :request {:Bucket bucket-name 
            :Key "hello.txt"
            :Body (.getBytes "Hello, World")}}))
;; {:ETag "\"82bb413746aee42f89dea2b59614f9ef\""}

Здесь мы снова:

  1. вызвать операцию :PutObject для передачи s3-client на карте request
  2. карта request содержит информацию о bucket-name, где вы хотите создать объект, Key, который в основном является fully qualified path to the object/file on the S3 bucket that you want created и
  3. затем фактическое содержимое объекта, :Body, которое представляет собой не что иное, как поток байтов, представляющий строку Hello, World

Теперь объект создан и может быть проверен следующим образом:

(-> s3-client
    (aws/invoke {:op :ListObjectsV2 :request {:Bucket bucket-name}}))
;; {:Prefix "", :Contents [{:Key "hello.txt", :LastModified #inst "2022-10-06T07:41:34.000-00:00", :ETag "\"82bb413746aee42f89dea2b59614f9ef\"", :Size 12, :StorageClass "STANDARD"}], :MaxKeys 1000, :IsTruncated false, :Name "clojures3demo", :KeyCount 1}

→ вызов операции ListObjectsV2 на клиенте сообщает, что в корзине есть файл с Key — hello.txt, а также дает :KeyCount из 1, так как на данный момент это единственный файл в корзине, фантастика!!

Просто чтобы убедиться, что все в порядке, давайте создадим еще один файл/объект в корзине S3.

(-> s3-client
    (aws/invoke {:op :PutObject :request {:Bucket bucket-name 
            :Key "wonderful.txt"
            :Body (.getBytes "What a wonderful day!!")}}))
;; {:ETag "\"7e7634f1a88c50de19060fec6d4fefdc\""}

Давайте проверим, что это сработало:

(-> s3-client
    (aws/invoke {:op :ListObjectsV2 :request {:Bucket bucket-name}}))
;; {:Prefix "", :Contents [{:Key "hello.txt", :LastModified #inst "2022-10-06T07:41:34.000-00:00", :ETag "\"82bb413746aee42f89dea2b59614f9ef\"", :Size 12, :StorageClass "STANDARD"} {:Key "wonderful.txt", :LastModified #inst "2022-10-06T08:23:48.000-00:00", :ETag "\"7e7634f1a88c50de19060fec6d4fefdc\"", :Size 22, :StorageClass "STANDARD"}], :MaxKeys 1000, :IsTruncated false, :Name "clojures3demo", :KeyCount 2}

Все отлично выглядит!

Теперь давайте вернем содержимое созданного нами файла hello.txt.

В REPL введите:

(-> s3-client
    (aws/invoke {:op :GetObject :request {:Bucket bucket-name :Key "hello.txt"}}))
;; {:LastModified #inst "2022-10-06T07:41:34.000-00:00", :ETag "\"82bb413746aee42f89dea2b59614f9ef\"", :Metadata {}, :ContentLength 12, :ContentType "application/octet-stream", :AcceptRanges "bytes", :Body #object[java.io.BufferedInputStream 0x131cadc1 "java.io.BufferedInputStream@131cadc1"]}

Мы вызываем операцию GetObject на клиенте, указав bucket name и object Key для получения.

Но тут произошло кое-что очень интересное. Вместо того, чтобы получить содержимое файла/объекта, указанного :Key hello.txt, мы получаем что-то загадочное. По умолчанию ответ Body представляет собой stream, содержащий текст, ожидающий чтения.

Мы можем получить содержимое следующим образом:

(slurp (:Body *1))
;; "Hello, World"

объединение этих двух вместе даст что-то вроде этого:

(-> s3-client
    (aws/invoke {:op :GetObject :request {:Bucket bucket-name :Key "hello.txt"}})
    (:Body)
    (slurp))
;; "Hello, World"

Давайте попробуем это и со вторым файлом, который мы создали:

(-> s3-client
    (aws/invoke {:op :GetObject :request {:Bucket bucket-name :Key "wonderful.txt"}})
    (:Body)
    (slurp))
;; "What a wonderful day!!"

Ура! это прекрасно работает.

Благодаря этому мы смогли успешно создать корзину S3, загрузить в нее контент и вернуть загруженный контент.

Теперь займемся уборкой дома.

Чтобы удалить файлы/объекты, загруженные в корзину S3, введите в REPL следующее:

(-> s3-client
    (aws/invoke {:op :DeleteObject :request {:Bucket bucket-name :Key "hello.txt"}}))
;; {}

Давайте проверим, что файл/объект действительно был удален:

(-> s3-client
    (aws/invoke {:op :ListObjectsV2 :request {:Bucket bucket-name}}))
;; {:Prefix "", :Contents [{:Key "wonderful.txt", :LastModified #inst "2022-10-06T08:23:48.000-00:00", :ETag "\"7e7634f1a88c50de19060fec6d4fefdc\"", :Size 22, :StorageClass "STANDARD"}], :MaxKeys 1000, :IsTruncated false, :Name "clojures3demo", :KeyCount 1}

hello.txt был удален.

Давайте также удалим файл wonderful.txt.

(-> s3-client
    (aws/invoke {:op :DeleteObject :request {:Bucket bucket-name :Key "wonderful.txt"}}))
;; {}

и проверьте то же самое:

(-> s3-client
    (aws/invoke {:op :ListObjectsV2 :request {:Bucket bucket-name}}))
;; {:Prefix "", :MaxKeys 1000, :IsTruncated false, :Name "clojures3demo", :KeyCount 0}

Объекты удаляются. Давайте также удалим корзину S3 и закончим очистку.

(-> s3-client
    (aws/invoke {:op :DeleteBucket :request {:Bucket bucket-name}}))
;; {}

проверка:

(-> s3-client
      (aws/invoke {:op :ListBuckets})
      (:Buckets))
;; []

Ведро пропало!

Объединяем весь поток фрагментов кода, с которыми мы работали:

;; require the aws-api library
(require '[cognitect.aws.client.api :as aws]
         '[cognitect.aws.credentials :as credentials])
;; create a s3 client
(def s3-client (aws/client {:api           :s3
                            :region               "us-east-1"
                            :credentials-provider    
                            (credentials/basic-credentials-provider
                            {:access-key-id "<your_access_key_id>"
                             :secret-access-key "<your_secret_access_key>"})}))
;; list the S3 buckets
(-> s3-client
      (aws/invoke {:op :ListBuckets})
      (:Buckets))
;; create a bucket
(def bucket-name "clojures3demo")
(-> s3-client
      (aws/invoke {:op :CreateBucket :request {:Bucket bucket-name}}))
;; Create a file/object with some string content on the bucket
(-> s3-client
    (aws/invoke {:op :PutObject :request {:Bucket bucket-name 
            :Key "hello.txt"
            :Body (.getBytes "Hello, World")}}))
;; List the objects/files in the bucket
(-> s3-client
    (aws/invoke {:op :ListObjectsV2 :request {:Bucket bucket-name}}))
;; Create a file/object with some string content on the bucket
(-> s3-client
    (aws/invoke {:op :PutObject :request {:Bucket bucket-name 
            :Key "wonderful.txt"
            :Body (.getBytes "What a wonderful day!!")}}))
;; Fetch the contents of an object/file in the bucket
(-> s3-client
    (aws/invoke {:op :GetObject :request {:Bucket bucket-name :Key "hello.txt"}})
    (--> (slurp :Body)))
;; Fetch the contents of an object/file in the bucket
(-> s3-client
    (aws/invoke {:op :GetObject :request {:Bucket bucket-name :Key "hello.txt"}})
    (:Body)
    (slurp))
;; Fetch the contents of an object/file in the bucket
(-> s3-client
    (aws/invoke {:op :GetObject :request {:Bucket bucket-name :Key "wonderful.txt"}})
    (:Body)
    (slurp))
;; Delete the object/file specified by :Key
(-> s3-client
    (aws/invoke {:op :DeleteObject :request {:Bucket bucket-name :Key "hello.txt"}}))
;; Delete the object/file specified by :Key
(-> s3-client
    (aws/invoke {:op :DeleteObject :request {:Bucket bucket-name :Key "wonderful.txt"}}))
;; Delete the bucket
(-> s3-client
    (aws/invoke {:op :DeleteBucket :request {:Bucket bucket-name}}))
;; Delete the bucket
(-> s3-client
    (aws/invoke {:op :DeleteBucket :request {:Bucket bucket-name}}))

Потрясающий! это должно было дать вам представление о том, как работать с AWS S3 в Clojure. Надеюсь, это было полезно.

В следующей статье давайте рассмотрим, как использовать другую библиотеку Clojure amazonica для взаимодействия с сервисами AWS, взяв в качестве примера другой сервис AWS.