makabe.t 773c8894e7 Merged PR 208: API実装(音声ファイルDL元)
## 概要
[Task2038: API実装(音声ファイルDL元)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2038)

- 音声ファイルのダウンロードURL取得API&テストを実装しました。
  - SASトークンの有効期限を2時間にしています。
- タスク作成APIでURLのSASトークンを取り除く処理を追加しています。

## レビューポイント
- URLの生成に問題はないか
- テストのためにmodule生成処理を追加したが問題ないか
- タスク作成APIでのURL処理は認識通りか

※対象外:テンプレートファイル関連
以下はコード整形による変更なので対象外
- licenses.repository.module.ts
- tasks.service.spec.ts

## UIの変更
なし

## 動作確認状況
- ローカルで確認
2023-07-07 06:57:29 +00:00

308 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { AccessToken } from '../../common/token';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
import { AudioOptionItem, AudioUploadFinishedResponse } from './types/types';
import { OPTION_ITEM_NUM, USER_ROLES } from '../../constants/index';
import { User } from '../../repositories/users/entity/user.entity';
import { AudioFileNotFoundError } from './errors/types';
import { TasksNotFoundError } from '../../repositories/tasks/errors/types';
@Injectable()
export class FilesService {
private readonly logger = new Logger(FilesService.name);
constructor(
private readonly usersRepository: UsersRepositoryService,
private readonly tasksRepository: TasksRepositoryService,
private readonly tasksRepositoryService: TasksRepositoryService,
private readonly blobStorageService: BlobstorageService,
) {}
/**
* Uploads finished
* @param url アップロード先Blob Storage(ファイル名含む)
* @param authorId 自分自身(ログイン認証)したAuthorID
* @param fileName 音声ファイル名
* @param duration 音声ファイルの録音時間(ミリ秒の整数値)
* @param createdDate 音声ファイルの録音作成日時(開始日時)yyyy-mm-ddThh:mm:ss.sss'
* @param finishedDate 音声ファイルの録音作成終了日時yyyy-mm-ddThh:mm:ss.sss
* @param uploadedDate 音声ファイルのアップロード日時yyyy-mm-ddThh:mm:ss.sss
* @param fileSize 音声ファイルのファイルサイズByte
* @param priority 優先度 "00":Normal / "01":High
* @param audioFormat 録音形式: DSS/DS2(SP)/DS2(QP)
* @param comment コメント
* @param workType WorkType
* @param optionItemList オプションアイテム音声メタデータ10個固定
* @param isEncrypted 暗号化されているか
* @returns finished
*/
async uploadFinished(
userId: string,
url: string,
authorId: string,
fileName: string,
duration: string,
createdDate: string,
finishedDate: string,
uploadedDate: string,
fileSize: number,
priority: string,
audioFormat: string,
comment: string,
workType: string,
optionItemList: AudioOptionItem[],
isEncrypted: boolean,
): Promise<AudioUploadFinishedResponse> {
const formattedCreatedDate = new Date(createdDate);
const formattedFinishedDate = new Date(finishedDate);
const formattedUploadedDate = new Date(uploadedDate);
const isInvalidCreatedDate = isNaN(formattedCreatedDate.getTime());
const isInvalidFinishedDate = isNaN(formattedFinishedDate.getTime());
const isInvalidUploadedDate = isNaN(formattedUploadedDate.getTime());
// 日付フォーマットが不正ならパラメータ不正
if (
isInvalidCreatedDate ||
isInvalidFinishedDate ||
isInvalidUploadedDate
) {
if (isInvalidCreatedDate) {
this.logger.error(
`param createdDate is invalid format:[createdDate=${createdDate}]`,
);
}
if (isInvalidFinishedDate) {
this.logger.error(
`param finishedDate is invalid format:[finishedDate=${finishedDate}]`,
);
}
if (isInvalidUploadedDate) {
this.logger.error(
`param uploadedDate is invalid format:[uploadedDate=${uploadedDate}]`,
);
}
throw new HttpException(
makeErrorResponse('E010001'),
HttpStatus.BAD_REQUEST,
);
}
// オプションアイテムが10個ない場合はパラメータ不正
if (optionItemList.length !== OPTION_ITEM_NUM) {
this.logger.error(
`param optionItemList expects ${OPTION_ITEM_NUM} items, but has ${optionItemList.length} items`,
);
throw new HttpException(
makeErrorResponse('E010001'),
HttpStatus.BAD_REQUEST,
);
}
let user: User;
try {
// ユーザー取得
user = await this.usersRepository.findUserByExternalId(userId);
} catch (e) {
this.logger.error(`error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
// URLにSASトークンがついている場合は取り除く
const urlObj = new URL(url);
urlObj.search = '';
const fileUrl = urlObj.toString();
this.logger.log(`Request URL: ${url}, Without param URL${fileUrl}`);
// 文字起こしタスク追加(音声ファイルとオプションアイテムも同時に追加)
// 追加時に末尾のJOBナンバーにインクリメントする
const task = await this.tasksRepositoryService.create(
user.account_id,
user.id,
priority,
fileUrl,
fileName,
authorId,
workType,
formattedCreatedDate,
duration,
formattedFinishedDate,
formattedUploadedDate,
fileSize,
audioFormat,
comment,
isEncrypted,
optionItemList,
);
return { jobNumber: task.job_number };
} catch (e) {
this.logger.error(`error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
/**
* Publishs upload sas
* @param companyName
* @returns upload sas
*/
async publishUploadSas(token: AccessToken): Promise<string> {
//DBから国情報とアカウントIDを取得する
let accountId: number;
let country: string;
let userId: number;
try {
const user = await this.usersRepository.findUserByExternalId(
token.userId,
);
accountId = user.account.id;
userId = user.id;
country = user.account.country;
} catch (e) {
this.logger.error(`error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
// 国に応じたリージョンのBlobストレージにコンテナが存在するか確認
const isContainerExist = await this.blobStorageService.containerExists(
accountId,
country,
);
//TODO コンテナが無ければ作成しているが、アカウント登録時に作成するので本処理は削除予定。
if (!isContainerExist) {
await this.blobStorageService.createContainer(accountId, country);
}
} catch (e) {
this.logger.error(`error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
// SASトークン発行
const url = await this.blobStorageService.publishUploadSas(
accountId,
userId,
country,
);
return url;
} catch (e) {
this.logger.error(`error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
/**
* 指定したIDの音声ファイルのダウンロードURLを取得する
* @param externalId
* @param audioFileId
* @returns audio file download sas
*/
async publishAudioFileDownloadSas(
externalId: string,
audioFileId: number,
): Promise<string> {
//DBから国情報とアカウントID,ユーザーIDを取得する
let accountId: number;
let country: string;
let userId: number;
let isTypist: boolean;
try {
const user = await this.usersRepository.findUserByExternalId(externalId);
accountId = user.account.id;
userId = user.id;
country = user.account.country;
isTypist = user.role === USER_ROLES.TYPIST;
} catch (e) {
this.logger.error(`error=${e}`);
console.log(e);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
const { file } = await this.tasksRepository.getTaskAndAudioFile(
audioFileId,
accountId,
isTypist,
);
// タスクに紐づく音声ファイルだけが消される場合がある。
// その場合はダウンロード不可なので不在エラーとして扱う
if (!file) {
throw new AudioFileNotFoundError(
`Audio file is not exists in DB. audio_file_id:${audioFileId}`,
);
}
const filePath = `${userId}/${file.file_name}`;
const isFileExist = await this.blobStorageService.fileExists(
accountId,
country,
filePath
);
if (!isFileExist) {
throw new AudioFileNotFoundError(
`Audio file is not exists in blob storage. audio_file_id:${audioFileId}, url:${file.url}, fileName:${file.file_name}`,
);
}
// SASトークン発行
const url = await this.blobStorageService.publishDownloadSas(
accountId,
country,
filePath,
);
return url;
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case TasksNotFoundError:
throw new HttpException(
makeErrorResponse('E010603'),
HttpStatus.BAD_REQUEST,
);
case AudioFileNotFoundError:
throw new HttpException(
makeErrorResponse('E010701'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}