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:
makabe.t 2023-07-07 06:57:29 +00:00
parent 766c995ceb
commit 773c8894e7
18 changed files with 643 additions and 16 deletions

View File

@ -13,3 +13,4 @@ TENANT_NAME=adb2codmsdev
SIGNIN_FLOW_NAME=b2c_1_signin_dev SIGNIN_FLOW_NAME=b2c_1_signin_dev
EMAIL_CONFIRM_LIFETIME=86400000 EMAIL_CONFIRM_LIFETIME=86400000
APP_DOMAIN=https://10.1.0.10:4443/ APP_DOMAIN=https://10.1.0.10:4443/
STORAGE_TOKEN_EXPIRE_TIME=2

View File

@ -16,7 +16,6 @@ MAIL_FROM=xxxxx@xxxxx.xxxx
NOTIFICATION_HUB_NAME=ntf-odms-shared NOTIFICATION_HUB_NAME=ntf-odms-shared
NOTIFICATION_HUB_CONNECT_STRING=XXXXXXXXXXXXXXXXXX NOTIFICATION_HUB_CONNECT_STRING=XXXXXXXXXXXXXXXXXX
APP_DOMAIN=http://localhost:8081/ APP_DOMAIN=http://localhost:8081/
STORAGE_TOKEN_EXPIRE_TIME=30
STORAGE_ACCOUNT_NAME_US=saodmsusdev STORAGE_ACCOUNT_NAME_US=saodmsusdev
STORAGE_ACCOUNT_NAME_AU=saodmsaudev STORAGE_ACCOUNT_NAME_AU=saodmsaudev
STORAGE_ACCOUNT_NAME_EU=saodmseudev STORAGE_ACCOUNT_NAME_EU=saodmseudev

View File

@ -36,4 +36,6 @@ export const ErrorCodes = [
'E010501', // アカウント不在エラー 'E010501', // アカウント不在エラー
'E010601', // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない) 'E010601', // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない)
'E010602', // タスク変更権限不足エラー 'E010602', // タスク変更権限不足エラー
'E010603', // タスク不在エラー
'E010701', // Blobファイル不在エラー
] as const; ] as const;

View File

@ -25,4 +25,6 @@ export const errors: Errors = {
E010501: 'Account not Found Error.', E010501: 'Account not Found Error.',
E010601: 'Task is not Editable Error', E010601: 'Task is not Editable Error',
E010602: 'No task edit permissions Error', E010602: 'No task edit permissions Error',
E010603: 'Task not found Error.',
E010701: 'File not found in Blob Storage Error.',
}; };

View File

@ -0,0 +1,2 @@
// 音声ファイル不在エラー
export class AudioFileNotFoundError extends Error {}

View File

@ -178,16 +178,24 @@ export class FilesController {
'指定した音声ファイルのBlob Storage上のダウンロード先アクセスURLを取得します', '指定した音声ファイルのBlob Storage上のダウンロード先アクセスURLを取得します',
}) })
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(
RoleGuard.requireds({ roles: [USER_ROLES.AUTHOR, USER_ROLES.TYPIST] }),
)
async downloadLocation( async downloadLocation(
@Headers() headers, @Req() req: Request,
@Query() body: AudioDownloadLocationRequest, @Query() body: AudioDownloadLocationRequest,
): Promise<AudioDownloadLocationResponse> { ): Promise<AudioDownloadLocationResponse> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { audioFileId } = body; 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') @Get('template/download-location')

View File

@ -6,8 +6,16 @@ import {
makeDefaultUsersRepositoryMockValue, makeDefaultUsersRepositoryMockValue,
makeFilesServiceMock, makeFilesServiceMock,
} from './test/files.service.mock'; } 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 () => { it('アップロードSASトークンが乗っているURLを返却する', async () => {
const blobParam = makeBlobstorageServiceMockValue(); const blobParam = makeBlobstorageServiceMockValue();
const userRepoParam = makeDefaultUsersRepositoryMockValue(); const userRepoParam = makeDefaultUsersRepositoryMockValue();
@ -95,7 +103,9 @@ describe('FilesService', () => {
new HttpException(makeErrorResponse('E009999'), HttpStatus.UNAUTHORIZED), new HttpException(makeErrorResponse('E009999'), HttpStatus.UNAUTHORIZED),
); );
}); });
});
describe('タスク作成', () => {
it('文字起こしタスクを作成できる', async () => { it('文字起こしタスクを作成できる', async () => {
const blobParam = makeBlobstorageServiceMockValue(); const blobParam = makeBlobstorageServiceMockValue();
const userRepoParam = makeDefaultUsersRepositoryMockValue(); 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 = [ const optionItemList = [
{ {
optionItemLabel: 'label_01', optionItemLabel: 'label_01',

View File

@ -5,14 +5,17 @@ import { UsersRepositoryService } from '../../repositories/users/users.repositor
import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service'; import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
import { AudioOptionItem, AudioUploadFinishedResponse } from './types/types'; 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 { User } from '../../repositories/users/entity/user.entity';
import { AudioFileNotFoundError } from './errors/types';
import { TasksNotFoundError } from '../../repositories/tasks/errors/types';
@Injectable() @Injectable()
export class FilesService { export class FilesService {
private readonly logger = new Logger(FilesService.name); private readonly logger = new Logger(FilesService.name);
constructor( constructor(
private readonly usersRepository: UsersRepositoryService, private readonly usersRepository: UsersRepositoryService,
private readonly tasksRepository: TasksRepositoryService,
private readonly tasksRepositoryService: TasksRepositoryService, private readonly tasksRepositoryService: TasksRepositoryService,
private readonly blobStorageService: BlobstorageService, private readonly blobStorageService: BlobstorageService,
) {} ) {}
@ -112,13 +115,19 @@ export class FilesService {
} }
try { 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ナンバーにインクリメントする // 追加時に末尾のJOBナンバーにインクリメントする
const task = await this.tasksRepositoryService.create( const task = await this.tasksRepositoryService.create(
user.account_id, user.account_id,
user.id, user.id,
priority, priority,
url, fileUrl,
fileName, fileName,
authorId, authorId,
workType, 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,
);
}
}
} }

View File

@ -9,7 +9,9 @@ import { Task } from '../../../repositories/tasks/entity/task.entity';
export type BlobstorageServiceMockValue = { export type BlobstorageServiceMockValue = {
createContainer: void | Error; createContainer: void | Error;
containerExists: boolean | Error; containerExists: boolean | Error;
fileExists: boolean | Error;
publishUploadSas: string | Error; publishUploadSas: string | Error;
publishDownloadSas: string | Error;
}; };
export type UsersRepositoryMockValue = { export type UsersRepositoryMockValue = {
@ -47,13 +49,23 @@ export const makeFilesServiceMock = async (
export const makeBlobstorageServiceMock = ( export const makeBlobstorageServiceMock = (
value: BlobstorageServiceMockValue, value: BlobstorageServiceMockValue,
) => { ) => {
const { containerExists, createContainer, publishUploadSas } = value; const {
containerExists,
fileExists,
createContainer,
publishUploadSas,
publishDownloadSas,
} = value;
return { return {
containerExists: containerExists:
containerExists instanceof Error containerExists instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(containerExists) ? jest.fn<Promise<void>, []>().mockRejectedValue(containerExists)
: jest.fn<Promise<boolean>, []>().mockResolvedValue(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:
createContainer instanceof Error createContainer instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(createContainer) ? jest.fn<Promise<void>, []>().mockRejectedValue(createContainer)
@ -62,6 +74,10 @@ export const makeBlobstorageServiceMock = (
publishUploadSas instanceof Error publishUploadSas instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(publishUploadSas) ? jest.fn<Promise<void>, []>().mockRejectedValue(publishUploadSas)
: jest.fn<Promise<string>, []>().mockResolvedValue(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 => { (): BlobstorageServiceMockValue => {
return { return {
containerExists: true, containerExists: true,
fileExists: true,
publishUploadSas: 'https://blob-storage?sas-token', publishUploadSas: 'https://blob-storage?sas-token',
publishDownloadSas: 'https://blob-storage?sas-token',
createContainer: undefined, createContainer: undefined,
}; };
}; };

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

View File

@ -19,7 +19,6 @@ import {
} from './test/utility'; } from './test/utility';
import { Adb2cTooManyRequestsError } from '../../gateways/adb2c/adb2c.service'; import { Adb2cTooManyRequestsError } from '../../gateways/adb2c/adb2c.service';
import { makeTestingModule } from '../../common/test/modules'; import { makeTestingModule } from '../../common/test/modules';
import { TasksNotFoundError } from '../../repositories/tasks/errors/types';
describe('TasksService', () => { describe('TasksService', () => {
it('タスク一覧を取得できるadmin', async () => { it('タスク一覧を取得できるadmin', async () => {

View File

@ -4,7 +4,9 @@ import {
StorageSharedKeyCredential, StorageSharedKeyCredential,
ContainerClient, ContainerClient,
ContainerSASPermissions, ContainerSASPermissions,
BlobSASPermissions,
generateBlobSASQueryParameters, generateBlobSASQueryParameters,
BlobClient,
} from '@azure/storage-blob'; } from '@azure/storage-blob';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { import {
@ -83,6 +85,27 @@ export class BlobstorageService {
return exists; 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を生成し返却します * SASトークン付きのBlobStorageアップロードURLを生成し返却します
* @param accountId * @param accountId
@ -109,10 +132,9 @@ export class BlobstorageService {
} }
//SASの有効期限を設定 //SASの有効期限を設定
//TODO 有効期限は仮で30分
const expiryDate = new Date(); const expiryDate = new Date();
expiryDate.setMinutes( expiryDate.setHours(
expiryDate.getMinutes() + expiryDate.getHours() +
this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'), this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'),
); );
@ -137,6 +159,63 @@ export class BlobstorageService {
return url.toString(); 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 * Gets container client
* @param companyName * @param companyName

View File

@ -1,10 +1,22 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; 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'; import { LicensesRepositoryService } from './licenses.repository.service';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([LicenseOrder,License,CardLicense,CardLicenseIssue])], imports: [
TypeOrmModule.forFeature([
LicenseOrder,
License,
CardLicense,
CardLicenseIssue,
]),
],
providers: [LicensesRepositoryService], providers: [LicensesRepositoryService],
exports: [LicensesRepositoryService], exports: [LicensesRepositoryService],
}) })

View File

@ -9,6 +9,7 @@ import {
JoinColumn, JoinColumn,
OneToMany, OneToMany,
} from 'typeorm'; } from 'typeorm';
import { TemplateFile } from '../../template_files/entity/template_file.entity';
@Entity({ name: 'tasks' }) @Entity({ name: 'tasks' })
export class Task { export class Task {
@ -44,4 +45,6 @@ export class Task {
@OneToOne(() => User, (user) => user.id) @OneToOne(() => User, (user) => user.id)
@JoinColumn({ name: 'typist_user_id' }) @JoinColumn({ name: 'typist_user_id' })
typist_user?: User; typist_user?: User;
@JoinColumn({ name: 'template_file_id' })
template_file?: TemplateFile;
} }

View File

@ -32,6 +32,43 @@ import { Roles } from '../../common/types/role';
@Injectable() @Injectable()
export class TasksRepositoryService { export class TasksRepositoryService {
constructor(private dataSource: DataSource) {} 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を取得する * IDに紐づいたTaskを取得する

View File

@ -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;
}

View File

@ -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 {}

View File

@ -0,0 +1,7 @@
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
@Injectable()
export class TemplateFilesRepositoryService {
constructor(private dataSource: DataSource) {}
}