|
@@ -1,19 +1,16 @@
|
|
|
-import path from 'path';
|
|
|
|
|
-
|
|
|
|
|
import { ClientSecretCredential, TokenCredential } from '@azure/identity';
|
|
import { ClientSecretCredential, TokenCredential } from '@azure/identity';
|
|
|
import {
|
|
import {
|
|
|
|
|
+ generateBlobSASQueryParameters,
|
|
|
BlobServiceClient,
|
|
BlobServiceClient,
|
|
|
BlobClient,
|
|
BlobClient,
|
|
|
BlockBlobClient,
|
|
BlockBlobClient,
|
|
|
BlobDeleteOptions,
|
|
BlobDeleteOptions,
|
|
|
ContainerClient,
|
|
ContainerClient,
|
|
|
- generateBlobSASQueryParameters,
|
|
|
|
|
ContainerSASPermissions,
|
|
ContainerSASPermissions,
|
|
|
SASProtocol,
|
|
SASProtocol,
|
|
|
type BlobDeleteIfExistsResponse,
|
|
type BlobDeleteIfExistsResponse,
|
|
|
type BlockBlobUploadResponse,
|
|
type BlockBlobUploadResponse,
|
|
|
type BlockBlobParallelUploadOptions,
|
|
type BlockBlobParallelUploadOptions,
|
|
|
- type BlockBlobUploadStreamOptions,
|
|
|
|
|
} from '@azure/storage-blob';
|
|
} from '@azure/storage-blob';
|
|
|
|
|
|
|
|
import { ResponseMode, type RespondOptions } from '~/server/interfaces/attachment';
|
|
import { ResponseMode, type RespondOptions } from '~/server/interfaces/attachment';
|
|
@@ -62,33 +59,6 @@ async function getContainerClient(): Promise<ContainerClient> {
|
|
|
return blobServiceClient.getContainerClient(containerName);
|
|
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) {
|
|
function getFilePathOnStorage(attachment) {
|
|
|
const dirName = (attachment.page != null) ? 'attachment' : 'user';
|
|
const dirName = (attachment.page != null) ? 'attachment' : 'user';
|
|
|
return urljoin(dirName, attachment.fileName);
|
|
return urljoin(dirName, attachment.fileName);
|
|
@@ -165,27 +135,51 @@ class AzureFileUploader extends AbstractFileUploader {
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* @inheritDoc
|
|
* @inheritDoc
|
|
|
|
|
+ * @see https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blob-create-user-delegation-sas-javascript
|
|
|
*/
|
|
*/
|
|
|
override async generateTemporaryUrl(attachment: IAttachmentDocument, opts?: RespondOptions): Promise<TemporaryUrl> {
|
|
override async generateTemporaryUrl(attachment: IAttachmentDocument, opts?: RespondOptions): Promise<TemporaryUrl> {
|
|
|
if (!this.getIsUploadable()) {
|
|
if (!this.getIsUploadable()) {
|
|
|
throw new Error('Azure Blob is not configured.');
|
|
throw new Error('Azure Blob is not configured.');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const containerClient = await getContainerClient();
|
|
|
|
|
- const filePath = getFilePathOnStorage(attachment);
|
|
|
|
|
- const blockBlobClient = await containerClient.getBlockBlobClient(filePath);
|
|
|
|
|
const lifetimeSecForTemporaryUrl = configManager.getConfig('crowi', 'azure:lifetimeSecForTemporaryUrl');
|
|
const lifetimeSecForTemporaryUrl = configManager.getConfig('crowi', 'azure:lifetimeSecForTemporaryUrl');
|
|
|
|
|
|
|
|
- const sasToken = await getSasToken(lifetimeSecForTemporaryUrl);
|
|
|
|
|
- const signedUrl = `${blockBlobClient.url}?${sasToken}`;
|
|
|
|
|
-
|
|
|
|
|
- // TODO: re-impl using generateSasUrl
|
|
|
|
|
- // const isDownload = opts?.download ?? false;
|
|
|
|
|
- // const contentHeaders = new ContentHeaders(attachment, { inline: !isDownload });
|
|
|
|
|
- // const signedUrl = blockBlobClient.generateSasUrl({
|
|
|
|
|
- // contentType: contentHeaders.contentType?.value.toString(),
|
|
|
|
|
- // contentDisposition: contentHeaders.contentDisposition?.value.toString(),
|
|
|
|
|
- // });
|
|
|
|
|
|
|
+ const url = await (async() => {
|
|
|
|
|
+ const containerClient = await getContainerClient();
|
|
|
|
|
+ const filePath = getFilePathOnStorage(attachment);
|
|
|
|
|
+ const blockBlobClient = await containerClient.getBlockBlobClient(filePath);
|
|
|
|
|
+ return blockBlobClient.url;
|
|
|
|
|
+ })();
|
|
|
|
|
+
|
|
|
|
|
+ const sasToken = await (async() => {
|
|
|
|
|
+ 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 + lifetimeSecForTemporaryUrl * 1000);
|
|
|
|
|
+ const userDelegationKey = await blobServiceClient.getUserDelegationKey(startsOn, expiresOn);
|
|
|
|
|
+
|
|
|
|
|
+ const isDownload = opts?.download ?? false;
|
|
|
|
|
+ const contentHeaders = new ContentHeaders(attachment, { inline: !isDownload });
|
|
|
|
|
+
|
|
|
|
|
+ // 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,
|
|
|
|
|
+ contentType: contentHeaders.contentType?.value.toString(),
|
|
|
|
|
+ contentDisposition: contentHeaders.contentDisposition?.value.toString(),
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return generateBlobSASQueryParameters(sasOptions, userDelegationKey, accountName).toString();
|
|
|
|
|
+ })();
|
|
|
|
|
+
|
|
|
|
|
+ const signedUrl = `${url}?${sasToken}`;
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
url: signedUrl,
|
|
url: signedUrl,
|
|
@@ -233,15 +227,15 @@ module.exports = (crowi) => {
|
|
|
const filePath = getFilePathOnStorage(attachment);
|
|
const filePath = getFilePathOnStorage(attachment);
|
|
|
const containerClient = await getContainerClient();
|
|
const containerClient = await getContainerClient();
|
|
|
const blockBlobClient: BlockBlobClient = containerClient.getBlockBlobClient(filePath);
|
|
const blockBlobClient: BlockBlobClient = containerClient.getBlockBlobClient(filePath);
|
|
|
- const DEFAULT_BLOCK_BUFFER_SIZE_BYTES: number = 8 * 1024 * 1024; // 8MB
|
|
|
|
|
- const DEFAULT_MAX_CONCURRENCY = 5;
|
|
|
|
|
- const options: BlockBlobUploadStreamOptions = {
|
|
|
|
|
|
|
+ const contentHeaders = new ContentHeaders(attachment);
|
|
|
|
|
+
|
|
|
|
|
+ return blockBlobClient.uploadStream(readStream, undefined, undefined, {
|
|
|
blobHTTPHeaders: {
|
|
blobHTTPHeaders: {
|
|
|
- blobContentType: attachment.fileFormat,
|
|
|
|
|
- blobContentDisposition: `attachment;filename*=UTF-8''${encodeURIComponent(attachment.originalName)}`,
|
|
|
|
|
|
|
+ // put type and the file name for reference information when uploading
|
|
|
|
|
+ blobContentType: contentHeaders.contentType?.value.toString(),
|
|
|
|
|
+ blobContentDisposition: contentHeaders.contentDisposition?.value.toString(),
|
|
|
},
|
|
},
|
|
|
- };
|
|
|
|
|
- return blockBlobClient.uploadStream(readStream, DEFAULT_BLOCK_BUFFER_SIZE_BYTES, DEFAULT_MAX_CONCURRENCY, options);
|
|
|
|
|
|
|
+ });
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
lib.saveFile = async function({ filePath, contentType, data }) {
|
|
lib.saveFile = async function({ filePath, contentType, data }) {
|
|
@@ -250,7 +244,6 @@ module.exports = (crowi) => {
|
|
|
const options: BlockBlobParallelUploadOptions = {
|
|
const options: BlockBlobParallelUploadOptions = {
|
|
|
blobHTTPHeaders: {
|
|
blobHTTPHeaders: {
|
|
|
blobContentType: contentType,
|
|
blobContentType: contentType,
|
|
|
- blobContentDisposition: `attachment;filename*=UTF-8''${encodeURIComponent(path.basename(filePath))}`,
|
|
|
|
|
},
|
|
},
|
|
|
};
|
|
};
|
|
|
const blockBlobUploadResponse: BlockBlobUploadResponse = await blockBlobClient.upload(data, data.length, options);
|
|
const blockBlobUploadResponse: BlockBlobUploadResponse = await blockBlobClient.upload(data, data.length, options);
|