湯本 開 ac3d523c0e Merged PR 826: Azure Functions実装(音声ファイル削除)
## 概要
[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でチェック予定
- 行った修正がデグレを発生させていないことを確認できるか
  - 既存処理の変更はなし
2024-03-19 07:36:03 +00:00

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>();
}