目的
AWS S3 Glacier内の各ボールドのボールドインベントリからアーカイブ一覧を参照し、アーカイブを取得する。
やり方
- InitiateJobCommandを実行しアーカイブを要求する(ボールドインベントリ取得時と同一APIを使用するが指定する引数が異なる)
- DescribeJobCommandを実行し進捗状況を取得する
- アーカイブの取得が可能となったら、GetJobOutputCommandを実行しアーカイブを取得する
- 取得したアーカイブは不要なので、DeleteArchiveCommandを実行しボールドからアーカイブを削除する
ソースコード
バックエンド
InitiateJobCommandを実行するinitiateArchiveJob API
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 number = params["number"];
const client = new GlacierClient({});
let input: {
accountId: string;
jobParameters: { ArchiveId: string; SNSTopic: string; Type: string };
vaultName: string;
} = {
accountId: "",
jobParameters: {
SNSTopic: "",
Type: "",
ArchiveId: "",
},
vaultName: "",
};
const dbclient = await MongoClient.connect(
"mongodb://127.0.0.1/27017/glacier"
);
const db = dbclient.db("glacier");
const userVaults = db.collection("userVaults");
const query = { VaultName: vaultName };
const cursor = userVaults.find(query);
let count = 0;
let archiveIds = [];
for await (const doc of cursor) {
for (let archive of doc.ArchiveList) {
archiveIds.push(archive.ArchiveId);
count = count + 1;
if (count >= Number(number)) {
break;
}
}
}
input.accountId = "-";
input["jobParameters"]["SNSTopic"] =
"arn:aws:sns:<リージョンID>:<アカウントID>:gtool-sns";
input["jobParameters"]["Type"] = "archive-retrieval";
if (vaultName) {
input["vaultName"] = vaultName;
}
for (let archiveId of archiveIds) {
input["jobParameters"]["ArchiveId"] = archiveId;
const command = new InitiateJobCommand(input);
let response = await client.send(command);
const userArchiveJobs = db.collection("userArchiveJobs");
userArchiveJobs.insertOne({
info: response,
vauleName: vaultName,
archiveId: archiveId,
});
}
return NextResponse.json({}, { status: 200, headers: corsHeaders });
}
export async function OPTIONS() {
return NextResponse.json({}, { headers: corsHeaders });
}
DescribeJobCommandを実行するdescribeArchiveJob API
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("userArchiveJobs");
const cursor = userArchiveJobs.find();
for await (const doc of cursor) {
input.accountId = "-";
input["jobId"] = doc.info.jobId;
input["vaultName"] = doc.vauleName;
const command = new DescribeJobCommand(input);
let response = await client.send(command);
const userVaults = db.collection("userArchiveJobs");
const query = { "info.jobId": response.JobId };
const update = {
$set: {
info: {
Action: response.Action,
Completed: response.Completed,
CreationDate: response.CreationDate,
jobId: response.JobId,
SNSTopic: response.SNSTopic,
StatusCode: response.StatusCode,
VaultARN: response.VaultARN,
},
},
};
userVaults.updateOne(query, update);
}
return NextResponse.json({}, { status: 200, headers: corsHeaders });
}
GetJobOutputCommandとDeleteArchiveCommandを実行するgetArchiveJobOutput API
import { NextRequest, NextResponse } from "next/server";
import {
GlacierClient,
GetJobOutputCommand,
DeleteArchiveCommand,
} from "@aws-sdk/client-glacier";
import { pipeline } from "stream/promises";
import fs from "fs";
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: "",
};
let input2: {
accountId: string;
vaultName: string;
archiveId: string;
} = {
accountId: "",
vaultName: "",
archiveId: "",
};
const dbclient = await MongoClient.connect(
"mongodb://127.0.0.1/27017/glacier"
);
const db = dbclient.db("glacier");
const userArchiveJobs = db.collection("userArchiveJobs");
const cursor = userArchiveJobs.find();
for await (const doc of cursor) {
input.accountId = "-";
input2.accountId = "-";
input.jobId = doc.info.jobId;
input["vaultName"] = doc.vauleName;
input2["vaultName"] = doc.vauleName;
const command = new GetJobOutputCommand(input);
let response = await client.send(command);
if (response.body) {
await pipeline<any, any>(
response.body,
fs.createWriteStream(`${input.vaultName}/${input.jobId}`)
);
input2["archiveId"] = doc.archiveId;
const command2 = new DeleteArchiveCommand(input2);
let response2 = await client.send(command2);
} else {
console.error(`No body in response for job ${input.jobId}`);
return NextResponse.json({}, { status: 400, headers: corsHeaders });
}
}
return NextResponse.json({}, { status: 200, headers: corsHeaders });
}
フロントエンド
"use client";
import axios from "axios";
import { TextField, Button, Stack, Paper } from "@mui/material";
import { useForm, SubmitHandler } from "react-hook-form";
type Inputs = {
inventoryVaultName: string;
+ archiveVaultName: string;
+ archiveNumber: string;
};
export default function Home() {
const { register, handleSubmit } = useForm<Inputs>({
defaultValues: {
inventoryVaultName: "",
+ archiveVaultName: "",
+ archiveNumber: "",
},
});
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");
};
+ const initiateArchiveJob: SubmitHandler<Inputs> = async (data) => {
+ await axios.post("http://localhost:4000/api/initiateArchiveJob", {
+ vaultName: data.archiveVaultName,
+ number: data.archiveNumber,
+ });
+ };
+ const describeArchiveJob = async () => {
+ await axios.post("http://localhost:4000/api/describeArchiveJob");
+ };
+ const getArchiveJobOutput = async () => {
+ await axios.post("http://localhost:4000/api/getArchiveJobOutput");
+ };
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>
+ <Paper>
+ <TextField
+ {...register("archiveVaultName")}
+ label="ボールド名"
+ variant="outlined"
+ />
+ <TextField
+ {...register("archiveNumber")}
+ label="アーカイブ数"
+ variant="outlined"
+ />
+ <Button
+ variant="outlined"
+ onClick={handleSubmit(initiateArchiveJob)}
+ style={{
+ textTransform: "none",
+ }}
+ >
+ アーカイブを要求する
+ </Button>
+ <Button
+ variant="outlined"
+ onClick={describeArchiveJob}
+ style={{
+ textTransform: "none",
+ }}
+ >
+ 進捗状況を確認する
+ </Button>
+ <Button
+ variant="outlined"
+ onClick={getArchiveJobOutput}
+ style={{
+ textTransform: "none",
+ }}
+ >
+ アーカイブを取得する
+ </Button>
+ </Paper>
</Stack>
</div>
</main>
);
}
実行結果
フロントエンド
ブラウザを開き、http://localhost:3000
にアクセスする。
アーカイブの要求
アーカイブを要求するボールド名と取得するアーカイブ数を入力後、[アーカイブを要求する]ボタンを押すと、アーカイブを取得するジョブの情報がMongoDBに登録される。
進捗状況の取得
[進捗状況を確認する]ボタンを押すと、アーカイブを取得するジョブの情報に進捗情報が追加される。
アーカイブの取得
アーカイブの要求後数時間待つと、アーカイブ取得の準備が整ったことを通知するメールが送信される。メールを受信後、バックエンド直下にボールド名の名前をつけたディレクトリを作成し、[アーカイブを取得する]ボタンを押すと、アーカイブが取得される。
取得したアーカイブは、ボールドから削除され、以降アーカイブを要求してもボールドに存在しないというエラーが返るようになる。
入力値に取得するアーカイブ数を設ける理由
アーカイブの取得は、アーカイブ取得の準備が整ってから24時間以内に行う必要があり、それを過ぎると取得できなくなり再度アーカイブを要求する必要がある。
通信回線の品質にもよるが、写真や動画のデータが万単位である場合、24時間以内にダウンロードが完了することはまずなく、せいぜい数千が限度である。当然、取得できなかった残りのデータに対しても、取り出し準備を行っているため課金が発生してしまう。
そのため、余計な課金が発生しないよう、あらかじめ要求するアーカイブ数を調整する必要があり、そのためにアーカイブ数を入力値として設けている。
今後の取り組み
ここまでで、アーカイブの取得に必要な最低限のプログラムを実装できたので、今後は、エラー処理の実装やフロントエンドの拡張、機能そのものの拡張に取り組んでいく。