Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .commitlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"extends": ["@commitlint/config-angular"],
"rules": {
"subject-case": [
2,
"always",
["sentence-case", "start-case", "pascal-case", "upper-case", "lower-case"]
],
"type-enum": [
2,
"always",
[
"build",
"chore",
"ci",
"docs",
"feat",
"fix",
"perf",
"refactor",
"revert",
"style",
"test",
"sample"
]
]
}
}
1 change: 0 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ tslint.json

# schematics
schematics/install/*.ts
schematics/install/express-engine/*.ts

# misc
.DS_Store
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
schematics/install/files/**/*.ts
8 changes: 8 additions & 0 deletions .release-it.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"git": {
"commitMessage": "chore(): release v${version}"
},
"github": {
"release": true
}
}
13 changes: 13 additions & 0 deletions jest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"moduleFileExtensions": ["js", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".(test|spec).ts$",
"transform": {
"^.+\\.(t)s$": "ts-jest"
},
"coverageDirectory": "./coverage",
"verbose": true,
"bail": true,
"testPathIgnorePatterns": ["/node_modules/", "files"]
}
26 changes: 2 additions & 24 deletions lib/azure-storage.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
jest.mock('@azure/storage-blob');
import * as Azure from '@azure/storage-blob';

import {
AzureStorageService,
AzureStorageOptions,
AzureStorageService,
UploadedFileMetadata,
} from './azure-storage.service';

Expand All @@ -18,27 +16,7 @@ const file: UploadedFileMetadata = {
storageUrl: null,
};

Azure.ServiceURL.prototype.listContainersSegment = (...args: any): any => {
return {
nextMarker: null,
containerItems: [],
};
};

Azure.ContainerURL.fromServiceURL = (...args: any): any => {
return {
create(...args: any) {},
};
};

Azure.BlockBlobURL.fromBlobURL = (...args: any): any => {
return {
upload() {},
url: 'FAKE_URL',
};
};

let storage = null;
let storage: AzureStorageService = null;

describe('AzureStorageService', () => {
beforeEach(() => {
Expand Down
108 changes: 62 additions & 46 deletions lib/azure-storage.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as Azure from '@azure/storage-blob';
import { AbortController } from '@azure/abort-controller';
import { ServiceClientOptions } from '@azure/ms-rest-js';

import { AnonymousCredential, BlobServiceClient } from '@azure/storage-blob';
import { Inject, Injectable, Logger } from '@nestjs/common';
import { AZURE_STORAGE_MODULE_OPTIONS } from './azure-storage.constant';

Expand All @@ -9,7 +9,7 @@ export const APP_NAME = 'AzureStorageService';
export interface AzureStorageOptions {
accountName: string;
containerName: string;
sasKey?: string;
accessKey: string;
clientOptions?: ServiceClientOptions;
}

Expand Down Expand Up @@ -46,9 +46,9 @@ export class AzureStorageService {
);
}

if (!perRequestOptions.sasKey) {
if (!perRequestOptions.accessKey) {
throw new Error(
`Error encountered: "AZURE_STORAGE_SAS_KEY" was not provided.`,
`Error encountered: "AZURE_STORAGE_ACCESS_KEY" was not provided.`,
);
}

Expand All @@ -60,25 +60,53 @@ export class AzureStorageService {
);
}

const url = this.getServiceUrl(perRequestOptions);
const anonymousCredential = new Azure.AnonymousCredential();
const pipeline = Azure.StorageURL.newPipeline(anonymousCredential);
const serviceURL = new Azure.ServiceURL(
// When using AnonymousCredential, following url should include a valid SAS
url,
pipeline,
);
// detect access type
let accessType: 'sasKey' | 'connectionString' = null;
if (perRequestOptions.accessKey.startsWith('BlobEndpoint')) {
accessType = 'connectionString';
} else {
accessType = 'sasKey';
}

// Create a container
const containerURL = Azure.ContainerURL.fromServiceURL(
serviceURL,
let blobServiceClient = null;

// connection string
if (accessType === 'connectionString') {
// Create Blob Service Client from Account connection string or SAS connection string
// Account connection string example - `DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=accountKey;EndpointSuffix=core.windows.net`
// SAS connection string example - `BlobEndpoint=https://myaccount.blob.core.windows.net/;...;SharedAccessSignature=sasString`
blobServiceClient = BlobServiceClient.fromConnectionString(
perRequestOptions.accessKey,
);
} else if (accessType === 'sasKey') {
// SAS key
// remove the first ? symbol if present
perRequestOptions.accessKey = perRequestOptions.accessKey.replace(
'?',
'',
);
const url = this.getServiceUrl(perRequestOptions);
const anonymousCredential = new AnonymousCredential();
blobServiceClient = new BlobServiceClient(
// When using AnonymousCredential, following url should include a valid SAS or support public access
url,
anonymousCredential,
);
} else {
throw new Error(
`Error encountered: Connection string or SAS Token are missing`,
);
}

const containerClient = blobServiceClient.getContainerClient(
perRequestOptions.containerName,
);

// Create a container
let doesContainerExists = false;
try {
doesContainerExists = await this._doesContainerExist(
serviceURL,
blobServiceClient,
perRequestOptions.containerName,
);
} catch (error) {
Expand All @@ -95,14 +123,15 @@ export class AzureStorageService {
`Account not found: "${perRequestOptions.accountName}". Please check your "AZURE_STORAGE_ACCOUNT" value.`,
);
} else {
throw new Error(error);
// throw new Error(error);
}
}

if (doesContainerExists === false) {
const createContainerResponse = await containerURL.create(
Azure.Aborter.none,
);
const _createContainerResponse = await containerClient.create({
abortSignal: AbortController.timeout(10 * 60 * 1000), // Abort uploading with timeout in 10mins
});

Logger.log(
`Container "${perRequestOptions.containerName}" created successfully`,
APP_NAME,
Expand All @@ -115,14 +144,13 @@ export class AzureStorageService {
}

const blobName = file.originalname;
const blobURL = Azure.BlobURL.fromContainerURL(containerURL, blobName);
const blockBlobURL = Azure.BlockBlobURL.fromBlobURL(blobURL);
const blockBlobClient = containerClient.getBlockBlobClient(blobName);
try {
const uploadBlobResponse = await blockBlobURL.upload(
Azure.Aborter.none,
buffer,
const uploadBlobResponse = await blockBlobClient.upload(
file.buffer,
buffer.byteLength,
{
abortSignal: null,
blobHTTPHeaders: {
blobContentType: mimetype || 'application/octet-stream',
},
Expand All @@ -133,37 +161,25 @@ export class AzureStorageService {
throw new Error(error);
}

return blockBlobURL.url;
return blockBlobClient.url;
}

getServiceUrl(perRequestOptions: Partial<AzureStorageOptions>) {
// remove the first ? symbol if present
perRequestOptions.sasKey = perRequestOptions.sasKey.replace('?', '');
return `https://${perRequestOptions.accountName}.blob.core.windows.net/?${perRequestOptions.sasKey}`;
return `https://${perRequestOptions.accountName}.blob.core.windows.net/?${perRequestOptions.accessKey}`;
}

private async _listContainers(serviceURL: Azure.ServiceURL) {
let marker: string;
private async _listContainers(blobServiceClient: BlobServiceClient) {
const containers = [];
do {
const listContainersResponse: Azure.Models.ServiceListContainersSegmentResponse = await serviceURL.listContainersSegment(
Azure.Aborter.none,
marker,
);

marker = listContainersResponse.nextMarker;
for (const container of listContainersResponse.containerItems) {
containers.push(container.name);
}
} while (marker);

for await (const container of blobServiceClient.listContainers()) {
containers.push(container.name);
}
return containers;
}

private async _doesContainerExist(
serviceURL: Azure.ServiceURL,
blobServiceClient: BlobServiceClient,
name: string,
) {
return (await this._listContainers(serviceURL)).includes(name);
return (await this._listContainers(blobServiceClient)).includes(name);
}
}
Loading