Merged PR 1100: 2025/6/30 本番リリース

PH2開発分を本番リリース用ブランチにマージ
This commit is contained in:
下田 雅人 2025-06-20 02:44:46 +00:00
parent 07043786c8
commit 0e8f0703e2
264 changed files with 49499 additions and 7117 deletions

View File

@ -1,16 +1,16 @@
#ビルドイメージ #ビルドイメージ
FROM node:18.17.1-buster AS build-container FROM node:22.14-bookworm AS build-container
WORKDIR /app WORKDIR /app
RUN mkdir dictation_function RUN mkdir dictation_function
COPY dictation_function/ dictation_function/ COPY dictation_function/ dictation_function/
RUN npm install --force -g n && n 18.17.1 \ RUN npm install --force -g n && n 22.14 \
&& cd dictation_function \ && cd dictation_function \
&& npm ci \ && npm ci \
&& npm run build \ && npm run build \
&& cd .. && cd ..
# 成果物イメージ # 成果物イメージ
FROM mcr.microsoft.com/azure-functions/node:4-node18 FROM mcr.microsoft.com/azure-functions/node:4-node22
WORKDIR /home/site/wwwroot WORKDIR /home/site/wwwroot
RUN mkdir build \ RUN mkdir build \

View File

@ -0,0 +1,47 @@
FROM node:22.14-bookworm-slim AS build-container
WORKDIR /app
RUN mkdir dictation_auto_transcription_file_server
RUN apt-get update \
&& apt-get install -y curl
COPY dictation_auto_transcription_file_server/ dictation_auto_transcription_file_server/
RUN npm install --force -g n && n 22.14 \
&& cd dictation_auto_transcription_file_server \
&& npm ci \
&& npm run build \
&& cd ..
RUN apt-get clean \
&& rm -rf /var/lib/apt/lists/*
FROM ubuntu:24.04
ENV TZ=Asia/Tokyo
RUN apt-get update \
&& apt-get install -y tzdata \
&& apt-get install -y unzip \
&& ln -fs /usr/share/zoneinfo/$TZ /etc/localtime \
&& dpkg-reconfigure -f noninteractive tzdata \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# nodeをbuild-containerからコピー
COPY --from=build-container /usr/local/include/ /usr/local/include/
COPY --from=build-container /usr/local/lib/ /usr/local/lib/
COPY --from=build-container /usr/local/bin/ /usr/local/bin/
# シンボリックリンクをリセット
RUN corepack disable && corepack enable
WORKDIR /app
RUN mkdir build \
&& mkdir dist \
&& mkdir node_modules \
# 変換ツールのパス
&& mkdir bin
COPY --from=build-container app/dictation_auto_transcription_file_server/dist/ dist/
COPY --from=build-container app/dictation_auto_transcription_file_server/.env ./
COPY --from=build-container app/dictation_auto_transcription_file_server/node_modules/ node_modules/
COPY --from=build-container app/dictation_auto_transcription_file_server/bin/ bin/
ARG BUILD_VERSION
ENV BUILD_VERSION=${BUILD_VERSION}
# 変換ツールのパスを通す
ENV PATH="/app/bin:$PATH"
CMD ["node", "./dist/main.js" ]

View File

@ -1,15 +1,18 @@
FROM node:18.17.1-buster AS build-container FROM node:22.14-bookworm AS build-container
WORKDIR /app WORKDIR /app
RUN mkdir dictation_server RUN mkdir dictation_server
COPY dictation_server/ dictation_server/ COPY dictation_server/ dictation_server/
RUN npm install --force -g n && n 18.17.1 \ RUN npm install --force -g n && n 22.14 \
&& cd dictation_server \ && cd dictation_server \
&& npm ci \ && npm ci \
&& npm run build \ && npm run build \
&& cd .. && cd ..
FROM node:18.17.1-alpine FROM node:22.14-alpine
RUN apk --no-cache add tzdata \ RUN apk add --no-cache \
tzdata \
zip \
unzip \
&& cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime \ && cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime \
&& apk del tzdata \ && apk del tzdata \
&& rm -rf /var/cache/apk/* && rm -rf /var/cache/apk/*

View File

@ -0,0 +1,81 @@
# Pipeline側でKeyVaultやDocker、AppService等に対する操作権限を持ったServiceConenctionを作成しておくこと
# また、環境変数 STATIC_DICTATION_DEPLOYMENT_TOKEN の値として静的WebAppsのデプロイトークンを設定しておくこと
trigger:
tags:
include:
- pre-release-*
# Job 1 : Initialize
jobs:
- job: initialize
displayName: Initialize
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
persistCredentials: true
- script: |
git fetch origin main:main
if git merge-base --is-ancestor $(Build.SourceVersion) main; then
echo "This commit is in the main branch."
else
echo "This commit is not in the main branch."
exit 1
fi
displayName: 'タグが付けられたCommitがmainブランチに存在するか確認'
# Job 2 : Convert Audio File Service Deploy
- job: convert_audio_file_service_deploy
dependsOn: initialize
condition: succeeded('initialize')
displayName: Convert Audio File Service Deploy
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureWebAppContainer@1
inputs:
azureSubscription: 'omds-service-connection-prod'
appName: 'app-odms-convert-audio-prod'
deployToSlotOrASE: true
resourceGroupName: 'odms-prod-rg'
slotName: 'staging'
containers: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/auto_transcription:$(Build.SourceVersion)'
# Job 3 : Smoke Test
- job: smoke_test
dependsOn: convert_audio_file_service_deploy
condition: succeeded('convert_audio_file_service_deploy')
displayName: 'smoke test'
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
# スモークテスト用にjobを確保
# Job 4 : Convert Audio File Service Slot Swap
- job: convert_audio_file_swap_slot
dependsOn: smoke_test
condition: succeeded('smoke_test')
displayName: 'Swap Convert Audio File Service Staging and Production'
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureAppServiceManage@0
displayName: 'Azure App Service Manage: app-odms-convert-audio-prod'
inputs:
azureSubscription: 'omds-service-connection-prod'
action: 'Swap Slots'
WebAppName: 'app-odms-convert-audio-prod'
ResourceGroupName: 'odms-prod-rg'
SourceSlot: 'staging'
SwapWithProduction: true

View File

@ -5,6 +5,7 @@ trigger:
include: include:
- release-* - release-*
# Job 1 : Initialize
jobs: jobs:
- job: initialize - job: initialize
displayName: Initialize displayName: Initialize
@ -24,6 +25,8 @@ jobs:
exit 1 exit 1
fi fi
displayName: 'タグが付けられたCommitがmainブランチに存在するか確認' displayName: 'タグが付けられたCommitがmainブランチに存在するか確認'
# Job 2 : Backend Deploy
- job: backend_deploy - job: backend_deploy
dependsOn: initialize dependsOn: initialize
condition: succeeded('initialize') condition: succeeded('initialize')
@ -42,6 +45,8 @@ jobs:
resourceGroupName: 'odms-prod-rg' resourceGroupName: 'odms-prod-rg'
slotName: 'staging' slotName: 'staging'
containers: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation:$(Build.SourceVersion)' containers: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation:$(Build.SourceVersion)'
# Job 3 : Frontend Deploy
- job: frontend_deploy - job: frontend_deploy
dependsOn: backend_deploy dependsOn: backend_deploy
condition: succeeded('backend_deploy') condition: succeeded('backend_deploy')
@ -83,6 +88,8 @@ jobs:
is_static_export: false is_static_export: false
verbose: false verbose: false
azure_static_web_apps_api_token: $(STATIC_DICTATION_DEPLOYMENT_TOKEN) azure_static_web_apps_api_token: $(STATIC_DICTATION_DEPLOYMENT_TOKEN)
# Job 4 : Function Deploy
- job: function_deploy - job: function_deploy
dependsOn: frontend_deploy dependsOn: frontend_deploy
condition: succeeded('frontend_deploy') condition: succeeded('frontend_deploy')
@ -98,9 +105,31 @@ jobs:
azureSubscription: 'omds-service-connection-prod' azureSubscription: 'omds-service-connection-prod'
appName: 'func-odms-dictation-prod' appName: 'func-odms-dictation-prod'
imageName: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation_function:$(Build.SourceVersion)' imageName: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation_function:$(Build.SourceVersion)'
- job: smoke_test
# Job 5 : Convert Audio File Service Deploy
- job: convert_audio_file_service_deploy
dependsOn: function_deploy dependsOn: function_deploy
condition: succeeded('function_deploy') condition: succeeded('function_deploy')
displayName: Convert Audio File Service Deploy
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureWebAppContainer@1
inputs:
azureSubscription: 'omds-service-connection-prod'
appName: 'app-odms-convert-audio-prod'
deployToSlotOrASE: true
resourceGroupName: 'odms-prod-rg'
slotName: 'staging'
containers: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/auto_transcription:$(Build.SourceVersion)'
# Job 6 : Smoke Test
- job: smoke_test
dependsOn: convert_audio_file_service_deploy
condition: succeeded('convert_audio_file_service_deploy')
displayName: 'smoke test' displayName: 'smoke test'
pool: pool:
name: odms-deploy-pipeline name: odms-deploy-pipeline
@ -109,10 +138,12 @@ jobs:
clean: true clean: true
fetchDepth: 1 fetchDepth: 1
# スモークテスト用にjobを確保 # スモークテスト用にjobを確保
- job: swap_slot
# Job 7 : Backend Slot Swap
- job: backend_swap_slot
dependsOn: smoke_test dependsOn: smoke_test
condition: succeeded('smoke_test') condition: succeeded('smoke_test')
displayName: 'Swap Staging and Production' displayName: 'Swap Backend Staging and Production'
pool: pool:
name: odms-deploy-pipeline name: odms-deploy-pipeline
steps: steps:
@ -128,9 +159,32 @@ jobs:
ResourceGroupName: 'odms-prod-rg' ResourceGroupName: 'odms-prod-rg'
SourceSlot: 'staging' SourceSlot: 'staging'
SwapWithProduction: true SwapWithProduction: true
# Job 8 : Convert Audio File Service Slot Swap
- job: convert_audio_file_swap_slot
dependsOn: backend_swap_slot
condition: succeeded('backend_swap_slot')
displayName: 'Swap Convert Audio File Service Staging and Production'
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureAppServiceManage@0
displayName: 'Azure App Service Manage: app-odms-convert-audio-prod'
inputs:
azureSubscription: 'omds-service-connection-prod'
action: 'Swap Slots'
WebAppName: 'app-odms-convert-audio-prod'
ResourceGroupName: 'odms-prod-rg'
SourceSlot: 'staging'
SwapWithProduction: true
# Job 9 : DB migration
- job: migration - job: migration
dependsOn: swap_slot dependsOn: convert_audio_file_swap_slot
condition: succeeded('swap_slot') condition: succeeded('convert_audio_file_swap_slot')
displayName: DB migration displayName: DB migration
pool: pool:
name: odms-deploy-pipeline name: odms-deploy-pipeline

View File

@ -0,0 +1,488 @@
# Pipeline側でKeyVaultやDocker、AppService等に対する操作権限を持ったServiceConenctionを作成しておくこと
# また、環境変数 STATIC_DICTATION_DEPLOYMENT_TOKEN の値として静的WebAppsのデプロイトークンを設定しておくこと
trigger:
branches:
include:
- release-ph2
tags:
include:
- stage-*
# Job 1 : Initialize
jobs:
- job: initialize
displayName: Initialize
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
persistCredentials: true
- script: |
git fetch origin release-ph2:release-ph2
if git merge-base --is-ancestor $(Build.SourceVersion) release-ph2; then
echo "Commit is in the release-ph2 branch."
else
echo "Commit is not in the release-ph2 branch."
exit 1
fi
displayName: 'タグが付けられたCommitがrelease-ph2ブランチに存在するか確認'
# Job 2 : Backend Test
- job: backend_test
dependsOn: initialize
condition: succeeded('initialize')
displayName: Unit Test Backend
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Bash@3
displayName: Bash Script (Backend Unit Tests)
inputs:
targetType: inline
workingDirectory: dictation_server/.devcontainer
script: |
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
docker-compose -f pipeline-docker-compose.yml build
docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_server sudo npm ci
docker-compose exec -T dictation_server sudo npm run migrate:up:test
docker-compose exec -T dictation_server sudo npm run test
# Job 3 : Backend Build & Push
- job: backend_build
dependsOn: backend_test
condition: succeeded('backend_test')
displayName: Build and Push Backend Image
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_server
verbose: false
- task: Docker@0
displayName: Build Backend Image
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
dockerFile: DockerfileServerDictation.dockerfile
imageName: odmscloud/staging/dictation:$(Build.SourceVersion)
buildArguments: |
BUILD_VERSION=$(Build.SourceVersion)
- task: Docker@0
displayName: Push Backend Image
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
action: Push an image
imageName: odmscloud/staging/dictation:$(Build.SourceVersion)
# Job 4 : Frontend Staging Build
- job: frontend_build_staging
dependsOn: backend_build
condition: succeeded('backend_build')
displayName: Build Frontend Files(staging)
variables:
storageAccountName: saomdspipeline
environment: staging
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_client
verbose: false
- task: Bash@3
displayName: Bash Script
inputs:
targetType: inline
script: cd dictation_client && npm run build:stg
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: dictation_client/build
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip'
replaceExistingArchive: true
- task: AzureCLI@2
inputs:
azureSubscription: 'omds-service-connection-stg'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az storage blob upload \
--auth-mode login \
--account-name $(storageAccountName) \
--container-name $(environment) \
--name $(Build.SourceVersion).zip \
--type block \
--overwrite \
--file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip
# Job 5 : Frontend Production Build
- job: frontend_build_production
dependsOn: frontend_build_staging
condition: succeeded('frontend_build_staging')
displayName: Build Frontend Files(production)
variables:
storageAccountName: saomdspipeline
environment: production
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_client
verbose: false
- task: Bash@3
displayName: Bash Script
inputs:
targetType: inline
script: cd dictation_client && npm run build:prod
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: dictation_client/build
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip'
replaceExistingArchive: true
- task: AzureCLI@2
inputs:
azureSubscription: 'omds-service-connection-stg'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az storage blob upload \
--auth-mode login \
--account-name $(storageAccountName) \
--container-name $(environment) \
--name $(Build.SourceVersion).zip \
--type block \
--overwrite \
--file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip
# Job 6 : Function Unit Test
- job: function_test
dependsOn: frontend_build_production
condition: succeeded('frontend_build_production')
displayName: Unit Test Function
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Bash@3
displayName: Bash Script (Test)
inputs:
targetType: inline
workingDirectory: dictation_function/.devcontainer
script: |
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
docker-compose -f pipeline-docker-compose.yml build
docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_function sudo npm ci
docker-compose exec -T dictation_function sudo npm run test
# Job 7 : Function Build & Push
- job: function_build
dependsOn: function_test
condition: succeeded('function_test')
displayName: Build And Push Function Image
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_function
verbose: false
- task: Docker@0
displayName: build
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
dockerFile: DockerfileFunctionDictation.dockerfile
imageName: odmscloud/staging/dictation_function:$(Build.SourceVersion)
buildArguments: |
BUILD_VERSION=$(Build.SourceVersion)
- task: Docker@0
displayName: push
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
action: Push an image
imageName: odmscloud/staging/dictation_function:$(Build.SourceVersion)
# Job 8 : Convert Audio File Test
- job: convert_audio_file_service_test
dependsOn: function_build
condition: succeeded('function_build')
displayName: Unit Test Convert Audio File
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Bash@3
displayName: Bash Script (Convert Audio File Unit Tests)
inputs:
targetType: inline
workingDirectory: dictation_auto_transcription_file_server/.devcontainer
script: |
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
docker-compose -f pipeline-docker-compose.yml build
docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_auto_transcription_file_server sudo npm ci
docker-compose exec -T dictation_auto_transcription_file_server npm run test
# Job 9 : Convert Audio File Build & Push
- job: convert_audio_file_service_build
dependsOn: convert_audio_file_service_test
condition: succeeded('convert_audio_file_service_test')
displayName: Build and Push Convert Audio File Image
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_auto_transcription_file_server
verbose: false
- task: Docker@0
displayName: Build Convert Audio File Image
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
dockerFile: DockerfileServerAutoTranscription.dockerfile
imageName: odmscloud/staging/auto_transcription:$(Build.SourceVersion)
buildArguments: |
BUILD_VERSION=$(Build.SourceVersion)
- task: Docker@0
displayName: Push Convert Audio File Image
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
action: Push an image
imageName: odmscloud/staging/auto_transcription:$(Build.SourceVersion)
# Job 10 : Backend Deploy
- job: backend_deploy
dependsOn: convert_audio_file_service_build
condition: succeeded('convert_audio_file_service_build')
displayName: Backend Deploy
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureWebAppContainer@1
inputs:
azureSubscription: 'omds-service-connection-stg'
appName: 'app-odms-dictation-stg'
deployToSlotOrASE: true
resourceGroupName: 'stg-application-rg'
slotName: 'staging'
containers: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation:$(Build.SourceVersion)'
# Job 11 : Frontend Deploy
- job: frontend_deploy
dependsOn: backend_deploy
condition: succeeded('backend_deploy')
displayName: Deploy Frontend Files
variables:
storageAccountName: saomdspipeline
environment: staging
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureCLI@2
inputs:
azureSubscription: 'omds-service-connection-stg'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az storage blob download \
--auth-mode login \
--account-name $(storageAccountName) \
--container-name $(environment) \
--name $(Build.SourceVersion).zip \
--file $(Build.SourcesDirectory)/$(Build.SourceVersion).zip
- task: Bash@3
displayName: Bash Script
inputs:
targetType: inline
script: unzip $(Build.SourcesDirectory)/$(Build.SourceVersion).zip -d $(Build.SourcesDirectory)/$(Build.SourceVersion)
- task: AzureStaticWebApp@0
displayName: 'Static Web App: '
inputs:
workingDirectory: '$(Build.SourcesDirectory)'
app_location: '/$(Build.SourceVersion)'
config_file_location: /dictation_client
skip_app_build: true
skip_api_build: true
is_static_export: false
verbose: false
azure_static_web_apps_api_token: $(STATIC_DICTATION_DEPLOYMENT_TOKEN)
# Job 12 : Function Deploy
- job: function_deploy
dependsOn: frontend_deploy
condition: succeeded('frontend_deploy')
displayName: Function Deploy
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureFunctionAppContainer@1
inputs:
azureSubscription: 'omds-service-connection-stg'
appName: 'func-odms-dictation-stg'
imageName: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation_function:$(Build.SourceVersion)'
# Job 13 : Convert Audio File Deploy
- job: convert_audio_file_service_deploy
dependsOn: function_deploy
condition: succeeded('function_deploy')
displayName: Convert Audio File Deploy
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureWebAppContainer@1
inputs:
azureSubscription: 'omds-service-connection-stg'
appName: 'app-odms-convert-audio-stg'
deployToSlotOrASE: true
resourceGroupName: 'stg-application-rg'
slotName: 'staging'
containers: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/auto_transcription:$(Build.SourceVersion)'
# Job 14 : Smoke Test
- job: smoke_test
dependsOn: convert_audio_file_service_deploy
condition: succeeded('convert_audio_file_service_deploy')
displayName: 'smoke test'
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
# スモークテスト用にjobを確保
# Job 15 : Backend Slot Swap
- job: backend_swap_slot
dependsOn: smoke_test
condition: succeeded('smoke_test')
displayName: 'Swap Backend Staging and Production'
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureAppServiceManage@0
displayName: 'Azure App Service Manage: app-odms-dictation-stg'
inputs:
azureSubscription: 'omds-service-connection-stg'
action: 'Swap Slots'
WebAppName: 'app-odms-dictation-stg'
ResourceGroupName: 'stg-application-rg'
SourceSlot: 'staging'
SwapWithProduction: true
# Job 16 : Convert Audio File Slot Swap
- job: convert_audio_file_swap_slot
dependsOn: backend_swap_slot
condition: succeeded('backend_swap_slot')
displayName: 'Swap Convert Audio File Staging and Production'
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureAppServiceManage@0
displayName: 'Azure App Service Manage: app-odms-convert-audio-stg'
inputs:
azureSubscription: 'omds-service-connection-stg'
action: 'Swap Slots'
WebAppName: 'app-odms-convert-audio-stg'
ResourceGroupName: 'stg-application-rg'
SourceSlot: 'staging'
SwapWithProduction: true
# Job 17 : DB migration
- job: migration
dependsOn: convert_audio_file_swap_slot
condition: succeeded('convert_audio_file_swap_slot')
displayName: DB migration
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureKeyVault@2
displayName: 'Azure Key Vault: kv-odms-secret-stg'
inputs:
ConnectedServiceName: 'omds-service-connection-stg'
KeyVaultName: kv-odms-secret-stg
- task: CmdLine@2
displayName: migration
inputs:
script: >2
# DB接続情報書き換え
sed -i -e "s/DB_NAME/$(db-name-ph2)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_PASS/$(admin-db-pass)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_USERNAME/$(admin-db-user)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_PORT/$(db-port)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_HOST/$(db-host)/g" ./dictation_server/db/dbconfig.yml
sql-migrate --version
cat ./dictation_server/db/dbconfig.yml
# migration実行
sql-migrate up -config=./dictation_server/db/dbconfig.yml -env=ci

View File

@ -8,6 +8,7 @@ trigger:
include: include:
- stage-* - stage-*
# Job 1 : Initialize
jobs: jobs:
- job: initialize - job: initialize
displayName: Initialize displayName: Initialize
@ -27,10 +28,11 @@ jobs:
exit 1 exit 1
fi fi
displayName: 'タグが付けられたCommitがmainブランチに存在するか確認' displayName: 'タグが付けられたCommitがmainブランチに存在するか確認'
# Job 2 : Backend Test
- job: backend_test - job: backend_test
dependsOn: initialize dependsOn: initialize
condition: succeeded('initialize') condition: succeeded('initialize')
displayName: UnitTest displayName: Unit Test Backend
pool: pool:
vmImage: ubuntu-latest vmImage: ubuntu-latest
steps: steps:
@ -38,23 +40,25 @@ jobs:
clean: true clean: true
fetchDepth: 1 fetchDepth: 1
- task: Bash@3 - task: Bash@3
displayName: Bash Script (Test) displayName: Bash Script (Backend Unit Tests)
inputs: inputs:
targetType: inline targetType: inline
workingDirectory: dictation_server/.devcontainer workingDirectory: dictation_server/.devcontainer
script: | script: |
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version docker-compose --version
docker-compose -f pipeline-docker-compose.yml build docker-compose -f pipeline-docker-compose.yml build
docker-compose -f pipeline-docker-compose.yml up -d docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_server sudo npm ci docker-compose exec -T dictation_server sudo npm ci
docker-compose exec -T dictation_server sudo npm run migrate:up:test docker-compose exec -T dictation_server sudo npm run migrate:up:test
docker-compose exec -T dictation_server sudo npm run test docker-compose exec -T dictation_server sudo npm run test
# Job 3 : Backend Build & Push
- job: backend_build - job: backend_build
dependsOn: backend_test dependsOn: backend_test
condition: succeeded('backend_test') condition: succeeded('backend_test')
displayName: Build And Push Backend Image displayName: Build and Push Backend Image
pool: pool:
name: odms-deploy-pipeline name: odms-deploy-pipeline
steps: steps:
@ -68,7 +72,7 @@ jobs:
workingDir: dictation_server workingDir: dictation_server
verbose: false verbose: false
- task: Docker@0 - task: Docker@0
displayName: build displayName: Build Backend Image
inputs: inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg' azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}' azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
@ -77,12 +81,14 @@ jobs:
buildArguments: | buildArguments: |
BUILD_VERSION=$(Build.SourceVersion) BUILD_VERSION=$(Build.SourceVersion)
- task: Docker@0 - task: Docker@0
displayName: push displayName: Push Backend Image
inputs: inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg' azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}' azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
action: Push an image action: Push an image
imageName: odmscloud/staging/dictation:$(Build.SourceVersion) imageName: odmscloud/staging/dictation:$(Build.SourceVersion)
# Job 4 : Frontend Staging Build
- job: frontend_build_staging - job: frontend_build_staging
dependsOn: backend_build dependsOn: backend_build
condition: succeeded('backend_build') condition: succeeded('backend_build')
@ -128,6 +134,8 @@ jobs:
--type block \ --type block \
--overwrite \ --overwrite \
--file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip --file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip
# Job 5 : Frontend Production Build
- job: frontend_build_production - job: frontend_build_production
dependsOn: frontend_build_staging dependsOn: frontend_build_staging
condition: succeeded('frontend_build_staging') condition: succeeded('frontend_build_staging')
@ -173,10 +181,12 @@ jobs:
--type block \ --type block \
--overwrite \ --overwrite \
--file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip --file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip
# Job 6 : Function Unit Test
- job: function_test - job: function_test
dependsOn: frontend_build_production dependsOn: frontend_build_production
condition: succeeded('frontend_build_production') condition: succeeded('frontend_build_production')
displayName: UnitTest displayName: Unit Test Function
pool: pool:
vmImage: ubuntu-latest vmImage: ubuntu-latest
steps: steps:
@ -196,6 +206,8 @@ jobs:
docker-compose -f pipeline-docker-compose.yml up -d docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_function sudo npm ci docker-compose exec -T dictation_function sudo npm ci
docker-compose exec -T dictation_function sudo npm run test docker-compose exec -T dictation_function sudo npm run test
# Job 7 : Function Build & Push
- job: function_build - job: function_build
dependsOn: function_test dependsOn: function_test
condition: succeeded('function_test') condition: succeeded('function_test')
@ -228,9 +240,70 @@ jobs:
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}' azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
action: Push an image action: Push an image
imageName: odmscloud/staging/dictation_function:$(Build.SourceVersion) imageName: odmscloud/staging/dictation_function:$(Build.SourceVersion)
- job: backend_deploy
# Job 8 : Convert Audio File Test
- job: convert_audio_file_service_test
dependsOn: function_build dependsOn: function_build
condition: succeeded('function_build') condition: succeeded('function_build')
displayName: Unit Test Convert Audio File
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Bash@3
displayName: Bash Script (Convert Audio File Unit Tests)
inputs:
targetType: inline
workingDirectory: dictation_auto_transcription_file_server/.devcontainer
script: |
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
docker-compose -f pipeline-docker-compose.yml build
docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_auto_transcription_file_server sudo npm ci
docker-compose exec -T dictation_auto_transcription_file_server npm run test
# Job 9 : Convert Audio File Build & Push
- job: convert_audio_file_service_build
dependsOn: convert_audio_file_service_test
condition: succeeded('convert_audio_file_service_test')
displayName: Build and Push Convert Audio File Image
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_auto_transcription_file_server
verbose: false
- task: Docker@0
displayName: Build Convert Audio File Image
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
dockerFile: DockerfileServerAutoTranscription.dockerfile
imageName: odmscloud/staging/auto_transcription:$(Build.SourceVersion)
buildArguments: |
BUILD_VERSION=$(Build.SourceVersion)
- task: Docker@0
displayName: Push Convert Audio File Image
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
action: Push an image
imageName: odmscloud/staging/auto_transcription:$(Build.SourceVersion)
# Job 10 : Backend Deploy
- job: backend_deploy
dependsOn: convert_audio_file_service_build
condition: succeeded('convert_audio_file_service_build')
displayName: Backend Deploy displayName: Backend Deploy
pool: pool:
vmImage: ubuntu-latest vmImage: ubuntu-latest
@ -246,6 +319,8 @@ jobs:
resourceGroupName: 'stg-application-rg' resourceGroupName: 'stg-application-rg'
slotName: 'staging' slotName: 'staging'
containers: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation:$(Build.SourceVersion)' containers: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation:$(Build.SourceVersion)'
# Job 11 : Frontend Deploy
- job: frontend_deploy - job: frontend_deploy
dependsOn: backend_deploy dependsOn: backend_deploy
condition: succeeded('backend_deploy') condition: succeeded('backend_deploy')
@ -287,6 +362,8 @@ jobs:
is_static_export: false is_static_export: false
verbose: false verbose: false
azure_static_web_apps_api_token: $(STATIC_DICTATION_DEPLOYMENT_TOKEN) azure_static_web_apps_api_token: $(STATIC_DICTATION_DEPLOYMENT_TOKEN)
# Job 12 : Function Deploy
- job: function_deploy - job: function_deploy
dependsOn: frontend_deploy dependsOn: frontend_deploy
condition: succeeded('frontend_deploy') condition: succeeded('frontend_deploy')
@ -302,9 +379,31 @@ jobs:
azureSubscription: 'omds-service-connection-stg' azureSubscription: 'omds-service-connection-stg'
appName: 'func-odms-dictation-stg' appName: 'func-odms-dictation-stg'
imageName: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation_function:$(Build.SourceVersion)' imageName: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation_function:$(Build.SourceVersion)'
- job: smoke_test
# Job 13 : Convert Audio File Deploy
- job: convert_audio_file_service_deploy
dependsOn: function_deploy dependsOn: function_deploy
condition: succeeded('function_deploy') condition: succeeded('function_deploy')
displayName: Convert Audio File Deploy
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureWebAppContainer@1
inputs:
azureSubscription: 'omds-service-connection-stg'
appName: 'app-odms-convert-audio-stg'
deployToSlotOrASE: true
resourceGroupName: 'stg-application-rg'
slotName: 'staging'
containers: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/auto_transcription:$(Build.SourceVersion)'
# Job 14 : Smoke Test
- job: smoke_test
dependsOn: convert_audio_file_service_deploy
condition: succeeded('convert_audio_file_service_deploy')
displayName: 'smoke test' displayName: 'smoke test'
pool: pool:
name: odms-deploy-pipeline name: odms-deploy-pipeline
@ -313,10 +412,12 @@ jobs:
clean: true clean: true
fetchDepth: 1 fetchDepth: 1
# スモークテスト用にjobを確保 # スモークテスト用にjobを確保
- job: swap_slot
# Job 15 : Backend Slot Swap
- job: backend_swap_slot
dependsOn: smoke_test dependsOn: smoke_test
condition: succeeded('smoke_test') condition: succeeded('smoke_test')
displayName: 'Swap Staging and Production' displayName: 'Swap Backend Staging and Production'
pool: pool:
name: odms-deploy-pipeline name: odms-deploy-pipeline
steps: steps:
@ -332,9 +433,32 @@ jobs:
ResourceGroupName: 'stg-application-rg' ResourceGroupName: 'stg-application-rg'
SourceSlot: 'staging' SourceSlot: 'staging'
SwapWithProduction: true SwapWithProduction: true
# Job 16 : Convert Audio File Slot Swap
- job: convert_audio_file_swap_slot
dependsOn: backend_swap_slot
condition: succeeded('backend_swap_slot')
displayName: 'Swap Convert Audio File Staging and Production'
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureAppServiceManage@0
displayName: 'Azure App Service Manage: app-odms-convert-audio-stg'
inputs:
azureSubscription: 'omds-service-connection-stg'
action: 'Swap Slots'
WebAppName: 'app-odms-convert-audio-stg'
ResourceGroupName: 'stg-application-rg'
SourceSlot: 'staging'
SwapWithProduction: true
# Job 17 : DB migration
- job: migration - job: migration
dependsOn: swap_slot dependsOn: convert_audio_file_swap_slot
condition: succeeded('swap_slot') condition: succeeded('convert_audio_file_swap_slot')
displayName: DB migration displayName: DB migration
pool: pool:
name: odms-deploy-pipeline name: odms-deploy-pipeline
@ -351,7 +475,7 @@ jobs:
displayName: migration displayName: migration
inputs: inputs:
script: >2 script: >2
# DB接続情報書き換え # DB接続情報書き換え
sed -i -e "s/DB_NAME/$(db-name)/g" ./dictation_server/db/dbconfig.yml sed -i -e "s/DB_NAME/$(db-name)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_PASS/$(admin-db-pass)/g" ./dictation_server/db/dbconfig.yml sed -i -e "s/DB_PASS/$(admin-db-pass)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_USERNAME/$(admin-db-user)/g" ./dictation_server/db/dbconfig.yml sed -i -e "s/DB_USERNAME/$(admin-db-user)/g" ./dictation_server/db/dbconfig.yml

View File

@ -1,4 +1,4 @@
FROM node:18.13.0-buster FROM node:22.14-bookworm
RUN /bin/cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \ RUN /bin/cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
echo "Asia/Tokyo" > /etc/timezone echo "Asia/Tokyo" > /etc/timezone

View File

@ -0,0 +1,43 @@
FROM ubuntu:24.04
ENV TZ=Asia/Tokyo
# Options for setup script
ARG INSTALL_ZSH="true"
ARG UPGRADE_PACKAGES="false"
ARG USERNAME=vscode
# 1000 はnodeで使われているためずらす
ARG USER_UID=1001
ARG USER_GID=$USER_UID
# Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies.
COPY library-scripts/common-debian.sh /tmp/library-scripts/
RUN bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" \
&& apt-get install default-jre -y \
&& apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts
RUN \
apt-get install -y tzdata && \
ln -fs /usr/share/zoneinfo/$TZ /etc/localtime && \
dpkg-reconfigure -f noninteractive tzdata && \
curl -sL https://deb.nodesource.com/setup_22.x | bash - && \
apt-get install -y nodejs build-essential
# Update NPM
RUN npm install -g npm
# Install NestJS
RUN npm i -g @nestjs/cli
# 以下 ユーザー権限で実施
USER $USERNAME
# copy init-script
COPY --chown=$USERNAME:$USERNAME init.sh /home/${USERNAME}/
RUN chmod +x /home/${USERNAME}/init.sh
# 変換ツールのパスを通す
ENV PATH="/app/dictation_auto_transcription_file_server/bin/:$PATH"
# 初期化を行う
# node imageのデフォルトENTRYPOINTが邪魔するため上書き
ENTRYPOINT /home/vscode/init.sh

View File

@ -0,0 +1,56 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/javascript-node
{
"name": "Dev Dictation Auto Transcription File Server",
"dockerComposeFile": [
"docker-compose.yml"
],
"service": "dictation_auto_transcription_file_server",
//
"shutdownAction": "none",
"workspaceFolder": "/app/dictation_auto_transcription_file_server",
"runArgs": [
"--cap-add=SYS_PTRACE",
"--security-opt",
"seccomp=unconfined"
],
// Set *default* container specific settings.json values on container create.
"settings": {
"terminal.integrated.shell.linux": "/bin/bash",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.format.enable": false,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// formatter
"editor.formatOnPaste": true,
"editor.formatOnType": true,
"editor.renderWhitespace": "all",
"editor.insertSpaces": false,
"editor.renderLineHighlight": "all"
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"dbaeumer.vscode-eslint",
"salbert.comment-ts",
"gruntfuggly.todo-tree",
"esbenp.prettier-vscode",
"ms-vsliveshare.vsliveshare",
"albymor.increment-selection",
"eamodio.gitlens",
"wmaurer.change-case"
],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "yarn install",
"postCreateCommand": "sudo chown -R vscode:vscode /app/dictation_auto_transcription_file_server",
// Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode"
}

View File

@ -0,0 +1,24 @@
services:
dictation_auto_transcription_file_server:
container_name: dictation_auto_transcription_file_server_dev_container
env_file: ../.env
build: .
working_dir: /app/dictation_auto_transcription_file_server
# platform: linux/x86_64
ports:
- '8083:8083'
volumes:
- ../../:/app
- node_modules:/app/dictation_auto_transcription_file_server/node_modules:delegate
expose:
- '8081'
environment:
- CHOKIDAR_USEPOLLING=true
networks:
- external
volumes:
node_modules:
networks:
external:
name: omds_network
external: true

View File

@ -0,0 +1,20 @@
#!/bin/bash
#
# Init Script for server Container
#
echo [init.sh] dictation_auto_transcription_file_server initialize.
# /app の権限がデフォルトでは node ユーザーになっているため、
# 権限確認し、vscode ユーザでない場合付け替える
ls -ld /app | grep vscode
if [ $? -ne 0 ]; then
echo [init.sh] change /app owner to vscode.
sudo chown -R vscode:vscode /app
fi
cd /app/dictation_auto_transcription_file_server
echo [init.sh] initialize completed!
sleep infinity

View File

@ -0,0 +1,454 @@
#!/usr/bin/env bash
#-------------------------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
#-------------------------------------------------------------------------------------------------------------
#
# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/common.md
# Maintainer: The VS Code and Codespaces Teams
#
# Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] [install Oh My Zsh! flag] [Add non-free packages]
set -e
INSTALL_ZSH=${1:-"true"}
USERNAME=${2:-"automatic"}
USER_UID=${3:-"automatic"}
USER_GID=${4:-"automatic"}
UPGRADE_PACKAGES=${5:-"true"}
INSTALL_OH_MYS=${6:-"true"}
ADD_NON_FREE_PACKAGES=${7:-"false"}
SCRIPT_DIR="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)"
MARKER_FILE="/usr/local/etc/vscode-dev-containers/common"
if [ "$(id -u)" -ne 0 ]; then
echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.'
exit 1
fi
# Ensure that login shells get the correct path if the user updated the PATH using ENV.
rm -f /etc/profile.d/00-restore-env.sh
echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh
chmod +x /etc/profile.d/00-restore-env.sh
# If in automatic mode, determine if a user already exists, if not use vscode
if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then
USERNAME=""
POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)")
for CURRENT_USER in ${POSSIBLE_USERS[@]}; do
if id -u ${CURRENT_USER} > /dev/null 2>&1; then
USERNAME=${CURRENT_USER}
break
fi
done
if [ "${USERNAME}" = "" ]; then
USERNAME=vscode
fi
elif [ "${USERNAME}" = "none" ]; then
USERNAME=root
USER_UID=0
USER_GID=0
fi
# Load markers to see which steps have already run
if [ -f "${MARKER_FILE}" ]; then
echo "Marker file found:"
cat "${MARKER_FILE}"
source "${MARKER_FILE}"
fi
# Ensure apt is in non-interactive to avoid prompts
export DEBIAN_FRONTEND=noninteractive
# Function to call apt-get if needed
apt_get_update_if_needed()
{
if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then
echo "Running apt-get update..."
apt-get update
else
echo "Skipping apt-get update."
fi
}
# Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies
if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then
package_list="apt-utils \
openssh-client \
gnupg2 \
dirmngr \
iproute2 \
procps \
lsof \
htop \
net-tools \
psmisc \
curl \
wget \
rsync \
ca-certificates \
unzip \
zip \
nano \
vim-tiny \
less \
jq \
lsb-release \
apt-transport-https \
dialog \
libc6 \
libgcc1 \
libkrb5-3 \
libgssapi-krb5-2 \
libicu[0-9][0-9] \
liblttng-ust[0-9] \
libstdc++6 \
zlib1g \
locales \
sudo \
ncdu \
man-db \
strace \
manpages \
manpages-dev \
init-system-helpers"
# Needed for adding manpages-posix and manpages-posix-dev which are non-free packages in Debian
if [ "${ADD_NON_FREE_PACKAGES}" = "true" ]; then
# Bring in variables from /etc/os-release like VERSION_CODENAME
. /etc/os-release
sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list
sed -i -E "s/deb-src http:\/\/(deb|httredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list
sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list
sed -i -E "s/deb-src http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list
sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list
sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list
sed -i "s/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list
sed -i "s/deb-src http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list
# Handle bullseye location for security https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html
sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list
sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list
echo "Running apt-get update..."
apt-get update
package_list="${package_list} manpages-posix manpages-posix-dev"
else
apt_get_update_if_needed
fi
# Install libssl1.1 if available
if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then
package_list="${package_list} libssl1.1"
fi
# Install appropriate version of libssl1.0.x if available
libssl_package=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '')
if [ "$(echo "$LIlibssl_packageBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then
if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then
# Debian 9
package_list="${package_list} libssl1.0.2"
elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then
# Ubuntu 18.04, 16.04, earlier
package_list="${package_list} libssl1.0.0"
fi
fi
echo "Packages to verify are installed: ${package_list}"
apt-get -y install --no-install-recommends ${package_list} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 )
# Install git if not already installed (may be more recent than distro version)
if ! type git > /dev/null 2>&1; then
apt-get -y install --no-install-recommends git
fi
PACKAGES_ALREADY_INSTALLED="true"
fi
# Get to latest versions of all packages
if [ "${UPGRADE_PACKAGES}" = "true" ]; then
apt_get_update_if_needed
apt-get -y upgrade --no-install-recommends
apt-get autoremove -y
fi
# Ensure at least the en_US.UTF-8 UTF-8 locale is available.
# Common need for both applications and things like the agnoster ZSH theme.
if [ "${LOCALE_ALREADY_SET}" != "true" ] && ! grep -o -E '^\s*en_US.UTF-8\s+UTF-8' /etc/locale.gen > /dev/null; then
echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen
locale-gen
LOCALE_ALREADY_SET="true"
fi
# Create or update a non-root user to match UID/GID.
group_name="${USERNAME}"
if id -u ${USERNAME} > /dev/null 2>&1; then
# User exists, update if needed
if [ "${USER_GID}" != "automatic" ] && [ "$USER_GID" != "$(id -g $USERNAME)" ]; then
group_name="$(id -gn $USERNAME)"
groupmod --gid $USER_GID ${group_name}
usermod --gid $USER_GID $USERNAME
fi
if [ "${USER_UID}" != "automatic" ] && [ "$USER_UID" != "$(id -u $USERNAME)" ]; then
usermod --uid $USER_UID $USERNAME
fi
else
# Create user
if [ "${USER_GID}" = "automatic" ]; then
groupadd $USERNAME
else
groupadd --gid $USER_GID $USERNAME
fi
if [ "${USER_UID}" = "automatic" ]; then
useradd -s /bin/bash --gid $USERNAME -m $USERNAME
else
useradd -s /bin/bash --uid $USER_UID --gid $USERNAME -m $USERNAME
fi
fi
# Add sudo support for non-root user
if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then
echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME
chmod 0440 /etc/sudoers.d/$USERNAME
EXISTING_NON_ROOT_USER="${USERNAME}"
fi
# ** Shell customization section **
if [ "${USERNAME}" = "root" ]; then
user_rc_path="/root"
else
user_rc_path="/home/${USERNAME}"
fi
# Restore user .bashrc defaults from skeleton file if it doesn't exist or is empty
if [ ! -f "${user_rc_path}/.bashrc" ] || [ ! -s "${user_rc_path}/.bashrc" ] ; then
cp /etc/skel/.bashrc "${user_rc_path}/.bashrc"
fi
# Restore user .profile defaults from skeleton file if it doesn't exist or is empty
if [ ! -f "${user_rc_path}/.profile" ] || [ ! -s "${user_rc_path}/.profile" ] ; then
cp /etc/skel/.profile "${user_rc_path}/.profile"
fi
# .bashrc/.zshrc snippet
rc_snippet="$(cat << 'EOF'
if [ -z "${USER}" ]; then export USER=$(whoami); fi
if [[ "${PATH}" != *"$HOME/.local/bin"* ]]; then export PATH="${PATH}:$HOME/.local/bin"; fi
# Display optional first run image specific notice if configured and terminal is interactive
if [ -t 1 ] && [[ "${TERM_PROGRAM}" = "vscode" || "${TERM_PROGRAM}" = "codespaces" ]] && [ ! -f "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed" ]; then
if [ -f "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" ]; then
cat "/usr/local/etc/vscode-dev-containers/first-run-notice.txt"
elif [ -f "/workspaces/.codespaces/shared/first-run-notice.txt" ]; then
cat "/workspaces/.codespaces/shared/first-run-notice.txt"
fi
mkdir -p "$HOME/.config/vscode-dev-containers"
# Mark first run notice as displayed after 10s to avoid problems with fast terminal refreshes hiding it
((sleep 10s; touch "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed") &)
fi
# Set the default git editor if not already set
if [ -z "$(git config --get core.editor)" ] && [ -z "${GIT_EDITOR}" ]; then
if [ "${TERM_PROGRAM}" = "vscode" ]; then
if [[ -n $(command -v code-insiders) && -z $(command -v code) ]]; then
export GIT_EDITOR="code-insiders --wait"
else
export GIT_EDITOR="code --wait"
fi
fi
fi
EOF
)"
# code shim, it fallbacks to code-insiders if code is not available
cat << 'EOF' > /usr/local/bin/code
#!/bin/sh
get_in_path_except_current() {
which -a "$1" | grep -A1 "$0" | grep -v "$0"
}
code="$(get_in_path_except_current code)"
if [ -n "$code" ]; then
exec "$code" "$@"
elif [ "$(command -v code-insiders)" ]; then
exec code-insiders "$@"
else
echo "code or code-insiders is not installed" >&2
exit 127
fi
EOF
chmod +x /usr/local/bin/code
# systemctl shim - tells people to use 'service' if systemd is not running
cat << 'EOF' > /usr/local/bin/systemctl
#!/bin/sh
set -e
if [ -d "/run/systemd/system" ]; then
exec /bin/systemctl "$@"
else
echo '\n"systemd" is not running in this container due to its overhead.\nUse the "service" command to start services instead. e.g.: \n\nservice --status-all'
fi
EOF
chmod +x /usr/local/bin/systemctl
# Codespaces bash and OMZ themes - partly inspired by https://github.com/ohmyzsh/ohmyzsh/blob/master/themes/robbyrussell.zsh-theme
codespaces_bash="$(cat \
<<'EOF'
# Codespaces bash prompt theme
__bash_prompt() {
local userpart='`export XIT=$? \
&& [ ! -z "${GITHUB_USER}" ] && echo -n "\[\033[0;32m\]@${GITHUB_USER} " || echo -n "\[\033[0;32m\]\u " \
&& [ "$XIT" -ne "0" ] && echo -n "\[\033[1;31m\]➜" || echo -n "\[\033[0m\]➜"`'
local gitbranch='`\
if [ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ]; then \
export BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null); \
if [ "${BRANCH}" != "" ]; then \
echo -n "\[\033[0;36m\](\[\033[1;31m\]${BRANCH}" \
&& if git ls-files --error-unmatch -m --directory --no-empty-directory -o --exclude-standard ":/*" > /dev/null 2>&1; then \
echo -n " \[\033[1;33m\]✗"; \
fi \
&& echo -n "\[\033[0;36m\]) "; \
fi; \
fi`'
local lightblue='\[\033[1;34m\]'
local removecolor='\[\033[0m\]'
PS1="${userpart} ${lightblue}\w ${gitbranch}${removecolor}\$ "
unset -f __bash_prompt
}
__bash_prompt
EOF
)"
codespaces_zsh="$(cat \
<<'EOF'
# Codespaces zsh prompt theme
__zsh_prompt() {
local prompt_username
if [ ! -z "${GITHUB_USER}" ]; then
prompt_username="@${GITHUB_USER}"
else
prompt_username="%n"
fi
PROMPT="%{$fg[green]%}${prompt_username} %(?:%{$reset_color%}➜ :%{$fg_bold[red]%}➜ )" # User/exit code arrow
PROMPT+='%{$fg_bold[blue]%}%(5~|%-1~/…/%3~|%4~)%{$reset_color%} ' # cwd
PROMPT+='$([ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ] && git_prompt_info)' # Git status
PROMPT+='%{$fg[white]%}$ %{$reset_color%}'
unset -f __zsh_prompt
}
ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[cyan]%}(%{$fg_bold[red]%}"
ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} "
ZSH_THEME_GIT_PROMPT_DIRTY=" %{$fg_bold[yellow]%}✗%{$fg_bold[cyan]%})"
ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg_bold[cyan]%})"
__zsh_prompt
EOF
)"
# Add RC snippet and custom bash prompt
if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then
echo "${rc_snippet}" >> /etc/bash.bashrc
echo "${codespaces_bash}" >> "${user_rc_path}/.bashrc"
echo 'export PROMPT_DIRTRIM=4' >> "${user_rc_path}/.bashrc"
if [ "${USERNAME}" != "root" ]; then
echo "${codespaces_bash}" >> "/root/.bashrc"
echo 'export PROMPT_DIRTRIM=4' >> "/root/.bashrc"
fi
chown ${USERNAME}:${group_name} "${user_rc_path}/.bashrc"
RC_SNIPPET_ALREADY_ADDED="true"
fi
# Optionally install and configure zsh and Oh My Zsh!
if [ "${INSTALL_ZSH}" = "true" ]; then
if ! type zsh > /dev/null 2>&1; then
apt_get_update_if_needed
apt-get install -y zsh
fi
if [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then
echo "${rc_snippet}" >> /etc/zsh/zshrc
ZSH_ALREADY_INSTALLED="true"
fi
# Adapted, simplified inline Oh My Zsh! install steps that adds, defaults to a codespaces theme.
# See https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/install.sh for official script.
oh_my_install_dir="${user_rc_path}/.oh-my-zsh"
if [ ! -d "${oh_my_install_dir}" ] && [ "${INSTALL_OH_MYS}" = "true" ]; then
template_path="${oh_my_install_dir}/templates/zshrc.zsh-template"
user_rc_file="${user_rc_path}/.zshrc"
umask g-w,o-w
mkdir -p ${oh_my_install_dir}
git clone --depth=1 \
-c core.eol=lf \
-c core.autocrlf=false \
-c fsck.zeroPaddedFilemode=ignore \
-c fetch.fsck.zeroPaddedFilemode=ignore \
-c receive.fsck.zeroPaddedFilemode=ignore \
"https://github.com/ohmyzsh/ohmyzsh" "${oh_my_install_dir}" 2>&1
echo -e "$(cat "${template_path}")\nDISABLE_AUTO_UPDATE=true\nDISABLE_UPDATE_PROMPT=true" > ${user_rc_file}
sed -i -e 's/ZSH_THEME=.*/ZSH_THEME="codespaces"/g' ${user_rc_file}
mkdir -p ${oh_my_install_dir}/custom/themes
echo "${codespaces_zsh}" > "${oh_my_install_dir}/custom/themes/codespaces.zsh-theme"
# Shrink git while still enabling updates
cd "${oh_my_install_dir}"
git repack -a -d -f --depth=1 --window=1
# Copy to non-root user if one is specified
if [ "${USERNAME}" != "root" ]; then
cp -rf "${user_rc_file}" "${oh_my_install_dir}" /root
chown -R ${USERNAME}:${group_name} "${user_rc_path}"
fi
fi
fi
# Persist image metadata info, script if meta.env found in same directory
meta_info_script="$(cat << 'EOF'
#!/bin/sh
. /usr/local/etc/vscode-dev-containers/meta.env
# Minimal output
if [ "$1" = "version" ] || [ "$1" = "image-version" ]; then
echo "${VERSION}"
exit 0
elif [ "$1" = "release" ]; then
echo "${GIT_REPOSITORY_RELEASE}"
exit 0
elif [ "$1" = "content" ] || [ "$1" = "content-url" ] || [ "$1" = "contents" ] || [ "$1" = "contents-url" ]; then
echo "${CONTENTS_URL}"
exit 0
fi
#Full output
echo
echo "Development container image information"
echo
if [ ! -z "${VERSION}" ]; then echo "- Image version: ${VERSION}"; fi
if [ ! -z "${DEFINITION_ID}" ]; then echo "- Definition ID: ${DEFINITION_ID}"; fi
if [ ! -z "${VARIANT}" ]; then echo "- Variant: ${VARIANT}"; fi
if [ ! -z "${GIT_REPOSITORY}" ]; then echo "- Source code repository: ${GIT_REPOSITORY}"; fi
if [ ! -z "${GIT_REPOSITORY_RELEASE}" ]; then echo "- Source code release/branch: ${GIT_REPOSITORY_RELEASE}"; fi
if [ ! -z "${BUILD_TIMESTAMP}" ]; then echo "- Timestamp: ${BUILD_TIMESTAMP}"; fi
if [ ! -z "${CONTENTS_URL}" ]; then echo && echo "More info: ${CONTENTS_URL}"; fi
echo
EOF
)"
if [ -f "${SCRIPT_DIR}/meta.env" ]; then
mkdir -p /usr/local/etc/vscode-dev-containers/
cp -f "${SCRIPT_DIR}/meta.env" /usr/local/etc/vscode-dev-containers/meta.env
echo "${meta_info_script}" > /usr/local/bin/devcontainer-info
chmod +x /usr/local/bin/devcontainer-info
fi
# Write marker file
mkdir -p "$(dirname "${MARKER_FILE}")"
echo -e "\
PACKAGES_ALREADY_INSTALLED=${PACKAGES_ALREADY_INSTALLED}\n\
LOCALE_ALREADY_SET=${LOCALE_ALREADY_SET}\n\
EXISTING_NON_ROOT_USER=${EXISTING_NON_ROOT_USER}\n\
RC_SNIPPET_ALREADY_ADDED=${RC_SNIPPET_ALREADY_ADDED}\n\
ZSH_ALREADY_INSTALLED=${ZSH_ALREADY_INSTALLED}" > "${MARKER_FILE}"
echo "Done!"

View File

@ -0,0 +1,19 @@
version: '3'
services:
dictation_auto_transcription_file_server:
container_name: dictation_auto_transcription_file_server_dev_container
env_file: ../.env
build: .
working_dir: /app/dictation_auto_transcription_file_server
ports:
- '8083:8083'
volumes:
- ../../:/app
- node_modules:/app/dictation_auto_transcription_file_server/node_modules
expose:
- '8083'
environment:
- CHOKIDAR_USEPOLLING=true
volumes:
node_modules:

View File

@ -0,0 +1,16 @@
STAGE=local
NO_COLOR=TRUE
CORS=TRUE
PORT=8083
JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA5IZZNgDew9eGmuFTezwdHYLSaJvUPPIKYoiOeVLD1paWNI51\n7Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3yCTR6wcWR3PfFJrl9vh5SOo79koZ\noJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbWFJXnDe0DVXYXpJLb4LAlF2XAyYX0\nSYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qSfiL9zWk9dvHoKzSnfSDzDFoFcEoV\nchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//mBNNaDHv83Yuw3mGShT73iJ0JQdk\nTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GOOQIDAQABAoIBADrwp7u097+dK/tw\nWD61n3DIGAqg/lmFt8X4IH8MKLSE/FKr16CS1bqwOEuIM3ZdUtDeXd9Xs7IsyEPE\n5ZwuXK7DSF0M4+Mj8Ip49Q0Aww9aUoLQU9HGfgN/r4599GTrt31clZXA/6Mlighq\ncOZgCcEfdItz8OMu5SQuOIW4CKkCuaWnPOP26UqZocaXNZfpZH0iFLATMMH/TT8x\nay9ToHTQYE17ijdQ/EOLSwoeDV1CU1CIE3P4YfLJjvpKptly5dTevriHEzBi70Jx\n/KEPUn9Jj2gZafrUxRVhmMbm1zkeYxL3gsqRuTzRjEeeILuZhSJyCkQZyUNARxsg\nQY4DZfECgYEA+YLKUtmYTx60FS6DJ4s31TAsXY8kwhq/lB9E3GBZKDd0DPayXEeK\n4UWRQDTT6MI6fedW69FOZJ5sFLp8HQpcssb4Weq9PCpDhNTx8MCbdH3Um5QR3vfW\naKq/1XM8MDUnx5XcNYd87Aw3azvJAvOPr69as8IPnj6sKaRR9uQjbYUCgYEA6nfV\n5j0qmn0EJXZJblk4mvvjLLoWSs17j9YlrZJlJxXMDFRYtgnelv73xMxOMvcGoxn5\nifs7dpaM2x5EmA6jVU5sYaB/beZGEPWqPYGyjIwXPvUGAAv8Gbnvpp+xlSco/Dum\nIq0w+43ry5/xWh6CjfrvKV0J2bDOiJwPEdu/8iUCgYEAnBBSvL+dpN9vhFAzeOh7\nY71eAqcmNsLEUcG9MJqTKbSFwhYMOewF0iHRWHeylEPokhfBJn8kqYrtz4lVWFTC\n5o/Nh3BsLNXCpbMMIapXkeWiti1HgE9ErPMgSkJpwz18RDpYIqM8X+jEQS6D7HSr\nyxfDg+w+GJza0rEVE3hfMIECgYBw+KZ2VfhmEWBjEHhXE+QjQMR3s320MwebCUqE\nNCpKx8TWF/naVC0MwfLtvqbbBY0MHyLN6d//xpA9r3rLbRojqzKrY2KiuDYAS+3n\nzssRzxoQOozWju+8EYu30/ADdqfXyIHG6X3VZs87AGiQzGyJLmP3oR1y5y7MQa09\nJI16hQKBgHK5uwJhGa281Oo5/FwQ3uYLymbNwSGrsOJXiEu2XwJEXwVi2ELOKh4/\n03pBk3Kva3fIwEK+vCzDNnxShIQqBE76/2I1K1whOfoUehhYvKHGaXl2j70Zz9Ks\nrkGW1cx7p+yDqATDrwHBHTHFh5bUTTn8dN40n0e0W/llurpbBkJM\n-----END RSA PRIVATE KEY-----\n"
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5IZZNgDew9eGmuFTezwd\nHYLSaJvUPPIKYoiOeVLD1paWNI517Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3\nyCTR6wcWR3PfFJrl9vh5SOo79koZoJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbW\nFJXnDe0DVXYXpJLb4LAlF2XAyYX0SYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qS\nfiL9zWk9dvHoKzSnfSDzDFoFcEoVchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//\nmBNNaDHv83Yuw3mGShT73iJ0JQdkTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GO\nOQIDAQAB\n-----END PUBLIC KEY-----\n"
STORAGE_ACCOUNT_NAME_US=saodmsusdev
STORAGE_ACCOUNT_NAME_AU=saodmsaudev
STORAGE_ACCOUNT_NAME_EU=saodmseudev
STORAGE_ACCOUNT_KEY_US=XXXXXXXXXXXXXXXXXXXXXXX
STORAGE_ACCOUNT_KEY_AU=XXXXXXXXXXXXXXXXXXXXXXX
STORAGE_ACCOUNT_KEY_EU=XXXXXXXXXXXXXXXXXXXXXXX
STORAGE_ACCOUNT_ENDPOINT_US=https://AAAAAAAAAAAAA
STORAGE_ACCOUNT_ENDPOINT_AU=https://AAAAAAAAAAAAA
STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA
AUDIO_FILE_ZIP_PASSWORD=***********

View File

@ -0,0 +1,17 @@
STAGE=production
NO_COLOR=TRUE
CORS=TRUE
PORT=8083
JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA5IZZNgDew9eGmuFTezwdHYLSaJvUPPIKYoiOeVLD1paWNI51\n7Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3yCTR6wcWR3PfFJrl9vh5SOo79koZ\noJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbWFJXnDe0DVXYXpJLb4LAlF2XAyYX0\nSYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qSfiL9zWk9dvHoKzSnfSDzDFoFcEoV\nchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//mBNNaDHv83Yuw3mGShT73iJ0JQdk\nTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GOOQIDAQABAoIBADrwp7u097+dK/tw\nWD61n3DIGAqg/lmFt8X4IH8MKLSE/FKr16CS1bqwOEuIM3ZdUtDeXd9Xs7IsyEPE\n5ZwuXK7DSF0M4+Mj8Ip49Q0Aww9aUoLQU9HGfgN/r4599GTrt31clZXA/6Mlighq\ncOZgCcEfdItz8OMu5SQuOIW4CKkCuaWnPOP26UqZocaXNZfpZH0iFLATMMH/TT8x\nay9ToHTQYE17ijdQ/EOLSwoeDV1CU1CIE3P4YfLJjvpKptly5dTevriHEzBi70Jx\n/KEPUn9Jj2gZafrUxRVhmMbm1zkeYxL3gsqRuTzRjEeeILuZhSJyCkQZyUNARxsg\nQY4DZfECgYEA+YLKUtmYTx60FS6DJ4s31TAsXY8kwhq/lB9E3GBZKDd0DPayXEeK\n4UWRQDTT6MI6fedW69FOZJ5sFLp8HQpcssb4Weq9PCpDhNTx8MCbdH3Um5QR3vfW\naKq/1XM8MDUnx5XcNYd87Aw3azvJAvOPr69as8IPnj6sKaRR9uQjbYUCgYEA6nfV\n5j0qmn0EJXZJblk4mvvjLLoWSs17j9YlrZJlJxXMDFRYtgnelv73xMxOMvcGoxn5\nifs7dpaM2x5EmA6jVU5sYaB/beZGEPWqPYGyjIwXPvUGAAv8Gbnvpp+xlSco/Dum\nIq0w+43ry5/xWh6CjfrvKV0J2bDOiJwPEdu/8iUCgYEAnBBSvL+dpN9vhFAzeOh7\nY71eAqcmNsLEUcG9MJqTKbSFwhYMOewF0iHRWHeylEPokhfBJn8kqYrtz4lVWFTC\n5o/Nh3BsLNXCpbMMIapXkeWiti1HgE9ErPMgSkJpwz18RDpYIqM8X+jEQS6D7HSr\nyxfDg+w+GJza0rEVE3hfMIECgYBw+KZ2VfhmEWBjEHhXE+QjQMR3s320MwebCUqE\nNCpKx8TWF/naVC0MwfLtvqbbBY0MHyLN6d//xpA9r3rLbRojqzKrY2KiuDYAS+3n\nzssRzxoQOozWju+8EYu30/ADdqfXyIHG6X3VZs87AGiQzGyJLmP3oR1y5y7MQa09\nJI16hQKBgHK5uwJhGa281Oo5/FwQ3uYLymbNwSGrsOJXiEu2XwJEXwVi2ELOKh4/\n03pBk3Kva3fIwEK+vCzDNnxShIQqBE76/2I1K1whOfoUehhYvKHGaXl2j70Zz9Ks\nrkGW1cx7p+yDqATDrwHBHTHFh5bUTTn8dN40n0e0W/llurpbBkJM\n-----END RSA PRIVATE KEY-----\n"
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5IZZNgDew9eGmuFTezwd\nHYLSaJvUPPIKYoiOeVLD1paWNI517Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3\nyCTR6wcWR3PfFJrl9vh5SOo79koZoJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbW\nFJXnDe0DVXYXpJLb4LAlF2XAyYX0SYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qS\nfiL9zWk9dvHoKzSnfSDzDFoFcEoVchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//\nmBNNaDHv83Yuw3mGShT73iJ0JQdkTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GO\nOQIDAQAB\n-----END PUBLIC KEY-----\n"
STORAGE_ACCOUNT_NAME_US=saxxxxusxxx
STORAGE_ACCOUNT_NAME_AU=saxxxxauxxx
STORAGE_ACCOUNT_NAME_EU=saxxxxeuxxx
STORAGE_ACCOUNT_KEY_US=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX==
STORAGE_ACCOUNT_KEY_AU=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX==
STORAGE_ACCOUNT_KEY_EU=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX==
STORAGE_ACCOUNT_ENDPOINT_US=https://xxxxxxxxxxxx.blob.core.windows.net/
STORAGE_ACCOUNT_ENDPOINT_AU=https://xxxxxxxxxxxx.blob.core.windows.net/
STORAGE_ACCOUNT_ENDPOINT_EU=https://xxxxxxxxxxxx.blob.core.windows.net/
# ↓src/features/convert-audio-file/test/testfile/zipのパスワード
AUDIO_FILE_ZIP_PASSWORD=password

View File

@ -0,0 +1,25 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir : __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

View File

@ -0,0 +1,13 @@
/dist
/node_modules
/dump.rdb
/build
/openapi/build
/.test
# credentials
credentials
.env.local
work_folder/
!work_folder/.gitkeep

View File

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

View File

@ -0,0 +1,21 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "start:debug"],
"envFile": "${workspaceFolder}/.env",
"console": "integratedTerminal"
},
{
"type": "node",
"request": "attach",
"name": "UnitTest",
"port": 9229,
"envFile": "${workspaceFolder}/.env",
}
]
}

View File

@ -0,0 +1,24 @@
{
"terminal.integrated.shell.linux": "/bin/bash",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.format.enable": false,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.formatOnPaste": true,
"editor.formatOnType": true,
"editor.renderWhitespace": "all",
"editor.insertSpaces": false,
"editor.renderLineHighlight": "all",
"prettier.prettierPath": "./node_modules/prettier",
"typescript.preferences.importModuleSpecifier": "relative"
}

Binary file not shown.

View File

@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"assets": ["templates/**/*.html", "templates/**/*.txt"],
"watchAssets": true
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,111 @@
{
"name": "server",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"apigen": "ts-node src/api/generate.ts && prettier --write \"src/api/odms/*.json\"",
"format": "prettier --write \"src/**/*.ts\" \"src/api/odms/*.json\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"tc": "tsc --noEmit",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
"lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest -w 1",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"og": "openapi-generator-cli",
"openapi-format": "cat \"src/api/odms/openapi.json\" | jq -c . > \"src/api/odms/openapi.json\" && prettier --write \"src/api/odms/*.json\"",
"tokengen": "ts-node src/common/test/tokengen.ts"
},
"dependencies": {
"@azure/identity": "^3.1.3",
"@azure/keyvault-secrets": "^4.6.0",
"@azure/notification-hubs": "^1.0.3",
"@azure/storage-blob": "^12.14.0",
"@microsoft/microsoft-graph-client": "^3.0.5",
"@nestjs/axios": "^0.1.0",
"@nestjs/common": "^9.3.12",
"@nestjs/config": "^2.2.0",
"@nestjs/core": "^9.3.12",
"@nestjs/platform-express": "^9.3.12",
"@nestjs/serve-static": "^3.0.1",
"@openapitools/openapi-generator-cli": "^0.0.6",
"@sendgrid/mail": "^7.7.0",
"@types/jsonwebtoken": "^9.0.1",
"@types/uuid": "^8.3.4",
"axios": "^1.3.4",
"cache-manager": "^5.2.4",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"connect-redis": "^6.1.3",
"cookie-parser": "^1.4.6",
"express-session": "^1.17.3",
"helmet": "^6.0.1",
"jsonwebtoken": "^9.0.0",
"jwk-to-pem": "^2.0.5",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0"
},
"devDependencies": {
"@apidevtools/swagger-cli": "^4.0.4",
"@nestjs/cli": "^9.3.0",
"@nestjs/schematics": "^8.0.0",
"@nestjs/swagger": "^6.3.0",
"@nestjs/testing": "^9.3.12",
"@types/cache-manager": "^4.0.4",
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.13",
"@types/express-session": "^1.17.5",
"@types/jest": "27.5.0",
"@types/jsonwebtoken": "^9.0.1",
"@types/jwk-to-pem": "^2.0.1",
"@types/node": "^16.0.0",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"base64url": "^3.0.1",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "28.0.3",
"license-checker": "^25.0.1",
"prettier": "^2.3.2",
"source-map-support": "^0.5.20",
"sqlite3": "^5.1.6",
"supertest": "^6.1.3",
"swagger-ui-express": "^4.5.0",
"ts-jest": "28.0.1",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "4.0.0",
"typescript": "^4.9.5"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"testTimeout": 120000,
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

@ -0,0 +1,25 @@
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from '../app.module';
import { promises as fs } from 'fs';
import { NestFactory } from '@nestjs/core';
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule, {
preview: true,
});
const options = new DocumentBuilder()
.setTitle('ODMS GenarateAutoTranscriptionFile OpenAPI')
.setVersion('1.0.0')
.addBearerAuth({
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
})
.build();
const document = SwaggerModule.createDocument(app, options);
await fs.writeFile(
'src/api/odms/openapi.json',
JSON.stringify(document, null, 0),
);
}
bootstrap();

View File

@ -0,0 +1,143 @@
{
"openapi": "3.0.0",
"paths": {
"/health": {
"get": {
"operationId": "checkHealth",
"summary": "",
"parameters": [],
"responses": { "200": { "description": "" } }
}
},
"/convert-audio-file": {
"post": {
"operationId": "generateAutoTranscriptionFile",
"summary": "",
"description": "自動文字起こし用ファイルを生成する",
"parameters": [
{
"name": "x-api-version",
"in": "header",
"description": "APIバージョン",
"schema": { "type": "string" }
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GenerateAutoTranscriptionFileRequest"
}
}
}
},
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GenerateAutoTranscriptionFileResponse"
}
}
}
},
"400": {
"description": "不正なパラメータ",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"401": {
"description": "認証エラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"404": {
"description": "指定したIDの音声ファイルが存在しない場合",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"500": {
"description": "想定外のサーバーエラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
},
"tags": ["convert-audio-file"],
"security": [{ "bearer": [] }]
}
}
},
"info": {
"title": "ODMS GenarateAutoTranscriptionFile OpenAPI",
"description": "",
"version": "1.0.0",
"contact": {}
},
"tags": [],
"servers": [],
"components": {
"securitySchemes": {
"bearer": { "scheme": "bearer", "bearerFormat": "JWT", "type": "http" }
},
"schemas": {
"GenerateAutoTranscriptionFileRequest": {
"type": "object",
"properties": {
"taskId": { "type": "number", "description": "タスクID" },
"country": {
"type": "string",
"description": "アカウントが所属している国情報"
},
"accountId": { "type": "number", "description": "アカウントID" },
"audioFileName": {
"type": "string",
"description": "音声ファイル名"
},
"encryptionPassword": {
"type": "string",
"description": "復号化パスワード"
}
},
"required": [
"taskId",
"country",
"accountId",
"audioFileName",
"encryptionPassword"
]
},
"GenerateAutoTranscriptionFileResponse": {
"type": "object",
"properties": {
"voiceFileName": {
"type": "string",
"description": "自動文字起こし用音声ファイル(wav形式)"
}
},
"required": ["voiceFileName"]
},
"ErrorResponse": {
"type": "object",
"properties": {
"message": { "type": "string" },
"code": { "type": "string" }
},
"required": ["message", "code"]
}
}
}
}

View File

@ -0,0 +1,27 @@
import { MiddlewareConsumer, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { LoggerMiddleware } from './common/loggerMiddleware';
import { VersionHeaderMiddleware } from './common/version-header.middleware';
import { validate } from './common/validators/env.validator';
import { ConvertAudioFileModule } from './features/convert-audio-file/convert-audio-file.module';
import { BlobstorageModule } from './gateways/blobstorage/blobstorage.module';
import { HealthController } from './health.controller';
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: ['.env.local', '.env'],
isGlobal: true,
validate,
}),
ConvertAudioFileModule,
BlobstorageModule,
],
controllers: [HealthController],
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes('');
consumer.apply(VersionHeaderMiddleware).forRoutes('');
}
}

View File

@ -0,0 +1,41 @@
/*
E+6
- 1~2...
- 3~4DB...
- 5~6
ex)
E00XXXX : システムエラーDB接続失敗など
E01XXXX : 業務エラー
EXX00XX : 内部エラー
EXX01XX : トークンエラー
EXX02XX : DBエラーDB関連
EXX03XX : ADB2CエラーDB関連
E03XXXX : 自動文字起こし用音声ファイル変換ツールのエラーコード
*/
export const ErrorCodes = [
'E009999', // 汎用エラー
'E000101', // トークン形式不正エラー
'E000107', // トークン不足エラー
'E000401', // IPアドレス未設定エラー
'E000501', // リクエストID未設定エラー
'E010701', // Blobファイル不在エラー
'E031000', // 入力ファイルのオープンに失敗
'E031001', // 入力ファイルが不正なフォーマット
'E031002', // 入力ファイルが非サポートの拡張子
'E031003', // 入力ファイルの読み出しに失敗
'E031004', // 入力ファイルが他社のdssファイル
'E032000', // 出力ファイルのオープンに失敗
'E032001', // 出力ファイルのwavヘッダー書き出しに失敗
'E032002', // 出力データの書き出し中に失敗
'E033000', // ds2ファイルの暗号化パスワードの指定がない
'E033001', // ds2ファイルの暗号化パスワードが異なる
'E034000', // mp3の初期化APIが失敗
'E034001', // mp3のオブジェクト生成に失敗
'E034002', // mp3のフォーマット設定に失敗
'E034003', // mp3のフォーマット取得に失敗
'E034004', // mp3のブロック出力に失敗
'E034005', // mp3のデータ読み出しに失敗
'E034006', // mp3ファイルの終了に失敗
'E039999', // 未定義のエラー
] as const;

View File

@ -0,0 +1,10 @@
import { errors } from './message';
import { ErrorCodeType, ErrorResponse } from './types/types';
export const makeErrorResponse = (errorcode: ErrorCodeType): ErrorResponse => {
const msg = errors[errorcode];
return {
code: errorcode,
message: msg,
};
};

View File

@ -0,0 +1,29 @@
import { Errors } from './types/types';
// エラーコードとメッセージ対応表
export const errors: Errors = {
E009999: 'Internal Server Error.',
E000101: 'Token invalid format Error.',
E000107: 'Token is not exist Error.',
E000401: 'IP address not found Error.',
E000501: 'Request ID not found Error.',
E010701: 'File not found in Blob Storage Error.',
E031000: 'Audio convert: Failed to open input file.',
E031001: 'Audio convert: Invalid input file format.',
E031002: 'Audio convert: Unsupported file extension.',
E031003: 'Audio convert: Unable to read input file.',
E031004: 'Audio convert: Input file is a third-party DSS format.',
E032000: 'Audio convert: Unable to open output file.',
E032001: 'Audio convert: Failed to write WAV header.',
E032002: 'Audio convert: Error while writing output data.',
E033000: 'Audio convert: DS2 encryption password is not specified.',
E033001: 'Audio convert: Incorrect DS2 encryption password.',
E034000: 'Audio convert: MP3 initialization API failed.',
E034001: 'Audio convert: Failed to generate MP3 object.',
E034002: 'Audio convert: Failed to configure MP3 format.',
E034003: 'Audio convert: Failed to retrieve MP3 format.',
E034004: 'Audio convert: Failed during MP3 block output.',
E034005: 'Audio convert: Failed to read MP3 data.',
E034006: 'Audio convert: Failed to finalize MP3 file.',
E039999: 'Audio convert: Undefined error code.',
};

View File

@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { ErrorCodes } from '../code';
export class ErrorResponse {
@ApiProperty()
message: string;
@ApiProperty()
code: string;
}
export type ErrorCodeType = (typeof ErrorCodes)[number];
export type Errors = {
[P in ErrorCodeType]: string;
};

View File

@ -0,0 +1,52 @@
import * as fs from 'fs/promises';
import { existsSync } from 'fs';
import * as path from 'path';
/**
*
* @param dirPath
*/
export const rmDirRecursive = async (dirPath: string) => {
if (!existsSync(dirPath)) {
return;
}
const items = await fs.readdir(dirPath);
for (const item of items) {
const deleteTarget = path.join(dirPath, item);
// 削除対象がフォルダの場合、階層を掘って再帰的に削除しに行く
if ((await fs.lstat(deleteTarget)).isDirectory()) {
await rmDirRecursive(deleteTarget);
} else {
await fs.unlink(deleteTarget);
}
}
await fs.rmdir(dirPath);
};
/**
*
* @param baseDir
* @param suffix
*/
export const makeWorkingDirectory = async (
baseDir: string,
suffix: string,
): Promise<string> => {
const workDirPath = path.join(baseDir, suffix);
await fs.mkdir(workDirPath);
return workDirPath;
};
/**
*
* @param fileName
* @returns
*/
export const toLowerCaseFileName = async (fileName: string) => {
const lowerCaseFileName = fileName.toLocaleLowerCase();
await fs.rename(fileName, lowerCaseFileName);
return lowerCaseFileName;
};

View File

@ -0,0 +1,2 @@
export * from './fileSystem';
export * from './zip';

View File

@ -0,0 +1,66 @@
import * as fs from 'fs';
import * as childProcess from 'child_process';
import * as path from 'path';
/**
* ZIPファイルを解凍します
* @param zipFilePath ZIPファイルが保存されているパス
* @param outputFilePathBaseDir
* @param zipFilePassword
*/
export const extractPasswordZipFile = async (
zipFilePath: string,
outputFilePathBaseDir: string,
zipFilePassword: string,
): Promise<string[]> => {
try {
await new Promise((resolve, reject: (stderrData: string) => void) => {
// ZIP解凍処理
const unzipFile = childProcess.spawn('unzip', [
'-P',
zipFilePassword,
zipFilePath,
'-d',
outputFilePathBaseDir,
]);
let stdoutData = '';
let stderrData = '';
// 標準出力を受け取る
unzipFile.stdout.on('data', (data) => {
stdoutData += data.toString();
});
// 標準エラー出力を受け取る
unzipFile.stderr.on('data', (data) => {
stderrData += data.toString();
});
// プロセスが終了したときの処理
// unzipコマンドによって出力されたステータスコードによって条件分岐する
unzipFile.on('close', (code) => {
// 正常にZIPの解凍が実行された場合
if (code === 0) {
resolve(stdoutData);
} else {
// ZIPの解凍がに失敗した場合
reject(stderrData);
}
});
});
} catch (e) {
throw e;
}
// ZIPファイルを解凍したフォルダに配置されているファイルのファイル名を取得
const filenames = fs.readdirSync(outputFilePathBaseDir);
return (
filenames
// 音声ファイル変換するファイル名のみを取得するため、ZIPファイルは取り除く
// ZIPの拡張子が大文字の場合があるため、小文字変換してから取り除く
.filter((filename) => !filename.toLowerCase().endsWith('.zip'))
// ZIPファイルから取得した音声ファイルのファイルのパスを作成する。
.map((filename) => path.join(outputFilePathBaseDir, filename))
);
};

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { SystemAccessGuard } from './accessguards';
@Module({
imports: [ConfigModule],
controllers: [],
providers: [SystemAccessGuard],
})
export class SystemAccessGuardsModule {}

View File

@ -0,0 +1,43 @@
import {
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { isVerifyError, verify } from '../../jwt';
import { Request } from 'express';
import { retrieveAuthorizationToken } from '../../../common/http/helper';
import { makeErrorResponse } from '../../../common/error/makeErrorResponse';
import { SystemAccessToken } from '../../token/types';
import { ConfigService } from '@nestjs/config';
import { getPublicKey } from '../../jwt/jwt';
/**
*
**/
@Injectable()
export class SystemAccessGuard implements CanActivate {
constructor(private readonly configService: ConfigService) {}
canActivate(context: ExecutionContext): boolean | Promise<boolean> {
const pubkey = getPublicKey(this.configService);
const req = context.switchToHttp().getRequest<Request>();
const token = retrieveAuthorizationToken(req);
if (!token) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const payload = verify<SystemAccessToken>(token, pubkey);
if (isVerifyError(payload)) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
return true;
}
}

View File

@ -0,0 +1,19 @@
import { Request } from 'express';
/**
* Authorizationヘッダに格納された文字列(jwt)
* @param {Request}
* @return {string | undefined}
*/
export const retrieveAuthorizationToken = (
req: Request,
): string | undefined => {
const header = req.header('Authorization');
if (typeof header === 'string') {
if (header.startsWith('Bearer ')) {
return header.substring('Bearer '.length, header.length);
}
}
return undefined;
};

View File

@ -0,0 +1,3 @@
import { isVerifyError, sign, verify, decode } from './jwt';
export { isVerifyError, sign, verify, decode };

View File

@ -0,0 +1,145 @@
import { ConfigService } from '@nestjs/config';
import * as jwt from 'jsonwebtoken';
// XXX: decodeがうまく使えないことがあるので応急対応 バージョン9以降だとなる
import { decode as jwtDecode } from 'jsonwebtoken';
export type VerifyError = {
reason: 'ExpiredError' | 'InvalidToken' | 'InvalidTimeStamp' | 'Unknown';
message: string;
};
export const isVerifyError = (arg: unknown): arg is VerifyError => {
const value = arg as VerifyError;
if (value.message === undefined) {
return false;
}
if (value.reason === undefined) {
return false;
}
switch (value.reason) {
case 'ExpiredError':
case 'InvalidTimeStamp':
case 'InvalidToken':
case 'Unknown':
return true;
default:
return false;
}
};
/**
* Payloadと秘密鍵を使用して署名されたJWTを生成します
* @param {T} payload payloadの型
* @param {number} expirationSeconds ()
* @param {string} privateKey 使
* @return {string}
* @throws {Error} Errorオブジェクト
*/
export const sign = <T extends object>(
payload: T,
expirationSeconds: number,
privateKey: string,
): string => {
try {
const token = jwt.sign(payload, privateKey, {
expiresIn: expirationSeconds,
algorithm: 'RS256',
});
return token;
} catch (e) {
throw e;
}
};
/**
* tokenと公開鍵を使用して検証済みJWTのpayloadを取得します
* @param {string} token JWT
* @param {string} publicKey 使
* @return {T | VerifyError} Payload
*/
export const verify = <T extends object>(
token: string,
publicKey: string,
): T | VerifyError => {
try {
const payload = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
}) as T;
return payload;
} catch (e) {
if (e instanceof jwt.TokenExpiredError) {
return {
reason: 'ExpiredError',
message: e.message,
};
} else if (e instanceof jwt.NotBeforeError) {
return {
reason: 'InvalidTimeStamp',
message: e.message,
};
} else if (e instanceof jwt.JsonWebTokenError) {
return {
reason: 'InvalidToken',
message: e.message,
};
} else {
return {
reason: 'Unknown',
message: e.message,
};
}
}
};
/**
* tokenから未検証のJWTのpayloadを取得します
* @param {string} token JWT
* @return {T | VerifyError} Payload
*/
export const decode = <T extends object>(token: string): T | VerifyError => {
try {
const payload = jwtDecode(token, {
json: true,
}) as T;
return payload;
} catch (e) {
if (e instanceof jwt.TokenExpiredError) {
return {
reason: 'ExpiredError',
message: e.message,
};
} else if (e instanceof jwt.NotBeforeError) {
return {
reason: 'InvalidTimeStamp',
message: e.message,
};
} else if (e instanceof jwt.JsonWebTokenError) {
return {
reason: 'InvalidToken',
message: e.message,
};
} else {
return {
reason: 'Unknown',
message: e.message,
};
}
}
};
export const getPrivateKey = (configService: ConfigService): string => {
return (
// 開発環境用に改行コードを置換する
// 本番環境では\\nが含まれないため、置換が行われない想定
configService.getOrThrow<string>('JWT_PRIVATE_KEY').replace(/\\n/g, '\n')
);
};
export const getPublicKey = (configService: ConfigService): string => {
return (
// 開発環境用に改行コードを置換する
// 本番環境では\\nが含まれないため、置換が行われない想定
configService.getOrThrow<string>('JWT_PUBLIC_KEY').replace(/\\n/g, '\n')
);
};

View File

@ -0,0 +1,32 @@
import { Request } from 'express';
import { Context } from './types';
export const makeContext = (
externalId: string,
requestId: string,
delegationId?: string,
): Context => {
return new Context(externalId, requestId, delegationId);
};
// リクエストヘッダーからrequestIdを取得する
export const retrieveRequestId = (req: Request): string | undefined => {
return req.header('x-request-id');
};
/**
* IPアドレスを取得します
* @param {Request} req
* @returns {string | undefined}
*/
export const retrieveIp = (req: Request): string | undefined => {
// ローカル環境では直近の送信元IPを取得する
if (process.env.STAGE === 'local') {
return req.ip;
}
const ip = req.header('x-forwarded-for');
if (typeof ip === 'string') {
return ip;
}
return undefined;
};

View File

@ -0,0 +1,4 @@
import { Context } from './types';
import { makeContext, retrieveRequestId, retrieveIp } from './context';
export { Context, makeContext, retrieveRequestId, retrieveIp };

View File

@ -0,0 +1,34 @@
export class Context {
/**
* APIの操作ユーザーを追跡するためのID
*/
trackingId: string;
/**
* APIの操作ユーザーのIPアドレス
*/
ip: string;
/**
* ID
*/
requestId: string;
/**
* APIの代行操作ユーザーを追跡するためのID
*/
delegationId?: string | undefined;
constructor(externalId: string, requestId: string, delegationId?: string) {
this.trackingId = externalId;
this.delegationId = delegationId;
this.requestId = requestId;
}
/**
*
*/
getTrackingId(): string {
if (this.delegationId) {
return `${this.requestId}_${this.trackingId} by ${this.delegationId}`;
} else {
return `${this.requestId}_${this.trackingId}`;
}
}
}

View File

@ -0,0 +1,37 @@
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
private readonly logger = new Logger(LoggerMiddleware.name);
use(req: Request, res: Response, next: () => void): void {
// ここで一意のリクエストIDを生成して、リクエストヘッダーに設定する
const requestId = uuidv4();
req.headers['x-request-id'] = requestId;
this.logger.log(this.createReqMsg(req));
res.on('close', () => {
this.logger.log(this.createResMsg(res));
});
next();
}
private createReqMsg(req: Request): string {
const message = `[${req.header('x-request-id')}] Request [url=${
req.url
}, method=${req.method}]`;
return message;
}
private createResMsg(res: Response): string {
const message = `[${res.req.header('x-request-id')}] Response [statusCode=${
res.statusCode
}, message=${res.statusMessage}]`;
return message;
}
}

View File

@ -0,0 +1,27 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigModule } from '@nestjs/config';
import { ConvertAudioFileService } from '../../features/convert-audio-file/convert-audio-file.service';
import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module';
import { CacheModule } from '@nestjs/common';
export const makeTestingModule = async (): Promise<
TestingModule | undefined
> => {
try {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
envFilePath: ['.env.test', '.env'],
isGlobal: true,
}),
BlobstorageModule,
CacheModule.register({ isGlobal: true, ttl: 86400 }),
],
providers: [ConvertAudioFileService],
}).compile();
return module;
} catch (e) {
console.log(e);
}
};

View File

@ -0,0 +1,69 @@
import { Context } from '../log';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
// ### ユニットテスト用コード以外では絶対に使用してはいけないダーティな手段を使用しているが、他の箇所では使用しないこと ###
/**
* blobStorageServiceのモックを作成してTServiceが依存するサービス(blobStorageService)
* serviceに指定するオブジェクトは`blobstorageService: blobStorageService`
* @param service TService
* @param overrides blobStorageServiceの各種メソッドのモックが返す値
*/
export const overrideBlobstorageService = <TService>(
service: TService,
overrides: {
containerExists?: (
context: Context,
accountId: number,
country: string,
) => Promise<boolean>;
fileExists?: (
context: Context,
accountId: number,
country: string,
fileName: string,
) => Promise<boolean>;
downloadFile?: (
context: Context,
accountId: number,
country: string,
fileName: string,
downloadFilePath: string,
) => Promise<void>;
uploadFile?: (
context: Context,
accountId: number,
country: string,
taskId: number,
fileName: string,
localFilePath: string,
) => Promise<void>;
},
): void => {
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
const obj = (service as any).blobStorageService as BlobstorageService;
if (overrides.containerExists) {
Object.defineProperty(obj, obj.containerExists.name, {
value: overrides.containerExists,
writable: true,
});
}
if (overrides.fileExists) {
Object.defineProperty(obj, obj.fileExists.name, {
value: overrides.fileExists,
writable: true,
});
}
if (overrides.downloadFile) {
Object.defineProperty(obj, obj.downloadFile.name, {
value: overrides.downloadFile,
writable: true,
});
}
if (overrides.uploadFile) {
Object.defineProperty(obj, obj.uploadFile.name, {
value: overrides.uploadFile,
writable: true,
});
}
};

View File

@ -0,0 +1,37 @@
import { sign } from '../jwt/jwt';
import { SystemAccessToken } from '../token/types';
import { AppModule } from '../../app.module';
import { NestFactory } from '@nestjs/core';
/**
* テスト用: API認証トークンを発行する
*/
async function bootstrap(): Promise<void> {
await NestFactory.create(AppModule, {
preview: true,
});
// システムトークンを発行
const systemToken = await generateSystemToken();
console.log(`{ system_token: ${systemToken} }`);
}
/**
* System用のアクセストークンを生成します
* @param context
* @returns system token
*/
async function generateSystemToken(): Promise<string> {
// 要求されたトークンの寿命を決定
// デフォルト2時間
const tokenLifetime = Number(process.env.ACCESS_TOKEN_LIFETIME_WEB || 7200);
const privateKey = process.env.JWT_PRIVATE_KEY?.replace(/\\n/g, '\n') ?? '';
const token = sign<SystemAccessToken>(
{
externalId: 'test',
},
tokenLifetime,
privateKey,
);
return token;
}
bootstrap();

View File

@ -0,0 +1 @@
export * from './types';

View File

@ -0,0 +1,15 @@
/**
*
* : ODMS Cloud API API使
*/
export type SystemAccessToken = {
/**
*
*/
externalId: string;
/**
*
*/
context?: string;
};

View File

@ -0,0 +1,95 @@
import { plainToClass } from 'class-transformer';
import {
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
validateSync,
} from 'class-validator';
/**
*
*/
export class EnvValidator {
// .env
// .env.local
@IsOptional()
@IsString()
STAGE: string;
@IsOptional()
@IsString()
NO_COLOR: string;
@IsOptional()
@IsString()
CORS: string;
@IsOptional()
@IsNumber()
PORT: number;
@IsNotEmpty()
@IsString()
JWT_PRIVATE_KEY: string;
@IsNotEmpty()
@IsString()
JWT_PUBLIC_KEY: string;
@IsNotEmpty()
@IsString()
STORAGE_ACCOUNT_NAME_US: string;
@IsNotEmpty()
@IsString()
STORAGE_ACCOUNT_NAME_AU: string;
@IsNotEmpty()
@IsString()
STORAGE_ACCOUNT_NAME_EU: string;
@IsNotEmpty()
@IsString()
STORAGE_ACCOUNT_KEY_US: string;
@IsNotEmpty()
@IsString()
STORAGE_ACCOUNT_KEY_AU: string;
@IsNotEmpty()
@IsString()
STORAGE_ACCOUNT_KEY_EU: string;
@IsNotEmpty()
@IsString()
STORAGE_ACCOUNT_ENDPOINT_US: string;
@IsNotEmpty()
@IsString()
STORAGE_ACCOUNT_ENDPOINT_AU: string;
@IsNotEmpty()
@IsString()
STORAGE_ACCOUNT_ENDPOINT_EU: string;
@IsNotEmpty()
@IsString()
AUDIO_FILE_ZIP_PASSWORD: string;
}
export function validate(config: Record<string, unknown>) {
const validatedConfig = plainToClass(EnvValidator, config, {
enableImplicitConversion: true,
});
const errors = validateSync(validatedConfig, {
skipMissingProperties: false,
});
if (errors.length > 0) {
throw new Error(errors.toString());
}
return validatedConfig;
}

View File

@ -0,0 +1,18 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
/**
* VersionHeaderMiddleware
* APIバージョン情報を管理するミドルウェア
*/
@Injectable()
export class VersionHeaderMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction): void {
// リクエストヘッダにAPIバージョンが含まれていない場合、デフォルトのAPIバージョンを利用するようにする。
if (!Object.keys(req.headers).includes('x-api-version')) {
// 環境変数にDEFAULT_API_VERSIONを追加する
req.headers['x-api-version'] = process.env.DEFAULT_API_VERSION || '1';
}
next();
}
}

View File

@ -0,0 +1,58 @@
/**
* East USに保存する国リスト
* @const {number}
*/
export const BLOB_STORAGE_REGION_US = ['CA', 'KY', 'US'];
/**
* Australia Eastに保存する国リスト
* @const {number}
*/
export const BLOB_STORAGE_REGION_AU = ['AU', 'NZ'];
/**
* North Europeに保存する国リスト
* @const {number}
*/
export const BLOB_STORAGE_REGION_EU = [
'AT',
'BE',
'BG',
'HR',
'CY',
'CZ',
'DK',
'EE',
'FI',
'FR',
'DE',
'GR',
'HU',
'IS',
'IE',
'IT',
'LV',
'LI',
'LT',
'LU',
'MT',
'NL',
'NO',
'PL',
'PT',
'RO',
'RS',
'SK',
'SI',
'ZA',
'ES',
'SE',
'CH',
'TR',
'GB',
];
/**
* NODE_ENVの値
* @const {string[]}
*/
export const NODE_ENV_TEST = 'test';

View File

@ -0,0 +1,23 @@
import * as errors from '../errors/types';
export const AUDIO_FILE_CONVERT_ERROR_MAP: {
[key: number]: typeof errors.AudioFileConvertToolError;
} = {
1000: errors.InputAudioFileOpenError,
1001: errors.InputAudioFileFormatError,
1002: errors.InputAudioFileExtError,
1003: errors.InputAudioFileReadError,
1004: errors.InputAudioFileInvalidDssFormatError,
2000: errors.OutputAudioFileOpenError,
2001: errors.OutputAudioFileWriteHeaderError,
2002: errors.OutputAudioFileWriteError,
3000: errors.DS2AudioFileEncryptionPasswordNotSpecifiedError,
3001: errors.DS2AudioFileEncryptionPasswordInvalidError,
4000: errors.MP3AudioFileInitializeError,
4001: errors.MP3AudioFileObjectGenerateError,
4002: errors.MP3AudioFileFormatConfigurationError,
4003: errors.MP3AudioFileGetFormatError,
4004: errors.MP3AudioFileBlockOutputError,
4005: errors.MP3AudioFileReadError,
4006: errors.MP3AudioFileCloseError,
};

View File

@ -0,0 +1,132 @@
import {
Body,
Controller,
HttpException,
HttpStatus,
Logger,
Post,
Req,
UseGuards,
Version,
} from '@nestjs/common';
import {
ApiResponse,
ApiOperation,
ApiTags,
ApiBearerAuth,
ApiHeader,
} from '@nestjs/swagger';
import { ErrorResponse } from '../../common/error/types/types';
import { Request } from 'express';
import { ConvertAudioFileService } from './convert-audio-file.service';
import {
GenerateAutoTranscriptionFileRequest,
GenerateAutoTranscriptionFileResponse,
} from './types/types';
import { retrieveAuthorizationToken } from '../../common/http/helper';
import { SystemAccessGuard } from '../../common/guards/system/accessguards';
import jwt from 'jsonwebtoken';
import { makeContext, retrieveRequestId, retrieveIp } from '../../common/log';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { SystemAccessToken } from '../../common/token';
@ApiTags('convert-audio-file')
@Controller('convert-audio-file')
export class ConvertAudioFileController {
private readonly logger = new Logger(ConvertAudioFileController.name);
constructor(private readonly taskService: ConvertAudioFileService) {}
@Post()
@Version(['1', '2'])
@ApiResponse({
status: HttpStatus.OK,
type: GenerateAutoTranscriptionFileResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: '不正なパラメータ',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: '指定したIDの音声ファイルが存在しない場合',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({
operationId: 'generateAutoTranscriptionFile',
description: '自動文字起こし用ファイルを生成する',
})
@ApiBearerAuth()
@ApiHeader({
name: 'x-api-version',
description: 'APIバージョン',
})
@UseGuards(SystemAccessGuard)
async generateAutoTranscriptionFile(
@Req() req: Request,
@Body() body: GenerateAutoTranscriptionFileRequest,
): Promise<GenerateAutoTranscriptionFileResponse> {
// SystemAccessGuardでチェック済みなのでここでのアクセストークンチェックはしない
const { accountId, country, taskId, audioFileName, encryptionPassword } =
body;
const systemAccessToken = retrieveAuthorizationToken(req);
if (!systemAccessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const ip = retrieveIp(req);
if (!ip) {
throw new HttpException(
makeErrorResponse('E000401'),
HttpStatus.UNAUTHORIZED,
);
}
const requestId = retrieveRequestId(req);
if (!requestId) {
throw new HttpException(
makeErrorResponse('E000501'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const decodedAccessToken = jwt.decode(systemAccessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { externalId } = decodedAccessToken as SystemAccessToken;
const context = makeContext(externalId, requestId);
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
const { voiceFileName } =
await this.taskService.generateAutoTranscriptionFile(
context,
accountId,
country,
taskId,
audioFileName,
encryptionPassword,
);
return { voiceFileName };
}
}

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ConvertAudioFileService } from './convert-audio-file.service';
import { ConvertAudioFileController } from './convert-audio-file.controller';
import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module';
@Module({
imports: [BlobstorageModule, ConfigModule],
providers: [ConvertAudioFileService],
controllers: [ConvertAudioFileController],
})
export class ConvertAudioFileModule {}

View File

@ -0,0 +1,455 @@
import * as fs from 'fs/promises';
import { existsSync } from 'fs';
import * as path from 'path';
import { HttpException, HttpStatus } from '@nestjs/common';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { makeBlobstorageServiceMockValue } from './test/convert-audio-file.service.mock';
import { makeTestingModuleWithBlob } from './test/utility';
import { ConvertAudioFileService } from './convert-audio-file.service';
import { makeContext } from '../../common/log';
import { rmDirRecursive } from '../../common/files';
const testFilePath = `${__dirname}/test/testfile/zip`;
const workFolderPath = `${__dirname}/test/work_folder`;
jest.mock('../../common/files/fileSystem', () => {
const originalModule = jest.requireActual('../../common/files/fileSystem');
return {
...Object.assign({}, originalModule),
makeWorkingDirectory: () => workFolderPath,
};
});
describe('generateAutoTranscriptionFile', () => {
beforeEach(async () => {
if (!existsSync(workFolderPath)) {
await fs.mkdir(workFolderPath);
}
});
afterEach(async () => {
await rmDirRecursive(workFolderPath);
});
it('自動文字起こし用ファイルを生成できること(DS2暗号化なし)', async () => {
// BlobServiceをモック化
const blobStorageServiceMockValue = makeBlobstorageServiceMockValue();
const module = await makeTestingModuleWithBlob(blobStorageServiceMockValue);
if (!module) throw new Error('module not defined.');
const service = module.get<ConvertAudioFileService>(
ConvertAudioFileService,
);
const context = makeContext('externalId', 'requestId');
// テスト用のファイルをワークディレクトリにコピー
await fs.copyFile(
path.join(testFilePath, 'TEST_NO_ENCRYPTION.ds2.zip'),
path.join(workFolderPath, 'TEST_NO_ENCRYPTION.ds2.zip'),
);
const { voiceFileName } = await service.generateAutoTranscriptionFile(
context,
1,
'US',
1,
'TEST_NO_ENCRYPTION.ds2.zip',
'password',
);
expect(voiceFileName).toBe(`TEST_NO_ENCRYPTION.ds2.wav`);
});
it('自動文字起こし用ファイルを生成できること(DS2暗号化あり)', async () => {
// BlobServiceをモック化
const blobStorageServiceMockValue = makeBlobstorageServiceMockValue();
const module = await makeTestingModuleWithBlob(blobStorageServiceMockValue);
if (!module) throw new Error('module not defined.');
const service = module.get<ConvertAudioFileService>(
ConvertAudioFileService,
);
const context = makeContext('externalId', 'requestId');
// テスト用のファイルをワークディレクトリにコピー
await fs.copyFile(
path.join(testFilePath, 'TEST_ENCRYPTION.DS2.zip'),
path.join(workFolderPath, 'TEST_ENCRYPTION.DS2.zip'),
);
const { voiceFileName } = await service.generateAutoTranscriptionFile(
context,
1,
'US',
1,
'TEST_ENCRYPTION.DS2.zip',
'OZ0030',
);
expect(voiceFileName).toBe(`TEST_ENCRYPTION.DS2.wav`);
});
it('blobストレージにコンテナが存在しない場合はエラーになること', async () => {
// BlobServiceをモック化
const blobStorageServiceMockValue = makeBlobstorageServiceMockValue();
// コンテナが存在しないこととする
blobStorageServiceMockValue.containerExists = false;
const module = await makeTestingModuleWithBlob(blobStorageServiceMockValue);
if (!module) throw new Error('module not defined.');
const service = module.get<ConvertAudioFileService>(
ConvertAudioFileService,
);
const context = makeContext('externalId', 'requestId');
// テスト用のファイルをワークディレクトリにコピー
await fs.copyFile(
path.join(testFilePath, 'TEST_NO_ENCRYPTION.ds2.zip'),
path.join(workFolderPath, 'TEST_NO_ENCRYPTION.ds2.zip'),
);
try {
await service.generateAutoTranscriptionFile(
context,
1,
'US',
1,
'TEST_NO_ENCRYPTION.ds2.zip',
'password',
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E010701'));
return;
} else {
throw new Error(e);
}
}
throw new Error('Error not thrown');
});
it('blobストレージに音声ファイルが存在しない場合はエラーになること', async () => {
// BlobServiceをモック化
const blobStorageServiceMockValue = makeBlobstorageServiceMockValue();
// ファイルが存在しないこととする
blobStorageServiceMockValue.fileExists = false;
const module = await makeTestingModuleWithBlob(blobStorageServiceMockValue);
if (!module) throw new Error('module not defined.');
const service = module.get<ConvertAudioFileService>(
ConvertAudioFileService,
);
const context = makeContext('externalId', 'requestId');
// テスト用のファイルをワークディレクトリにコピー
await fs.copyFile(
path.join(testFilePath, 'TEST_NO_ENCRYPTION.ds2.zip'),
path.join(workFolderPath, 'TEST_NO_ENCRYPTION.ds2.zip'),
);
try {
await service.generateAutoTranscriptionFile(
context,
1,
'US',
1,
'TEST_NO_ENCRYPTION.ds2.zip',
'password',
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E010701'));
return;
} else {
throw new Error(e);
}
}
throw new Error('Error not thrown');
});
it('blobストレージに音声ファイルが存在しない場合はエラーになること', async () => {
// BlobServiceをモック化
const blobStorageServiceMockValue = makeBlobstorageServiceMockValue();
// ファイルが存在しないこととする
blobStorageServiceMockValue.fileExists = false;
const module = await makeTestingModuleWithBlob(blobStorageServiceMockValue);
if (!module) throw new Error('module not defined.');
const service = module.get<ConvertAudioFileService>(
ConvertAudioFileService,
);
const context = makeContext('externalId', 'requestId');
// テスト用のファイルをワークディレクトリにコピー
await fs.copyFile(
path.join(testFilePath, 'TEST_NO_ENCRYPTION.ds2.zip'),
path.join(workFolderPath, 'TEST_NO_ENCRYPTION.ds2.zip'),
);
try {
await service.generateAutoTranscriptionFile(
context,
1,
'US',
1,
'TEST_NO_ENCRYPTION.ds2.zip',
'password',
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E010701'));
return;
} else {
throw new Error(e);
}
}
throw new Error('Error not thrown');
});
it('blobストレージからダウンロードに失敗した場合エラーになること', async () => {
// BlobServiceをモック化
const blobStorageServiceMockValue = makeBlobstorageServiceMockValue();
// ダウンロードに失敗したことにする
blobStorageServiceMockValue.downloadFile = new Error('download faild.');
const module = await makeTestingModuleWithBlob(blobStorageServiceMockValue);
if (!module) throw new Error('module not defined.');
const service = module.get<ConvertAudioFileService>(
ConvertAudioFileService,
);
const context = makeContext('externalId', 'requestId');
// テスト用のファイルをワークディレクトリにコピー
await fs.copyFile(
path.join(testFilePath, 'TEST_NO_ENCRYPTION.ds2.zip'),
path.join(workFolderPath, 'TEST_NO_ENCRYPTION.ds2.zip'),
);
try {
await service.generateAutoTranscriptionFile(
context,
1,
'US',
1,
'TEST_NO_ENCRYPTION.ds2.zip',
'password',
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
return;
} else {
throw new Error(e);
}
}
throw new Error('Error not thrown');
});
it('blobストレージへアップロードに失敗した場合エラーになること', async () => {
// BlobServiceをモック化
const blobStorageServiceMockValue = makeBlobstorageServiceMockValue();
// アップロードに失敗したことにする
blobStorageServiceMockValue.uploadFile = new Error('upload faild.');
const module = await makeTestingModuleWithBlob(blobStorageServiceMockValue);
if (!module) throw new Error('module not defined.');
const service = module.get<ConvertAudioFileService>(
ConvertAudioFileService,
);
const context = makeContext('externalId', 'requestId');
// テスト用のファイルをワークディレクトリにコピー
await fs.copyFile(
path.join(testFilePath, 'TEST_NO_ENCRYPTION.ds2.zip'),
path.join(workFolderPath, 'TEST_NO_ENCRYPTION.ds2.zip'),
);
try {
await service.generateAutoTranscriptionFile(
context,
1,
'US',
1,
'TEST_NO_ENCRYPTION.ds2.zip',
'password',
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
return;
} else {
throw new Error(e);
}
}
throw new Error('Error not thrown');
});
it('zipの解凍に失敗した時、エラーになることzipファイルがない', async () => {
// BlobServiceをモック化
const blobStorageServiceMockValue = makeBlobstorageServiceMockValue();
const module = await makeTestingModuleWithBlob(blobStorageServiceMockValue);
if (!module) throw new Error('module not defined.');
const service = module.get<ConvertAudioFileService>(
ConvertAudioFileService,
);
const context = makeContext('externalId', 'requestId');
// zipファイルをテストフォルダからコピーしないことで、zipがないことをテスト
try {
await service.generateAutoTranscriptionFile(
context,
1,
'US',
1,
'TEST_NO_ENCRYPTION.ds2.zip',
'password',
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
return;
} else {
throw new Error(e);
}
}
throw new Error('Error not thrown');
});
it('zipの解凍に失敗した時、エラーになることパスワード不正', async () => {
// BlobServiceをモック化
const blobStorageServiceMockValue = makeBlobstorageServiceMockValue();
const module = await makeTestingModuleWithBlob(blobStorageServiceMockValue);
if (!module) throw new Error('module not defined.');
const service = module.get<ConvertAudioFileService>(
ConvertAudioFileService,
);
const context = makeContext('externalId', 'requestId');
// テスト用のファイルをワークディレクトリにコピー
// パスワードが異なるzipファイル
await fs.copyFile(
path.join(testFilePath, 'TEST_UNMATCH_PASSWORD.ds2.zip'),
path.join(workFolderPath, 'TEST_UNMATCH_PASSWORD.ds2.zip'),
);
try {
await service.generateAutoTranscriptionFile(
context,
1,
'US',
1,
'TEST_UNMATCH_PASSWORD.ds2.zip',
'password',
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
return;
} else {
throw new Error(e);
}
}
throw new Error('Error not thrown');
});
it('zipの解凍に失敗した時、エラーになることzipファイルではない', async () => {
// BlobServiceをモック化
const blobStorageServiceMockValue = makeBlobstorageServiceMockValue();
const module = await makeTestingModuleWithBlob(blobStorageServiceMockValue);
if (!module) throw new Error('module not defined.');
const service = module.get<ConvertAudioFileService>(
ConvertAudioFileService,
);
const context = makeContext('externalId', 'requestId');
// テスト用のファイルをワークディレクトリにコピー
await fs.copyFile(
path.join(testFilePath, 'TEST_NO_ZIPFILE.DS2.zip'),
path.join(workFolderPath, 'TEST_NO_ZIPFILE.DS2.zip'),
);
try {
await service.generateAutoTranscriptionFile(
context,
1,
'US',
1,
'TEST_NO_ZIPFILE.DS2.zip',
'password',
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
return;
} else {
throw new Error(e);
}
}
throw new Error('Error not thrown');
});
it('音声ファイルの変換に失敗した時、エラーになること(パスワード不正)', async () => {
// BlobServiceをモック化
const blobStorageServiceMockValue = makeBlobstorageServiceMockValue();
const module = await makeTestingModuleWithBlob(blobStorageServiceMockValue);
if (!module) throw new Error('module not defined.');
const service = module.get<ConvertAudioFileService>(
ConvertAudioFileService,
);
const context = makeContext('externalId', 'requestId');
// テスト用のファイルをワークディレクトリにコピー
await fs.copyFile(
path.join(testFilePath, 'TEST_ENCRYPTION.DS2.zip'),
path.join(workFolderPath, 'TEST_ENCRYPTION.DS2.zip'),
);
try {
await service.generateAutoTranscriptionFile(
context,
1,
'US',
1,
'TEST_ENCRYPTION.DS2.zip',
// 誤った復号化パスワードを指定
'invalid',
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E033001'));
return;
} else {
throw new Error(e);
}
}
throw new Error('Error not thrown');
});
it('音声ファイルの変換に失敗した時、エラー(フォーマット不正)', async () => {
// BlobServiceをモック化
const blobStorageServiceMockValue = makeBlobstorageServiceMockValue();
const module = await makeTestingModuleWithBlob(blobStorageServiceMockValue);
if (!module) throw new Error('module not defined.');
const service = module.get<ConvertAudioFileService>(
ConvertAudioFileService,
);
const context = makeContext('externalId', 'requestId');
// テスト用のファイルをワークディレクトリにコピー
await fs.copyFile(
path.join(testFilePath, 'TEST_NOT_DS2.ds2.zip'),
path.join(workFolderPath, 'TEST_NOT_DS2.ds2.zip'),
);
try {
await service.generateAutoTranscriptionFile(
context,
1,
'US',
1,
'TEST_NOT_DS2.ds2.zip',
// 誤った復号化パスワードを指定
'invalid',
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E031001'));
return;
} else {
throw new Error(e);
}
}
throw new Error('Error not thrown');
});
});

View File

@ -0,0 +1,247 @@
import * as childProcess from 'child_process';
import * as path from 'path';
import {
rmDirRecursive,
makeWorkingDirectory,
extractPasswordZipFile,
toLowerCaseFileName,
} from '../../common/files';
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { Context } from '../../common/log';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
import { GenerateAutoTranscriptionFileResponse } from './types/types';
import * as errors from './errors';
import { AUDIO_FILE_CONVERT_ERROR_MAP } from './constants';
import { ConfigService } from '@nestjs/config';
import { ErrorCodeType } from '../../common/error/types/types';
import { ErrorCodes } from '../../common/error/code';
@Injectable()
export class ConvertAudioFileService {
private readonly logger = new Logger(ConvertAudioFileService.name);
private readonly zipFilePassword = this.configService.getOrThrow<string>(
'AUDIO_FILE_ZIP_PASSWORD',
);
constructor(
private readonly blobStorageService: BlobstorageService,
private readonly configService: ConfigService,
) {}
/**
*
* @param accountId
* @param country
* @param taskId
* @param audioFileName
* @param encryptionPassword
* @returns voiceFileName
*/
async generateAutoTranscriptionFile(
context: Context,
accountId: number,
country: string,
taskId: number,
audioFileName: string,
encryptionPassword: string,
): Promise<GenerateAutoTranscriptionFileResponse> {
// 作業用ディレクトリを作成
const workDirPath = await makeWorkingDirectory(
process.cwd(),
// ファイル変換ツールの仕様で、暗号化されていて拡張子が大文字の音声ファイルは読み込めず、変換に失敗する(エラーコード1000が出力される)。
// 変換エラーを防ぐため音声ファイル名を小文字に変更してから変換ツールを通すが、
// フォルダパスも含めて小文字に変更するため、ファイル読み込みエラーにならないようにあらかじめ小文字でフォルダを作っておく。
context.getTrackingId().toLocaleLowerCase(),
);
try {
this.logger.log(
`[IN] [${context.getTrackingId()}]` +
`${this.generateAutoTranscriptionFile.name} | params: { ` +
`accountId: ${accountId}, ` +
`country: ${country}, ` +
`audioFileId: ${taskId}, ` +
`audioFileName: ${audioFileName} ` +
`};`,
);
// コンテナが存在しないことは本来ありえないため、エラー
if (
!(await this.blobStorageService.containerExists(
context,
accountId,
country,
))
) {
throw new errors.StorageAccountContainerNotFoundError(
`container not found. country: ${country}, accountId: ${accountId}`,
);
}
// 音声ファイルが存在しないことは本来ありえないため、エラー
if (
!(await this.blobStorageService.fileExists(
context,
accountId,
country,
audioFileName,
))
) {
throw new errors.AudioFileNotFoundError(
`Audio file is not exists in blob storage. task_id:${taskId}, fileName:${audioFileName}`,
);
}
// 音声ファイル(zip)をダウンロード
// trackingIdでワークフォルダを作って、音声ファイル(zip)をダウンロードする
const workFilePath = path.join(workDirPath, audioFileName);
await this.blobStorageService.downloadFile(
context,
accountId,
country,
audioFileName,
workFilePath,
);
let unzipFiles: string[];
this.logger.log(`[${context.getTrackingId()}] zip file extract start.`);
try {
// ダウンロードしたzipファイルを解凍
unzipFiles = await extractPasswordZipFile(
workFilePath,
workDirPath,
this.zipFilePassword,
);
} catch (e) {
throw new errors.AudioFileUnzippingError(
`Audio file unzipping error. reason: ${e}`,
);
}
this.logger.log(`[${context.getTrackingId()}] zip file extract end.`);
// zipファイル内にXMLが含まれているため、取り除いて解凍する
const targetFiles = unzipFiles.filter(
(filePath) => !filePath.endsWith('.xml'),
);
// 解凍後のフォルダには音声ファイルのみが残っている前提。見つからなければエラー
const audioFilePath = targetFiles.pop();
if (targetFiles.length !== 0 || !audioFilePath) {
throw new Error(
`audio file not found in zipfile. audioFileName: ${audioFileName}`,
);
}
// ファイル変換ツールの仕様で、暗号化されていて拡張子が大文字の音声ファイルは読み込めず、変換に失敗する(エラーコード1000が出力される)ため、
// 変換エラーを防ぐため音声ファイル名を小文字に変更してから変換ツールを通す。
// 実際にアップロードするファイル名は元の拡張子にする。
const audioFilePathLower = await toLowerCaseFileName(audioFilePath);
// 音声ファイルを変換する
this.logger.log(`[${context.getTrackingId()}] audio file convert start.`);
try {
await new Promise(
async (resolve, reject: (resultCode: number) => void) => {
const child = childProcess.spawn('dec2wav.out', [
audioFilePathLower,
`${audioFilePath}.wav`,
encryptionPassword,
]);
let stdoutData = '';
for await (const chunk of child.stdout) {
stdoutData += chunk;
}
// 改行文字が含まれているので取り除く
const resultCode = Number(stdoutData.replace('\n', ''));
this.logger.log(
`[${context.getTrackingId()}] wav convert tool result code: ${resultCode}`,
);
// 成功コードではない場合、エラーとする
if (resultCode > 0) {
reject(Number(resultCode));
}
resolve(resultCode);
},
);
} catch (e) {
const errorCode = e as unknown as number;
const reason = this.handleAudioConvertToolError(errorCode);
throw reason;
}
this.logger.log(`[${context.getTrackingId()}] audio file convert end.`);
// BlobStorageにアップロードする
await this.blobStorageService.uploadFile(
context,
accountId,
country,
taskId,
`auto-transcribe/${taskId}/transcribe-request/${
`${audioFilePath}.wav`.split('/').slice(-1)[0]
}`,
`${audioFilePath}.wav`,
);
return {
voiceFileName: `${audioFilePath}.wav`.split('/').slice(-1)[0],
};
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof errors.AudioFileConvertToolError) {
// モジュールエラーコード保持し、エラー返す
const underlyingCode = e.toolErrorCode;
const errorCode = `E03${underlyingCode}`;
const mappedErrorCode: ErrorCodeType = ErrorCodes.includes(
errorCode as ErrorCodeType,
)
? (errorCode as ErrorCodeType)
: 'E039999';
throw new HttpException(
makeErrorResponse(mappedErrorCode),
HttpStatus.BAD_REQUEST,
);
}
if (e instanceof Error) {
switch (e.constructor) {
case errors.StorageAccountContainerNotFoundError:
case errors.AudioFileNotFoundError:
throw new HttpException(
makeErrorResponse('E010701'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
// ローカルのファイルは必ず消す
try {
await rmDirRecursive(workDirPath);
} catch (e) {
// ファイル削除時のエラーは本来の処理は完了できているので、握りつぶす。
this.logger.error(
`[${context.getTrackingId()}] work file delete error. reason: ${e})}`,
);
}
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${
this.generateAutoTranscriptionFile.name
}`,
);
}
}
private handleAudioConvertToolError(errorCode: number) {
return new AUDIO_FILE_CONVERT_ERROR_MAP[errorCode](
`Audio file convert tool error. error code: ${errorCode}`,
errorCode,
);
}
}

View File

@ -0,0 +1 @@
export * from './types';

View File

@ -0,0 +1,209 @@
/**
* SAコンテナが存在しないエラー
*/
export class StorageAccountContainerNotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = StorageAccountContainerNotFoundError.name;
}
}
/**
*
*/
export class AudioFileNotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = AudioFileNotFoundError.name;
}
}
/**
* ZIP解凍失敗エラー
*/
export class AudioFileUnzippingError extends Error {
constructor(message: string) {
super(message);
this.name = AudioFileUnzippingError.name;
}
}
/**
* WAV変換ツール関連エラー
*/
export class AudioFileConvertToolError extends Error {
constructor(message: string, public readonly toolErrorCode: number) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}
/**
*
*/
export class InputAudioFileOpenError extends AudioFileConvertToolError {
constructor(message: string, code = 1000) {
super(message, code);
this.name = InputAudioFileOpenError.name;
}
}
/**
*
*/
export class InputAudioFileFormatError extends AudioFileConvertToolError {
constructor(message: string, code = 1001) {
super(message, code);
this.name = InputAudioFileFormatError.name;
}
}
/**
*
*/
export class InputAudioFileExtError extends AudioFileConvertToolError {
constructor(message: string, code = 1002) {
super(message, code);
this.name = InputAudioFileExtError.name;
}
}
/**
*
*/
export class InputAudioFileReadError extends AudioFileConvertToolError {
constructor(message: string, code = 1003) {
super(message, code);
this.name = InputAudioFileReadError.name;
}
}
/**
* dssファイル
*/
export class InputAudioFileInvalidDssFormatError extends AudioFileConvertToolError {
constructor(message: string, code = 1004) {
super(message, code);
this.name = InputAudioFileInvalidDssFormatError.name;
}
}
/**
* ;
*/
export class OutputAudioFileOpenError extends AudioFileConvertToolError {
constructor(message: string, code = 2000) {
super(message, code);
this.name = OutputAudioFileOpenError.name;
}
}
/**
* wavヘッダー書き出しに失敗;
*/
export class OutputAudioFileWriteHeaderError extends AudioFileConvertToolError {
constructor(message: string, code = 2001) {
super(message, code);
this.name = OutputAudioFileWriteHeaderError.name;
}
}
/**
* ;
*/
export class OutputAudioFileWriteError extends AudioFileConvertToolError {
constructor(message: string, code = 2002) {
super(message, code);
this.name = OutputAudioFileWriteError.name;
}
}
/**
* ds2ファイルの暗号化パスワードの指定がない;
*/
export class DS2AudioFileEncryptionPasswordNotSpecifiedError extends AudioFileConvertToolError {
constructor(message: string, code = 3000) {
super(message, code);
this.name = DS2AudioFileEncryptionPasswordNotSpecifiedError.name;
}
}
/**
* ds2ファイルの暗号化パスワードが異なる;
*/
export class DS2AudioFileEncryptionPasswordInvalidError extends AudioFileConvertToolError {
constructor(message: string, code = 3001) {
super(message, code);
this.name = DS2AudioFileEncryptionPasswordInvalidError.name;
}
}
/**
* mp3の初期化APIが失敗;
*/
export class MP3AudioFileInitializeError extends AudioFileConvertToolError {
constructor(message: string, code = 4000) {
super(message, code);
this.name = MP3AudioFileInitializeError.name;
}
}
/**
* mp3のオブジェクト生成に失敗;
*/
export class MP3AudioFileObjectGenerateError extends AudioFileConvertToolError {
constructor(message: string, code = 4001) {
super(message, code);
this.name = MP3AudioFileInitializeError.name;
}
}
/**
* mp3のフォーマット設定に失敗;
*/
export class MP3AudioFileFormatConfigurationError extends AudioFileConvertToolError {
constructor(message: string, code = 4002) {
super(message, code);
this.name = MP3AudioFileFormatConfigurationError.name;
}
}
/**
* mp3のフォーマット取得に失敗
*/
export class MP3AudioFileGetFormatError extends AudioFileConvertToolError {
constructor(message: string, code = 4003) {
super(message, code);
this.name = MP3AudioFileGetFormatError.name;
}
}
/**
* mp3のブロック出力に失敗;
*/
export class MP3AudioFileBlockOutputError extends AudioFileConvertToolError {
constructor(message: string, code = 4004) {
super(message, code);
this.name = MP3AudioFileBlockOutputError.name;
}
}
/**
* mp3のデータ読み出しに失敗;
*/
export class MP3AudioFileReadError extends AudioFileConvertToolError {
constructor(message: string, code = 4005) {
super(message, code);
this.name = MP3AudioFileReadError.name;
}
}
/**
* mp3ファイルの終了に失敗;
*/
export class MP3AudioFileCloseError extends AudioFileConvertToolError {
constructor(message: string, code = 4006) {
super(message, code);
this.name = MP3AudioFileCloseError.name;
}
}

View File

@ -0,0 +1,41 @@
export type BlobstorageServiceMockValue = {
containerExists: boolean | Error;
fileExists: boolean | Error;
downloadFile: void | Error;
uploadFile: void | Error;
};
export const makeBlobstorageServiceMock = (
value: BlobstorageServiceMockValue,
) => {
const { containerExists, fileExists, downloadFile, uploadFile } = 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),
downloadFile:
downloadFile instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(downloadFile)
: jest.fn<Promise<void>, []>().mockResolvedValue(downloadFile),
uploadFile:
uploadFile instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(uploadFile)
: jest.fn<Promise<void>, []>().mockResolvedValue(uploadFile),
};
};
export const makeBlobstorageServiceMockValue =
(): BlobstorageServiceMockValue => {
return {
containerExists: true,
fileExists: true,
uploadFile: undefined,
downloadFile: undefined,
};
};

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<test>Test file</test>
</root>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<test>NOT DS2</test>
</root>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<test>NOT DS2</test>
</root>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<test>TEST XML</test>
</root>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<test>password is 'p@ssword'</test>
</root>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<test>Not zipfile.</test>
</root>

View File

@ -0,0 +1,33 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigModule } from '@nestjs/config';
import { BlobstorageModule } from '../../../gateways/blobstorage/blobstorage.module';
import { BlobstorageService } from '../../../gateways/blobstorage/blobstorage.service';
import { ConvertAudioFileService } from '../convert-audio-file.service';
import {
BlobstorageServiceMockValue,
makeBlobstorageServiceMock,
} from './convert-audio-file.service.mock';
export const makeTestingModuleWithBlob = async (
blobStorageService: BlobstorageServiceMockValue,
): Promise<TestingModule | undefined> => {
try {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
envFilePath: ['.env.test', '.env'],
isGlobal: true,
}),
BlobstorageModule,
],
providers: [ConvertAudioFileService],
})
.overrideProvider(BlobstorageService)
.useValue(makeBlobstorageServiceMock(blobStorageService))
.compile();
return module;
} catch (e) {
console.log(e);
}
};

View File

@ -0,0 +1,46 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsString, Min } from 'class-validator';
export class GenerateAutoTranscriptionFileResponse {
@ApiProperty({
description: '自動文字起こし用音声ファイル(wav形式)',
})
@Type(() => String)
@IsString()
voiceFileName: string;
}
export class GenerateAutoTranscriptionFileRequest {
@ApiProperty({
description: 'タスクID',
})
@Type(() => Number)
@Min(1)
taskId: number;
@ApiProperty({
description: 'アカウントが所属している国情報',
})
@Type(() => String)
country: string;
@ApiProperty({
description: 'アカウントID',
})
@Type(() => Number)
@Min(1)
accountId: number;
@ApiProperty({
description: '音声ファイル名',
})
@Type(() => String)
audioFileName: string;
@ApiProperty({
description: '復号化パスワード',
})
@Type(() => String)
encryptionPassword: string;
}

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { BlobstorageService } from './blobstorage.service';
import { ConfigModule } from '@nestjs/config';
@Module({
exports: [BlobstorageService],
imports: [ConfigModule],
providers: [BlobstorageService],
})
export class BlobstorageModule {}

View File

@ -0,0 +1,219 @@
import { Injectable, Logger } from '@nestjs/common';
import {
BlobServiceClient,
StorageSharedKeyCredential,
ContainerClient,
} from '@azure/storage-blob';
import { ConfigService } from '@nestjs/config';
import {
BLOB_STORAGE_REGION_AU,
BLOB_STORAGE_REGION_EU,
BLOB_STORAGE_REGION_US,
} from '../../constants';
import { Context } from '../../common/log';
@Injectable()
export class BlobstorageService {
private readonly logger = new Logger(BlobstorageService.name);
private readonly blobServiceClientUS: BlobServiceClient;
private readonly blobServiceClientEU: BlobServiceClient;
private readonly blobServiceClientAU: BlobServiceClient;
private readonly sharedKeyCredentialUS: StorageSharedKeyCredential;
private readonly sharedKeyCredentialAU: StorageSharedKeyCredential;
private readonly sharedKeyCredentialEU: StorageSharedKeyCredential;
constructor(private readonly configService: ConfigService) {
this.sharedKeyCredentialUS = new StorageSharedKeyCredential(
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_NAME_US'),
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_KEY_US'),
);
this.sharedKeyCredentialAU = new StorageSharedKeyCredential(
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_NAME_AU'),
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_KEY_AU'),
);
this.sharedKeyCredentialEU = new StorageSharedKeyCredential(
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_NAME_EU'),
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_KEY_EU'),
);
this.blobServiceClientUS = new BlobServiceClient(
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_ENDPOINT_US'),
this.sharedKeyCredentialUS,
);
this.blobServiceClientAU = new BlobServiceClient(
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_ENDPOINT_AU'),
this.sharedKeyCredentialAU,
);
this.blobServiceClientEU = new BlobServiceClient(
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_ENDPOINT_EU'),
this.sharedKeyCredentialEU,
);
}
/**
* Containers exists
* @param country
* @param accountId
* @returns exists
*/
async containerExists(
context: Context,
accountId: number,
country: string,
): Promise<boolean> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.containerExists.name
} | params: { ` + `accountId: ${accountId} };`,
);
// 国に応じたリージョンでコンテナ名を指定してClientを取得
const containerClient = this.getContainerClient(
context,
accountId,
country,
);
const exists = await containerClient.exists();
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.containerExists.name}`,
);
return exists;
}
/**
* Files exists
* @param context
* @param accountId
* @param country
* @param fileName
* @returns exists
*/
async fileExists(
context: Context,
accountId: number,
country: string,
fileName: string,
): Promise<boolean> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.fileExists.name} | params: { ` +
`accountId: ${accountId},` +
`country: ${country},` +
`fileName: ${fileName} };`,
);
const containerClient = this.getContainerClient(
context,
accountId,
country,
);
const blob = containerClient.getBlobClient(`${fileName}`);
const exists = await blob.exists();
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.fileExists.name}`,
);
return exists;
}
/**
* Download File in countainer
* @param context
* @param accountId
* @param country
* @param fileName
* @param downloadFilePath
*/
async downloadFile(
context: Context,
accountId: number,
country: string,
fileName: string,
downloadFilePath: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.downloadFile.name
} | params: { ` +
`accountId: ${accountId},` +
`country: ${country},` +
`fileName: ${fileName} };`,
);
const containerClient = this.getContainerClient(
context,
accountId,
country,
);
const blob = containerClient.getBlobClient(`${fileName}`);
await blob.downloadToFile(downloadFilePath);
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.downloadFile.name}`,
);
}
/**
* Upload File to countainer
* @param context
* @param accountId
* @param country
* @param taskId
* @param fileName
* @param localFilePath
* @returns
*/
async uploadFile(
context: Context,
accountId: number,
country: string,
taskId: number,
fileName: string,
localFilePath: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.uploadFile.name} | params: { ` +
`accountId: ${accountId},` +
`country: ${country},` +
`taskId: ${taskId},` +
`fileName: ${fileName} };` +
`localFilePath: ${localFilePath} };`,
);
const containerClient = this.getContainerClient(
context,
accountId,
country,
);
const blockBlobClient = containerClient.getBlockBlobClient(fileName);
await blockBlobClient.uploadFile(localFilePath);
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.uploadFile.name}`,
);
}
/**
* Gets container client
* @param companyName
* @returns container client
*/
private getContainerClient(
context: Context,
accountId: number,
country: string,
): ContainerClient {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.getContainerClient.name
} | params: { ` + `accountId: ${accountId} };`,
);
const containerName = `account-${accountId}`;
if (BLOB_STORAGE_REGION_US.includes(country)) {
return this.blobServiceClientUS.getContainerClient(containerName);
} else if (BLOB_STORAGE_REGION_AU.includes(country)) {
return this.blobServiceClientAU.getContainerClient(containerName);
} else if (BLOB_STORAGE_REGION_EU.includes(country)) {
return this.blobServiceClientEU.getContainerClient(containerName);
} else {
throw new Error('invalid country');
}
}
}

View File

@ -0,0 +1,11 @@
import { Controller, Get } from '@nestjs/common';
import { ApiOperation } from '@nestjs/swagger';
@Controller('health')
export class HealthController {
@Get()
@ApiOperation({ operationId: 'checkHealth' })
checkHealth(): string {
return 'ODMS Auto Transcription File App Service Health OK';
}
}

View File

@ -0,0 +1,62 @@
import { NestFactory } from '@nestjs/core';
import cookieParser from 'cookie-parser';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
import { ValidationPipe, VersioningType } from '@nestjs/common';
import helmet from 'helmet';
const helmetDirectives = helmet.contentSecurityPolicy.getDefaultDirectives();
helmetDirectives['connect-src'] =
process.env.STAGE === 'local'
? [
"'self'",
process.env.ADB2C_ORIGIN ?? '',
process.env.STORAGE_ACCOUNT_ENDPOINT_US ?? '',
process.env.STORAGE_ACCOUNT_ENDPOINT_AU ?? '',
process.env.STORAGE_ACCOUNT_ENDPOINT_EU ?? '',
]
: ["'self'"];
helmetDirectives['navigate-to'] = ["'self'"];
helmetDirectives['style-src'] = ["'self'", 'https:'];
helmetDirectives['report-uri'] = ["'self'"];
async function bootstrap() {
console.log(`BUILD_VERSION: ${process.env.BUILD_VERSION}`);
const app = await NestFactory.create(AppModule);
app.use(
helmet({
contentSecurityPolicy: {
directives: helmetDirectives,
},
}),
cookieParser(),
);
// バリデーター(+型の自動変換機能)を適用
app.useGlobalPipes(
new ValidationPipe({ transform: true, forbidUnknownValues: false }),
);
if (process.env.STAGE === 'local') {
const options = new DocumentBuilder()
.setTitle('ODMSOpenAPI')
.setVersion('1.0.0')
.addBearerAuth({
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
})
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api', app, document);
}
app.enableVersioning({
type: VersioningType.HEADER,
header: 'x-api-version',
});
await app.listen(process.env.PORT || 80);
}
bootstrap();

View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false,
"esModuleInterop": true
}
}

View File

@ -1,4 +1,4 @@
FROM node:18.17.1-buster FROM node:22.14-bookworm
RUN /bin/cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \ RUN /bin/cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
echo "Asia/Tokyo" > /etc/timezone echo "Asia/Tokyo" > /etc/timezone

View File

@ -3,31 +3,54 @@
# Copyright (c) Microsoft Corporation. All rights reserved. # Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
#------------------------------------------------------------------------------------------------------------- #-------------------------------------------------------------------------------------------------------------
#
# Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] # Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/common.md
# Maintainer: The VS Code and Codespaces Teams
INSTALL_ZSH=${1:-"true"} #
USERNAME=${2:-"vscode"} # Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] [install Oh My Zsh! flag] [Add non-free packages]
USER_UID=${3:-1000}
USER_GID=${4:-1000}
UPGRADE_PACKAGES=${5:-"true"}
set -e set -e
INSTALL_ZSH=${1:-"true"}
USERNAME=${2:-"automatic"}
USER_UID=${3:-"automatic"}
USER_GID=${4:-"automatic"}
UPGRADE_PACKAGES=${5:-"true"}
INSTALL_OH_MYS=${6:-"true"}
ADD_NON_FREE_PACKAGES=${7:-"false"}
SCRIPT_DIR="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)"
MARKER_FILE="/usr/local/etc/vscode-dev-containers/common"
if [ "$(id -u)" -ne 0 ]; then if [ "$(id -u)" -ne 0 ]; then
echo -e 'Script must be run a root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.'
exit 1 exit 1
fi fi
# Treat a user name of "none" as root # Ensure that login shells get the correct path if the user updated the PATH using ENV.
if [ "${USERNAME}" = "none" ] || [ "${USERNAME}" = "root" ]; then rm -f /etc/profile.d/00-restore-env.sh
echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh
chmod +x /etc/profile.d/00-restore-env.sh
# If in automatic mode, determine if a user already exists, if not use vscode
if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then
USERNAME=""
POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)")
for CURRENT_USER in ${POSSIBLE_USERS[@]}; do
if id -u ${CURRENT_USER} > /dev/null 2>&1; then
USERNAME=${CURRENT_USER}
break
fi
done
if [ "${USERNAME}" = "" ]; then
USERNAME=vscode
fi
elif [ "${USERNAME}" = "none" ]; then
USERNAME=root USERNAME=root
USER_UID=0 USER_UID=0
USER_GID=0 USER_GID=0
fi fi
# Load markers to see which steps have already run # Load markers to see which steps have already run
MARKER_FILE="/usr/local/etc/vscode-dev-containers/common"
if [ -f "${MARKER_FILE}" ]; then if [ -f "${MARKER_FILE}" ]; then
echo "Marker file found:" echo "Marker file found:"
cat "${MARKER_FILE}" cat "${MARKER_FILE}"
@ -38,7 +61,7 @@ fi
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
# Function to call apt-get if needed # Function to call apt-get if needed
apt-get-update-if-needed() apt_get_update_if_needed()
{ {
if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then
echo "Running apt-get update..." echo "Running apt-get update..."
@ -50,132 +73,373 @@ apt-get-update-if-needed()
# Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies # Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies
if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then
apt-get-update-if-needed
PACKAGE_LIST="apt-utils \ package_list="apt-utils \
git \
openssh-client \ openssh-client \
less \ gnupg2 \
dirmngr \
iproute2 \ iproute2 \
procps \ procps \
lsof \
htop \
net-tools \
psmisc \
curl \ curl \
wget \ wget \
rsync \
ca-certificates \
unzip \ unzip \
zip \ zip \
nano \ nano \
vim-tiny \
less \
jq \ jq \
lsb-release \ lsb-release \
ca-certificates \
apt-transport-https \ apt-transport-https \
dialog \ dialog \
gnupg2 \
libc6 \ libc6 \
libgcc1 \ libgcc1 \
libkrb5-3 \
libgssapi-krb5-2 \ libgssapi-krb5-2 \
libicu[0-9][0-9] \ libicu[0-9][0-9] \
liblttng-ust0 \ liblttng-ust[0-9] \
libstdc++6 \ libstdc++6 \
zlib1g \ zlib1g \
locales \ locales \
sudo" sudo \
ncdu \
man-db \
strace \
manpages \
manpages-dev \
init-system-helpers"
# Needed for adding manpages-posix and manpages-posix-dev which are non-free packages in Debian
if [ "${ADD_NON_FREE_PACKAGES}" = "true" ]; then
# Bring in variables from /etc/os-release like VERSION_CODENAME
. /etc/os-release
sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list
sed -i -E "s/deb-src http:\/\/(deb|httredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list
sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list
sed -i -E "s/deb-src http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list
sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list
sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list
sed -i "s/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list
sed -i "s/deb-src http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list
# Handle bullseye location for security https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html
sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list
sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list
echo "Running apt-get update..."
apt-get update
package_list="${package_list} manpages-posix manpages-posix-dev"
else
apt_get_update_if_needed
fi
# Install libssl1.1 if available # Install libssl1.1 if available
if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then
PACKAGE_LIST="${PACKAGE_LIST} libssl1.1" package_list="${package_list} libssl1.1"
fi fi
# Install appropriate version of libssl1.0.x if available # Install appropriate version of libssl1.0.x if available
LIBSSL=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '') libssl_package=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '')
if [ "$(echo "$LIBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then if [ "$(echo "$LIlibssl_packageBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then
if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then
# Debian 9 # Debian 9
PACKAGE_LIST="${PACKAGE_LIST} libssl1.0.2" package_list="${package_list} libssl1.0.2"
elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then
# Ubuntu 18.04, 16.04, earlier # Ubuntu 18.04, 16.04, earlier
PACKAGE_LIST="${PACKAGE_LIST} libssl1.0.0" package_list="${package_list} libssl1.0.0"
fi fi
fi fi
echo "Packages to verify are installed: ${PACKAGE_LIST}" echo "Packages to verify are installed: ${package_list}"
apt-get -y install --no-install-recommends ${PACKAGE_LIST} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 ) apt-get -y install --no-install-recommends ${package_list} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 )
# Install git if not already installed (may be more recent than distro version)
if ! type git > /dev/null 2>&1; then
apt-get -y install --no-install-recommends git
fi
PACKAGES_ALREADY_INSTALLED="true" PACKAGES_ALREADY_INSTALLED="true"
fi fi
# Get to latest versions of all packages # Get to latest versions of all packages
if [ "${UPGRADE_PACKAGES}" = "true" ]; then if [ "${UPGRADE_PACKAGES}" = "true" ]; then
apt-get-update-if-needed apt_get_update_if_needed
apt-get -y upgrade --no-install-recommends apt-get -y upgrade --no-install-recommends
apt-get autoremove -y apt-get autoremove -y
fi fi
# Ensure at least the en_US.UTF-8 UTF-8 locale is available. # Ensure at least the en_US.UTF-8 UTF-8 locale is available.
# Common need for both applications and things like the agnoster ZSH theme. # Common need for both applications and things like the agnoster ZSH theme.
if [ "${LOCALE_ALREADY_SET}" != "true" ]; then if [ "${LOCALE_ALREADY_SET}" != "true" ] && ! grep -o -E '^\s*en_US.UTF-8\s+UTF-8' /etc/locale.gen > /dev/null; then
echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen
locale-gen locale-gen
LOCALE_ALREADY_SET="true" LOCALE_ALREADY_SET="true"
fi fi
# Create or update a non-root user to match UID/GID - see https://aka.ms/vscode-remote/containers/non-root-user. # Create or update a non-root user to match UID/GID.
if id -u $USERNAME > /dev/null 2>&1; then group_name="${USERNAME}"
if id -u ${USERNAME} > /dev/null 2>&1; then
# User exists, update if needed # User exists, update if needed
if [ "$USER_GID" != "$(id -G $USERNAME)" ]; then if [ "${USER_GID}" != "automatic" ] && [ "$USER_GID" != "$(id -g $USERNAME)" ]; then
groupmod --gid $USER_GID $USERNAME group_name="$(id -gn $USERNAME)"
groupmod --gid $USER_GID ${group_name}
usermod --gid $USER_GID $USERNAME usermod --gid $USER_GID $USERNAME
fi fi
if [ "$USER_UID" != "$(id -u $USERNAME)" ]; then if [ "${USER_UID}" != "automatic" ] && [ "$USER_UID" != "$(id -u $USERNAME)" ]; then
usermod --uid $USER_UID $USERNAME usermod --uid $USER_UID $USERNAME
fi fi
else else
# Create user # Create user
groupadd --gid $USER_GID $USERNAME if [ "${USER_GID}" = "automatic" ]; then
useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME groupadd $USERNAME
else
groupadd --gid $USER_GID $USERNAME
fi
if [ "${USER_UID}" = "automatic" ]; then
useradd -s /bin/bash --gid $USERNAME -m $USERNAME
else
useradd -s /bin/bash --uid $USER_UID --gid $USERNAME -m $USERNAME
fi
fi fi
# Add add sudo support for non-root user # Add sudo support for non-root user
if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then
echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME
chmod 0440 /etc/sudoers.d/$USERNAME chmod 0440 /etc/sudoers.d/$USERNAME
EXISTING_NON_ROOT_USER="${USERNAME}" EXISTING_NON_ROOT_USER="${USERNAME}"
fi fi
# .bashrc/.zshrc snippet # ** Shell customization section **
RC_SNIPPET="$(cat << EOF if [ "${USERNAME}" = "root" ]; then
export USER=\$(whoami) user_rc_path="/root"
else
export PATH=\$PATH:\$HOME/.local/bin user_rc_path="/home/${USERNAME}"
if [[ \$(which code-insiders 2>&1) && ! \$(which code 2>&1) ]]; then
alias code=code-insiders
fi fi
# Restore user .bashrc defaults from skeleton file if it doesn't exist or is empty
if [ ! -f "${user_rc_path}/.bashrc" ] || [ ! -s "${user_rc_path}/.bashrc" ] ; then
cp /etc/skel/.bashrc "${user_rc_path}/.bashrc"
fi
# Restore user .profile defaults from skeleton file if it doesn't exist or is empty
if [ ! -f "${user_rc_path}/.profile" ] || [ ! -s "${user_rc_path}/.profile" ] ; then
cp /etc/skel/.profile "${user_rc_path}/.profile"
fi
# .bashrc/.zshrc snippet
rc_snippet="$(cat << 'EOF'
if [ -z "${USER}" ]; then export USER=$(whoami); fi
if [[ "${PATH}" != *"$HOME/.local/bin"* ]]; then export PATH="${PATH}:$HOME/.local/bin"; fi
# Display optional first run image specific notice if configured and terminal is interactive
if [ -t 1 ] && [[ "${TERM_PROGRAM}" = "vscode" || "${TERM_PROGRAM}" = "codespaces" ]] && [ ! -f "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed" ]; then
if [ -f "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" ]; then
cat "/usr/local/etc/vscode-dev-containers/first-run-notice.txt"
elif [ -f "/workspaces/.codespaces/shared/first-run-notice.txt" ]; then
cat "/workspaces/.codespaces/shared/first-run-notice.txt"
fi
mkdir -p "$HOME/.config/vscode-dev-containers"
# Mark first run notice as displayed after 10s to avoid problems with fast terminal refreshes hiding it
((sleep 10s; touch "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed") &)
fi
# Set the default git editor if not already set
if [ -z "$(git config --get core.editor)" ] && [ -z "${GIT_EDITOR}" ]; then
if [ "${TERM_PROGRAM}" = "vscode" ]; then
if [[ -n $(command -v code-insiders) && -z $(command -v code) ]]; then
export GIT_EDITOR="code-insiders --wait"
else
export GIT_EDITOR="code --wait"
fi
fi
fi
EOF EOF
)" )"
# Ensure ~/.local/bin is in the PATH for root and non-root users for bash. (zsh is later) # code shim, it fallbacks to code-insiders if code is not available
if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then cat << 'EOF' > /usr/local/bin/code
echo "${RC_SNIPPET}" | tee -a /root/.bashrc >> /etc/skel/.bashrc #!/bin/sh
if [ "${USERNAME}" != "root" ]; then
echo "${RC_SNIPPET}" >> /home/$USERNAME/.bashrc get_in_path_except_current() {
chown $USER_UID:$USER_GID /home/$USERNAME/.bashrc which -a "$1" | grep -A1 "$0" | grep -v "$0"
}
code="$(get_in_path_except_current code)"
if [ -n "$code" ]; then
exec "$code" "$@"
elif [ "$(command -v code-insiders)" ]; then
exec code-insiders "$@"
else
echo "code or code-insiders is not installed" >&2
exit 127
fi
EOF
chmod +x /usr/local/bin/code
# systemctl shim - tells people to use 'service' if systemd is not running
cat << 'EOF' > /usr/local/bin/systemctl
#!/bin/sh
set -e
if [ -d "/run/systemd/system" ]; then
exec /bin/systemctl "$@"
else
echo '\n"systemd" is not running in this container due to its overhead.\nUse the "service" command to start services instead. e.g.: \n\nservice --status-all'
fi
EOF
chmod +x /usr/local/bin/systemctl
# Codespaces bash and OMZ themes - partly inspired by https://github.com/ohmyzsh/ohmyzsh/blob/master/themes/robbyrussell.zsh-theme
codespaces_bash="$(cat \
<<'EOF'
# Codespaces bash prompt theme
__bash_prompt() {
local userpart='`export XIT=$? \
&& [ ! -z "${GITHUB_USER}" ] && echo -n "\[\033[0;32m\]@${GITHUB_USER} " || echo -n "\[\033[0;32m\]\u " \
&& [ "$XIT" -ne "0" ] && echo -n "\[\033[1;31m\]➜" || echo -n "\[\033[0m\]➜"`'
local gitbranch='`\
if [ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ]; then \
export BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null); \
if [ "${BRANCH}" != "" ]; then \
echo -n "\[\033[0;36m\](\[\033[1;31m\]${BRANCH}" \
&& if git ls-files --error-unmatch -m --directory --no-empty-directory -o --exclude-standard ":/*" > /dev/null 2>&1; then \
echo -n " \[\033[1;33m\]✗"; \
fi \
&& echo -n "\[\033[0;36m\]) "; \
fi; \
fi`'
local lightblue='\[\033[1;34m\]'
local removecolor='\[\033[0m\]'
PS1="${userpart} ${lightblue}\w ${gitbranch}${removecolor}\$ "
unset -f __bash_prompt
}
__bash_prompt
EOF
)"
codespaces_zsh="$(cat \
<<'EOF'
# Codespaces zsh prompt theme
__zsh_prompt() {
local prompt_username
if [ ! -z "${GITHUB_USER}" ]; then
prompt_username="@${GITHUB_USER}"
else
prompt_username="%n"
fi fi
PROMPT="%{$fg[green]%}${prompt_username} %(?:%{$reset_color%}➜ :%{$fg_bold[red]%}➜ )" # User/exit code arrow
PROMPT+='%{$fg_bold[blue]%}%(5~|%-1~/…/%3~|%4~)%{$reset_color%} ' # cwd
PROMPT+='$([ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ] && git_prompt_info)' # Git status
PROMPT+='%{$fg[white]%}$ %{$reset_color%}'
unset -f __zsh_prompt
}
ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[cyan]%}(%{$fg_bold[red]%}"
ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} "
ZSH_THEME_GIT_PROMPT_DIRTY=" %{$fg_bold[yellow]%}✗%{$fg_bold[cyan]%})"
ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg_bold[cyan]%})"
__zsh_prompt
EOF
)"
# Add RC snippet and custom bash prompt
if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then
echo "${rc_snippet}" >> /etc/bash.bashrc
echo "${codespaces_bash}" >> "${user_rc_path}/.bashrc"
echo 'export PROMPT_DIRTRIM=4' >> "${user_rc_path}/.bashrc"
if [ "${USERNAME}" != "root" ]; then
echo "${codespaces_bash}" >> "/root/.bashrc"
echo 'export PROMPT_DIRTRIM=4' >> "/root/.bashrc"
fi
chown ${USERNAME}:${group_name} "${user_rc_path}/.bashrc"
RC_SNIPPET_ALREADY_ADDED="true" RC_SNIPPET_ALREADY_ADDED="true"
fi fi
# Optionally install and configure zsh # Optionally install and configure zsh and Oh My Zsh!
if [ "${INSTALL_ZSH}" = "true" ] && [ ! -d "/root/.oh-my-zsh" ] && [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then if [ "${INSTALL_ZSH}" = "true" ]; then
apt-get-update-if-needed if ! type zsh > /dev/null 2>&1; then
apt-get install -y zsh apt_get_update_if_needed
curl -fsSLo- https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh | bash 2>&1 apt-get install -y zsh
echo -e "${RC_SNIPPET}\nDEFAULT_USER=\$USER\nprompt_context(){}" >> /root/.zshrc
cp -fR /root/.oh-my-zsh /etc/skel
cp -f /root/.zshrc /etc/skel
sed -i -e "s/\/root\/.oh-my-zsh/\/home\/\$(whoami)\/.oh-my-zsh/g" /etc/skel/.zshrc
if [ "${USERNAME}" != "root" ]; then
cp -fR /etc/skel/.oh-my-zsh /etc/skel/.zshrc /home/$USERNAME
chown -R $USER_UID:$USER_GID /home/$USERNAME/.oh-my-zsh /home/$USERNAME/.zshrc
fi fi
ZSH_ALREADY_INSTALLED="true" if [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then
echo "${rc_snippet}" >> /etc/zsh/zshrc
ZSH_ALREADY_INSTALLED="true"
fi
# Adapted, simplified inline Oh My Zsh! install steps that adds, defaults to a codespaces theme.
# See https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/install.sh for official script.
oh_my_install_dir="${user_rc_path}/.oh-my-zsh"
if [ ! -d "${oh_my_install_dir}" ] && [ "${INSTALL_OH_MYS}" = "true" ]; then
template_path="${oh_my_install_dir}/templates/zshrc.zsh-template"
user_rc_file="${user_rc_path}/.zshrc"
umask g-w,o-w
mkdir -p ${oh_my_install_dir}
git clone --depth=1 \
-c core.eol=lf \
-c core.autocrlf=false \
-c fsck.zeroPaddedFilemode=ignore \
-c fetch.fsck.zeroPaddedFilemode=ignore \
-c receive.fsck.zeroPaddedFilemode=ignore \
"https://github.com/ohmyzsh/ohmyzsh" "${oh_my_install_dir}" 2>&1
echo -e "$(cat "${template_path}")\nDISABLE_AUTO_UPDATE=true\nDISABLE_UPDATE_PROMPT=true" > ${user_rc_file}
sed -i -e 's/ZSH_THEME=.*/ZSH_THEME="codespaces"/g' ${user_rc_file}
mkdir -p ${oh_my_install_dir}/custom/themes
echo "${codespaces_zsh}" > "${oh_my_install_dir}/custom/themes/codespaces.zsh-theme"
# Shrink git while still enabling updates
cd "${oh_my_install_dir}"
git repack -a -d -f --depth=1 --window=1
# Copy to non-root user if one is specified
if [ "${USERNAME}" != "root" ]; then
cp -rf "${user_rc_file}" "${oh_my_install_dir}" /root
chown -R ${USERNAME}:${group_name} "${user_rc_path}"
fi
fi
fi
# Persist image metadata info, script if meta.env found in same directory
meta_info_script="$(cat << 'EOF'
#!/bin/sh
. /usr/local/etc/vscode-dev-containers/meta.env
# Minimal output
if [ "$1" = "version" ] || [ "$1" = "image-version" ]; then
echo "${VERSION}"
exit 0
elif [ "$1" = "release" ]; then
echo "${GIT_REPOSITORY_RELEASE}"
exit 0
elif [ "$1" = "content" ] || [ "$1" = "content-url" ] || [ "$1" = "contents" ] || [ "$1" = "contents-url" ]; then
echo "${CONTENTS_URL}"
exit 0
fi
#Full output
echo
echo "Development container image information"
echo
if [ ! -z "${VERSION}" ]; then echo "- Image version: ${VERSION}"; fi
if [ ! -z "${DEFINITION_ID}" ]; then echo "- Definition ID: ${DEFINITION_ID}"; fi
if [ ! -z "${VARIANT}" ]; then echo "- Variant: ${VARIANT}"; fi
if [ ! -z "${GIT_REPOSITORY}" ]; then echo "- Source code repository: ${GIT_REPOSITORY}"; fi
if [ ! -z "${GIT_REPOSITORY_RELEASE}" ]; then echo "- Source code release/branch: ${GIT_REPOSITORY_RELEASE}"; fi
if [ ! -z "${BUILD_TIMESTAMP}" ]; then echo "- Timestamp: ${BUILD_TIMESTAMP}"; fi
if [ ! -z "${CONTENTS_URL}" ]; then echo && echo "More info: ${CONTENTS_URL}"; fi
echo
EOF
)"
if [ -f "${SCRIPT_DIR}/meta.env" ]; then
mkdir -p /usr/local/etc/vscode-dev-containers/
cp -f "${SCRIPT_DIR}/meta.env" /usr/local/etc/vscode-dev-containers/meta.env
echo "${meta_info_script}" > /usr/local/bin/devcontainer-info
chmod +x /usr/local/bin/devcontainer-info
fi fi
# Write marker file # Write marker file

View File

@ -34,5 +34,11 @@
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.formatOnPaste": true, "editor.formatOnPaste": true,
"editor.formatOnType": true, "editor.formatOnType": true,
"prettier.disableLanguages": ["markdown"] "prettier.disableLanguages": ["markdown"],
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
} }

View File

@ -140,6 +140,12 @@ export interface AllocatableLicenseInfo {
* @memberof AllocatableLicenseInfo * @memberof AllocatableLicenseInfo
*/ */
'expiryDate'?: string; 'expiryDate'?: string;
/**
*
* @type {string}
* @memberof AllocatableLicenseInfo
*/
'licenseType': string;
} }
/** /**
* *
@ -333,6 +339,18 @@ export interface AudioUploadFinishedResponse {
* @memberof AudioUploadFinishedResponse * @memberof AudioUploadFinishedResponse
*/ */
'jobNumber': string; 'jobNumber': string;
/**
* ID
* @type {number}
* @memberof AudioUploadFinishedResponse
*/
'taskId'?: number;
/**
*
* @type {boolean}
* @memberof AudioUploadFinishedResponse
*/
'isAutoTranscription'?: boolean;
} }
/** /**
* *
@ -366,6 +384,37 @@ export interface Author {
*/ */
'authorId': string; 'authorId': string;
} }
/**
*
* @export
* @interface AutoTranscribeCompleteRequest
*/
export interface AutoTranscribeCompleteRequest {
/**
* ID
* @type {number}
* @memberof AutoTranscribeCompleteRequest
*/
'taskId': number;
/**
*
* @type {string}
* @memberof AutoTranscribeCompleteRequest
*/
'transcribeResultStatus': string;
/**
*
* @type {string}
* @memberof AutoTranscribeCompleteRequest
*/
'transcribeResultName'?: string;
/**
*
* @type {string}
* @memberof AutoTranscribeCompleteRequest
*/
'timelineTextName'?: string;
}
/** /**
* *
* @export * @export
@ -424,6 +473,32 @@ export interface ConfirmRequest {
*/ */
'token': string; 'token': string;
} }
/**
*
* @export
* @interface ConvertAudioFileRequest
*/
export interface ConvertAudioFileRequest {
/**
* ID
* @type {number}
* @memberof ConvertAudioFileRequest
*/
'taskId': number;
}
/**
*
* @export
* @interface ConvertAudioFileResponse
*/
export interface ConvertAudioFileResponse {
/**
* (wav形式)
* @type {string}
* @memberof ConvertAudioFileResponse
*/
'voiceFileName': string;
}
/** /**
* *
* @export * @export
@ -509,6 +584,12 @@ export interface CreateOrdersRequest {
* @memberof CreateOrdersRequest * @memberof CreateOrdersRequest
*/ */
'orderCount': number; 'orderCount': number;
/**
* ライセンス種別: NORMAL / SPEECH_RECOGNITION / SPEECH_RECOGNITION_UPGRADE
* @type {string}
* @memberof CreateOrdersRequest
*/
'licenseType': string;
} }
/** /**
* *
@ -578,6 +659,12 @@ export interface CreateWorkflowsRequest {
* @memberof CreateWorkflowsRequest * @memberof CreateWorkflowsRequest
*/ */
'worktypeId'?: number; 'worktypeId'?: number;
/**
*
* @type {boolean}
* @memberof CreateWorkflowsRequest
*/
'isAutoTranscription': boolean;
/** /**
* ID * ID
* @type {number} * @type {number}
@ -796,6 +883,25 @@ export interface GetAllocatableLicensesResponse {
*/ */
'allocatableLicenses': Array<AllocatableLicenseInfo>; 'allocatableLicenses': Array<AllocatableLicenseInfo>;
} }
/**
*
* @export
* @interface GetAuthorLicenseResponse
*/
export interface GetAuthorLicenseResponse {
/**
* ライセンス種別: NONE / NORMAL / TRIAL / CARD / SPEECH_RECOGNITION
* @type {string}
* @memberof GetAuthorLicenseResponse
*/
'licenseType': string;
/**
*
* @type {object}
* @memberof GetAuthorLicenseResponse
*/
'expiryDate': object;
}
/** /**
* *
* @export * @export
@ -869,52 +975,46 @@ export interface GetLicenseSummaryRequest {
export interface GetLicenseSummaryResponse { export interface GetLicenseSummaryResponse {
/** /**
* *
* @type {number} * @type {PartnerLicenseInfoAggregates}
* @memberof GetLicenseSummaryResponse * @memberof GetLicenseSummaryResponse
*/ */
'totalLicense': number; 'totalLicense': PartnerLicenseInfoAggregates;
/** /**
* *
* @type {number} * @type {PartnerLicenseInfoAggregates}
* @memberof GetLicenseSummaryResponse * @memberof GetLicenseSummaryResponse
*/ */
'allocatedLicense': number; 'allocatedLicense': PartnerLicenseInfoAggregates;
/** /**
* *
* @type {number} * @type {PartnerLicenseInfoAggregates}
* @memberof GetLicenseSummaryResponse * @memberof GetLicenseSummaryResponse
*/ */
'reusableLicense': number; 'reusableLicense': PartnerLicenseInfoAggregates;
/** /**
* *
* @type {number} * @type {PartnerLicenseInfoAggregates}
* @memberof GetLicenseSummaryResponse * @memberof GetLicenseSummaryResponse
*/ */
'freeLicense': number; 'freeLicense': PartnerLicenseInfoAggregates;
/** /**
* *
* @type {number} * @type {PartnerLicenseInfoAggregates}
* @memberof GetLicenseSummaryResponse * @memberof GetLicenseSummaryResponse
*/ */
'expiringWithin14daysLicense': number; 'expiringWithin14daysLicense': PartnerLicenseInfoAggregates;
/** /**
* *
* @type {number} * @type {PartnerLicenseInfoAggregates}
* @memberof GetLicenseSummaryResponse * @memberof GetLicenseSummaryResponse
*/ */
'issueRequesting': number; 'issueRequesting': PartnerLicenseInfoAggregates;
/** /**
* *
* @type {number} * @type {PartnerLicenseInfoAggregates}
* @memberof GetLicenseSummaryResponse * @memberof GetLicenseSummaryResponse
*/ */
'numberOfRequesting': number; 'shortage': PartnerLicenseInfoAggregates;
/**
*
* @type {number}
* @memberof GetLicenseSummaryResponse
*/
'shortage': number;
/** /**
* *
* @type {number} * @type {number}
@ -1054,6 +1154,12 @@ export interface GetPartnerLicensesRequest {
* @memberof GetPartnerLicensesRequest * @memberof GetPartnerLicensesRequest
*/ */
'accountId': number; 'accountId': number;
/**
*
* @type {string}
* @memberof GetPartnerLicensesRequest
*/
'licenseType': string;
} }
/** /**
* *
@ -1725,6 +1831,37 @@ export interface PartnerLicenseInfo {
*/ */
'issueRequesting': number; 'issueRequesting': number;
} }
/**
*
* @export
* @interface PartnerLicenseInfoAggregates
*/
export interface PartnerLicenseInfoAggregates {
/**
* ()
* @type {number}
* @memberof PartnerLicenseInfoAggregates
*/
'normal': number;
/**
*
* @type {number}
* @memberof PartnerLicenseInfoAggregates
*/
'trial': number;
/**
*
* @type {number}
* @memberof PartnerLicenseInfoAggregates
*/
'speechRecognition': number;
/**
*
* @type {number}
* @memberof PartnerLicenseInfoAggregates
*/
'speechRecognitionUpgrade': number;
}
/** /**
* *
* @export * @export
@ -1982,6 +2119,37 @@ export interface RegisterRequest {
*/ */
'handler': string; 'handler': string;
} }
/**
*
* @export
* @interface RequestAutoTranscriptionRequest
*/
export interface RequestAutoTranscriptionRequest {
/**
* ID
* @type {number}
* @memberof RequestAutoTranscriptionRequest
*/
'taskId': number;
/**
* (wav形式)
* @type {string}
* @memberof RequestAutoTranscriptionRequest
*/
'voiceFileName': string;
/**
*
* @type {string}
* @memberof RequestAutoTranscriptionRequest
*/
'voiceCodec': string;
/**
*
* @type {string}
* @memberof RequestAutoTranscriptionRequest
*/
'language': string;
}
/** /**
* *
* @export * @export
@ -2256,6 +2424,12 @@ export interface Task {
* @memberof Task * @memberof Task
*/ */
'transcriptionFinishedDate'?: string; 'transcriptionFinishedDate'?: string;
/**
* NotApplicable / InProgress / Success / Failure / Abort
* @type {string}
* @memberof Task
*/
'autoTranscriptionStatus': string;
} }
/** /**
* *
@ -2622,6 +2796,12 @@ export interface UpdateWorkflowRequest {
* @memberof UpdateWorkflowRequest * @memberof UpdateWorkflowRequest
*/ */
'worktypeId'?: number; 'worktypeId'?: number;
/**
*
* @type {boolean}
* @memberof UpdateWorkflowRequest
*/
'isAutoTranscription': boolean;
/** /**
* ID * ID
* @type {number} * @type {number}
@ -2744,6 +2924,12 @@ export interface User {
* @memberof User * @memberof User
*/ */
'licenseStatus': string; 'licenseStatus': string;
/**
* TRIAL/NORMAL/CARD/SPEECH_RECOGNITION/SPEECH_RECOGNITION_UPGRADE
* @type {string}
* @memberof User
*/
'licenseType'?: string;
} }
/** /**
* *
@ -2763,6 +2949,12 @@ export interface Workflow {
* @memberof Workflow * @memberof Workflow
*/ */
'author': Author; 'author': Author;
/**
*
* @type {boolean}
* @memberof Workflow
*/
'isAutoTranscription': boolean;
/** /**
* *
* @type {WorkflowWorktype} * @type {WorkflowWorktype}
@ -6117,13 +6309,56 @@ export const FilesApiAxiosParamCreator = function (configuration?: Configuration
}; };
}, },
/** /**
* *
* @summary * @param {string} authorId Author Id
* @param {AudioUploadFinishedRequest} audioUploadFinishedRequest * @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
uploadFinished: async (audioUploadFinishedRequest: AudioUploadFinishedRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { filesControllerGetAuthorLicense: async (authorId: string, xApiVersion?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'authorId' is not null or undefined
assertParamExists('filesControllerGetAuthorLicense', 'authorId', authorId)
const localVarPath = `/files/author-license/{authorId}`
.replace(`{${"authorId"}}`, encodeURIComponent(String(authorId)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (xApiVersion != null) {
localVarHeaderParameter['x-api-version'] = String(xApiVersion);
}
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary
* @param {AudioUploadFinishedRequest} audioUploadFinishedRequest
* @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
uploadFinished: async (audioUploadFinishedRequest: AudioUploadFinishedRequest, xApiVersion?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'audioUploadFinishedRequest' is not null or undefined // verify required parameter 'audioUploadFinishedRequest' is not null or undefined
assertParamExists('uploadFinished', 'audioUploadFinishedRequest', audioUploadFinishedRequest) assertParamExists('uploadFinished', 'audioUploadFinishedRequest', audioUploadFinishedRequest)
const localVarPath = `/files/audio/upload-finished`; const localVarPath = `/files/audio/upload-finished`;
@ -6142,6 +6377,10 @@ export const FilesApiAxiosParamCreator = function (configuration?: Configuration
// http bearer authentication required // http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration) await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (xApiVersion != null) {
localVarHeaderParameter['x-api-version'] = String(xApiVersion);
}
localVarHeaderParameter['Content-Type'] = 'application/json'; localVarHeaderParameter['Content-Type'] = 'application/json';
@ -6314,14 +6553,28 @@ export const FilesApiFp = function(configuration?: Configuration) {
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
}, },
/** /**
* *
* @summary * @param {string} authorId Author Id
* @param {AudioUploadFinishedRequest} audioUploadFinishedRequest * @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async uploadFinished(audioUploadFinishedRequest: AudioUploadFinishedRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AudioUploadFinishedResponse>> { async filesControllerGetAuthorLicense(authorId: string, xApiVersion?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<GetAuthorLicenseResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFinished(audioUploadFinishedRequest, options); const localVarAxiosArgs = await localVarAxiosParamCreator.filesControllerGetAuthorLicense(authorId, xApiVersion, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['FilesApi.filesControllerGetAuthorLicense']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
},
/**
*
* @summary
* @param {AudioUploadFinishedRequest} audioUploadFinishedRequest
* @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async uploadFinished(audioUploadFinishedRequest: AudioUploadFinishedRequest, xApiVersion?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AudioUploadFinishedResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFinished(audioUploadFinishedRequest, xApiVersion, options);
const index = configuration?.serverIndex ?? 0; const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['FilesApi.uploadFinished']?.[index]?.url; const operationBasePath = operationServerMap['FilesApi.uploadFinished']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
@ -6404,14 +6657,25 @@ export const FilesApiFactory = function (configuration?: Configuration, basePath
return localVarFp.fileRename(fileRenameRequest, options).then((request) => request(axios, basePath)); return localVarFp.fileRename(fileRenameRequest, options).then((request) => request(axios, basePath));
}, },
/** /**
* *
* @summary * @param {string} authorId Author Id
* @param {AudioUploadFinishedRequest} audioUploadFinishedRequest * @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
uploadFinished(audioUploadFinishedRequest: AudioUploadFinishedRequest, options?: any): AxiosPromise<AudioUploadFinishedResponse> { filesControllerGetAuthorLicense(authorId: string, xApiVersion?: string, options?: any): AxiosPromise<GetAuthorLicenseResponse> {
return localVarFp.uploadFinished(audioUploadFinishedRequest, options).then((request) => request(axios, basePath)); return localVarFp.filesControllerGetAuthorLicense(authorId, xApiVersion, options).then((request) => request(axios, basePath));
},
/**
*
* @summary
* @param {AudioUploadFinishedRequest} audioUploadFinishedRequest
* @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
uploadFinished(audioUploadFinishedRequest: AudioUploadFinishedRequest, xApiVersion?: string, options?: any): AxiosPromise<AudioUploadFinishedResponse> {
return localVarFp.uploadFinished(audioUploadFinishedRequest, xApiVersion, options).then((request) => request(axios, basePath));
}, },
/** /**
* Blob Storage上の音声ファイルのアップロード先アクセスURLを取得します * Blob Storage上の音声ファイルのアップロード先アクセスURLを取得します
@ -6488,15 +6752,28 @@ export class FilesApi extends BaseAPI {
} }
/** /**
* *
* @summary * @param {string} authorId Author Id
* @param {AudioUploadFinishedRequest} audioUploadFinishedRequest * @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
* @memberof FilesApi * @memberof FilesApi
*/ */
public uploadFinished(audioUploadFinishedRequest: AudioUploadFinishedRequest, options?: AxiosRequestConfig) { public filesControllerGetAuthorLicense(authorId: string, xApiVersion?: string, options?: AxiosRequestConfig) {
return FilesApiFp(this.configuration).uploadFinished(audioUploadFinishedRequest, options).then((request) => request(this.axios, this.basePath)); return FilesApiFp(this.configuration).filesControllerGetAuthorLicense(authorId, xApiVersion, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary
* @param {AudioUploadFinishedRequest} audioUploadFinishedRequest
* @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof FilesApi
*/
public uploadFinished(audioUploadFinishedRequest: AudioUploadFinishedRequest, xApiVersion?: string, options?: AxiosRequestConfig) {
return FilesApiFp(this.configuration).uploadFinished(audioUploadFinishedRequest, xApiVersion, options).then((request) => request(this.axios, this.basePath));
} }
/** /**
@ -6665,10 +6942,13 @@ export const LicensesApiAxiosParamCreator = function (configuration?: Configurat
/** /**
* *
* @summary * @summary
* @param {number} userId ID
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getAllocatableLicenses: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => { getAllocatableLicenses: async (userId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'userId' is not null or undefined
assertParamExists('getAllocatableLicenses', 'userId', userId)
const localVarPath = `/licenses/allocatable`; const localVarPath = `/licenses/allocatable`;
// use dummy base URL string because the URL constructor only accepts absolute URLs. // use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -6685,6 +6965,10 @@ export const LicensesApiAxiosParamCreator = function (configuration?: Configurat
// http bearer authentication required // http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration) await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (userId !== undefined) {
localVarQueryParameter['userId'] = userId;
}
setSearchParams(localVarUrlObj, localVarQueryParameter); setSearchParams(localVarUrlObj, localVarQueryParameter);
@ -6828,11 +7112,12 @@ export const LicensesApiFp = function(configuration?: Configuration) {
/** /**
* *
* @summary * @summary
* @param {number} userId ID
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async getAllocatableLicenses(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<GetAllocatableLicensesResponse>> { async getAllocatableLicenses(userId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<GetAllocatableLicensesResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllocatableLicenses(options); const localVarAxiosArgs = await localVarAxiosParamCreator.getAllocatableLicenses(userId, options);
const index = configuration?.serverIndex ?? 0; const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['LicensesApi.getAllocatableLicenses']?.[index]?.url; const operationBasePath = operationServerMap['LicensesApi.getAllocatableLicenses']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
@ -6906,11 +7191,12 @@ export const LicensesApiFactory = function (configuration?: Configuration, baseP
/** /**
* *
* @summary * @summary
* @param {number} userId ID
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getAllocatableLicenses(options?: any): AxiosPromise<GetAllocatableLicensesResponse> { getAllocatableLicenses(userId: number, options?: any): AxiosPromise<GetAllocatableLicensesResponse> {
return localVarFp.getAllocatableLicenses(options).then((request) => request(axios, basePath)); return localVarFp.getAllocatableLicenses(userId, options).then((request) => request(axios, basePath));
}, },
/** /**
* *
@ -6981,12 +7267,13 @@ export class LicensesApi extends BaseAPI {
/** /**
* *
* @summary * @summary
* @param {number} userId ID
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
* @memberof LicensesApi * @memberof LicensesApi
*/ */
public getAllocatableLicenses(options?: AxiosRequestConfig) { public getAllocatableLicenses(userId: number, options?: AxiosRequestConfig) {
return LicensesApiFp(this.configuration).getAllocatableLicenses(options).then((request) => request(this.axios, this.basePath)); return LicensesApiFp(this.configuration).getAllocatableLicenses(userId, options).then((request) => request(this.axios, this.basePath));
} }
/** /**
@ -7136,6 +7423,96 @@ export class NotificationApi extends BaseAPI {
*/ */
export const TasksApiAxiosParamCreator = function (configuration?: Configuration) { export const TasksApiAxiosParamCreator = function (configuration?: Configuration) {
return { return {
/**
* APIAPI
* @summary
* @param {AutoTranscribeCompleteRequest} autoTranscribeCompleteRequest
* @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
autoTranscribeComplete: async (autoTranscribeCompleteRequest: AutoTranscribeCompleteRequest, xApiVersion?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'autoTranscribeCompleteRequest' is not null or undefined
assertParamExists('autoTranscribeComplete', 'autoTranscribeCompleteRequest', autoTranscribeCompleteRequest)
const localVarPath = `/tasks/auto-transcribe-complete`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (xApiVersion != null) {
localVarHeaderParameter['x-api-version'] = String(xApiVersion);
}
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(autoTranscribeCompleteRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
* Solに自動文字起こし要求を行う
* @summary
* @param {RequestAutoTranscriptionRequest} requestAutoTranscriptionRequest
* @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
autoTranscribeRequest: async (requestAutoTranscriptionRequest: RequestAutoTranscriptionRequest, xApiVersion?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'requestAutoTranscriptionRequest' is not null or undefined
assertParamExists('autoTranscribeRequest', 'requestAutoTranscriptionRequest', requestAutoTranscriptionRequest)
const localVarPath = `/tasks/auto-transcribe`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (xApiVersion != null) {
localVarHeaderParameter['x-api-version'] = String(xApiVersion);
}
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(requestAutoTranscriptionRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/** /**
* Backupにします * Backupにします
* @summary * @summary
@ -7203,6 +7580,49 @@ export const TasksApiAxiosParamCreator = function (configuration?: Configuration
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary
* @param {number} audioFileId ODMS Cloud上の音声ファイルID
* @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
cancelAutoTranscribe: async (audioFileId: number, xApiVersion?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'audioFileId' is not null or undefined
assertParamExists('cancelAutoTranscribe', 'audioFileId', audioFileId)
const localVarPath = `/tasks/{audioFileId}/auto-transcribe/cancel`
.replace(`{${"audioFileId"}}`, encodeURIComponent(String(audioFileId)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (xApiVersion != null) {
localVarHeaderParameter['x-api-version'] = String(xApiVersion);
}
setSearchParams(localVarUrlObj, localVarQueryParameter); setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@ -7332,6 +7752,51 @@ export const TasksApiAxiosParamCreator = function (configuration?: Configuration
options: localVarRequestOptions, options: localVarRequestOptions,
}; };
}, },
/**
* WAV形式BlobStorageに変換後のファイルを格納
* @summary
* @param {ConvertAudioFileRequest} convertAudioFileRequest
* @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
convertAudioFile: async (convertAudioFileRequest: ConvertAudioFileRequest, xApiVersion?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'convertAudioFileRequest' is not null or undefined
assertParamExists('convertAudioFile', 'convertAudioFileRequest', convertAudioFileRequest)
const localVarPath = `/tasks/convert-audio-file`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (xApiVersion != null) {
localVarHeaderParameter['x-api-version'] = String(xApiVersion);
}
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(convertAudioFileRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/** /**
* *
* @summary * @summary
@ -7566,6 +8031,34 @@ export const TasksApiAxiosParamCreator = function (configuration?: Configuration
export const TasksApiFp = function(configuration?: Configuration) { export const TasksApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = TasksApiAxiosParamCreator(configuration) const localVarAxiosParamCreator = TasksApiAxiosParamCreator(configuration)
return { return {
/**
* APIAPI
* @summary
* @param {AutoTranscribeCompleteRequest} autoTranscribeCompleteRequest
* @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async autoTranscribeComplete(autoTranscribeCompleteRequest: AutoTranscribeCompleteRequest, xApiVersion?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.autoTranscribeComplete(autoTranscribeCompleteRequest, xApiVersion, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['TasksApi.autoTranscribeComplete']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
},
/**
* Solに自動文字起こし要求を行う
* @summary
* @param {RequestAutoTranscriptionRequest} requestAutoTranscriptionRequest
* @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async autoTranscribeRequest(requestAutoTranscriptionRequest: RequestAutoTranscriptionRequest, xApiVersion?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.autoTranscribeRequest(requestAutoTranscriptionRequest, xApiVersion, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['TasksApi.autoTranscribeRequest']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
},
/** /**
* Backupにします * Backupにします
* @summary * @summary
@ -7592,6 +8085,20 @@ export const TasksApiFp = function(configuration?: Configuration) {
const operationBasePath = operationServerMap['TasksApi.cancel']?.[index]?.url; const operationBasePath = operationServerMap['TasksApi.cancel']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
}, },
/**
*
* @summary
* @param {number} audioFileId ODMS Cloud上の音声ファイルID
* @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async cancelAutoTranscribe(audioFileId: number, xApiVersion?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.cancelAutoTranscribe(audioFileId, xApiVersion, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['TasksApi.cancelAutoTranscribe']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
},
/** /**
* *
* @summary * @summary
@ -7632,6 +8139,20 @@ export const TasksApiFp = function(configuration?: Configuration) {
const operationBasePath = operationServerMap['TasksApi.checkout']?.[index]?.url; const operationBasePath = operationServerMap['TasksApi.checkout']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
}, },
/**
* WAV形式BlobStorageに変換後のファイルを格納
* @summary
* @param {ConvertAudioFileRequest} convertAudioFileRequest
* @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async convertAudioFile(convertAudioFileRequest: ConvertAudioFileRequest, xApiVersion?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ConvertAudioFileResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.convertAudioFile(convertAudioFileRequest, xApiVersion, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['TasksApi.convertAudioFile']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
},
/** /**
* *
* @summary * @summary
@ -7713,6 +8234,28 @@ export const TasksApiFp = function(configuration?: Configuration) {
export const TasksApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { export const TasksApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = TasksApiFp(configuration) const localVarFp = TasksApiFp(configuration)
return { return {
/**
* APIAPI
* @summary
* @param {AutoTranscribeCompleteRequest} autoTranscribeCompleteRequest
* @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
autoTranscribeComplete(autoTranscribeCompleteRequest: AutoTranscribeCompleteRequest, xApiVersion?: string, options?: any): AxiosPromise<object> {
return localVarFp.autoTranscribeComplete(autoTranscribeCompleteRequest, xApiVersion, options).then((request) => request(axios, basePath));
},
/**
* Solに自動文字起こし要求を行う
* @summary
* @param {RequestAutoTranscriptionRequest} requestAutoTranscriptionRequest
* @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
autoTranscribeRequest(requestAutoTranscriptionRequest: RequestAutoTranscriptionRequest, xApiVersion?: string, options?: any): AxiosPromise<object> {
return localVarFp.autoTranscribeRequest(requestAutoTranscriptionRequest, xApiVersion, options).then((request) => request(axios, basePath));
},
/** /**
* Backupにします * Backupにします
* @summary * @summary
@ -7733,6 +8276,17 @@ export const TasksApiFactory = function (configuration?: Configuration, basePath
cancel(audioFileId: number, options?: any): AxiosPromise<object> { cancel(audioFileId: number, options?: any): AxiosPromise<object> {
return localVarFp.cancel(audioFileId, options).then((request) => request(axios, basePath)); return localVarFp.cancel(audioFileId, options).then((request) => request(axios, basePath));
}, },
/**
*
* @summary
* @param {number} audioFileId ODMS Cloud上の音声ファイルID
* @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
cancelAutoTranscribe(audioFileId: number, xApiVersion?: string, options?: any): AxiosPromise<object> {
return localVarFp.cancelAutoTranscribe(audioFileId, xApiVersion, options).then((request) => request(axios, basePath));
},
/** /**
* *
* @summary * @summary
@ -7764,6 +8318,17 @@ export const TasksApiFactory = function (configuration?: Configuration, basePath
checkout(audioFileId: number, options?: any): AxiosPromise<object> { checkout(audioFileId: number, options?: any): AxiosPromise<object> {
return localVarFp.checkout(audioFileId, options).then((request) => request(axios, basePath)); return localVarFp.checkout(audioFileId, options).then((request) => request(axios, basePath));
}, },
/**
* WAV形式BlobStorageに変換後のファイルを格納
* @summary
* @param {ConvertAudioFileRequest} convertAudioFileRequest
* @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
convertAudioFile(convertAudioFileRequest: ConvertAudioFileRequest, xApiVersion?: string, options?: any): AxiosPromise<ConvertAudioFileResponse> {
return localVarFp.convertAudioFile(convertAudioFileRequest, xApiVersion, options).then((request) => request(axios, basePath));
},
/** /**
* *
* @summary * @summary
@ -7830,6 +8395,32 @@ export const TasksApiFactory = function (configuration?: Configuration, basePath
* @extends {BaseAPI} * @extends {BaseAPI}
*/ */
export class TasksApi extends BaseAPI { export class TasksApi extends BaseAPI {
/**
* APIAPI
* @summary
* @param {AutoTranscribeCompleteRequest} autoTranscribeCompleteRequest
* @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof TasksApi
*/
public autoTranscribeComplete(autoTranscribeCompleteRequest: AutoTranscribeCompleteRequest, xApiVersion?: string, options?: AxiosRequestConfig) {
return TasksApiFp(this.configuration).autoTranscribeComplete(autoTranscribeCompleteRequest, xApiVersion, options).then((request) => request(this.axios, this.basePath));
}
/**
* Solに自動文字起こし要求を行う
* @summary
* @param {RequestAutoTranscriptionRequest} requestAutoTranscriptionRequest
* @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof TasksApi
*/
public autoTranscribeRequest(requestAutoTranscriptionRequest: RequestAutoTranscriptionRequest, xApiVersion?: string, options?: AxiosRequestConfig) {
return TasksApiFp(this.configuration).autoTranscribeRequest(requestAutoTranscriptionRequest, xApiVersion, options).then((request) => request(this.axios, this.basePath));
}
/** /**
* Backupにします * Backupにします
* @summary * @summary
@ -7854,6 +8445,19 @@ export class TasksApi extends BaseAPI {
return TasksApiFp(this.configuration).cancel(audioFileId, options).then((request) => request(this.axios, this.basePath)); return TasksApiFp(this.configuration).cancel(audioFileId, options).then((request) => request(this.axios, this.basePath));
} }
/**
*
* @summary
* @param {number} audioFileId ODMS Cloud上の音声ファイルID
* @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof TasksApi
*/
public cancelAutoTranscribe(audioFileId: number, xApiVersion?: string, options?: AxiosRequestConfig) {
return TasksApiFp(this.configuration).cancelAutoTranscribe(audioFileId, xApiVersion, options).then((request) => request(this.axios, this.basePath));
}
/** /**
* *
* @summary * @summary
@ -7891,6 +8495,19 @@ export class TasksApi extends BaseAPI {
return TasksApiFp(this.configuration).checkout(audioFileId, options).then((request) => request(this.axios, this.basePath)); return TasksApiFp(this.configuration).checkout(audioFileId, options).then((request) => request(this.axios, this.basePath));
} }
/**
* WAV形式BlobStorageに変換後のファイルを格納
* @summary
* @param {ConvertAudioFileRequest} convertAudioFileRequest
* @param {string} [xApiVersion] APIバージョン
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof TasksApi
*/
public convertAudioFile(convertAudioFileRequest: ConvertAudioFileRequest, xApiVersion?: string, options?: AxiosRequestConfig) {
return TasksApiFp(this.configuration).convertAudioFile(convertAudioFileRequest, xApiVersion, options).then((request) => request(this.axios, this.basePath));
}
/** /**
* *
* @summary * @summary

View File

@ -6,6 +6,14 @@ export const STATUS = {
BACKUP: "Backup", BACKUP: "Backup",
} as const; } as const;
export const AUTO_TRANSCRIPTION_STATUS = {
NOTAPPLICABLE: "NotApplicable",
INPROGRESS: "InProgress",
SUCCESS: "Success",
FAILURE: "Failure",
ABORT: "Abort",
} as const;
export type StatusType = (typeof STATUS)[keyof typeof STATUS]; export type StatusType = (typeof STATUS)[keyof typeof STATUS];
export const LIMIT_TASK_NUM = 100; export const LIMIT_TASK_NUM = 100;
@ -64,6 +72,7 @@ export interface DisplayInfoType {
UploadDate: boolean; UploadDate: boolean;
TranscriptionStartedDate: boolean; TranscriptionStartedDate: boolean;
TranscriptionFinishedDate: boolean; TranscriptionFinishedDate: boolean;
AutoTranscriptionStatus: boolean;
Transcriptionist: boolean; Transcriptionist: boolean;
Comment: boolean; Comment: boolean;
OptionItem1: boolean; OptionItem1: boolean;
@ -93,6 +102,7 @@ export const INIT_DISPLAY_INFO: DisplayInfoType = {
UploadDate: false, UploadDate: false,
TranscriptionStartedDate: true, TranscriptionStartedDate: true,
TranscriptionFinishedDate: true, TranscriptionFinishedDate: true,
AutoTranscriptionStatus: true,
Transcriptionist: true, Transcriptionist: true,
Comment: true, Comment: true,
OptionItem1: false, OptionItem1: false,
@ -115,3 +125,19 @@ export const PRIORITY = {
NORMAL: "Normal", NORMAL: "Normal",
HIGH: "High", HIGH: "High",
} as const; } as const;
// 言語JSONのキーマッピング
export const AUTO_TRANSCRIPTION_STATUS_LANGUAGE_MAP: Record<
string,
| "dictationPage.label.autoTranscriptionStatusNotApplicable"
| "dictationPage.label.autoTranscriptionStatusInProgress"
| "dictationPage.label.autoTranscriptionStatusSuccess"
| "dictationPage.label.autoTranscriptionStatusFailure"
| "dictationPage.label.autoTranscriptionStatusAbort"
> = {
NotApplicable: "dictationPage.label.autoTranscriptionStatusNotApplicable",
InProgress: "dictationPage.label.autoTranscriptionStatusInProgress",
Success: "dictationPage.label.autoTranscriptionStatusSuccess",
Failure: "dictationPage.label.autoTranscriptionStatusFailure",
Abort: "dictationPage.label.autoTranscriptionStatusAbort",
};

View File

@ -13,6 +13,7 @@ import {
cancelAsync, cancelAsync,
deleteTaskAsync, deleteTaskAsync,
renameFileAsync, renameFileAsync,
cancelSpeechRecognitionAsync,
} from "./operations"; } from "./operations";
import { import {
SORTABLE_COLUMN, SORTABLE_COLUMN,
@ -239,7 +240,6 @@ export const dictationSlice = createSlice({
builder.addCase(deleteTaskAsync.rejected, (state) => { builder.addCase(deleteTaskAsync.rejected, (state) => {
state.apps.isLoading = false; state.apps.isLoading = false;
}); });
builder.addCase(renameFileAsync.pending, (state) => { builder.addCase(renameFileAsync.pending, (state) => {
state.apps.isLoading = true; state.apps.isLoading = true;
}); });
@ -249,6 +249,15 @@ export const dictationSlice = createSlice({
builder.addCase(renameFileAsync.rejected, (state) => { builder.addCase(renameFileAsync.rejected, (state) => {
state.apps.isLoading = false; state.apps.isLoading = false;
}); });
builder.addCase(cancelSpeechRecognitionAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(cancelSpeechRecognitionAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(cancelSpeechRecognitionAsync.rejected, (state) => {
state.apps.isLoading = false;
});
}, },
}); });

View File

@ -1014,3 +1014,69 @@ export const renameFileAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error }); return thunkApi.rejectWithValue({ error });
} }
}); });
export const cancelSpeechRecognitionAsync = createAsyncThunk<
{
/** empty */
},
{
audioFileId: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/cancelSpeechRecognitionAsync", async (args, thunkApi) => {
const { audioFileId } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const tasksApi = new TasksApi(config);
try {
await tasksApi.cancelAutoTranscribe(
audioFileId,
// APIバージョン2を固定で指定
"2",
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
// 自動文字起こしステータスが[InProgress]以外、またはタスクが存在しない場合
if (error.code === "E010601" || error.code === "E010603") {
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID(
"dictationPage.message.cancelSpeechRecognitionError"
),
})
);
return thunkApi.rejectWithValue({ error });
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -0,0 +1,7 @@
export const LICENSE_TYPE = {
TRIAL: "TRIAL",
NORMAL: "NORMAL",
CARD: "CARD",
SPEECH_RECOGNITION: "SPEECH_RECOGNITION",
SPEECH_RECOGNITION_UPGRADE: "SPEECH_RECOGNITION_UPGRADE",
} as const;

View File

@ -1,4 +1,5 @@
export * from "./state"; export * from "./state";
export * from "./constants";
export * from "./operations"; export * from "./operations";
export * from "./selectors"; export * from "./selectors";
export * from "./licenseSlice"; export * from "./licenseSlice";

View File

@ -6,6 +6,7 @@ const initialState: LicenseOrdersState = {
apps: { apps: {
poNumber: "", poNumber: "",
newOrder: 0, newOrder: 0,
licenseType: "",
isLoading: false, isLoading: false,
}, },
}; };
@ -21,6 +22,13 @@ export const licenseSlice = createSlice({
const { newOrder } = action.payload; const { newOrder } = action.payload;
state.apps.newOrder = newOrder; state.apps.newOrder = newOrder;
}, },
changeLicenseType: (
state,
action: PayloadAction<{ licenseType: string }>
) => {
const { licenseType } = action.payload;
state.apps.licenseType = licenseType;
},
cleanupApps: (state) => { cleanupApps: (state) => {
state.apps = initialState.apps; state.apps = initialState.apps;
}, },
@ -38,7 +46,11 @@ export const licenseSlice = createSlice({
}, },
}); });
export const { changePoNumber, changeNewOrder, cleanupApps } = export const {
licenseSlice.actions; changePoNumber,
changeNewOrder,
changeLicenseType,
cleanupApps,
} = licenseSlice.actions;
export default licenseSlice.reducer; export default licenseSlice.reducer;

View File

@ -15,6 +15,7 @@ export const orderLicenseAsync = createAsyncThunk<
// パラメータ // パラメータ
poNumber: string; poNumber: string;
orderCount: number; orderCount: number;
licenseType: string;
}, },
{ {
// rejectした時の返却値の型 // rejectした時の返却値の型
@ -23,7 +24,7 @@ export const orderLicenseAsync = createAsyncThunk<
}; };
} }
>("licenses/orderLicenseAsync", async (args, thunkApi) => { >("licenses/orderLicenseAsync", async (args, thunkApi) => {
const { poNumber, orderCount } = args; const { poNumber, orderCount, licenseType } = args;
// apiのConfigurationを取得する // apiのConfigurationを取得する
const { getState } = thunkApi; const { getState } = thunkApi;
@ -38,6 +39,7 @@ export const orderLicenseAsync = createAsyncThunk<
{ {
poNumber, poNumber,
orderCount, orderCount,
licenseType,
}, },
{ {
headers: { authorization: `Bearer ${accessToken}` }, headers: { authorization: `Bearer ${accessToken}` },

View File

@ -1,7 +1,8 @@
import { RootState } from "app/store"; import { RootState } from "app/store";
import { LICENSE_TYPE } from "./constants";
export const selectInputValidationErrors = (state: RootState) => { export const selectInputValidationErrors = (state: RootState) => {
const { poNumber, newOrder } = state.license.apps; const { poNumber, newOrder, licenseType } = state.license.apps;
// 必須項目のチェック // 必須項目のチェック
const hasErrorEmptyPoNumber = poNumber === ""; const hasErrorEmptyPoNumber = poNumber === "";
@ -9,11 +10,14 @@ export const selectInputValidationErrors = (state: RootState) => {
const hasErrorIncorrectPoNumber = checkErrorIncorrectPoNumber(poNumber); const hasErrorIncorrectPoNumber = checkErrorIncorrectPoNumber(poNumber);
const hasErrorIncorrectNewOrder = checkErrorIncorrectNewOrder(newOrder); const hasErrorIncorrectNewOrder = checkErrorIncorrectNewOrder(newOrder);
const hasErrorIncorrectLicenseType =
checkErrorIncorrectLicenseType(licenseType);
return { return {
hasErrorEmptyPoNumber, hasErrorEmptyPoNumber,
hasErrorIncorrectPoNumber, hasErrorIncorrectPoNumber,
hasErrorIncorrectNewOrder, hasErrorIncorrectNewOrder,
hasErrorIncorrectLicenseType,
}; };
}; };
export const checkErrorIncorrectPoNumber = (poNumber: string): boolean => { export const checkErrorIncorrectPoNumber = (poNumber: string): boolean => {
@ -23,6 +27,7 @@ export const checkErrorIncorrectPoNumber = (poNumber: string): boolean => {
return !charaType; return !charaType;
}; };
export const checkErrorIncorrectNewOrder = (newOrder: number): boolean => { export const checkErrorIncorrectNewOrder = (newOrder: number): boolean => {
// 0以下の場合はエラー // 0以下の場合はエラー
if (newOrder <= 0) { if (newOrder <= 0) {
@ -32,8 +37,25 @@ export const checkErrorIncorrectNewOrder = (newOrder: number): boolean => {
return false; return false;
}; };
export const checkErrorIncorrectLicenseType = (
licenseType: string
): boolean => {
const allowLicenseType: string[] = [
LICENSE_TYPE.NORMAL,
LICENSE_TYPE.SPEECH_RECOGNITION,
LICENSE_TYPE.SPEECH_RECOGNITION_UPGRADE,
];
if (!allowLicenseType.includes(licenseType)) {
return true;
}
return false;
};
export const selectPoNumber = (state: RootState) => state.license.apps.poNumber; export const selectPoNumber = (state: RootState) => state.license.apps.poNumber;
export const selectNewOrder = (state: RootState) => state.license.apps.newOrder; export const selectNewOrder = (state: RootState) => state.license.apps.newOrder;
export const selectLicenseType = (state: RootState) =>
state.license.apps.licenseType;
export const selectIsLoading = (state: RootState) => export const selectIsLoading = (state: RootState) =>
state.license.apps.isLoading; state.license.apps.isLoading;

View File

@ -5,5 +5,6 @@ export interface LicenseOrdersState {
export interface Apps { export interface Apps {
poNumber: string; poNumber: string;
newOrder: number; newOrder: number;
licenseType: string;
isLoading: boolean; isLoading: boolean;
} }

View File

@ -1,4 +1,4 @@
export const LIMIT_ORDER_HISORY_NUM = 50; export const LIMIT_ORDER_HISTORY_NUM = 50;
export const STATUS = { export const STATUS = {
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
@ -12,3 +12,18 @@ export const LICENSE_TYPE = {
NORMAL: "NORMAL", NORMAL: "NORMAL",
TRIAL: "TRIAL", TRIAL: "TRIAL",
} as const; } as const;
// 言語JSONのキーマッピング
export const LICENSE_TYPE_LANGUAGE_MAP: Record<
string,
| "orderHistoriesPage.label.normal"
| "orderHistoriesPage.label.speechRecognition"
| "orderHistoriesPage.label.speechRecognitionUpgrade"
| "orderHistoriesPage.label.trial"
> = {
NORMAL: "orderHistoriesPage.label.normal",
SPEECH_RECOGNITION: "orderHistoriesPage.label.speechRecognition",
SPEECH_RECOGNITION_UPGRADE:
"orderHistoriesPage.label.speechRecognitionUpgrade",
TRIAL: "orderHistoriesPage.label.trial",
};

View File

@ -6,7 +6,7 @@ import {
cancelOrderAsync, cancelOrderAsync,
cancelIssueAsync, cancelIssueAsync,
} from "./operations"; } from "./operations";
import { LIMIT_ORDER_HISORY_NUM } from "./constants"; import { LIMIT_ORDER_HISTORY_NUM } from "./constants";
const initialState: LicenseOrderHistoryState = { const initialState: LicenseOrderHistoryState = {
domain: { domain: {
@ -15,7 +15,7 @@ const initialState: LicenseOrderHistoryState = {
companyName: "", companyName: "",
}, },
apps: { apps: {
limit: LIMIT_ORDER_HISORY_NUM, limit: LIMIT_ORDER_HISTORY_NUM,
offset: 0, offset: 0,
isLoading: false, isLoading: false,
LicenseOrder: undefined, LicenseOrder: undefined,

View File

@ -9,14 +9,48 @@ import {
const initialState: LicenseSummaryState = { const initialState: LicenseSummaryState = {
domain: { domain: {
licenseSummaryInfo: { licenseSummaryInfo: {
totalLicense: 0, totalLicense: {
allocatedLicense: 0, normal: 0,
reusableLicense: 0, speechRecognition: 0,
freeLicense: 0, speechRecognitionUpgrade: 0,
expiringWithin14daysLicense: 0, trial: 0,
issueRequesting: 0, },
numberOfRequesting: 0, allocatedLicense: {
shortage: 0, normal: 0,
speechRecognition: 0,
speechRecognitionUpgrade: 0,
trial: 0,
},
reusableLicense: {
normal: 0,
speechRecognition: 0,
speechRecognitionUpgrade: 0,
trial: 0,
},
freeLicense: {
normal: 0,
speechRecognition: 0,
speechRecognitionUpgrade: 0,
trial: 0,
},
expiringWithin14daysLicense: {
normal: 0,
speechRecognition: 0,
speechRecognitionUpgrade: 0,
trial: 0,
},
issueRequesting: {
normal: 0,
speechRecognition: 0,
speechRecognitionUpgrade: 0,
trial: 0,
},
shortage: {
normal: 0,
speechRecognition: 0,
speechRecognitionUpgrade: 0,
trial: 0,
},
storageSize: 0, storageSize: 0,
usedSize: 0, usedSize: 0,
isStorageAvailable: false, isStorageAvailable: false,

View File

@ -1,22 +1,12 @@
import { GetLicenseSummaryResponse } from "../../../api";
export interface LicenseSummaryState { export interface LicenseSummaryState {
domain: Domain; domain: Domain;
apps: Apps; apps: Apps;
} }
export interface Domain { export interface Domain {
licenseSummaryInfo: { licenseSummaryInfo: GetLicenseSummaryResponse;
totalLicense: number;
allocatedLicense: number;
reusableLicense: number;
freeLicense: number;
expiringWithin14daysLicense: number;
issueRequesting: number;
numberOfRequesting: number;
shortage: number;
storageSize: number;
usedSize: number;
isStorageAvailable: boolean;
};
accountInfo: { accountInfo: {
companyName: string; companyName: string;
}; };

View File

@ -1 +1,8 @@
export const ACCOUNTS_VIEW_LIMIT = 10; export const ACCOUNTS_VIEW_LIMIT = 10;
export const LICENSE_TYPE = {
TRIAL: "TRIAL",
NORMAL: "NORMAL",
CARD: "CARD",
SPEECH_RECOGNITION: "SPEECH_RECOGNITION",
SPEECH_RECOGNITION_UPGRADE: "SPEECH_RECOGNITION_UPGRADE",
} as const;

Some files were not shown because too many files have changed in this diff Show More