湯本 開 cfb7bdb4dc Merged PR 250: [Sp13-1完了]Todoを一斉駆逐する
## 概要
[Task1774: [Sp13-1完了]Todoを一斉駆逐する](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1774)

- TODOコメントに修正予定Taskを追加
- cors関連の不要な実装を削除

## レビューポイント
- 作業方針は問題ないか
- client側は軽微なTODOのみだったので今回対処しなかったが問題ないか

## 動作確認状況
- テストとビルドが通ることを確認
2023-07-21 03:06:12 +00:00

468 lines
15 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,
TASK_STATUS,
USER_ROLES,
} from '../../constants/index';
import { User } from '../../repositories/users/entity/user.entity';
import {
AudioFileNotFoundError,
AuthorUserNotMatchError,
TemplateFileNotFoundError,
} from './errors/types';
import {
AccountNotMatchError,
StatusNotMatchError,
TasksNotFoundError,
TypistUserNotFoundError,
} 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 [Task2241] コンテナが無ければ作成しているが、アカウント登録時に作成するので本処理は削除予定。
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 userId: number;
let country: string;
let isTypist: boolean;
let authorId: string;
try {
const user = await this.usersRepository.findUserByExternalId(externalId);
accountId = user.account.id;
userId = user.id;
userId = user.id;
country = user.account.country;
isTypist = user.role === USER_ROLES.TYPIST;
authorId = user.author_id;
} catch (e) {
this.logger.error(`error=${e}`);
console.log(e);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
const status = isTypist
? [TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING]
: Object.values(TASK_STATUS);
const task = await this.tasksRepository.getTaskAndAudioFile(
audioFileId,
accountId,
status,
);
const file = task.file;
// タスクに紐づく音声ファイルだけが消される場合がある。
// その場合はダウンロード不可なので不在エラーとして扱う
if (!file) {
throw new AudioFileNotFoundError(
`Audio file is not exists in DB. audio_file_id:${audioFileId}`,
);
}
// ユーザーがAuthorの場合、自身が追加したタスクでない場合はエラー
if (!isTypist && task.file.author_id !== authorId) {
throw new AuthorUserNotMatchError(
`task author is not match. audio_file_id:${audioFileId}, task.file.author_id:${task.file.author_id}, authorId:${authorId}`,
);
}
// ユーザーがTypistの場合、自身が担当したタスクでない場合はエラー
if (isTypist && task.typist_user_id !== userId) {
throw new AuthorUserNotMatchError(
`task typist is not match. audio_file_id:${audioFileId}, task.typist_user_id:${task.typist_user_id}, userId:${userId}`,
);
}
const filePath = `${file.owner_user_id}/${file.file_name}`;
const isFileExist = await this.blobStorageService.fileExists(
accountId,
country,
filePath,
);
if (!isFileExist) {
this.logger.log(`filePath:${filePath}`);
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:
case AccountNotMatchError:
case StatusNotMatchError:
case AuthorUserNotMatchError:
case TypistUserNotFoundError:
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,
);
}
}
/**
* 指定したIDの音声ファイルに紐づいた文字起こしテンプレートファイルのダウンロードURLを取得する
* @param externalId
* @param audioFileId
* @returns template file download sas
*/
async publishTemplateFileDownloadSas(
externalId: string,
audioFileId: number,
): Promise<string> {
//DBから国情報とアカウントID,ユーザーIDを取得する
let accountId: number;
let userId: number;
let country: string;
let isTypist: boolean;
let authorId: string;
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;
authorId = user.author_id;
} catch (e) {
this.logger.error(`error=${e}`);
console.log(e);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
const status = isTypist
? [TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING]
: Object.values(TASK_STATUS);
const task = await this.tasksRepository.getTaskAndAudioFile(
audioFileId,
accountId,
status,
);
const template_file = task.template_file;
// タスクに紐づくテンプレートファイルがない場合がある。
// その場合はダウンロード不可なので不在エラーとして扱う
if (!template_file) {
throw new TemplateFileNotFoundError(
`Template file is not exists in DB. audio_file_id:${audioFileId}`,
);
}
// ユーザーがAuthorの場合、自身が追加したタスクでない場合はエラー
if (!isTypist && task.file.author_id !== authorId) {
throw new AuthorUserNotMatchError(
`task author is not match. audio_file_id:${audioFileId}, task.file.author_id:${task.file.author_id}, authorId:${authorId}`,
);
}
// ユーザーがTypistの場合、自身が担当したタスクでない場合はエラー
if (isTypist && task.typist_user_id !== userId) {
throw new AuthorUserNotMatchError(
`task typist is not match. audio_file_id:${audioFileId}, task.typist_user_id:${task.typist_user_id}, userId:${userId}`,
);
}
const filePath = `Templates/${template_file.file_name}`;
const isFileExist = await this.blobStorageService.fileExists(
accountId,
country,
filePath,
);
if (!isFileExist) {
throw new TemplateFileNotFoundError(
`Template file is not exists in blob storage. audio_file_id:${audioFileId}, url:${template_file.url}, fileName:${template_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:
case AccountNotMatchError:
case StatusNotMatchError:
case AuthorUserNotMatchError:
case TypistUserNotFoundError:
throw new HttpException(
makeErrorResponse('E010603'),
HttpStatus.BAD_REQUEST,
);
case TemplateFileNotFoundError:
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,
);
}
}
}