|
|
@@ -6,16 +6,15 @@ import {
|
|
|
BlobClient,
|
|
|
BlockBlobClient,
|
|
|
BlobDeleteOptions,
|
|
|
- BlobDeleteIfExistsResponse,
|
|
|
- BlockBlobUploadResponse,
|
|
|
ContainerClient,
|
|
|
generateBlobSASQueryParameters,
|
|
|
ContainerSASPermissions,
|
|
|
SASProtocol,
|
|
|
- BlockBlobParallelUploadOptions,
|
|
|
- BlockBlobUploadStreamOptions,
|
|
|
+ type BlobDeleteIfExistsResponse,
|
|
|
+ type BlockBlobUploadResponse,
|
|
|
+ type BlockBlobParallelUploadOptions,
|
|
|
+ type BlockBlobUploadStreamOptions,
|
|
|
} from '@azure/storage-blob';
|
|
|
-import type { Response } from 'express';
|
|
|
|
|
|
import type { RespondOptions } from '~/server/interfaces/attachment';
|
|
|
import type { IAttachmentDocument } from '~/server/models';
|
|
|
@@ -23,7 +22,10 @@ import loggerFactory from '~/utils/logger';
|
|
|
|
|
|
import { configManager } from '../config-manager';
|
|
|
|
|
|
-import { AbstractFileUploader, type SaveFileParam } from './file-uploader';
|
|
|
+import {
|
|
|
+ AbstractFileUploader, ResponseMode, type TemporaryUrl, type SaveFileParam,
|
|
|
+} from './file-uploader';
|
|
|
+import { ContentHeaders } from './utils';
|
|
|
|
|
|
const urljoin = require('url-join');
|
|
|
|
|
|
@@ -39,6 +41,59 @@ type AzureConfig = {
|
|
|
containerName: string,
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+function getAzureConfig(): AzureConfig {
|
|
|
+ return {
|
|
|
+ accountName: configManager.getConfig('crowi', 'azure:storageAccountName'),
|
|
|
+ containerName: configManager.getConfig('crowi', 'azure:storageContainerName'),
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function getCredential(): TokenCredential {
|
|
|
+ const tenantId = configManager.getConfig('crowi', 'azure:tenantId');
|
|
|
+ const clientId = configManager.getConfig('crowi', 'azure:clientId');
|
|
|
+ const clientSecret = configManager.getConfig('crowi', 'azure:clientSecret');
|
|
|
+ return new ClientSecretCredential(tenantId, clientId, clientSecret);
|
|
|
+}
|
|
|
+
|
|
|
+async function getContainerClient(): Promise<ContainerClient> {
|
|
|
+ const { accountName, containerName } = getAzureConfig();
|
|
|
+ const blobServiceClient = new BlobServiceClient(`https://${accountName}.blob.core.windows.net`, getCredential());
|
|
|
+ return blobServiceClient.getContainerClient(containerName);
|
|
|
+}
|
|
|
+
|
|
|
+// Server creates User Delegation SAS Token for container
|
|
|
+// https://learn.microsoft.com/ja-jp/azure/storage/blobs/storage-blob-create-user-delegation-sas-javascript
|
|
|
+async function getSasToken(lifetimeSec) {
|
|
|
+ const { accountName, containerName } = getAzureConfig();
|
|
|
+ const blobServiceClient = new BlobServiceClient(`https://${accountName}.blob.core.windows.net`, getCredential());
|
|
|
+
|
|
|
+ const now = Date.now();
|
|
|
+ const startsOn = new Date(now - 30 * 1000);
|
|
|
+ const expiresOn = new Date(now + lifetimeSec * 1000);
|
|
|
+ const userDelegationKey = await blobServiceClient.getUserDelegationKey(startsOn, expiresOn);
|
|
|
+
|
|
|
+ // https://github.com/Azure/azure-sdk-for-js/blob/d4d55f73/sdk/storage/storage-blob/src/ContainerSASPermissions.ts#L24
|
|
|
+ // r:read, a:add, c:create, w:write, d:delete, l:list
|
|
|
+ const containerPermissionsForAnonymousUser = 'rl';
|
|
|
+ const sasOptions = {
|
|
|
+ containerName,
|
|
|
+ permissions: ContainerSASPermissions.parse(containerPermissionsForAnonymousUser),
|
|
|
+ protocol: SASProtocol.HttpsAndHttp,
|
|
|
+ startsOn,
|
|
|
+ expiresOn,
|
|
|
+ };
|
|
|
+
|
|
|
+ const sasToken = generateBlobSASQueryParameters(sasOptions, userDelegationKey, accountName).toString();
|
|
|
+
|
|
|
+ return sasToken;
|
|
|
+}
|
|
|
+
|
|
|
+function getFilePathOnStorage(attachment) {
|
|
|
+ const dirName = (attachment.page != null) ? 'attachment' : 'user';
|
|
|
+ return urljoin(dirName, attachment.fileName);
|
|
|
+}
|
|
|
+
|
|
|
class AzureFileUploader extends AbstractFileUploader {
|
|
|
|
|
|
/**
|
|
|
@@ -72,97 +127,52 @@ class AzureFileUploader extends AbstractFileUploader {
|
|
|
/**
|
|
|
* @inheritdoc
|
|
|
*/
|
|
|
- override respond(res: Response, attachment: IAttachmentDocument, opts?: RespondOptions): void {
|
|
|
- throw new Error('Method not implemented.');
|
|
|
+ override determineResponseMode() {
|
|
|
+ return configManager.getConfig('crowi', 'azure:referenceFileWithRelayMode')
|
|
|
+ ? ResponseMode.RELAY
|
|
|
+ : ResponseMode.REDIRECT;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @inheritdoc
|
|
|
*/
|
|
|
- override findDeliveryFile(attachment: IAttachmentDocument): Promise<NodeJS.ReadableStream> {
|
|
|
- throw new Error('Method not implemented.');
|
|
|
- }
|
|
|
-
|
|
|
-}
|
|
|
-
|
|
|
-module.exports = (crowi) => {
|
|
|
- const lib = new AzureFileUploader(crowi);
|
|
|
-
|
|
|
- function getAzureConfig(): AzureConfig {
|
|
|
- return {
|
|
|
- accountName: configManager.getConfig('crowi', 'azure:storageAccountName'),
|
|
|
- containerName: configManager.getConfig('crowi', 'azure:storageContainerName'),
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- function getCredential(): TokenCredential {
|
|
|
- const tenantId = configManager.getConfig('crowi', 'azure:tenantId');
|
|
|
- const clientId = configManager.getConfig('crowi', 'azure:clientId');
|
|
|
- const clientSecret = configManager.getConfig('crowi', 'azure:clientSecret');
|
|
|
- return new ClientSecretCredential(tenantId, clientId, clientSecret);
|
|
|
- }
|
|
|
-
|
|
|
- async function getContainerClient(): Promise<ContainerClient> {
|
|
|
- const { accountName, containerName } = getAzureConfig();
|
|
|
- const blobServiceClient = new BlobServiceClient(`https://${accountName}.blob.core.windows.net`, getCredential());
|
|
|
- return blobServiceClient.getContainerClient(containerName);
|
|
|
+ override respond(): void {
|
|
|
+ throw new Error('AzureFileUploader does not support ResponseMode.DELEGATE.');
|
|
|
}
|
|
|
|
|
|
- // Server creates User Delegation SAS Token for container
|
|
|
- // https://learn.microsoft.com/ja-jp/azure/storage/blobs/storage-blob-create-user-delegation-sas-javascript
|
|
|
- async function getSasToken(lifetimeSec) {
|
|
|
- const { accountName, containerName } = getAzureConfig();
|
|
|
- const blobServiceClient = new BlobServiceClient(`https://${accountName}.blob.core.windows.net`, getCredential());
|
|
|
-
|
|
|
- const now = Date.now();
|
|
|
- const startsOn = new Date(now - 30 * 1000);
|
|
|
- const expiresOn = new Date(now + lifetimeSec * 1000);
|
|
|
- const userDelegationKey = await blobServiceClient.getUserDelegationKey(startsOn, expiresOn);
|
|
|
-
|
|
|
- // https://github.com/Azure/azure-sdk-for-js/blob/d4d55f73/sdk/storage/storage-blob/src/ContainerSASPermissions.ts#L24
|
|
|
- // r:read, a:add, c:create, w:write, d:delete, l:list
|
|
|
- const containerPermissionsForAnonymousUser = 'rl';
|
|
|
- const sasOptions = {
|
|
|
- containerName,
|
|
|
- permissions: ContainerSASPermissions.parse(containerPermissionsForAnonymousUser),
|
|
|
- protocol: SASProtocol.HttpsAndHttp,
|
|
|
- startsOn,
|
|
|
- expiresOn,
|
|
|
- };
|
|
|
-
|
|
|
- const sasToken = generateBlobSASQueryParameters(sasOptions, userDelegationKey, accountName).toString();
|
|
|
+ /**
|
|
|
+ * @inheritdoc
|
|
|
+ */
|
|
|
+ override async findDeliveryFile(attachment: IAttachmentDocument): Promise<NodeJS.ReadableStream> {
|
|
|
+ if (!this.getIsReadable()) {
|
|
|
+ throw new Error('Azure is not configured.');
|
|
|
+ }
|
|
|
|
|
|
- return sasToken;
|
|
|
- }
|
|
|
+ const filePath = getFilePathOnStorage(attachment);
|
|
|
+ const containerClient = await getContainerClient();
|
|
|
+ const blobClient: BlobClient = containerClient.getBlobClient(filePath);
|
|
|
+ const downloadResponse = await blobClient.download();
|
|
|
+ if (downloadResponse.errorCode) {
|
|
|
+ logger.error(downloadResponse.errorCode);
|
|
|
+ throw new Error(downloadResponse.errorCode);
|
|
|
+ }
|
|
|
+ if (!downloadResponse?.readableStreamBody) {
|
|
|
+ throw new Error(`Coudn't get file from Azure for the Attachment (${filePath})`);
|
|
|
+ }
|
|
|
|
|
|
- function getFilePathOnStorage(attachment) {
|
|
|
- const dirName = (attachment.page != null) ? 'attachment' : 'user';
|
|
|
- return urljoin(dirName, attachment.fileName);
|
|
|
+ return downloadResponse.readableStreamBody;
|
|
|
}
|
|
|
|
|
|
- lib.isValidUploadSettings = function() {
|
|
|
- return configManager.getConfig('crowi', 'azure:storageAccountName') != null
|
|
|
- && configManager.getConfig('crowi', 'azure:storageContainerName') != null;
|
|
|
- };
|
|
|
-
|
|
|
- lib.shouldDelegateToResponse = function() {
|
|
|
- return !configManager.getConfig('crowi', 'azure:referenceFileWithRelayMode');
|
|
|
- };
|
|
|
-
|
|
|
- lib.respond = async function(res, attachment, opts) {
|
|
|
- if (!lib.getIsUploadable()) {
|
|
|
+ /**
|
|
|
+ * @inheritDoc
|
|
|
+ */
|
|
|
+ override async generateTemporaryUrl(attachment: IAttachmentDocument, opts?: RespondOptions): Promise<TemporaryUrl> {
|
|
|
+ if (!this.getIsUploadable()) {
|
|
|
throw new Error('Azure Blob is not configured.');
|
|
|
}
|
|
|
|
|
|
const isDownload = opts?.download ?? false;
|
|
|
|
|
|
- if (!isDownload) {
|
|
|
- const temporaryUrl = attachment.getValidTemporaryUrl();
|
|
|
- if (temporaryUrl != null) {
|
|
|
- return res.redirect(temporaryUrl);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
const containerClient = await getContainerClient();
|
|
|
const filePath = getFilePathOnStorage(attachment);
|
|
|
const blockBlobClient = await containerClient.getBlockBlobClient(filePath);
|
|
|
@@ -172,21 +182,27 @@ module.exports = (crowi) => {
|
|
|
const signedUrl = `${blockBlobClient.url}?${sasToken}`;
|
|
|
|
|
|
// TODO: re-impl using generateSasUrl
|
|
|
- // const contentHeaders = new ContentHeaders(attachment, { inline: true });
|
|
|
+ const contentHeaders = new ContentHeaders(attachment, { inline: !isDownload });
|
|
|
// const signedUrl = blockBlobClient.generateSasUrl({
|
|
|
// contentType: contentHeaders.contentType?.value.toString(),
|
|
|
// contentDisposition: contentHeaders.contentDisposition?.value.toString(),
|
|
|
// });
|
|
|
|
|
|
- res.redirect(signedUrl);
|
|
|
+ return {
|
|
|
+ url: signedUrl,
|
|
|
+ lifetimeSec: lifetimeSecForTemporaryUrl,
|
|
|
+ };
|
|
|
|
|
|
- try {
|
|
|
- return attachment.cashTemporaryUrlByProvideSec(signedUrl, lifetimeSecForTemporaryUrl);
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- logger.error(err);
|
|
|
- }
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+module.exports = (crowi) => {
|
|
|
+ const lib = new AzureFileUploader(crowi);
|
|
|
|
|
|
+ lib.isValidUploadSettings = function() {
|
|
|
+ return configManager.getConfig('crowi', 'azure:storageAccountName') != null
|
|
|
+ && configManager.getConfig('crowi', 'azure:storageContainerName') != null;
|
|
|
};
|
|
|
|
|
|
(lib as any).deleteFile = async function(attachment) {
|
|
|
@@ -243,27 +259,6 @@ module.exports = (crowi) => {
|
|
|
return;
|
|
|
};
|
|
|
|
|
|
- lib.findDeliveryFile = async function(attachment) {
|
|
|
- if (!lib.getIsReadable()) {
|
|
|
- throw new Error('Azure is not configured.');
|
|
|
- }
|
|
|
-
|
|
|
- const filePath = getFilePathOnStorage(attachment);
|
|
|
- const containerClient = await getContainerClient();
|
|
|
- const blobClient: BlobClient = containerClient.getBlobClient(filePath);
|
|
|
- const downloadResponse = await blobClient.download();
|
|
|
- if (downloadResponse.errorCode) {
|
|
|
- logger.error(downloadResponse.errorCode);
|
|
|
- throw new Error(downloadResponse.errorCode);
|
|
|
- }
|
|
|
- if (!downloadResponse?.readableStreamBody) {
|
|
|
- throw new Error(`Coudn't get file from Azure for the Attachment (${filePath})`);
|
|
|
- }
|
|
|
-
|
|
|
- return downloadResponse.readableStreamBody;
|
|
|
- };
|
|
|
-
|
|
|
-
|
|
|
(lib as any).checkLimit = async function(uploadFileSize) {
|
|
|
const maxFileSize = configManager.getConfig('crowi', 'app:maxFileSize');
|
|
|
const gcsTotalLimit = configManager.getConfig('crowi', 'app:fileUploadTotalLimit');
|