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 { 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 { 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 */ // テスト容易性を高めて開発効率を上げるため、本来はexport不要だがexportを行う 2024/03/13 export async function deleteRecords( dataSource: DataSource, targets: TargetTaskInfo[] ): Promise { 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 { 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(); }