Merged PR 432: API実装(テンプレートファイルアップロード先取得API)
## 概要 [Task2654: API実装(テンプレートファイルアップロード先取得API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2654) - テンプレートファイルアップロード先取得APIとテストを実装しました。 - フォルダパス+SASトークンの形式で返却する。 ## レビューポイント - 返却URLは適切か - BlobServiceでSASトークン発行を既存のメソッドとは別で用意したが構成は適切か - UT用にBlobServiceのoverrideにメソッドを追加したが問題ないか。 - テストケースは適切か ## UIの変更 - なし ## 動作確認状況 - ローカルで確認
This commit is contained in:
parent
3f4d4ec436
commit
f994c23b51
@ -173,6 +173,16 @@ export const overrideBlobstorageService = <TService>(
|
||||
accountId: number,
|
||||
country: string,
|
||||
) => Promise<void>;
|
||||
containerExists?: (
|
||||
context: Context,
|
||||
accountId: number,
|
||||
country: string,
|
||||
) => Promise<boolean>;
|
||||
publishTemplateUploadSas?: (
|
||||
context: Context,
|
||||
accountId: number,
|
||||
country: string,
|
||||
) => Promise<string>;
|
||||
},
|
||||
): void => {
|
||||
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
|
||||
@ -189,6 +199,18 @@ export const overrideBlobstorageService = <TService>(
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
if (overrides.containerExists) {
|
||||
Object.defineProperty(obj, obj.containerExists.name, {
|
||||
value: overrides.containerExists,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
if (overrides.publishTemplateUploadSas) {
|
||||
Object.defineProperty(obj, obj.publishTemplateUploadSas.name, {
|
||||
value: overrides.publishTemplateUploadSas,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -290,13 +290,16 @@ export class FilesController {
|
||||
@Req() req: Request,
|
||||
): Promise<TemplateUploadLocationResponse> {
|
||||
const token = retrieveAuthorizationToken(req);
|
||||
const accessToken = jwt.decode(token, { json: true }) as AccessToken;
|
||||
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
|
||||
|
||||
const context = makeContext(accessToken.userId);
|
||||
const context = makeContext(userId);
|
||||
|
||||
console.log(context.trackingId);
|
||||
const url = await this.filesService.publishTemplateFileUploadSas(
|
||||
context,
|
||||
userId,
|
||||
);
|
||||
|
||||
return { url: '' };
|
||||
return { url };
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
|
||||
@ -10,7 +10,13 @@ import { DataSource } from 'typeorm';
|
||||
import { createTask, makeTestingModuleWithBlob } from './test/utility';
|
||||
import { FilesService } from './files.service';
|
||||
import { makeContext } from '../../common/log';
|
||||
import { makeTestSimpleAccount, makeTestUser } from '../../common/test/utility';
|
||||
import {
|
||||
makeTestAccount,
|
||||
makeTestSimpleAccount,
|
||||
makeTestUser,
|
||||
} from '../../common/test/utility';
|
||||
import { makeTestingModule } from '../../common/test/modules';
|
||||
import { overrideBlobstorageService } from '../../common/test/overrides';
|
||||
|
||||
describe('音声ファイルアップロードURL取得', () => {
|
||||
it('アップロードSASトークンが乗っているURLを返却する', async () => {
|
||||
@ -779,6 +785,102 @@ describe('テンプレートファイルダウンロードURL取得', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('publishTemplateFileUploadSas', () => {
|
||||
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 module = await makeTestingModule(source);
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
// 第五階層のアカウント作成
|
||||
const { account, admin } = await makeTestAccount(source, { tier: 5 });
|
||||
|
||||
const context = makeContext(admin.external_id);
|
||||
const baseUrl = `https://saodmsusdev.blob.core.windows.net/account-${account.id}/Templates`;
|
||||
|
||||
//SASトークンを返却する
|
||||
overrideBlobstorageService(service, {
|
||||
containerExists: async () => true,
|
||||
publishTemplateUploadSas: async () => `${baseUrl}?sas-token`,
|
||||
});
|
||||
|
||||
const url = await service.publishTemplateFileUploadSas(
|
||||
context,
|
||||
admin.external_id,
|
||||
);
|
||||
|
||||
expect(url).toBe(`${baseUrl}?sas-token`);
|
||||
});
|
||||
|
||||
it('blobストレージにコンテナが存在しない場合はエラーとなる', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
// 第五階層のアカウント作成
|
||||
const { admin } = await makeTestAccount(source, { tier: 5 });
|
||||
|
||||
const context = makeContext(admin.external_id);
|
||||
|
||||
//Blobコンテナ存在チェックに失敗するようにする
|
||||
overrideBlobstorageService(service, {
|
||||
containerExists: async () => false,
|
||||
publishTemplateUploadSas: async () => '',
|
||||
});
|
||||
|
||||
try {
|
||||
await service.publishTemplateFileUploadSas(context, admin.external_id);
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('SASトークンの取得に失敗した場合はエラーとなる', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
// 第五階層のアカウント作成
|
||||
const { admin } = await makeTestAccount(source, { tier: 5 });
|
||||
|
||||
const context = makeContext(admin.external_id);
|
||||
|
||||
//BlobのSASトークン生成に失敗するようにする
|
||||
overrideBlobstorageService(service, {
|
||||
containerExists: async () => true,
|
||||
publishTemplateUploadSas: async () => {
|
||||
throw new Error('blob failed');
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await service.publishTemplateFileUploadSas(context, admin.external_id);
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const optionItemList = [
|
||||
{
|
||||
optionItemLabel: 'label_01',
|
||||
|
||||
@ -535,4 +535,53 @@ export class FilesService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ログインユーザーアカウントのテンプレートファイルのアップロードURLを取得する
|
||||
* @param context
|
||||
* @param externalId
|
||||
* @returns template file upload sas
|
||||
*/
|
||||
async publishTemplateFileUploadSas(
|
||||
context: Context,
|
||||
externalId: string,
|
||||
): Promise<string> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.publishTemplateFileUploadSas.name} | params: { externalId: ${externalId} };`,
|
||||
);
|
||||
try {
|
||||
const {
|
||||
account: { id: accountId, country },
|
||||
} = await this.usersRepository.findUserByExternalId(externalId);
|
||||
|
||||
// 国に応じたリージョンのBlobストレージにコンテナが存在するか確認
|
||||
const isContainerExists = await this.blobStorageService.containerExists(
|
||||
context,
|
||||
accountId,
|
||||
country,
|
||||
);
|
||||
if (!isContainerExists) {
|
||||
throw new Error('container not found.');
|
||||
}
|
||||
|
||||
// SASトークン発行
|
||||
const url = await this.blobStorageService.publishTemplateUploadSas(
|
||||
context,
|
||||
accountId,
|
||||
country,
|
||||
);
|
||||
|
||||
return url;
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.publishTemplateFileUploadSas.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ export class BlobstorageService {
|
||||
private readonly sharedKeyCredentialUS: StorageSharedKeyCredential;
|
||||
private readonly sharedKeyCredentialAU: StorageSharedKeyCredential;
|
||||
private readonly sharedKeyCredentialEU: StorageSharedKeyCredential;
|
||||
private readonly sasTokenExpireHour: number;
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.sharedKeyCredentialUS = new StorageSharedKeyCredential(
|
||||
this.configService.get('STORAGE_ACCOUNT_NAME_US'),
|
||||
@ -50,6 +51,14 @@ export class BlobstorageService {
|
||||
this.configService.get('STORAGE_ACCOUNT_ENDPOINT_EU'),
|
||||
this.sharedKeyCredentialEU,
|
||||
);
|
||||
|
||||
const expireTime = Number(
|
||||
this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'),
|
||||
);
|
||||
if (Number.isNaN(expireTime)) {
|
||||
throw new Error(`STORAGE_TOKEN_EXPIRE_TIME is invalid value NaN`);
|
||||
}
|
||||
this.sasTokenExpireHour = expireTime;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -206,10 +215,7 @@ export class BlobstorageService {
|
||||
|
||||
//SASの有効期限を設定
|
||||
const expiryDate = new Date();
|
||||
expiryDate.setHours(
|
||||
expiryDate.getHours() +
|
||||
this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'),
|
||||
);
|
||||
expiryDate.setHours(expiryDate.getHours() + this.sasTokenExpireHour);
|
||||
|
||||
//SASの権限を設定。Pendingにしたものを再アップロードする運用をするため、上書き可能にする
|
||||
const permissions = new ContainerSASPermissions();
|
||||
@ -237,6 +243,59 @@ export class BlobstorageService {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* SASトークン付きのBlobStorageテンプレートファイルアップロードURLを生成し返却します
|
||||
* @param accountId
|
||||
* @param country
|
||||
* @returns template upload sas
|
||||
*/
|
||||
async publishTemplateUploadSas(
|
||||
context: Context,
|
||||
accountId: number,
|
||||
country: string,
|
||||
): Promise<string> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.publishTemplateUploadSas.name}`,
|
||||
);
|
||||
|
||||
try {
|
||||
// コンテナ名を指定してClientを取得
|
||||
const containerClient = this.getContainerClient(accountId, country);
|
||||
// 国に対応したリージョンの接続情報を取得する
|
||||
const sharedKeyCredential = this.getSharedKeyCredential(country);
|
||||
//SASの有効期限を設定
|
||||
const expiryDate = new Date();
|
||||
expiryDate.setHours(expiryDate.getHours() + this.sasTokenExpireHour);
|
||||
|
||||
//SASの権限を設定。同名ファイルを再アップロードできる運用をするため、上書き可能にする
|
||||
const permissions = new ContainerSASPermissions();
|
||||
permissions.write = true;
|
||||
|
||||
//SASを発行
|
||||
const sasToken = generateBlobSASQueryParameters(
|
||||
{
|
||||
containerName: containerClient.containerName,
|
||||
permissions: permissions,
|
||||
startsOn: new Date(),
|
||||
expiresOn: expiryDate,
|
||||
},
|
||||
sharedKeyCredential,
|
||||
);
|
||||
|
||||
const url = new URL('Templates', containerClient.url);
|
||||
url.search = `${sasToken}`;
|
||||
|
||||
return url.toString();
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
throw e;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.publishTemplateUploadSas.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SASトークン付きのBlobStorageダウンロードURLを生成し返却します
|
||||
* @param accountId
|
||||
@ -274,10 +333,7 @@ export class BlobstorageService {
|
||||
|
||||
//SASの有効期限を設定
|
||||
const expiryDate = new Date();
|
||||
expiryDate.setHours(
|
||||
expiryDate.getHours() +
|
||||
this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'),
|
||||
);
|
||||
expiryDate.setHours(expiryDate.getHours() + this.sasTokenExpireHour);
|
||||
|
||||
//SASの権限を設定(ダウンロードのため読み取り許可)
|
||||
const permissions = new BlobSASPermissions();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user