initial commit azure storage node

This commit is contained in:
Matthias Stallmann 2025-01-09 15:05:32 +01:00
parent 2c72047d0b
commit 38f8781817
12 changed files with 1370 additions and 0 deletions

View file

@ -0,0 +1,33 @@
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
export class MicrosoftStorageOAuth2Api implements ICredentialType {
name = 'microsoftStorageOAuth2Api';
displayName = 'Microsoft Storage API';
extends = ['microsoftOAuth2Api'];
documentationUrl = 'microsoftstorage';
properties: INodeProperties[] = [
{
displayName: 'Account',
name: 'account',
type: 'string',
default: '',
},
{
displayName: 'Base URL',
name: 'baseUrl',
type: 'hidden',
// default: '=https://{{ $self["account"] }}.blob.core.windows.net',
default: '=http://127.0.0.1:10000/{{ $self["account"] }}',
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden',
default: 'openid offline_access',
},
];
}

View file

@ -0,0 +1,93 @@
import type {
ICredentialDataDecryptedObject,
ICredentialType,
IHttpRequestOptions,
INodeProperties,
} from 'n8n-workflow';
import { createHmac } from 'node:crypto';
import {
getCanonicalizedHeadersString,
getCanonicalizedResourceString,
HeaderConstants,
} from '../nodes/Microsoft/Storage/GenericFunctions';
export class MicrosoftStorageSharedKeyApi implements ICredentialType {
name = 'microsoftStorageSharedKeyApi';
displayName = 'Microsoft Storage API';
documentationUrl = 'microsoftstorage';
properties: INodeProperties[] = [
{
displayName: 'Account',
name: 'account',
description: 'Account name',
type: 'string',
default: '',
},
{
displayName: 'Key',
name: 'key',
description: 'Account key',
type: 'string',
typeOptions: {
password: true,
},
default: '',
},
{
displayName: 'Base URL',
name: 'baseUrl',
type: 'hidden',
// default: '=https://{{ $self["account"] }}.blob.core.windows.net',
default: '=http://127.0.0.1:10000/{{ $self["account"] }}',
},
];
async authenticate(
credentials: ICredentialDataDecryptedObject,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
if (requestOptions.qs) {
for (const [key, value] of Object.entries(requestOptions.qs)) {
if (value === undefined) {
delete requestOptions.qs[key];
}
}
}
requestOptions.method ??= 'GET';
requestOptions.headers ??= {};
requestOptions.headers[HeaderConstants.X_MS_DATE] = new Date().toUTCString();
requestOptions.headers[HeaderConstants.X_MS_VERSION] = '2020-04-08'; // Minimum version: https://learn.microsoft.com/en-us/rest/api/storageservices/put-blob-from-url?tabs=microsoft-entra-id
// requestOptions.headers[HeaderConstants.X_MS_CLIENT_REQUEST_ID] = '123';
const stringToSign: string = [
requestOptions.method.toUpperCase(),
requestOptions.headers[HeaderConstants.CONTENT_LANGUAGE] ?? '',
requestOptions.headers[HeaderConstants.CONTENT_ENCODING] ?? '',
requestOptions.headers[HeaderConstants.CONTENT_LENGTH] ?? '',
requestOptions.headers[HeaderConstants.CONTENT_MD5] ?? '',
requestOptions.headers[HeaderConstants.CONTENT_TYPE] ?? '',
requestOptions.headers[HeaderConstants.DATE] ?? '',
requestOptions.headers[HeaderConstants.IF_MODIFIED_SINCE] ?? '',
requestOptions.headers[HeaderConstants.IF_MATCH] ?? '',
requestOptions.headers[HeaderConstants.IF_NONE_MATCH] ?? '',
requestOptions.headers[HeaderConstants.IF_UNMODIFIED_SINCE] ?? '',
requestOptions.headers[HeaderConstants.RANGE] ?? '',
getCanonicalizedHeadersString(requestOptions) +
getCanonicalizedResourceString(requestOptions, credentials),
].join('\n');
const signature: string = createHmac('sha256', Buffer.from(credentials.key as string, 'base64'))
.update(stringToSign, 'utf8')
.digest('base64');
requestOptions.headers[HeaderConstants.AUTHORIZATION] =
`SharedKey ${credentials.account as string}:${signature}`;
return requestOptions;
}
}

View file

@ -0,0 +1,298 @@
import type {
JsonObject,
IDataObject,
IExecuteFunctions,
IExecuteSingleFunctions,
IHttpRequestMethods,
IHttpRequestOptions,
ILoadOptionsFunctions,
IRequestOptions,
INodeExecutionData,
IN8nHttpFullResponse,
INodeListSearchResult,
INodeListSearchItems,
INodeParameterResourceLocator,
ICredentialDataDecryptedObject,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import { parseString } from 'xml2js';
import { compareHeader } from './compare-header';
export const HeaderConstants = {
AUTHORIZATION: 'Authorization',
// AUTHORIZATION_SCHEME: 'Bearer',
CONTENT_ENCODING: 'Content-Encoding',
// CONTENT_ID: 'Content-ID',
CONTENT_LANGUAGE: 'Content-Language',
CONTENT_LENGTH: 'Content-Length',
CONTENT_MD5: 'Content-Md5',
// CONTENT_TRANSFER_ENCODING: 'Content-Transfer-Encoding',
CONTENT_TYPE: 'Content-Type',
// COOKIE: 'Cookie',
DATE: 'date',
IF_MATCH: 'if-match',
IF_MODIFIED_SINCE: 'if-modified-since',
IF_NONE_MATCH: 'if-none-match',
IF_UNMODIFIED_SINCE: 'if-unmodified-since',
RANGE: 'Range',
// USER_AGENT: 'User-Agent',
PREFIX_FOR_STORAGE: 'x-ms-',
X_MS_CLIENT_REQUEST_ID: 'x-ms-client-request-id',
X_MS_COPY_SOURCE: 'x-ms-copy-source',
X_MS_DATE: 'x-ms-date',
// X_MS_ERROR_CODE: 'x-ms-error-code',
X_MS_VERSION: 'x-ms-version',
// X_MS_CopySourceErrorCode: 'x-ms-copy-source-error-code',
X_MS_BLOB_TYPE: 'x-ms-blob-type',
X_MS_BLOB_CONTENT_DISPOSITION: 'x-ms-blob-content-disposition',
};
export async function microsoftApiRequest(
this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject = {},
qs?: IDataObject,
headers?: IDataObject,
url?: string,
): Promise<any> {
const authentication = this.getNodeParameter('authentication', 0) as 'oAuth2' | 'sharedKey';
const credentialsType =
authentication === 'oAuth2' ? 'microsoftStorageOAuth2Api' : 'microsoftStorageSharedKeyApi';
const credentials = await this.getCredentials<{
baseUrl: string;
}>(credentialsType);
const options: IHttpRequestOptions = {
method,
url: url ?? `${credentials.baseUrl}${endpoint}`,
headers,
body,
qs,
};
const response = await this.helpers.requestWithAuthentication.call(
this,
credentialsType,
options,
);
return await new Promise((resolve, reject) => {
parseString(response as string, {}, (error, data) => {
if (error) {
return reject(error);
}
resolve(data);
});
});
}
export async function microsoftApiPaginateRequest(
this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject = {},
qs?: IDataObject,
headers?: IDataObject,
url?: string,
itemIndex: number = 0,
): Promise<IDataObject[]> {
// Todo: IHttpRequestOptions doesn't have uri property which is required for requestWithAuthenticationPaginated
const options: IRequestOptions = {
method,
uri: url ?? `https://myaccount.file.core.windows.net/myshare/mydirectorypath/myfile${endpoint}`,
json: true,
headers,
body,
qs,
};
const pages = await this.helpers.requestWithAuthenticationPaginated.call(
this,
options,
itemIndex,
{
continue: '={{ !!$response.body?.["@odata.nextLink"] }}',
request: {
url: '={{ $response.body?.["@odata.nextLink"] ?? $request.url }}',
},
requestInterval: 0,
},
'microsoftStorageOAuth2Api',
);
let results: IDataObject[] = [];
for (const page of pages) {
const items = page.body.value as IDataObject[];
if (items) {
results = results.concat(items);
}
}
return results;
}
export async function handleErrorPostReceive(
this: IExecuteSingleFunctions,
data: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
if (String(response.statusCode).startsWith('4') || String(response.statusCode).startsWith('5')) {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, { parseXml: true });
}
return data;
}
export function getCanonicalizedHeadersString(requestOptions: IHttpRequestOptions): string {
let headersArray: Array<{ name: string; value: string }> = [];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
for (const [name, value] of Object.entries(requestOptions.headers!) as unknown as [
string,
string,
]) {
if (name.toLowerCase().startsWith(HeaderConstants.PREFIX_FOR_STORAGE)) {
headersArray.push({ name, value });
}
}
headersArray.sort((a, b): number => {
return compareHeader(a.name.toLowerCase(), b.name.toLowerCase());
});
// Remove duplicate headers
headersArray = headersArray.filter((value, index, array) => {
if (index > 0 && value.name.toLowerCase() === array[index - 1].name.toLowerCase()) {
return false;
}
return true;
});
let canonicalizedHeadersStringToSign: string = '';
headersArray.forEach((header) => {
canonicalizedHeadersStringToSign += `${header.name
.toLowerCase()
.trimEnd()}:${header.value.trimStart()}\n`;
});
return canonicalizedHeadersStringToSign;
}
export function getCanonicalizedResourceString(
requestOptions: IHttpRequestOptions,
credentials: ICredentialDataDecryptedObject,
): string {
const path: string = new URL(requestOptions.baseURL + requestOptions.url).pathname || '/';
let canonicalizedResourceString = `/${credentials.account as string}${path}`;
if (requestOptions.qs && Object.keys(requestOptions.qs).length > 0) {
canonicalizedResourceString +=
'\n' +
Object.keys(requestOptions.qs)
.sort()
.map(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(key) => `${key.toLowerCase()}:${decodeURIComponent(requestOptions.qs![key] as string)}`,
)
.join('\n');
}
return canonicalizedResourceString;
}
export async function getBlobs(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const container = this.getNodeParameter('container') as INodeParameterResourceLocator;
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
let response: any;
const qs: IDataObject = {
restype: 'container',
comp: 'list',
};
if (paginationToken) {
qs.marker = paginationToken;
response = await microsoftApiRequest.call(this, 'GET', `/${container.value}`, {}, qs);
} else {
qs.maxresults = 5000;
if (filter) {
qs.prefix = filter;
}
response = await microsoftApiRequest.call(this, 'GET', `/${container.value}`, {}, qs);
}
const blobs: Array<{
name: string;
}> =
response.EnumerationResults.Blobs[0] !== ''
? response.EnumerationResults.Blobs[0].Blob.map((x: any) => ({ name: x.Name[0] }))
: [];
const results: INodeListSearchItems[] = blobs
.map((b) => ({
name: b.name,
value: b.name,
}))
.sort((a, b) =>
a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' }),
);
return {
results,
paginationToken: (response.EnumerationResults.NextMarker[0] as string) || undefined,
};
/* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
}
export async function getContainers(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
let response: any;
const qs: IDataObject = {
comp: 'list',
};
if (paginationToken) {
qs.marker = paginationToken;
response = await microsoftApiRequest.call(this, 'GET', '/', {}, qs);
} else {
qs.maxresults = 5000;
if (filter) {
qs.prefix = filter;
}
response = await microsoftApiRequest.call(this, 'GET', '/', {}, qs);
}
const containers: Array<{
name: string;
}> =
response.EnumerationResults.Containers[0] !== ''
? response.EnumerationResults.Containers[0].Container.map((x: any) => ({
name: x.Name[0],
}))
: [];
const results: INodeListSearchItems[] = containers
.map((c) => ({
name: c.name,
value: c.name,
}))
.sort((a, b) =>
a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' }),
);
return {
results,
paginationToken: (response.EnumerationResults.NextMarker[0] as string) || undefined,
};
/* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
}

View file

@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.microsoftStorage",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Data & Storage"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/microsoft/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.microsoftstorage/"
}
]
}
}

View file

@ -0,0 +1,97 @@
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow';
import { blobFields, blobOperations, containerFields, containerOperations } from './descriptions';
import { getBlobs, getContainers } from './GenericFunctions';
export class MicrosoftStorage implements INodeType {
description: INodeTypeDescription = {
displayName: 'Microsoft Storage',
name: 'microsoftStorage',
icon: {
light: 'file:microsoftStorage.svg',
dark: 'file:microsoftStorage.dark.svg',
},
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Interact with Microsoft Storage API',
defaults: {
name: 'Microsoft Storage',
},
inputs: [NodeConnectionType.Main],
outputs: [NodeConnectionType.Main],
credentials: [
{
name: 'microsoftStorageOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: ['oAuth2'],
},
},
},
{
name: 'microsoftStorageSharedKeyApi',
required: true,
displayOptions: {
show: {
authentication: ['sharedKey'],
},
},
},
],
requestDefaults: {
baseURL: '={{ $credentials.baseUrl }}',
},
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'OAuth2',
value: 'oAuth2',
},
{
name: 'Shared Key',
value: 'sharedKey',
},
],
default: 'sharedKey',
},
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Blob',
value: 'blob',
},
{
name: 'Container',
value: 'container',
},
],
default: 'container',
},
...blobOperations,
...blobFields,
...containerOperations,
...containerFields,
],
};
methods = {
loadOptions: {},
listSearch: {
getBlobs,
getContainers,
},
};
}

View file

@ -0,0 +1,72 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
/*
* We need to imitate .Net culture-aware sorting, which is used in storage service.
* Below tables contain sort-keys for en-US culture.
*/
const table_lv0 = new Uint32Array([
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x71c, 0x0, 0x71f, 0x721,
0x723, 0x725, 0x0, 0x0, 0x0, 0x72d, 0x803, 0x0, 0x0, 0x733, 0x0, 0xd03, 0xd1a, 0xd1c, 0xd1e,
0xd20, 0xd22, 0xd24, 0xd26, 0xd28, 0xd2a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe02, 0xe09, 0xe0a,
0xe1a, 0xe21, 0xe23, 0xe25, 0xe2c, 0xe32, 0xe35, 0xe36, 0xe48, 0xe51, 0xe70, 0xe7c, 0xe7e, 0xe89,
0xe8a, 0xe91, 0xe99, 0xe9f, 0xea2, 0xea4, 0xea6, 0xea7, 0xea9, 0x0, 0x0, 0x0, 0x743, 0x744, 0x748,
0xe02, 0xe09, 0xe0a, 0xe1a, 0xe21, 0xe23, 0xe25, 0xe2c, 0xe32, 0xe35, 0xe36, 0xe48, 0xe51, 0xe70,
0xe7c, 0xe7e, 0xe89, 0xe8a, 0xe91, 0xe99, 0xe9f, 0xea2, 0xea4, 0xea6, 0xea7, 0xea9, 0x0, 0x74c,
0x0, 0x750, 0x0,
]);
const table_lv2 = new Uint32Array([
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
0x12, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
]);
const table_lv4 = new Uint32Array([
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x8012, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8212, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
]);
function isLessThan(lhs: string, rhs: string): boolean {
const tables = [table_lv0, table_lv2, table_lv4];
let curr_level = 0;
let i = 0;
let j = 0;
while (curr_level < tables.length) {
if (curr_level === tables.length - 1 && i !== j) {
return i > j;
}
const weight1 = i < lhs.length ? tables[curr_level][lhs[i].charCodeAt(0)] : 0x1;
const weight2 = j < rhs.length ? tables[curr_level][rhs[j].charCodeAt(0)] : 0x1;
if (weight1 === 0x1 && weight2 === 0x1) {
i = 0;
j = 0;
++curr_level;
} else if (weight1 === weight2) {
++i;
++j;
} else if (weight1 === 0) {
++i;
} else if (weight2 === 0) {
++j;
} else {
return weight1 < weight2;
}
}
return false;
}
export function compareHeader(lhs: string, rhs: string): number {
if (isLessThan(lhs, rhs)) return -1;
return 1;
}

View file

@ -0,0 +1,438 @@
import type {
IExecuteSingleFunctions,
IHttpRequestOptions,
IN8nHttpFullResponse,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { handleErrorPostReceive, HeaderConstants } from '../GenericFunctions';
export const blobOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['blob'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new blob or replaces an existing blob within a container',
routing: {
request: {
ignoreHttpStatusErrors: true,
method: 'PUT',
url: '=/{{ $parameter["container"] }}/{{ $parameter["blob"] }}',
},
output: {
postReceive: [handleErrorPostReceive],
},
},
action: 'Create blob',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a blob',
routing: {
request: {
ignoreHttpStatusErrors: true,
method: 'DELETE',
url: '=/{{ $parameter["container"] }}/{{ $parameter["blob"] }}',
},
output: {
postReceive: [handleErrorPostReceive],
},
},
action: 'Delete blob',
},
{
name: 'Get',
value: 'get',
description: 'Retrieve data for a specific blob',
routing: {
request: {
ignoreHttpStatusErrors: true,
method: 'GET',
url: '=/{{ $parameter["container"] }}/{{ $parameter["blob"] }}',
},
output: {
postReceive: [
handleErrorPostReceive,
async function (
this: IExecuteSingleFunctions,
data: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
data[0].json.headers = response.headers;
return data;
},
],
},
},
action: 'Get blob',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Retrieve a list of blobs',
routing: {
request: {
ignoreHttpStatusErrors: true,
method: 'GET',
qs: {
restype: 'container',
comp: 'list',
},
url: '=/{{ $parameter["container"] }}',
},
output: {
postReceive: [handleErrorPostReceive],
},
},
action: 'Get many blobs',
},
],
default: 'getAll',
},
];
const createFields: INodeProperties[] = [
{
displayName: 'Container Name',
name: 'container',
default: {
mode: 'list',
value: '',
},
displayOptions: {
show: {
resource: ['blob'],
operation: ['create'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'getContainers',
searchable: true,
},
},
{
displayName: 'By Name',
name: 'id',
placeholder: 'e.g. mycontainer',
type: 'string',
},
],
required: true,
type: 'resourceLocator',
},
{
displayName: 'Blob Name',
name: 'blob',
default: '',
displayOptions: {
show: {
resource: ['blob'],
operation: ['create'],
},
},
placeholder: 'e.g. myblob',
required: true,
type: 'string',
validateType: 'string',
},
{
displayName: 'From',
name: 'from',
default: 'binary',
displayOptions: {
show: {
resource: ['blob'],
operation: ['create'],
},
},
options: [
{
name: 'Binary',
value: 'binary',
},
{
name: 'URL',
value: 'url',
},
],
required: true,
type: 'options',
},
{
displayName: 'Binary Property',
name: 'binaryPropertyName',
default: 'data',
description: 'The name of the input binary field containing the file to be written',
displayOptions: {
show: {
resource: ['blob'],
operation: ['create'],
from: ['binary'],
},
},
required: true,
routing: {
send: {
preSend: [
async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const binaryPropertyName = this.getNodeParameter('binaryPropertyName') as string;
const binaryData = this.helpers.assertBinaryData(binaryPropertyName);
const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(binaryPropertyName);
requestOptions.headers ??= {};
requestOptions.headers[HeaderConstants.CONTENT_LENGTH] = binaryDataBuffer.length;
requestOptions.headers[HeaderConstants.X_MS_BLOB_TYPE] = 'BlockBlob';
if (binaryData.mimeType) {
requestOptions.headers[HeaderConstants.CONTENT_TYPE] = binaryData.mimeType;
}
if (binaryData.fileName) {
requestOptions.headers[HeaderConstants.X_MS_BLOB_CONTENT_DISPOSITION] =
`attachment; filename="${binaryData.fileName}"`;
}
requestOptions.body = binaryDataBuffer;
return requestOptions;
},
],
},
},
type: 'string',
},
{
displayName: 'URL',
name: 'url',
default: 'data',
description: 'URL where to read of the blob contents from',
displayOptions: {
show: {
resource: ['blob'],
operation: ['create'],
from: ['url'],
},
},
required: true,
routing: {
send: {
preSend: [
async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const url = this.getNodeParameter('url') as string;
requestOptions.headers ??= {};
requestOptions.headers[HeaderConstants.CONTENT_LENGTH] = 0;
requestOptions.headers[HeaderConstants.X_MS_BLOB_TYPE] = 'BlockBlob';
requestOptions.headers[HeaderConstants.X_MS_COPY_SOURCE] = url;
return requestOptions;
},
],
},
},
type: 'string',
},
];
const deleteFields: INodeProperties[] = [
{
displayName: 'Container Name',
name: 'container',
default: {
mode: 'list',
value: '',
},
displayOptions: {
show: {
resource: ['blob'],
operation: ['delete'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'getContainers',
searchable: true,
},
},
{
displayName: 'By Name',
name: 'id',
placeholder: 'e.g. mycontainer',
type: 'string',
},
],
required: true,
type: 'resourceLocator',
},
{
displayName: 'Blob to Delete',
name: 'blob',
default: {
mode: 'list',
value: '',
},
displayOptions: {
show: {
resource: ['blob'],
operation: ['delete'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'getBlobs',
searchable: true,
},
},
{
displayName: 'By Name',
name: 'id',
placeholder: 'e.g. myblob',
type: 'string',
},
],
required: true,
type: 'resourceLocator',
},
];
const getFields: INodeProperties[] = [
{
displayName: 'Container Name',
name: 'container',
default: {
mode: 'list',
value: '',
},
displayOptions: {
show: {
resource: ['blob'],
operation: ['get'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'getContainers',
searchable: true,
},
},
{
displayName: 'By Name',
name: 'id',
placeholder: 'e.g. mycontainer',
type: 'string',
},
],
required: true,
type: 'resourceLocator',
},
{
displayName: 'Blob to Get',
name: 'blob',
default: {
mode: 'list',
value: '',
},
displayOptions: {
show: {
resource: ['blob'],
operation: ['get'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'getBlobs',
searchable: true,
},
},
{
displayName: 'By Name',
name: 'id',
placeholder: 'e.g. myblob',
type: 'string',
},
],
required: true,
type: 'resourceLocator',
},
];
const getAllFields: INodeProperties[] = [
{
displayName: 'Container Name',
name: 'container',
default: {
mode: 'list',
value: '',
},
displayOptions: {
show: {
resource: ['blob'],
operation: ['getAll'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'getContainers',
searchable: true,
},
},
{
displayName: 'By Name',
name: 'id',
placeholder: 'e.g. mycontainer',
type: 'string',
},
],
required: true,
type: 'resourceLocator',
},
];
export const blobFields: INodeProperties[] = [
...createFields,
...deleteFields,
...getFields,
...getAllFields,
];

View file

@ -0,0 +1,304 @@
import type { IExecuteSingleFunctions, IHttpRequestOptions, INodeProperties } from 'n8n-workflow';
import { handleErrorPostReceive } from '../GenericFunctions';
export const containerOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['container'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a container',
routing: {
request: {
ignoreHttpStatusErrors: true,
method: 'PUT',
qs: {
restype: 'container',
},
url: '=/{{ $parameter["container"] }}',
},
output: {
postReceive: [handleErrorPostReceive],
},
},
action: 'Create container',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a container',
routing: {
request: {
ignoreHttpStatusErrors: true,
method: 'DELETE',
qs: {
restype: 'container',
},
url: '=/{{ $parameter["container"] }}',
},
output: {
postReceive: [handleErrorPostReceive],
},
},
action: 'Delete container',
},
{
name: 'Get',
value: 'get',
description: 'Retrieve data for a specific container',
routing: {
request: {
ignoreHttpStatusErrors: true,
method: 'GET',
qs: {
restype: 'container',
},
url: '=/{{ $parameter["container"] }}',
},
output: {
postReceive: [handleErrorPostReceive],
},
},
action: 'Get container',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Retrieve a list of containers',
routing: {
request: {
ignoreHttpStatusErrors: true,
method: 'GET',
qs: {
comp: 'list',
},
url: '/',
},
output: {
postReceive: [handleErrorPostReceive],
},
},
action: 'Get many container',
},
],
default: 'getAll',
},
];
const createFields: INodeProperties[] = [
{
displayName: 'Container Name',
name: 'container',
default: '',
displayOptions: {
show: {
resource: ['container'],
operation: ['create'],
},
},
placeholder: 'e.g. mycontainer',
required: true,
type: 'string',
validateType: 'string',
},
];
const deleteFields: INodeProperties[] = [
{
displayName: 'Container to Delete',
name: 'container',
default: {
mode: 'list',
value: '',
},
displayOptions: {
show: {
resource: ['container'],
operation: ['delete'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'getContainers',
searchable: true,
},
},
{
displayName: 'By Name',
name: 'id',
placeholder: 'e.g. mycontainer',
type: 'string',
},
],
required: true,
type: 'resourceLocator',
},
];
const getFields: INodeProperties[] = [
{
displayName: 'Container to Get',
name: 'container',
default: {
mode: 'list',
value: '',
},
displayOptions: {
show: {
resource: ['container'],
operation: ['get'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'getContainers',
searchable: true,
},
},
{
displayName: 'By Name',
name: 'id',
placeholder: 'e.g. mycontainer',
type: 'string',
},
],
required: true,
type: 'resourceLocator',
},
];
const getAllFields: INodeProperties[] = [
{
displayName: 'Return All',
name: 'returnAll',
default: false,
description: 'Whether to return all results or only up to a given limit',
displayOptions: {
show: {
resource: ['container'],
operation: ['getAll'],
},
},
routing: {
send: {
preSend: [
async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
return requestOptions;
},
],
},
},
type: 'boolean',
},
{
displayName: 'Limit',
name: 'limit',
default: 50,
description: 'Max number of results to return',
displayOptions: {
show: {
resource: ['container'],
operation: ['getAll'],
returnAll: [false],
},
},
routing: {
send: {
property: 'maxresults',
type: 'query',
value: '={{ $value }}',
},
},
type: 'number',
typeOptions: {
minValue: 1,
},
validateType: 'number',
},
{
displayName: 'Filter',
name: 'filter',
default: '',
description:
'Filters the results to return only containers with a name that begins with the specified prefix',
displayOptions: {
show: {
resource: ['container'],
operation: ['getAll'],
},
},
placeholder: 'e.g. mycontainer',
routing: {
send: {
property: 'prefix',
type: 'query',
value: '={{ $value ? $value : undefined }}',
},
},
type: 'string',
validateType: 'string',
},
// {
// displayName: 'Fields',
// name: 'fields',
// default: [],
// description: 'The fields to add to the output',
// displayOptions: {
// show: {
// resource: ['container'],
// operation: ['getAll'],
// output: ['fields'],
// },
// },
// options: [
// {
// name: 'Metadata',
// value: 'metadata',
// },
// {
// name: 'Deleted',
// value: 'deleted',
// },
// {
// name: 'System',
// value: 'system',
// },
// ],
// routing: {
// send: {
// property: 'include',
// type: 'query',
// value: '={{ $value.join(",") }}',
// },
// },
// type: 'multiOptions',
// },
];
export const containerFields: INodeProperties[] = [
...createFields,
...deleteFields,
...getFields,
...getAllFields,
];

View file

@ -0,0 +1,2 @@
export * from './BlobDescription';
export * from './ContainerDescription';

View file

@ -0,0 +1,4 @@
<svg width="42" height="40" viewBox="0 0 42 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M40.2158 22.1487C41.5024 23.6021 41.1669 25.7976 39.498 26.8398V26.8376L22.8313 37.2554C22.33 37.5682 21.7635 37.7399 21.189 37.7704L21.1882 37.7906C20.5527 37.7906 19.7243 37.6044 19.1687 37.2577L9.44873 31.1821C10.311 31.7199 11.7443 32.3177 13.2598 32.3177C14.6398 32.3177 15.9221 31.9177 16.9843 31.2355C16.9843 31.2355 16.9865 31.2355 16.9887 31.2332L21.5727 28.3589L21.5723 28.3681L30.7024 22.6621L21.0047 11.7198L21.2469 11.4465C22.4691 10.0532 24.3069 9.1665 26.3624 9.1665C27.4091 9.1665 28.398 9.40206 29.2802 9.81317L40.2158 22.1487Z" stroke="white" stroke-width="1.2" stroke-linejoin="round"/>
<path d="M21.0022 2.22439V2.22217L21 2.22439L21.0022 2.22439ZM21.0022 2.22439C21.9415 2.22498 22.8827 2.60054 23.5467 3.35106L29.2778 9.8155C28.3956 9.40661 27.4089 9.16883 26.36 9.16883C24.3044 9.16883 22.4667 10.0555 21.2444 11.4488L21.0703 11.6453L21.0703 11.6484L11.3022 22.6644L21 28.7266L16.9889 31.2333C16.9867 31.2355 16.9844 31.2355 16.9844 31.2355C15.9222 31.9177 14.64 32.3177 13.26 32.3177C11.7444 32.3177 10.3111 31.7199 9.44888 31.1822C8.6711 30.6955 2.50221 26.8399 2.50221 26.8399C0.833325 25.7977 0.49777 23.6022 1.78444 22.1488L18.4511 3.34883C19.1091 2.60751 20.0714 2.2363 21.0022 2.22675V2.22439Z" stroke="white" stroke-width="1.2" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,8 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.44824 30.9595C9.31047 31.4972 10.7438 32.095 12.2594 32.095C13.6394 32.095 14.9216 31.695 15.9838 31.0128C15.9838 31.0128 15.986 31.0128 15.9883 31.0106L19.9994 28.5039V37.555C19.3638 37.555 18.7238 37.3817 18.1683 37.035L8.44824 30.9595Z" fill="#225086"/>
<path d="M17.4511 3.12667L0.784438 21.9267C-0.502231 23.38 -0.166675 25.5756 1.50222 26.6178C1.50222 26.6178 7.67112 30.4734 8.44889 30.96C9.31112 31.4978 10.7445 32.0956 12.26 32.0956C13.64 32.0956 14.9222 31.6956 15.9845 31.0134C15.9845 31.0134 15.9867 31.0134 15.9889 31.0112L20 28.5045L10.3022 22.4423L20.0022 11.5V2C19.06 2 18.1178 2.37556 17.4511 3.12667Z" fill="#66DDFF"/>
<path d="M10.3027 22.4422L10.4183 22.5133L20.0005 28.5045H20.0027V11.5022L20.0005 11.5L10.3027 22.4422Z" fill="#CBF8FF"/>
<path d="M38.498 26.6177C40.1669 25.5755 40.5025 23.3799 39.2158 21.9266L28.2802 9.591C27.398 9.17989 26.4091 8.94434 25.3624 8.94434C23.3069 8.94434 21.4691 9.831 20.2469 11.2243L20.0047 11.4977L29.7025 22.4399L20.0024 28.5021V37.5533C20.6402 37.5533 21.2758 37.3799 21.8313 37.0333L38.498 26.6155V26.6177Z" fill="#074793"/>
<path d="M20.0022 2V11.5L20.2444 11.2267C21.4667 9.83334 23.3044 8.94668 25.36 8.94668C26.4089 8.94668 27.3956 9.18445 28.2778 9.59334L22.5467 3.12889C21.8822 2.37778 20.94 2.00222 20 2.00222L20.0022 2Z" fill="#0294E4"/>
<path d="M29.7002 22.442L20.0024 11.502V28.502L29.7002 22.442Z" fill="#96BCC2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -225,6 +225,8 @@
"dist/credentials/MicrosoftOneDriveOAuth2Api.credentials.js",
"dist/credentials/MicrosoftOutlookOAuth2Api.credentials.js",
"dist/credentials/MicrosoftSharePointOAuth2Api.credentials.js",
"dist/credentials/MicrosoftStorageOAuth2Api.credentials.js",
"dist/credentials/MicrosoftStorageSharedKeyApi.credentials.js",
"dist/credentials/MicrosoftSql.credentials.js",
"dist/credentials/MicrosoftTeamsOAuth2Api.credentials.js",
"dist/credentials/MicrosoftToDoOAuth2Api.credentials.js",
@ -633,6 +635,7 @@
"dist/nodes/Microsoft/Outlook/MicrosoftOutlook.node.js",
"dist/nodes/Microsoft/Outlook/MicrosoftOutlookTrigger.node.js",
"dist/nodes/Microsoft/Sql/MicrosoftSql.node.js",
"dist/nodes/Microsoft/Storage/MicrosoftStorage.node.js",
"dist/nodes/Microsoft/Teams/MicrosoftTeams.node.js",
"dist/nodes/Microsoft/ToDo/MicrosoftToDo.node.js",
"dist/nodes/Mindee/Mindee.node.js",