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の変更 なし ## 動作確認状況 - ローカルで確認
This commit is contained in:
parent
766c995ceb
commit
773c8894e7
@ -13,3 +13,4 @@ TENANT_NAME=adb2codmsdev
|
||||
SIGNIN_FLOW_NAME=b2c_1_signin_dev
|
||||
EMAIL_CONFIRM_LIFETIME=86400000
|
||||
APP_DOMAIN=https://10.1.0.10:4443/
|
||||
STORAGE_TOKEN_EXPIRE_TIME=2
|
||||
@ -16,7 +16,6 @@ MAIL_FROM=xxxxx@xxxxx.xxxx
|
||||
NOTIFICATION_HUB_NAME=ntf-odms-shared
|
||||
NOTIFICATION_HUB_CONNECT_STRING=XXXXXXXXXXXXXXXXXX
|
||||
APP_DOMAIN=http://localhost:8081/
|
||||
STORAGE_TOKEN_EXPIRE_TIME=30
|
||||
STORAGE_ACCOUNT_NAME_US=saodmsusdev
|
||||
STORAGE_ACCOUNT_NAME_AU=saodmsaudev
|
||||
STORAGE_ACCOUNT_NAME_EU=saodmseudev
|
||||
|
||||
@ -36,4 +36,6 @@ export const ErrorCodes = [
|
||||
'E010501', // アカウント不在エラー
|
||||
'E010601', // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない)
|
||||
'E010602', // タスク変更権限不足エラー
|
||||
'E010603', // タスク不在エラー
|
||||
'E010701', // Blobファイル不在エラー
|
||||
] as const;
|
||||
|
||||
@ -25,4 +25,6 @@ export const errors: Errors = {
|
||||
E010501: 'Account not Found Error.',
|
||||
E010601: 'Task is not Editable Error',
|
||||
E010602: 'No task edit permissions Error',
|
||||
E010603: 'Task not found Error.',
|
||||
E010701: 'File not found in Blob Storage Error.',
|
||||
};
|
||||
|
||||
2
dictation_server/src/features/files/errors/types.ts
Normal file
2
dictation_server/src/features/files/errors/types.ts
Normal file
@ -0,0 +1,2 @@
|
||||
// 音声ファイル不在エラー
|
||||
export class AudioFileNotFoundError extends Error {}
|
||||
@ -178,16 +178,24 @@ export class FilesController {
|
||||
'指定した音声ファイルのBlob Storage上のダウンロード先アクセスURLを取得します',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [USER_ROLES.AUTHOR, USER_ROLES.TYPIST] }),
|
||||
)
|
||||
async downloadLocation(
|
||||
@Headers() headers,
|
||||
@Req() req: Request,
|
||||
@Query() body: AudioDownloadLocationRequest,
|
||||
): Promise<AudioDownloadLocationResponse> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { audioFileId } = body;
|
||||
// コンテナ作成処理の前にアクセストークンの認証を行う
|
||||
//
|
||||
|
||||
return { url: '' };
|
||||
const token = retrieveAuthorizationToken(req);
|
||||
const accessToken = jwt.decode(token, { json: true }) as AccessToken;
|
||||
const url = await this.filesService.publishAudioFileDownloadSas(
|
||||
accessToken.userId,
|
||||
audioFileId,
|
||||
);
|
||||
|
||||
return { url };
|
||||
}
|
||||
|
||||
@Get('template/download-location')
|
||||
|
||||
@ -6,8 +6,16 @@ import {
|
||||
makeDefaultUsersRepositoryMockValue,
|
||||
makeFilesServiceMock,
|
||||
} from './test/files.service.mock';
|
||||
import { DataSource } from 'typeorm';
|
||||
import {
|
||||
createAccount,
|
||||
createTask,
|
||||
createUser,
|
||||
makeTestingModuleWithBlob,
|
||||
} from './test/utility';
|
||||
import { FilesService } from './files.service';
|
||||
|
||||
describe('FilesService', () => {
|
||||
describe('音声ファイルアップロードURL取得', () => {
|
||||
it('アップロードSASトークンが乗っているURLを返却する', async () => {
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const userRepoParam = makeDefaultUsersRepositoryMockValue();
|
||||
@ -95,7 +103,9 @@ describe('FilesService', () => {
|
||||
new HttpException(makeErrorResponse('E009999'), HttpStatus.UNAUTHORIZED),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('タスク作成', () => {
|
||||
it('文字起こしタスクを作成できる', async () => {
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const userRepoParam = makeDefaultUsersRepositoryMockValue();
|
||||
@ -271,6 +281,139 @@ describe('FilesService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('音声ファイルダウンロードURL取得', () => {
|
||||
let source: DataSource = null;
|
||||
beforeEach(async () => {
|
||||
source = new DataSource({
|
||||
type: 'sqlite',
|
||||
database: ':memory:',
|
||||
logging: false,
|
||||
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
|
||||
synchronize: true, // trueにすると自動的にmigrationが行われるため注意
|
||||
});
|
||||
return source.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await source.destroy();
|
||||
source = null;
|
||||
});
|
||||
|
||||
it('ダウンロードSASトークンが乗っているURLを取得できる', async () => {
|
||||
const { accountId } = await createAccount(source);
|
||||
const { externalId, userId } = await createUser(
|
||||
source,
|
||||
accountId,
|
||||
'author-user-external-id',
|
||||
'author',
|
||||
'AUTHOR_ID',
|
||||
);
|
||||
const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/${userId}`;
|
||||
|
||||
const { audioFileId } = await createTask(
|
||||
source,
|
||||
accountId,
|
||||
url,
|
||||
'test.zip',
|
||||
);
|
||||
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
blobParam.publishDownloadSas = `${url}?sas-token`;
|
||||
blobParam.fileExists = true;
|
||||
|
||||
const module = await makeTestingModuleWithBlob(source, blobParam);
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
|
||||
expect(
|
||||
await service.publishAudioFileDownloadSas(externalId, audioFileId),
|
||||
).toEqual(`${url}?sas-token`);
|
||||
});
|
||||
|
||||
it('Typistの場合、タスクのステータスが[Uploaded,Inprogress,Pending]以外でエラー', async () => {
|
||||
const { accountId } = await createAccount(source);
|
||||
const { externalId, userId } = await createUser(
|
||||
source,
|
||||
accountId,
|
||||
'typist-user-external-id',
|
||||
'typist',
|
||||
undefined,
|
||||
);
|
||||
const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/${userId}`;
|
||||
|
||||
const { audioFileId } = await createTask(
|
||||
source,
|
||||
accountId,
|
||||
url,
|
||||
'test.zip',
|
||||
);
|
||||
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
blobParam.publishDownloadSas = `${url}?sas-token`;
|
||||
blobParam.fileExists = true;
|
||||
|
||||
const module = await makeTestingModuleWithBlob(source, blobParam);
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
|
||||
expect(
|
||||
await service.publishAudioFileDownloadSas(externalId, audioFileId),
|
||||
).toEqual(`${url}?sas-token`);
|
||||
});
|
||||
|
||||
it('Taskが存在しない場合はエラーとなる', async () => {
|
||||
const { accountId } = await createAccount(source);
|
||||
const { externalId } = await createUser(
|
||||
source,
|
||||
accountId,
|
||||
'author-user-external-id',
|
||||
'author',
|
||||
'AUTHOR_ID',
|
||||
);
|
||||
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
|
||||
const module = await makeTestingModuleWithBlob(source, blobParam);
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
|
||||
await expect(
|
||||
service.publishAudioFileDownloadSas(externalId, 1),
|
||||
).rejects.toEqual(
|
||||
new HttpException(makeErrorResponse('E010603'), HttpStatus.BAD_REQUEST),
|
||||
);
|
||||
});
|
||||
|
||||
it('blobストレージにファイルが存在しない場合はエラーとなる', async () => {
|
||||
const { accountId } = await createAccount(source);
|
||||
const { externalId, userId } = await createUser(
|
||||
source,
|
||||
accountId,
|
||||
'author-user-external-id',
|
||||
'author',
|
||||
'AUTHOR_ID',
|
||||
);
|
||||
const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/${userId}`;
|
||||
|
||||
const { audioFileId } = await createTask(
|
||||
source,
|
||||
accountId,
|
||||
url,
|
||||
'test.zip',
|
||||
);
|
||||
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
blobParam.publishDownloadSas = `${url}?sas-token`;
|
||||
blobParam.fileExists = false;
|
||||
|
||||
const module = await makeTestingModuleWithBlob(source, blobParam);
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
|
||||
await expect(
|
||||
service.publishAudioFileDownloadSas(externalId, audioFileId),
|
||||
).rejects.toEqual(
|
||||
new HttpException(makeErrorResponse('E010701'), HttpStatus.BAD_REQUEST),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const optionItemList = [
|
||||
{
|
||||
optionItemLabel: 'label_01',
|
||||
|
||||
@ -5,14 +5,17 @@ import { UsersRepositoryService } from '../../repositories/users/users.repositor
|
||||
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 } from '../../constants/index';
|
||||
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,
|
||||
) {}
|
||||
@ -112,13 +115,19 @@ export class FilesService {
|
||||
}
|
||||
|
||||
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,
|
||||
url,
|
||||
fileUrl,
|
||||
fileName,
|
||||
authorId,
|
||||
workType,
|
||||
@ -201,4 +210,98 @@ export class FilesService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定した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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,9 @@ import { Task } from '../../../repositories/tasks/entity/task.entity';
|
||||
export type BlobstorageServiceMockValue = {
|
||||
createContainer: void | Error;
|
||||
containerExists: boolean | Error;
|
||||
fileExists: boolean | Error;
|
||||
publishUploadSas: string | Error;
|
||||
publishDownloadSas: string | Error;
|
||||
};
|
||||
|
||||
export type UsersRepositoryMockValue = {
|
||||
@ -47,13 +49,23 @@ export const makeFilesServiceMock = async (
|
||||
export const makeBlobstorageServiceMock = (
|
||||
value: BlobstorageServiceMockValue,
|
||||
) => {
|
||||
const { containerExists, createContainer, publishUploadSas } = value;
|
||||
const {
|
||||
containerExists,
|
||||
fileExists,
|
||||
createContainer,
|
||||
publishUploadSas,
|
||||
publishDownloadSas,
|
||||
} = value;
|
||||
|
||||
return {
|
||||
containerExists:
|
||||
containerExists instanceof Error
|
||||
? jest.fn<Promise<void>, []>().mockRejectedValue(containerExists)
|
||||
: jest.fn<Promise<boolean>, []>().mockResolvedValue(containerExists),
|
||||
fileExists:
|
||||
fileExists instanceof Error
|
||||
? jest.fn<Promise<void>, []>().mockRejectedValue(fileExists)
|
||||
: jest.fn<Promise<boolean>, []>().mockResolvedValue(fileExists),
|
||||
createContainer:
|
||||
createContainer instanceof Error
|
||||
? jest.fn<Promise<void>, []>().mockRejectedValue(createContainer)
|
||||
@ -62,6 +74,10 @@ export const makeBlobstorageServiceMock = (
|
||||
publishUploadSas instanceof Error
|
||||
? jest.fn<Promise<void>, []>().mockRejectedValue(publishUploadSas)
|
||||
: jest.fn<Promise<string>, []>().mockResolvedValue(publishUploadSas),
|
||||
publishDownloadSas:
|
||||
publishDownloadSas instanceof Error
|
||||
? jest.fn<Promise<void>, []>().mockRejectedValue(publishDownloadSas)
|
||||
: jest.fn<Promise<string>, []>().mockResolvedValue(publishDownloadSas),
|
||||
};
|
||||
};
|
||||
|
||||
@ -80,7 +96,9 @@ export const makeBlobstorageServiceMockValue =
|
||||
(): BlobstorageServiceMockValue => {
|
||||
return {
|
||||
containerExists: true,
|
||||
fileExists: true,
|
||||
publishUploadSas: 'https://blob-storage?sas-token',
|
||||
publishDownloadSas: 'https://blob-storage?sas-token',
|
||||
createContainer: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
188
dictation_server/src/features/files/test/utility.ts
Normal file
188
dictation_server/src/features/files/test/utility.ts
Normal file
@ -0,0 +1,188 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { UserGroupsRepositoryModule } from '../../../repositories/user_groups/user_groups.repository.module';
|
||||
import { TasksRepositoryModule } from '../../../repositories/tasks/tasks.repository.module';
|
||||
import { AuthModule } from '../../../features/auth/auth.module';
|
||||
import { AdB2cModule } from '../../../gateways/adb2c/adb2c.module';
|
||||
import { AccountsModule } from '../../../features/accounts/accounts.module';
|
||||
import { UsersModule } from '../../../features/users/users.module';
|
||||
import { FilesModule } from '../../../features/files/files.module';
|
||||
import { TasksModule } from '../../../features/tasks/tasks.module';
|
||||
import { SendGridModule } from '../../../features/../gateways/sendgrid/sendgrid.module';
|
||||
import { LicensesModule } from '../../../features/licenses/licenses.module';
|
||||
import { AccountsRepositoryModule } from '../../../repositories/accounts/accounts.repository.module';
|
||||
import { UsersRepositoryModule } from '../../../repositories/users/users.repository.module';
|
||||
import { LicensesRepositoryModule } from '../../../repositories/licenses/licenses.repository.module';
|
||||
import { AudioFilesRepositoryModule } from '../../../repositories/audio_files/audio_files.repository.module';
|
||||
import { AudioOptionItemsRepositoryModule } from '../../../repositories/audio_option_items/audio_option_items.repository.module';
|
||||
import { CheckoutPermissionsRepositoryModule } from '../../../repositories/checkout_permissions/checkout_permissions.repository.module';
|
||||
import { NotificationModule } from '../../../features//notification/notification.module';
|
||||
import { NotificationhubModule } from '../../../gateways/notificationhub/notificationhub.module';
|
||||
import { BlobstorageModule } from '../../../gateways/blobstorage/blobstorage.module';
|
||||
import { AuthGuardsModule } from '../../../common/guards/auth/authguards.module';
|
||||
import { SortCriteriaRepositoryModule } from '../../../repositories/sort_criteria/sort_criteria.repository.module';
|
||||
import { AuthService } from '../../../features/auth/auth.service';
|
||||
import { AccountsService } from '../../../features/accounts/accounts.service';
|
||||
import { UsersService } from '../../../features/users/users.service';
|
||||
import { NotificationhubService } from '../../../gateways/notificationhub/notificationhub.service';
|
||||
import { FilesService } from '../../../features/files/files.service';
|
||||
import { LicensesService } from '../../../features/licenses/licenses.service';
|
||||
import { TasksService } from '../../../features/tasks/tasks.service';
|
||||
import { Task } from '../../../repositories/tasks/entity/task.entity';
|
||||
import { AudioFile } from '../../../repositories/audio_files/entity/audio_file.entity';
|
||||
import { BlobstorageService } from '../../../gateways/blobstorage/blobstorage.service';
|
||||
import {
|
||||
BlobstorageServiceMockValue,
|
||||
makeBlobstorageServiceMock,
|
||||
} from './files.service.mock';
|
||||
import { User } from '../../../repositories/users/entity/user.entity';
|
||||
import { Account } from '../../../repositories/accounts/entity/account.entity';
|
||||
|
||||
export const createAccount = async (
|
||||
datasource: DataSource,
|
||||
): Promise<{ accountId: number }> => {
|
||||
const { identifiers } = await datasource.getRepository(Account).insert({
|
||||
tier: 5,
|
||||
country: 'US',
|
||||
delegation_permission: false,
|
||||
locked: false,
|
||||
company_name: 'test inc.',
|
||||
verified: true,
|
||||
deleted_at: '',
|
||||
created_by: 'test_runner',
|
||||
created_at: new Date(),
|
||||
updated_by: 'updater',
|
||||
updated_at: new Date(),
|
||||
});
|
||||
const account = identifiers.pop() as Account;
|
||||
return { accountId: account.id };
|
||||
};
|
||||
|
||||
export const createUser = async (
|
||||
datasource: DataSource,
|
||||
accountId: number,
|
||||
external_id: string,
|
||||
role: string,
|
||||
author_id?: string | undefined,
|
||||
): Promise<{ userId: number; externalId: string }> => {
|
||||
const { identifiers } = await datasource.getRepository(User).insert({
|
||||
account_id: accountId,
|
||||
external_id: external_id,
|
||||
role: role,
|
||||
accepted_terms_version: '1.0',
|
||||
author_id: author_id,
|
||||
email_verified: true,
|
||||
auto_renew: true,
|
||||
license_alert: true,
|
||||
notification: true,
|
||||
created_by: 'test_runner',
|
||||
created_at: new Date(),
|
||||
updated_by: 'updater',
|
||||
updated_at: new Date(),
|
||||
});
|
||||
const user = identifiers.pop() as User;
|
||||
return { userId: user.id, externalId: external_id };
|
||||
};
|
||||
|
||||
export const createTask = async (
|
||||
datasource: DataSource,
|
||||
account_id: number,
|
||||
url: string,
|
||||
filename: string,
|
||||
): Promise<{ audioFileId: number }> => {
|
||||
const { identifiers: audioFileIdentifiers } = await datasource
|
||||
.getRepository(AudioFile)
|
||||
.insert({
|
||||
account_id: account_id,
|
||||
owner_user_id: 1,
|
||||
url: url,
|
||||
file_name: filename,
|
||||
author_id: 'AUTHOR_ID',
|
||||
work_type_id: 'work_type_id',
|
||||
started_at: new Date(),
|
||||
duration: '100000',
|
||||
finished_at: new Date(),
|
||||
uploaded_at: new Date(),
|
||||
file_size: 10000,
|
||||
priority: '00',
|
||||
audio_format: 'audio_format',
|
||||
is_encrypted: true,
|
||||
});
|
||||
const audioFile = audioFileIdentifiers.pop() as AudioFile;
|
||||
const { identifiers: taskIdentifiers } = await datasource
|
||||
.getRepository(Task)
|
||||
.insert({
|
||||
job_number: '00000001',
|
||||
account_id: account_id,
|
||||
is_job_number_enabled: true,
|
||||
audio_file_id: audioFile.id,
|
||||
status: 'Uploaded',
|
||||
priority: '01',
|
||||
started_at: new Date().toISOString(),
|
||||
created_at: new Date(),
|
||||
});
|
||||
|
||||
return { audioFileId: audioFile.id };
|
||||
};
|
||||
|
||||
export const makeTestingModuleWithBlob = async (
|
||||
datasource: DataSource,
|
||||
blobStorageService: BlobstorageServiceMockValue,
|
||||
): Promise<TestingModule> => {
|
||||
try {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
envFilePath: ['.env.local', '.env'],
|
||||
isGlobal: true,
|
||||
}),
|
||||
AuthModule,
|
||||
AdB2cModule,
|
||||
AccountsModule,
|
||||
UsersModule,
|
||||
FilesModule,
|
||||
TasksModule,
|
||||
UsersModule,
|
||||
SendGridModule,
|
||||
LicensesModule,
|
||||
AccountsRepositoryModule,
|
||||
UsersRepositoryModule,
|
||||
LicensesRepositoryModule,
|
||||
AudioFilesRepositoryModule,
|
||||
AudioOptionItemsRepositoryModule,
|
||||
TasksRepositoryModule,
|
||||
CheckoutPermissionsRepositoryModule,
|
||||
UserGroupsRepositoryModule,
|
||||
UserGroupsRepositoryModule,
|
||||
NotificationModule,
|
||||
NotificationhubModule,
|
||||
BlobstorageModule,
|
||||
AuthGuardsModule,
|
||||
SortCriteriaRepositoryModule,
|
||||
],
|
||||
providers: [
|
||||
AuthService,
|
||||
AccountsService,
|
||||
UsersService,
|
||||
NotificationhubService,
|
||||
FilesService,
|
||||
TasksService,
|
||||
LicensesService,
|
||||
],
|
||||
})
|
||||
.useMocker(async (token) => {
|
||||
switch (token) {
|
||||
case DataSource:
|
||||
return datasource;
|
||||
}
|
||||
})
|
||||
.overrideProvider(BlobstorageService)
|
||||
.useValue(makeBlobstorageServiceMock(blobStorageService))
|
||||
.compile();
|
||||
|
||||
return module;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
@ -19,7 +19,6 @@ import {
|
||||
} from './test/utility';
|
||||
import { Adb2cTooManyRequestsError } from '../../gateways/adb2c/adb2c.service';
|
||||
import { makeTestingModule } from '../../common/test/modules';
|
||||
import { TasksNotFoundError } from '../../repositories/tasks/errors/types';
|
||||
|
||||
describe('TasksService', () => {
|
||||
it('タスク一覧を取得できる(admin)', async () => {
|
||||
|
||||
@ -4,7 +4,9 @@ import {
|
||||
StorageSharedKeyCredential,
|
||||
ContainerClient,
|
||||
ContainerSASPermissions,
|
||||
BlobSASPermissions,
|
||||
generateBlobSASQueryParameters,
|
||||
BlobClient,
|
||||
} from '@azure/storage-blob';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
@ -83,6 +85,27 @@ export class BlobstorageService {
|
||||
return exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Files exists
|
||||
* @param accountId
|
||||
* @param country
|
||||
* @param path
|
||||
* @param fileName
|
||||
* @param containerUrl
|
||||
* @returns exists
|
||||
*/
|
||||
async fileExists(
|
||||
accountId: number,
|
||||
country: string,
|
||||
filePath: string,
|
||||
): Promise<boolean> {
|
||||
const containerClient = this.getContainerClient(accountId, country);
|
||||
const blob = containerClient.getBlobClient(`${filePath}`);
|
||||
const exists = await blob.exists();
|
||||
|
||||
return exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* SASトークン付きのBlobStorageアップロードURLを生成し返却します
|
||||
* @param accountId
|
||||
@ -109,10 +132,9 @@ export class BlobstorageService {
|
||||
}
|
||||
|
||||
//SASの有効期限を設定
|
||||
//TODO 有効期限は仮で30分
|
||||
const expiryDate = new Date();
|
||||
expiryDate.setMinutes(
|
||||
expiryDate.getMinutes() +
|
||||
expiryDate.setHours(
|
||||
expiryDate.getHours() +
|
||||
this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'),
|
||||
);
|
||||
|
||||
@ -137,6 +159,63 @@ export class BlobstorageService {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* SASトークン付きのBlobStorageダウンロードURLを生成し返却します
|
||||
* @param accountId
|
||||
* @param country
|
||||
* @param path
|
||||
* @param fileName
|
||||
* @returns download sas
|
||||
*/
|
||||
async publishDownloadSas(
|
||||
accountId: number,
|
||||
country: string,
|
||||
filePath: string,
|
||||
): Promise<string> {
|
||||
this.logger.log(`[IN] ${this.publishDownloadSas.name}`);
|
||||
let containerClient: ContainerClient;
|
||||
let blobClient: BlobClient;
|
||||
let sharedKeyCredential: StorageSharedKeyCredential;
|
||||
try {
|
||||
// コンテナ名を指定してClientを取得
|
||||
containerClient = this.getContainerClient(accountId, country);
|
||||
// コンテナ内のBlobパス名を指定してClientを取得
|
||||
blobClient = containerClient.getBlobClient(`${filePath}`);
|
||||
// 国に対応したリージョンの接続情報を取得する
|
||||
sharedKeyCredential = this.getSharedKeyCredential(country);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
throw e;
|
||||
}
|
||||
|
||||
//SASの有効期限を設定
|
||||
const expiryDate = new Date();
|
||||
expiryDate.setHours(
|
||||
expiryDate.getHours() +
|
||||
this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'),
|
||||
);
|
||||
|
||||
//SASの権限を設定(ダウンロードのため読み取り許可)
|
||||
const permissions = new BlobSASPermissions();
|
||||
permissions.read = true;
|
||||
|
||||
//SASを発行
|
||||
const sasToken = generateBlobSASQueryParameters(
|
||||
{
|
||||
containerName: containerClient.containerName,
|
||||
blobName: blobClient.name,
|
||||
permissions: permissions,
|
||||
startsOn: new Date(),
|
||||
expiresOn: expiryDate,
|
||||
},
|
||||
sharedKeyCredential,
|
||||
);
|
||||
|
||||
const url = new URL(blobClient.url);
|
||||
url.search = `${sasToken}`;
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets container client
|
||||
* @param companyName
|
||||
|
||||
@ -1,10 +1,22 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CardLicense, CardLicenseIssue, License, LicenseOrder } from './entity/license.entity';
|
||||
import {
|
||||
CardLicense,
|
||||
CardLicenseIssue,
|
||||
License,
|
||||
LicenseOrder,
|
||||
} from './entity/license.entity';
|
||||
import { LicensesRepositoryService } from './licenses.repository.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([LicenseOrder,License,CardLicense,CardLicenseIssue])],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
LicenseOrder,
|
||||
License,
|
||||
CardLicense,
|
||||
CardLicenseIssue,
|
||||
]),
|
||||
],
|
||||
providers: [LicensesRepositoryService],
|
||||
exports: [LicensesRepositoryService],
|
||||
})
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
JoinColumn,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { TemplateFile } from '../../template_files/entity/template_file.entity';
|
||||
|
||||
@Entity({ name: 'tasks' })
|
||||
export class Task {
|
||||
@ -44,4 +45,6 @@ export class Task {
|
||||
@OneToOne(() => User, (user) => user.id)
|
||||
@JoinColumn({ name: 'typist_user_id' })
|
||||
typist_user?: User;
|
||||
@JoinColumn({ name: 'template_file_id' })
|
||||
template_file?: TemplateFile;
|
||||
}
|
||||
|
||||
@ -32,6 +32,43 @@ import { Roles } from '../../common/types/role';
|
||||
@Injectable()
|
||||
export class TasksRepositoryService {
|
||||
constructor(private dataSource: DataSource) {}
|
||||
/**
|
||||
* 音声ファイルと紐づいたTaskを取得する
|
||||
* @param audioFileId
|
||||
* @param account_id
|
||||
* @returns task and audio file
|
||||
*/
|
||||
async getTaskAndAudioFile(
|
||||
audioFileId: number,
|
||||
account_id: number,
|
||||
isTypist: boolean
|
||||
): Promise<Task> {
|
||||
const status = isTypist
|
||||
? [TASK_STATUS.UPLOADED, TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING]
|
||||
: [TASK_STATUS.UPLOADED, TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING, TASK_STATUS.FINISHED, TASK_STATUS.BACKUP];
|
||||
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const taskRepo = entityManager.getRepository(Task);
|
||||
// 指定した音声ファイルIDに紐づくTaskの中でAuthorIDが一致するものを取得
|
||||
const task = await taskRepo.findOne({
|
||||
relations: {
|
||||
file: true,
|
||||
},
|
||||
where: {
|
||||
audio_file_id: audioFileId,
|
||||
account_id: account_id,
|
||||
status: In(status),
|
||||
},
|
||||
});
|
||||
if (!task) {
|
||||
throw new TasksNotFoundError(
|
||||
`task not found. audio_file_id:${audioFileId}`,
|
||||
);
|
||||
}
|
||||
|
||||
return task;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 音声ファイルIDに紐づいたTaskを取得する
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'audio_files' })
|
||||
export class TemplateFile {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
@Column()
|
||||
account_id: number;
|
||||
@Column()
|
||||
url: string;
|
||||
@Column()
|
||||
file_name: string;
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { TemplateFilesRepositoryService } from './template_files.repository.service';
|
||||
import { TemplateFile } from './entity/template_file.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([TemplateFile])],
|
||||
providers: [TemplateFilesRepositoryService],
|
||||
exports: [TemplateFilesRepositoryService],
|
||||
})
|
||||
export class TemplateFilesRepositoryModule {}
|
||||
@ -0,0 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class TemplateFilesRepositoryService {
|
||||
constructor(private dataSource: DataSource) {}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user