diff --git a/dictation_server/.env b/dictation_server/.env index cfcf9f8..eba8c6a 100644 --- a/dictation_server/.env +++ b/dictation_server/.env @@ -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 \ No newline at end of file diff --git a/dictation_server/.env.local.example b/dictation_server/.env.local.example index 64e244c..c4deccc 100644 --- a/dictation_server/.env.local.example +++ b/dictation_server/.env.local.example @@ -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 diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index 08823d3..3bba8ea 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -36,4 +36,6 @@ export const ErrorCodes = [ 'E010501', // アカウント不在エラー 'E010601', // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない) 'E010602', // タスク変更権限不足エラー + 'E010603', // タスク不在エラー + 'E010701', // Blobファイル不在エラー ] as const; diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index 4e74380..f4ef422 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -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.', }; diff --git a/dictation_server/src/features/files/errors/types.ts b/dictation_server/src/features/files/errors/types.ts new file mode 100644 index 0000000..6b20c5b --- /dev/null +++ b/dictation_server/src/features/files/errors/types.ts @@ -0,0 +1,2 @@ +// 音声ファイル不在エラー +export class AudioFileNotFoundError extends Error {} diff --git a/dictation_server/src/features/files/files.controller.ts b/dictation_server/src/features/files/files.controller.ts index cabea34..a9a402a 100644 --- a/dictation_server/src/features/files/files.controller.ts +++ b/dictation_server/src/features/files/files.controller.ts @@ -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 { - // 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') diff --git a/dictation_server/src/features/files/files.service.spec.ts b/dictation_server/src/features/files/files.service.spec.ts index 645be8c..4c71073 100644 --- a/dictation_server/src/features/files/files.service.spec.ts +++ b/dictation_server/src/features/files/files.service.spec.ts @@ -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); + + 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); + + 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); + + 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); + + await expect( + service.publishAudioFileDownloadSas(externalId, audioFileId), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010701'), HttpStatus.BAD_REQUEST), + ); + }); +}); + const optionItemList = [ { optionItemLabel: 'label_01', diff --git a/dictation_server/src/features/files/files.service.ts b/dictation_server/src/features/files/files.service.ts index 6ddd5b3..86a901c 100644 --- a/dictation_server/src/features/files/files.service.ts +++ b/dictation_server/src/features/files/files.service.ts @@ -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 { + //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, + ); + } + } } diff --git a/dictation_server/src/features/files/test/files.service.mock.ts b/dictation_server/src/features/files/test/files.service.mock.ts index c6b87cb..a841f05 100644 --- a/dictation_server/src/features/files/test/files.service.mock.ts +++ b/dictation_server/src/features/files/test/files.service.mock.ts @@ -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, []>().mockRejectedValue(containerExists) : jest.fn, []>().mockResolvedValue(containerExists), + fileExists: + fileExists instanceof Error + ? jest.fn, []>().mockRejectedValue(fileExists) + : jest.fn, []>().mockResolvedValue(fileExists), createContainer: createContainer instanceof Error ? jest.fn, []>().mockRejectedValue(createContainer) @@ -62,6 +74,10 @@ export const makeBlobstorageServiceMock = ( publishUploadSas instanceof Error ? jest.fn, []>().mockRejectedValue(publishUploadSas) : jest.fn, []>().mockResolvedValue(publishUploadSas), + publishDownloadSas: + publishDownloadSas instanceof Error + ? jest.fn, []>().mockRejectedValue(publishDownloadSas) + : jest.fn, []>().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, }; }; diff --git a/dictation_server/src/features/files/test/utility.ts b/dictation_server/src/features/files/test/utility.ts new file mode 100644 index 0000000..93df7e1 --- /dev/null +++ b/dictation_server/src/features/files/test/utility.ts @@ -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 => { + 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); + } +}; diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index 06adcbd..67c6d70 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -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 () => { diff --git a/dictation_server/src/gateways/blobstorage/blobstorage.service.ts b/dictation_server/src/gateways/blobstorage/blobstorage.service.ts index 824ce13..b7e92b6 100644 --- a/dictation_server/src/gateways/blobstorage/blobstorage.service.ts +++ b/dictation_server/src/gateways/blobstorage/blobstorage.service.ts @@ -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 { + 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 { + 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 diff --git a/dictation_server/src/repositories/licenses/licenses.repository.module.ts b/dictation_server/src/repositories/licenses/licenses.repository.module.ts index a1ec5bf..a142c1f 100644 --- a/dictation_server/src/repositories/licenses/licenses.repository.module.ts +++ b/dictation_server/src/repositories/licenses/licenses.repository.module.ts @@ -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], }) diff --git a/dictation_server/src/repositories/tasks/entity/task.entity.ts b/dictation_server/src/repositories/tasks/entity/task.entity.ts index 1c13ba6..e73cf1a 100644 --- a/dictation_server/src/repositories/tasks/entity/task.entity.ts +++ b/dictation_server/src/repositories/tasks/entity/task.entity.ts @@ -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; } diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index d7535f3..caf1ec3 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -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 { + 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を取得する diff --git a/dictation_server/src/repositories/template_files/entity/template_file.entity.ts b/dictation_server/src/repositories/template_files/entity/template_file.entity.ts new file mode 100644 index 0000000..ac23433 --- /dev/null +++ b/dictation_server/src/repositories/template_files/entity/template_file.entity.ts @@ -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; +} diff --git a/dictation_server/src/repositories/template_files/template_files.repository.module.ts b/dictation_server/src/repositories/template_files/template_files.repository.module.ts new file mode 100644 index 0000000..000232a --- /dev/null +++ b/dictation_server/src/repositories/template_files/template_files.repository.module.ts @@ -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 {} diff --git a/dictation_server/src/repositories/template_files/template_files.repository.service.ts b/dictation_server/src/repositories/template_files/template_files.repository.service.ts new file mode 100644 index 0000000..b35c191 --- /dev/null +++ b/dictation_server/src/repositories/template_files/template_files.repository.service.ts @@ -0,0 +1,7 @@ +import { Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; + +@Injectable() +export class TemplateFilesRepositoryService { + constructor(private dataSource: DataSource) {} +}