0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

氷河(Glacier)に眠っている写真たちをサルベージする 〜 その3 ボールドインベントリの登録

Last updated at Posted at 2024-03-27

目的

AWS S3 Glacier内の各ボールドのボールドインベントリをMongoDBに登録する。

やり方

  • InitiateJobCommandを実行しボールドインベントリを要求する

  • DescribeJobCommandを実行し進捗状況を取得する

  • ボールドインベントリの取得が可能となったら、GetJobOutputCommandを実行しボールドインベントリを取得する

事前準備

ボールドインベントリの取得可能となったことを通知するために、ボールドを作成したリージョンと同一リージョンにて、Amazon SNSのトピックとサブスクリプションを作成する。

トピックの作成

  • タイプ: スタンダード
  • 名前: gtool-sns

サブスクリプションの作成

上記で作成したgtool-snsトピックを開き、サブスクリプションを作成する。

  • トピックARN: arn:aws:sns:<リージョンID>:<アカウントID>:gtool-sns
  • プロトコル: Eメール
  • エンドポイント: 通知を受け取るメールアドレス

ソースコード

バックエンド

InitiateJobCommandを実行するinitiateInventoryJob API

src/app/api/initiateInventoryJob/route.ts
import { NextRequest, NextResponse } from "next/server";
import { GlacierClient, InitiateJobCommand } from "@aws-sdk/client-glacier";
import { MongoClient } from "mongodb";

export const corsHeaders = {
  "Access-Control-Allow-Origin": "http://localhost:3000",
  "Access-Control-Allow-Methods": "POST, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type",
};

export async function POST(request: NextRequest): Promise<NextResponse> {
  const params = await request.json();
  const vaultName = params["vaultName"];
  const client = new GlacierClient({});
  let input: {
    accountId: string;
    jobParameters: { Format: string; SNSTopic: string; Type: string };
    vaultName: string;
  } = {
    accountId: "",
    jobParameters: {
      Format: "",
      SNSTopic: "",
      Type: "",
    },
    vaultName: "",
  };
  input.accountId = "-";
  input.jobParameters.Format = "JSON";
  input["jobParameters"]["SNSTopic"] =
    "arn:aws:sns:<リージョンID>:<アカウントID>:gtool-sns";
  input["jobParameters"]["Type"] = "inventory-retrieval";
  if (vaultName) {
    input["vaultName"] = vaultName;
  }
  const command = new InitiateJobCommand(input);
  let response = await client.send(command);

  const dbclient = await MongoClient.connect(
    "mongodb://127.0.0.1/27017/glacier"
  );

  const db = dbclient.db("glacier");
  const userVaults = db.collection("userInventoryJobs");
  userVaults.insertOne({ info: response, vaultName: vaultName });

  return NextResponse.json({}, { status: 200, headers: corsHeaders });
}

export async function OPTIONS() {
  return NextResponse.json({}, { headers: corsHeaders });
}

DescribeJobCommandを実行するdescribeInventoryJob API

src/app/api/describeInventoryJob/route.ts
import { NextRequest, NextResponse } from "next/server";
import { GlacierClient, DescribeJobCommand } from "@aws-sdk/client-glacier";
import { MongoClient } from "mongodb";

export const corsHeaders = {
  "Access-Control-Allow-Origin": "http://localhost:3000",
  "Access-Control-Allow-Methods": "POST",
  "Access-Control-Allow-Headers": "Content-Type",
};

export async function POST(request: NextRequest): Promise<NextResponse> {
  const client = new GlacierClient({});
  let input: {
    accountId: string;
    jobId: string;
    vaultName: string;
  } = {
    accountId: "",
    jobId: "",
    vaultName: "",
  };

  const dbclient = await MongoClient.connect(
    "mongodb://127.0.0.1/27017/glacier"
  );

  const db = dbclient.db("glacier");
  const userArchiveJobs = db.collection("userInventoryJobs");
  const cursor = userArchiveJobs.find();

  for await (const doc of cursor) {
    input.accountId = "-";
    input["jobId"] = doc.info.jobId;
    input["vaultName"] = doc.vaultName;
    const command = new DescribeJobCommand(input);
    let response = await client.send(command);

    const userVaults = db.collection("userInventoryJobs");
    const query = { "info.jobId": response.JobId };
    const update = {
      $set: {
        info: {
          Action: response.Action,
          Completed: response.Completed,
          CreationDate: response.CreationDate,
          InventoryRetrievalParameters: response.InventoryRetrievalParameters,
          jobId: response.JobId,
          SNSTopic: response.SNSTopic,
          StatusCode: response.StatusCode,
          VaultARN: response.VaultARN,
        },
      },
    };
    userVaults.updateOne(query, update);
  }

  return NextResponse.json({}, { status: 200, headers: corsHeaders });
}

GetJobOutputCommandを実行するgetInventoryJobOutput API

src/app/api/getInventoryJobOutput/route.ts
import { NextRequest, NextResponse } from "next/server";
import { GlacierClient, GetJobOutputCommand } from "@aws-sdk/client-glacier";
import { MongoClient } from "mongodb";

export const corsHeaders = {
  "Access-Control-Allow-Origin": "http://localhost:3000",
  "Access-Control-Allow-Methods": "POST",
  "Access-Control-Allow-Headers": "Content-Type",
};

export async function POST(request: NextRequest): Promise<NextResponse> {
  const client = new GlacierClient({});
  let input: {
    accountId: string;
    jobId: string;
    range: string;
    vaultName: string;
  } = {
    accountId: "",
    jobId: "",
    range: "",
    vaultName: "",
  };

  const dbclient = await MongoClient.connect(
    "mongodb://127.0.0.1/27017/glacier"
  );

  const db = dbclient.db("glacier");
  const userArchiveJobs = db.collection("userInventoryJobs");
  const cursor = userArchiveJobs.find();

  for await (const doc of cursor) {
    input.accountId = "-";
    input.jobId = doc.info.jobId;
    input["vaultName"] = doc.vaultName;
    const command = new GetJobOutputCommand(input);
    let response = await client.send(command);
    let data;
    if (response.body) {
      data = JSON.parse(await response.body.transformToString("utf-8"));
    } else {
      console.error(`No body in response for job ${input.jobId}`);
      return NextResponse.json({}, { status: 400, headers: corsHeaders });
    }

    const userVaults = db.collection("userVaults");
    const query = { VaultARN: data.VaultARN };
    const update: any = {
      $set: {
        VaultARN: data.VaultARN,
        InventoryDate: data.InventoryDate,
        ArchiveList: data.ArchiveList,
      },
    };
    const options = { upsert: true };
    userVaults.updateOne(query, update, options);
  }

  return NextResponse.json({}, { status: 200, headers: corsHeaders });
}

フロントエンド

src/app/page.tsx
"use client";
import axios from "axios";
- import { Button, Stack, Paper } from "@mui/material";
+ import { TextField, Button, Stack, Paper } from "@mui/material";
+ import { useForm, SubmitHandler } from "react-hook-form";

+ type Inputs = {
+   inventoryVaultName: string;
+ };

export default function Home() {
+   const { register, handleSubmit } = useForm<Inputs>({
+     defaultValues: { inventoryVaultName: "" },
+   });
  const listVaults = async () => {
    await axios.post("http://localhost:4000/api/listVaults");
  };
+   const initiateInventoryJob: SubmitHandler<Inputs> = async (data) => {
+     await axios.post("http://localhost:4000/api/initiateInventoryJob", {
+       vaultName: data.inventoryVaultName,
+     });
+   };
+   const describeInventoryJob = async () => {
+     await axios.post("http://localhost:4000/api/describeInventoryJob");
+   };
+   const getInventoryJobOutput = async () => {
+     await axios.post("http://localhost:4000/api/getInventoryJobOutput");
+   };
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <div className="z-10 max-w-10xl w-full items-center justify-between font-mono text-sm lg:flex">
        <Stack spacing={2}>
          <Paper>
            <Button
              variant="outlined"
              onClick={listVaults}
              style={{
                textTransform: "none",
              }}
            >
              ボールド一覧を登録する
            </Button>
          </Paper>
+           <Paper>
+             <TextField
+               {...register("inventoryVaultName")}
+               label="ボールド名"
+               variant="outlined"
+             />
+             <Button
+               variant="outlined"
+               onClick={handleSubmit(initiateInventoryJob)}
+               style={{
+                 textTransform: "none",
+               }}
+             >
+               ボールドインベントリを要求する
+             </Button>
+             <Button
+               variant="outlined"
+               onClick={describeInventoryJob}
+               style={{
+                 textTransform: "none",
+               }}
+             >
+               進捗状況を確認する
+             </Button>
+             <Button
+               variant="outlined"
+               onClick={getInventoryJobOutput}
+               style={{
+                 textTransform: "none",
+               }}
+             >
+               ボールドインベントリを登録する
+             </Button>
+           </Paper>
        </Stack>
      </div>
    </main>
  );
}

実行結果

フロントエンド

ブラウザを開き、http://localhost:3000 にアクセスする。
Screenshot from 2024-03-27 11-37-00.png

ボールドインベントリの要求

ボールドインベントリを要求するボールド名を入力後、[ボールドインベントリを要求する]ボタンを押すと、ボールドインベントリを取得するジョブの情報がMongoDBに登録される。
Screenshot from 2024-03-27 11-40-39.png

進捗状況の取得

[進捗状況を確認する]ボタンを押すと、ボールドインベントリを取得するジョブの情報に進捗情報が追加される。
Screenshot from 2024-03-27 11-43-18.png

ボールドインベントリの登録

ボールドインベントリの要求後数時間待つと、ボールドインベントリ取得の準備が整ったことを通知するメールが送信される。メールを受信後、[ボールドインベントリを登録する]ボタンを押して数分待つと、ボールド一覧のボールド情報に、ボールドインベントリが追加される。
(ボールドインベントリのダウンロードと情報の反映のため数分待つ必要がある。進捗状況を示す等のフロントエンドの改善は今後検討する。)
Screenshot from 2024-03-27 11-52-32.png

その4に続く。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?