## 概要 [Task3880: Azure Functions実装(音声ファイル削除)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3880) - 自動音声ファイル削除を実装 - 上記のテストを実装 - テストにMySQLを使用する仕組みを導入 ## レビューポイント - テストケースは十分か - テスト内容は妥当か - developにデプロイする前の動作確認・ユニットテストとして十分か ## クエリの変更 - 新規処理のため、既存からの変更はなし ## 動作確認状況 - DBが空の状態でローカル環境で実行し、0件削除のログが出ることを確認 - 削除対象が正しいか等はdevelopでチェック予定 - 行った修正がデグレを発生させていないことを確認できるか - 既存処理の変更はなし
188 lines
5.5 KiB
TypeScript
188 lines
5.5 KiB
TypeScript
import { app, InvocationContext, Timer } from "@azure/functions";
|
|
import { DataSource, In } from "typeorm";
|
|
import { Task } from "../entity/task.entity";
|
|
import { AudioFile } from "../entity/audio_file.entity";
|
|
import { AudioOptionItem } from "../entity/audio_option_item.entity";
|
|
import { MANUAL_RECOVERY_REQUIRED } from "../constants";
|
|
import * as dotenv from "dotenv";
|
|
import { initializeDataSource } from "../database/initializeDataSource";
|
|
import { AudioBlobStorageService } from "../blobstorage/audioBlobStorage.service";
|
|
|
|
app.timer("deleteAudioFiles", {
|
|
schedule: "0 3 * * *", // 毎日UTC 3:00に実行
|
|
handler: deleteAudioFiles,
|
|
});
|
|
|
|
// 削除対象となる音声ファイルとタスクを特定する為の情報
|
|
type TargetTaskInfo = {
|
|
/**
|
|
* タスクID
|
|
* column: tasks.id
|
|
*/
|
|
id: number;
|
|
|
|
/**
|
|
* ファイルID
|
|
* column: audio_files.id
|
|
*/
|
|
audio_file_id: number;
|
|
|
|
/**
|
|
* ファイル名
|
|
* column: audio_files.file_name
|
|
*/
|
|
file_name: string;
|
|
|
|
/**
|
|
* アカウントID
|
|
* column: accounts.id
|
|
*/
|
|
account_id: number;
|
|
|
|
/**
|
|
* アカウントの所属する地域
|
|
* column: accounts.country
|
|
*/
|
|
country: string;
|
|
};
|
|
|
|
export async function deleteAudioFilesProcessing(
|
|
context: InvocationContext,
|
|
dataSource: DataSource,
|
|
blobStorageService: AudioBlobStorageService,
|
|
now: Date
|
|
): Promise<void> {
|
|
context.log(`[IN] deleteAudioFilesProcessing. now=${now}`);
|
|
try {
|
|
// 削除対象のタスクとファイルを取得
|
|
const targets = await getProcessTargets(dataSource, now);
|
|
context.log(`delete targets: ${JSON.stringify(targets)}`);
|
|
|
|
// DBからレコードを削除
|
|
await deleteRecords(dataSource, targets);
|
|
|
|
// タスクに紐づくファイルを削除
|
|
for (const target of targets) {
|
|
try {
|
|
const { account_id, country, file_name } = target;
|
|
await blobStorageService.deleteFile(
|
|
context,
|
|
account_id,
|
|
country,
|
|
file_name
|
|
);
|
|
context.log(`file delete success. target=${JSON.stringify(target)}`);
|
|
} catch (e) {
|
|
context.log(
|
|
`${MANUAL_RECOVERY_REQUIRED} file delete failed. target=${JSON.stringify(
|
|
target
|
|
)}`
|
|
);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// DB関連で例外が発生した場合、アラートを出す為のログを出力する
|
|
context.error(
|
|
`${MANUAL_RECOVERY_REQUIRED} Failed to execute auto file deletion function. error=${error}`
|
|
);
|
|
throw error;
|
|
} finally {
|
|
context.log(`[OUT] deleteAudioFilesProcessing`);
|
|
}
|
|
}
|
|
|
|
async function deleteAudioFiles(
|
|
myTimer: Timer,
|
|
context: InvocationContext
|
|
): Promise<void> {
|
|
context.log("[IN]deleteAudioFiles");
|
|
|
|
dotenv.config({ path: ".env" });
|
|
dotenv.config({ path: ".env.local", override: true });
|
|
const dataSource = await initializeDataSource(context);
|
|
try {
|
|
await deleteAudioFilesProcessing(
|
|
context,
|
|
dataSource,
|
|
new AudioBlobStorageService(),
|
|
new Date()
|
|
);
|
|
} catch (e) {
|
|
context.log("deleteAudioFilesProcessing failed.");
|
|
context.error(e);
|
|
throw e;
|
|
} finally {
|
|
await dataSource.destroy();
|
|
context.log("[OUT]deleteAudioFiles");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* タスクに紐づくファイルを削除する
|
|
* @param dataSource
|
|
* @param targets
|
|
* @returns Promise<void>
|
|
*/
|
|
// テスト容易性を高めて開発効率を上げるため、本来はexport不要だがexportを行う 2024/03/13
|
|
export async function deleteRecords(
|
|
dataSource: DataSource,
|
|
targets: TargetTaskInfo[]
|
|
): Promise<void> {
|
|
const taskIds = targets.map((target) => target.id);
|
|
const audioFileIds = targets.map((target) => target.audio_file_id);
|
|
|
|
// taskIdsに紐づくタスクを削除
|
|
await dataSource.transaction(async (manager) => {
|
|
const taskRepository = manager.getRepository(Task);
|
|
const audioFileRepository = manager.getRepository(AudioFile);
|
|
const audioOptionItem = manager.getRepository(AudioOptionItem);
|
|
|
|
// tasksテーブルから削除
|
|
await taskRepository.delete({
|
|
id: In(taskIds),
|
|
});
|
|
// audio_filesテーブルから削除
|
|
await audioFileRepository.delete({
|
|
id: In(audioFileIds),
|
|
});
|
|
// audio_option_itemsテーブルから削除
|
|
await audioOptionItem.delete({
|
|
audio_file_id: In(audioFileIds),
|
|
});
|
|
});
|
|
}
|
|
|
|
/*
|
|
* ファイル削除対象のタスクを取得する
|
|
* @param dataSource
|
|
* @param now
|
|
* @returns Promise<{ account_id: number; audio_file_id: number; file_name: string; }[]>
|
|
*/
|
|
// テスト容易性を高めて開発効率を上げるため、本来はexport不要だがexportを行う 2024/03/13
|
|
export async function getProcessTargets(
|
|
dataSource: DataSource,
|
|
now: Date
|
|
): Promise<TargetTaskInfo[]> {
|
|
return await dataSource
|
|
.createQueryBuilder(Task, "tasks")
|
|
// SELECTでエイリアスを指定して、結果オブジェクトのプロパティとTargetTaskInfoのプロパティを一致させる
|
|
.select("tasks.id", "id")
|
|
.addSelect("tasks.audio_file_id", "audio_file_id")
|
|
.addSelect("audio_files.file_name", "file_name")
|
|
.addSelect("accounts.id", "account_id")
|
|
.addSelect("accounts.country", "country")
|
|
.where("tasks.finished_at IS NOT NULL")
|
|
.innerJoin("accounts", "accounts", "accounts.id = tasks.account_id")
|
|
.innerJoin(
|
|
"audio_files",
|
|
"audio_files",
|
|
"audio_files.id = tasks.audio_file_id"
|
|
)
|
|
.andWhere("accounts.auto_file_delete = true")
|
|
.andWhere(
|
|
"DATE_ADD(tasks.finished_at, INTERVAL accounts.file_retention_days DAY) < :now",
|
|
{ now }
|
|
)
|
|
.getRawMany<TargetTaskInfo>();
|
|
}
|