Browse Source

chore: migrate to aws sdk v3 (#9104)

* feat: backblaze migrate

* feat: scaleway migrate

* feat: digital ocean

* chore: migrate to aws-sdk v3

* fix: refactor storagePlugins

* fix: handle error inside callback

* fix: return data as Buffer

* fix: backblaze handle

* feat: acl support for storage plugins

* feat: minio refactor

* feat: R2 plugin

* fix: lock file

* fix: use buffer only for images

* fix: update fileCreateByUrl

* fix: upocloud case

* fix: upocloud case

* chore: sync dependencies
pull/9139/head
Anbarasu 4 months ago committed by GitHub
parent
commit
bcd28c819d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. BIN
      packages/nc-gui/public/plugins/r2.png
  2. 14
      packages/nocodb/package.json
  3. 2
      packages/nocodb/src/helpers/NcPluginMgrv2.ts
  4. 199
      packages/nocodb/src/plugins/GenericS3/GenericS3.ts
  5. 197
      packages/nocodb/src/plugins/backblaze/Backblaze.ts
  6. 9
      packages/nocodb/src/plugins/backblaze/index.ts
  7. 165
      packages/nocodb/src/plugins/gcs/Gcs.ts
  8. 175
      packages/nocodb/src/plugins/linode/LinodeObjectStorage.ts
  9. 9
      packages/nocodb/src/plugins/linode/index.ts
  10. 243
      packages/nocodb/src/plugins/mino/Minio.ts
  11. 168
      packages/nocodb/src/plugins/ovhCloud/OvhCloud.ts
  12. 9
      packages/nocodb/src/plugins/ovhCloud/index.ts
  13. 42
      packages/nocodb/src/plugins/r2/R2.ts
  14. 18
      packages/nocodb/src/plugins/r2/R2Plugin.ts
  15. 68
      packages/nocodb/src/plugins/r2/index.ts
  16. 183
      packages/nocodb/src/plugins/s3/S3.ts
  17. 9
      packages/nocodb/src/plugins/s3/index.ts
  18. 161
      packages/nocodb/src/plugins/scaleway/ScalewayObjectStorage.ts
  19. 9
      packages/nocodb/src/plugins/scaleway/index.ts
  20. 17
      packages/nocodb/src/plugins/ses/SES.ts
  21. 172
      packages/nocodb/src/plugins/spaces/Spaces.ts
  22. 7
      packages/nocodb/src/plugins/spaces/index.ts
  23. 8
      packages/nocodb/src/plugins/storage/Local.ts
  24. 164
      packages/nocodb/src/plugins/upcloud/UpoCloud.ts
  25. 9
      packages/nocodb/src/plugins/upcloud/index.ts
  26. 160
      packages/nocodb/src/plugins/vultr/Vultr.ts
  27. 9
      packages/nocodb/src/plugins/vultr/index.ts
  28. 6
      packages/nocodb/src/services/attachments.service.ts
  29. 2220
      pnpm-lock.yaml

BIN
packages/nc-gui/public/plugins/r2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

14
packages/nocodb/package.json

@ -45,9 +45,13 @@
"docker:build": "EE=\"true-xc-test\" webpack --config docker/webpack.config.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.504.0",
"@aws-sdk/lib-storage": "^3.504.0",
"@aws-sdk/s3-request-presigner": "^3.504.0",
"@aws-sdk/client-kafka": "^3.620.0",
"@aws-sdk/client-kinesis": "^3.620.0",
"@aws-sdk/client-s3": "^3.620.0",
"@aws-sdk/client-ses": "^3.620.0",
"@aws-sdk/client-sns": "^3.620.0",
"@aws-sdk/lib-storage": "^3.620.0",
"@aws-sdk/s3-request-presigner": "^3.620.0",
"@google-cloud/storage": "^7.7.0",
"@jm18457/kafkajs-msk-iam-authentication-mechanism": "^3.1.2",
"@nestjs/bull": "^10.0.1",
@ -112,7 +116,7 @@
"lodash": "^4.17.21",
"mailersend": "^1.5.0",
"marked": "^4.3.0",
"minio": "^7.1.3",
"minio": "^8.0.1",
"mkdirp": "^2.1.6",
"mssql": "^10.0.2",
"multer": "^1.4.5-lts.1",
@ -120,7 +124,7 @@
"nanoid": "^3.3.7",
"nc-help": "0.3.1",
"nc-lib-gui": "0.251.3",
"nc-plugin": "^0.1.3",
"nc-plugin": "^0.1.6",
"nestjs-throttler-storage-redis": "^0.4.4",
"nocodb-sdk": "workspace:^",
"nodemailer": "^6.9.13",

2
packages/nocodb/src/helpers/NcPluginMgrv2.ts

@ -28,6 +28,7 @@ import TwilioWhatsappPluginConfig from '~/plugins/twilioWhatsapp';
import UpcloudPluginConfig from '~/plugins/upcloud';
import VultrPluginConfig from '~/plugins/vultr';
import SESPluginConfig from '~/plugins/ses';
import R2PluginConfig from '~/plugins/r2';
import Noco from '~/Noco';
import Local from '~/plugins/storage/Local';
import { MetaTable, RootScopes } from '~/utils/globals';
@ -53,6 +54,7 @@ const defaultPlugins = [
MailerSendConfig,
ScalewayPluginConfig,
SESPluginConfig,
R2PluginConfig,
];
class NcPluginMgrv2 {

199
packages/nocodb/src/plugins/GenericS3/GenericS3.ts

@ -0,0 +1,199 @@
import fs from 'fs';
import { promisify } from 'util';
import axios from 'axios';
import { useAgent } from 'request-filtering-agent';
import {
GetObjectCommand,
type PutObjectCommandInput,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Upload } from '@aws-sdk/lib-storage';
import type { PutObjectRequest, S3 as S3Client } from '@aws-sdk/client-s3';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
import { generateTempFilePath, waitForStreamClose } from '~/utils/pluginUtils';
interface GenerocObjectStorageInput {
bucket: string;
region?: string;
access_key: string;
access_secret: string;
}
export default class GenericS3 implements IStorageAdapterV2 {
protected s3Client: S3Client;
protected input: GenerocObjectStorageInput;
constructor(input: unknown) {
this.input = input as GenerocObjectStorageInput;
}
protected get defaultParams() {
return {
Bucket: this.input.bucket,
};
}
public async init(): Promise<any> {
// Placeholder, should be initalized in child class
}
public async test(): Promise<boolean> {
try {
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});
await promisify(fs.unlink)(tempFile);
return true;
} catch (e) {
throw e;
}
}
public async fileRead(key: string): Promise<any> {
const command = new GetObjectCommand({
Key: key,
Bucket: this.input.bucket,
});
const { Body } = await this.s3Client.send(command);
const fileStream = Body as Readable;
return new Promise((resolve, reject) => {
const chunks: any[] = [];
fileStream.on('data', (chunk) => {
chunks.push(chunk);
});
fileStream.on('end', () => {
const buffer = Buffer.concat(chunks);
resolve(buffer);
});
fileStream.on('error', (err) => {
reject(err);
});
});
}
async fileCreate(key: string, file: XcFile): Promise<any> {
const fileStream = fs.createReadStream(file.path);
return this.fileCreateByStream(key, fileStream, {
mimetype: file?.mimetype,
});
}
async fileCreateByStream(
key: string,
stream: Readable,
options?: {
mimetype?: string;
},
): Promise<void> {
try {
const streamError = new Promise<void>((_, reject) => {
stream.on('error', (err) => {
reject(err);
});
});
const uploadParams = {
...this.defaultParams,
Body: stream,
Key: key,
ContentType: options?.mimetype || 'application/octet-stream',
};
const upload = this.upload(uploadParams);
return await Promise.race([upload, streamError]);
} catch (error) {
throw error;
}
}
async fileCreateByUrl(
key: string,
url: string,
{ fetchOptions: { buffer } = { buffer: false } },
): Promise<any> {
try {
const response = await axios.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
responseType: buffer ? 'arraybuffer' : 'stream',
});
const uploadParams: PutObjectRequest = {
...this.defaultParams,
Body: response.data,
Key: key,
ContentType: response.headers['content-type'],
};
const data = await this.upload(uploadParams);
return {
url: data,
data: response.data,
};
} catch (error) {
throw error;
}
}
public async getSignedUrl(
key,
expiresInSeconds = 7200,
pathParameters?: { [key: string]: string },
) {
const command = new GetObjectCommand({
Key: key,
Bucket: this.input.bucket,
...pathParameters,
});
return getSignedUrl(this.s3Client, command, {
expiresIn: expiresInSeconds,
});
}
protected async upload(uploadParams: PutObjectCommandInput): Promise<any> {
try {
const upload = new Upload({
client: this.s3Client,
params: {
ACL: 'public-read',
...uploadParams,
},
});
const data = await upload.done();
return data.Location;
} catch (error) {
console.error('Error uploading file', error);
throw error;
}
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
}
// TODO - implement
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
}

197
packages/nocodb/src/plugins/backblaze/Backblaze.ts

@ -1,101 +1,86 @@
import fs from 'fs';
import { promisify } from 'util';
import AWS from 'aws-sdk';
import axios from 'axios';
import { useAgent } from 'request-filtering-agent';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
import { generateTempFilePath, waitForStreamClose } from '~/utils/pluginUtils';
import { Upload } from '@aws-sdk/lib-storage';
import { GetObjectCommand, S3 as S3Client } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import type { PutObjectCommandInput, S3ClientConfig } from '@aws-sdk/client-s3';
import type { IStorageAdapterV2 } from 'nc-plugin';
import GenericS3 from '~/plugins/GenericS3/GenericS3';
export default class Backblaze implements IStorageAdapterV2 {
private s3Client: AWS.S3;
private input: any;
constructor(input: any) {
this.input = input;
interface BackblazeObjectStorageInput {
bucket: string;
region: string;
access_key: string;
access_secret: string;
acl?: string;
}
async fileCreate(key: string, file: XcFile): Promise<any> {
const fileStream = fs.createReadStream(file.path);
export default class Backblaze extends GenericS3 implements IStorageAdapterV2 {
protected input: BackblazeObjectStorageInput;
return this.fileCreateByStream(key, fileStream, {
mimetype: file?.mimetype,
});
constructor(input: unknown) {
super(input as BackblazeObjectStorageInput);
}
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
protected get defaultParams() {
return {
Bucket: this.input.bucket,
ACL: this.input?.acl || 'public-read',
};
return new Promise((resolve, reject) => {
axios
.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer',
})
.then((response) => {
uploadParams.Body = response.data;
uploadParams.Key = key;
uploadParams.ContentType = response.headers['content-type'];
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
if (err1) {
console.log('Error', err1);
reject(err1);
}
if (data) {
resolve({
url: data.Location,
data: response.data,
});
}
});
})
.catch((error) => {
reject(error);
});
});
}
async fileCreateByStream(
key: string,
stream: Readable,
options?: {
mimetype?: string;
public async init(): Promise<any> {
const s3Options: S3ClientConfig = {
region: this.patchRegion(this.input.region),
credentials: {
accessKeyId: this.input.access_key,
secretAccessKey: this.input.access_secret,
},
): Promise<void> {
const uploadParams: any = {
ACL: 'public-read',
Body: stream,
Key: key,
ContentType: options?.mimetype || 'application/octet-stream',
endpoint: `https://s3.${this.patchRegion(
this.input.region,
)}.backblazeb2.com`,
};
return new Promise((resolve, reject) => {
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err, data) => {
if (err) {
console.log('Error', err);
reject(err);
this.s3Client = new S3Client(s3Options);
}
if (data) {
resolve(data.Location);
public async getSignedUrl(
key,
expiresInSeconds = 7200,
pathParameters?: { [key: string]: string },
) {
let tempKey = key;
if (
tempKey.startsWith(`${this.input.bucket}/nc/uploads`) ||
tempKey.startsWith(`${this.input.bucket}/nc/thumbnails`)
) {
tempKey = tempKey.replace(`${this.input.bucket}/`, '');
}
const command = new GetObjectCommand({
Key: tempKey,
Bucket: this.input.bucket,
...pathParameters,
});
return getSignedUrl(this.s3Client, command, {
expiresIn: expiresInSeconds,
});
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
}
protected async upload(uploadParams: PutObjectCommandInput): Promise<any> {
try {
const upload = new Upload({
client: this.s3Client,
params: uploadParams,
});
const data = await upload.done();
// TODO - implement
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
if (data) {
return `https://${this.input.bucket}.s3.${this.input.region}.backblazeb2.com/${uploadParams.Key}`;
}
} catch (error) {
console.error('Error uploading file', error);
throw error;
}
}
patchRegion(region: string): string {
@ -107,56 +92,4 @@ export default class Backblaze implements IStorageAdapterV2 {
}
return region;
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
public async fileRead(key: string): Promise<any> {
return new Promise((resolve, reject) => {
this.s3Client.getObject({ Key: key } as any, (err, data) => {
if (err) {
return reject(err);
}
if (!data?.Body) {
return reject(data);
}
return resolve(data.Body);
});
});
}
public async init(): Promise<any> {
const s3Options: any = {
params: { Bucket: this.input.bucket },
region: this.patchRegion(this.input.region),
};
s3Options.accessKeyId = this.input.access_key;
s3Options.secretAccessKey = this.input.access_secret;
s3Options.endpoint = new AWS.Endpoint(
`s3.${s3Options.region}.backblazeb2.com`,
);
this.s3Client = new AWS.S3(s3Options);
}
public async test(): Promise<boolean> {
try {
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});
await promisify(fs.unlink)(tempFile);
return true;
} catch (e) {
throw e;
}
}
}

9
packages/nocodb/src/plugins/backblaze/index.ts

@ -5,7 +5,7 @@ import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: BackblazePlugin,
title: 'Backblaze B2',
version: '0.0.2',
version: '0.0.3',
logo: 'plugins/backblaze.jpeg',
tags: 'Storage',
description:
@ -41,6 +41,13 @@ const config: XcPluginConfig = {
type: XcType.Password,
required: true,
},
{
key: 'acl',
label: 'Access Control Lists (ACL)',
placeholder: 'Default set to public-read',
type: XcType.SingleLineText,
required: false,
},
],
actions: [
{

165
packages/nocodb/src/plugins/gcs/Gcs.ts

@ -3,73 +3,29 @@ import { promisify } from 'util';
import { Storage } from '@google-cloud/storage';
import axios from 'axios';
import { useAgent } from 'request-filtering-agent';
import type { GetSignedUrlConfig, StorageOptions } from '@google-cloud/storage';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
import type { StorageOptions } from '@google-cloud/storage';
import { generateTempFilePath, waitForStreamClose } from '~/utils/pluginUtils';
interface GoogleCloudStorageInput {
client_email: string;
private_key: string;
bucket: string;
project_id: string;
}
export default class Gcs implements IStorageAdapterV2 {
private storageClient: Storage;
private bucketName: string;
private input: any;
constructor(input: any) {
this.input = input;
}
private input: GoogleCloudStorageInput;
async fileCreate(key: string, file: XcFile): Promise<any> {
const uploadResponse = await this.storageClient
.bucket(this.bucketName)
.upload(file.path, {
destination: key,
contentType: file?.mimetype || 'application/octet-stream',
// Support for HTTP requests made with `Accept-Encoding: gzip`
gzip: true,
// By setting the option `destination`, you can change the name of the
// object you are uploading to a bucket.
metadata: {
// Enable long-lived HTTP caching headers
// Use only if the contents of the file will never change
// (If the contents will change, use cacheControl: 'no-cache')
cacheControl: 'public, max-age=31536000',
},
});
return uploadResponse[0].publicUrl();
}
fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
public fileRead(key: string): Promise<any> {
return new Promise((resolve, reject) => {
const file = this.storageClient.bucket(this.bucketName).file(key);
// Check for existence, since gcloud-node seemed to be caching the result
file.exists((err, exists) => {
if (exists) {
file.download((downerr, data) => {
if (err) {
return reject(downerr);
}
return resolve(data);
});
} else {
reject(err);
}
});
});
constructor(input: unknown) {
this.input = input as GoogleCloudStorageInput;
}
public async init(): Promise<any> {
const options: StorageOptions = {};
// options.credentials = {
// client_email: process.env.NC_GCS_CLIENT_EMAIL,
// private_key: process.env.NC_GCS_PRIVATE_KEY
// }
//
// this.bucketName = process.env.NC_GCS_BUCKET;
options.credentials = {
client_email: this.input.client_email,
// replace \n with real line breaks to avoid
@ -81,9 +37,7 @@ export default class Gcs implements IStorageAdapterV2 {
if (this.input.project_id) {
options.projectId = this.input.project_id;
}
this.bucketName = this.input.bucket;
this.storageClient = new Storage(options);
}
@ -105,29 +59,40 @@ export default class Gcs implements IStorageAdapterV2 {
}
}
fileCreateByUrl(destPath: string, url: string): Promise<any> {
public fileRead(key: string): Promise<any> {
return new Promise((resolve, reject) => {
axios
.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer',
})
.then((response) => {
this.storageClient
.bucket(this.bucketName)
.file(destPath)
.save(response.data)
.then((res) => resolve({ url: res, data: response.data }))
.catch(reject);
})
.catch((error) => {
reject(error);
const file = this.storageClient.bucket(this.bucketName).file(key);
// Check for existence, since gcloud-node seemed to be caching the result
file.exists((err, exists) => {
if (exists) {
file.download((downerr, data) => {
if (err) {
return reject(downerr);
}
return resolve(data);
});
} else {
reject(err);
}
});
});
}
async fileCreate(key: string, file: XcFile): Promise<any> {
const uploadResponse = await this.storageClient
.bucket(this.bucketName)
.upload(file.path, {
destination: key,
contentType: file?.mimetype || 'application/octet-stream',
gzip: true,
metadata: {
cacheControl: 'public, max-age=31536000',
},
});
return uploadResponse[0].publicUrl();
}
async fileCreateByStream(
key: string,
stream: Readable,
@ -155,6 +120,36 @@ export default class Gcs implements IStorageAdapterV2 {
return uploadResponse[0].publicUrl();
}
async fileCreateByUrl(
destPath: string,
url: string,
{ fetchOptions: { buffer } = { buffer: false } },
): Promise<any> {
return new Promise((resolve, reject) => {
axios
.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
responseType: buffer ? 'arraybuffer' : 'stream',
})
.then((response) => {
this.storageClient
.bucket(this.bucketName)
.file(destPath)
.save(response.data)
.then((res) => resolve({ url: res, data: response.data }))
.catch(reject);
})
.catch((error) => {
reject(error);
});
});
}
fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
@ -164,4 +159,24 @@ export default class Gcs implements IStorageAdapterV2 {
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
}
public async getSignedUrl(
key,
expiresInSeconds = 7200,
pathParameters?: { [key: string]: string },
) {
const options: GetSignedUrlConfig = {
version: 'v4',
action: 'read',
expires: Date.now() + expiresInSeconds * 1000,
extensionHeaders: pathParameters,
};
const [url] = await this.storageClient
.bucket(this.bucketName)
.file(key)
.getSignedUrl(options);
return url;
}
}

175
packages/nocodb/src/plugins/linode/LinodeObjectStorage.ts

@ -1,151 +1,42 @@
import fs from 'fs';
import { promisify } from 'util';
import AWS from 'aws-sdk';
import axios from 'axios';
import { useAgent } from 'request-filtering-agent';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
import { generateTempFilePath, waitForStreamClose } from '~/utils/pluginUtils';
export default class LinodeObjectStorage implements IStorageAdapterV2 {
private s3Client: AWS.S3;
private input: any;
constructor(input: any) {
this.input = input;
}
async fileCreate(key: string, file: XcFile): Promise<any> {
const fileStream = fs.createReadStream(file.path);
return this.fileCreateByStream(key, fileStream, {
mimetype: file?.mimetype,
});
}
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
};
return new Promise((resolve, reject) => {
axios
.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer',
})
.then((response) => {
uploadParams.Body = response.data;
uploadParams.Key = key;
uploadParams.ContentType = response.headers['content-type'];
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
if (err1) {
console.log('Error', err1);
reject(err1);
}
if (data) {
resolve({
url: data.Location,
data: response.data,
});
}
});
})
.catch((error) => {
reject(error);
});
});
}
public async fileCreateByStream(
key: string,
stream: Readable,
options?: {
mimetype?: string;
},
): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
Body: stream,
Key: key,
ContentType: options?.mimetype || 'application/octet-stream',
import { S3 as S3Client } from '@aws-sdk/client-s3';
import type { S3ClientConfig } from '@aws-sdk/client-s3';
import type { IStorageAdapterV2 } from 'nc-plugin';
import GenericS3 from '~/plugins/GenericS3/GenericS3';
interface LinodeObjectStorageInput {
bucket: string;
region: string;
access_key: string;
access_secret: string;
acl?: string;
}
export default class LinodeObjectStorage
extends GenericS3
implements IStorageAdapterV2
{
protected input: LinodeObjectStorageInput;
constructor(input: unknown) {
super(input as LinodeObjectStorageInput);
}
protected get defaultParams() {
return {
Bucket: this.input.bucket,
ACL: this.input?.acl || 'public-read',
};
return new Promise((resolve, reject) => {
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err, data) => {
if (err) {
console.log('Error', err);
reject(err);
}
if (data) {
resolve(data.Location);
}
});
});
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
}
// TODO - implement
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
public async fileRead(key: string): Promise<any> {
return new Promise((resolve, reject) => {
this.s3Client.getObject({ Key: key } as any, (err, data) => {
if (err) {
return reject(err);
}
if (!data?.Body) {
return reject(data);
}
return resolve(data.Body);
});
});
}
public async init(): Promise<any> {
const s3Options: any = {
params: { Bucket: this.input.bucket },
const s3Options: S3ClientConfig = {
region: this.input.region,
credentials: {
accessKeyId: this.input.access_key,
secretAccessKey: this.input.access_secret,
},
endpoint: `https://${this.input.region}.linodeobjects.com`,
};
s3Options.accessKeyId = this.input.access_key;
s3Options.secretAccessKey = this.input.access_secret;
s3Options.endpoint = new AWS.Endpoint(
`${this.input.region}.linodeobjects.com`,
);
this.s3Client = new AWS.S3(s3Options);
}
public async test(): Promise<boolean> {
try {
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});
await promisify(fs.unlink)(tempFile);
return true;
} catch (e) {
throw e;
}
this.s3Client = new S3Client(s3Options);
}
}

9
packages/nocodb/src/plugins/linode/index.ts

@ -5,7 +5,7 @@ import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: LinodeObjectStoragePlugin,
title: 'Linode Object Storage',
version: '0.0.1',
version: '0.0.2',
logo: 'plugins/linode.svg',
tags: 'Storage',
description:
@ -41,6 +41,13 @@ const config: XcPluginConfig = {
type: XcType.Password,
required: true,
},
{
key: 'acl',
label: 'Access Control Lists (ACL)',
placeholder: 'Default set to public-read',
type: XcType.SingleLineText,
required: false,
},
],
actions: [
{

243
packages/nocodb/src/plugins/mino/Minio.ts

@ -1,141 +1,182 @@
import fs from 'fs';
import { promisify } from 'util';
import { Readable } from 'stream';
import { Client as MinioClient } from 'minio';
import axios from 'axios';
import { useAgent } from 'request-filtering-agent';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
import { generateTempFilePath, waitForStreamClose } from '~/utils/pluginUtils';
export default class Minio implements IStorageAdapterV2 {
private minioClient: MinioClient;
private input: any;
constructor(input: any) {
this.input = input;
interface MinioObjectStorageInput {
bucket: string;
access_key: string;
access_secret: string;
useSSL?: boolean;
endPoint: string;
port: number;
}
async fileCreate(key: string, file: XcFile): Promise<any> {
const fileStream = fs.createReadStream(file.path);
return this.fileCreateByStream(key, fileStream, {
mimetype: file?.mimetype,
});
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
export default class Minio implements IStorageAdapterV2 {
private minioClient: MinioClient;
private input: MinioObjectStorageInput;
public async fileRead(key: string): Promise<any> {
return new Promise((resolve, reject) => {
this.minioClient.getObject(this.input.bucket, key, (err, data) => {
if (err) {
return reject(err);
}
if (!data) {
return reject(data);
}
return resolve(data);
});
});
constructor(input: unknown) {
this.input = input as MinioObjectStorageInput;
}
public async init(): Promise<any> {
// todo: update in ui(checkbox and number field)
this.input.port = +this.input.port || 9000;
this.input.useSSL = this.input.useSSL === true;
this.input.accessKey = this.input.access_key;
this.input.secretKey = this.input.access_secret;
const minioOptions = {
port: +this.input.port || 9000,
endPoint: this.input.endPoint,
useSSL: this.input.useSSL === true,
accessKey: this.input.access_key,
secretKey: this.input.access_secret,
};
this.minioClient = new MinioClient(this.input);
this.minioClient = new MinioClient(minioOptions);
}
public async test(): Promise<boolean> {
try {
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: '',
originalname: 'temp.txt',
size: '',
});
await promisify(fs.unlink)(tempFile);
const createStream = Readable.from(['Hello from Minio, NocoDB']);
await this.fileCreateByStream('nc-test-file.txt', createStream);
return true;
} catch (e) {
throw e;
}
}
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
};
public async fileRead(key: string): Promise<any> {
const data = await this.minioClient.getObject(this.input.bucket, key);
return new Promise((resolve, reject) => {
axios
.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer',
})
.then((response) => {
uploadParams.Body = response.data;
uploadParams.Key = key;
uploadParams.ContentType = response.headers['content-type'];
const metaData = {
// 'Content-Type': file.mimetype
// 'X-Amz-Meta-Testing': 1234,
// 'run': 5678
};
// call S3 to retrieve upload file to specified bucket
this.minioClient
.putObject(this.input?.bucket, key, response.data, metaData)
.then(() => {
resolve({
url: `http${this.input.useSSL ? 's' : ''}://${
this.input.endPoint
}:${this.input.port}/${this.input.bucket}/${key}`,
data: response.data,
const chunks: any[] = [];
data.on('data', (chunk) => {
chunks.push(chunk);
});
})
.catch(reject);
})
.catch((error) => {
reject(error);
data.on('end', () => {
const buffer = Buffer.concat(chunks);
resolve(buffer);
});
data.on('error', (err) => {
reject(err);
});
});
}
async fileCreate(key: string, file: XcFile): Promise<any> {
const fileStream = fs.createReadStream(file.path);
return this.fileCreateByStream(key, fileStream, {
mimetype: file?.mimetype,
});
}
async fileCreateByStream(
key: string,
stream: Readable,
options?: {
mimetype?: string;
size?: number;
},
): Promise<any> {
return new Promise((resolve, reject) => {
// uploadParams.Body = fileStream;
// uploadParams.Key = key;
const metaData = {
'Content-Type': options?.mimetype,
// 'X-Amz-Meta-Testing': 1234,
// 'run': 5678
try {
const streamError = new Promise<void>((_, reject) => {
stream.on('error', (err) => {
reject(err);
});
});
const uploadParams = {
Key: key,
Body: stream,
metaData: {
ContentType: options?.mimetype,
size: options?.size,
},
};
const upload = this.upload(uploadParams);
return await Promise.race([upload, streamError]);
} catch (error) {
throw error;
}
}
async fileCreateByUrl(
key: string,
url: string,
{ fetchOptions: { buffer } = { buffer: false } },
): Promise<any> {
const response = await axios.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
responseType: buffer ? 'arraybuffer' : 'stream',
});
const uploadParams = {
ACL: 'public-read',
Key: key,
Body: response.data,
metaData: {
ContentType: response.headers['content-type'],
},
};
// call S3 to retrieve upload file to specified bucket
this.minioClient
.putObject(this.input?.bucket, key, stream, metaData)
.then(() => {
resolve(
`http${this.input.useSSL ? 's' : ''}://${this.input.endPoint}:${
const responseUrl = this.upload(uploadParams);
return {
url: responseUrl,
data: response.data,
};
}
private async upload(uploadParams: {
Key: string;
Body: Readable;
metaData: { [key: string]: string | number };
}): Promise<any> {
try {
this.minioClient.putObject(
this.input.bucket,
uploadParams.Key,
uploadParams.Body,
uploadParams.metaData as any,
);
if (this.input.useSSL && this.input.port === 443) {
return `https://${this.input.endPoint}/${uploadParams.Key}`;
} else if (!this.input.useSSL && this.input.port === 80) {
return `http://${this.input.endPoint}/${uploadParams.Key}`;
} else {
return `http${this.input.useSSL ? 's' : ''}://${this.input.endPoint}:${
this.input.port
}/${this.input.bucket}/${key}`,
}/${uploadParams.Key}`;
}
} catch (error) {
console.error('Error uploading file', error);
throw error;
}
}
public async getSignedUrl(
key,
expiresInSeconds = 7200,
pathParameters?: { [key: string]: string },
) {
if (
key.startsWith(`${this.input.bucket}/nc/uploads`) ||
key.startsWith(`${this.input.bucket}/nc/thumbnails`)
) {
key = key.replace(`${this.input.bucket}/`, '');
}
return this.minioClient.presignedGetObject(
this.input.bucket,
key,
expiresInSeconds,
pathParameters,
);
})
.catch(reject);
});
}
// TODO - implement
@ -147,4 +188,8 @@ export default class Minio implements IStorageAdapterV2 {
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
}

168
packages/nocodb/src/plugins/ovhCloud/OvhCloud.ts

@ -1,156 +1,42 @@
import fs from 'fs';
import { promisify } from 'util';
import AWS from 'aws-sdk';
import axios from 'axios';
import { useAgent } from 'request-filtering-agent';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
import { generateTempFilePath, waitForStreamClose } from '~/utils/pluginUtils';
import { S3 as S3Client } from '@aws-sdk/client-s3';
import type { S3ClientConfig } from '@aws-sdk/client-s3';
import type { IStorageAdapterV2 } from 'nc-plugin';
import GenericS3 from '~/plugins/GenericS3/GenericS3';
export default class OvhCloud implements IStorageAdapterV2 {
private s3Client: AWS.S3;
private input: any;
constructor(input: any) {
this.input = input;
interface OvhCloudStorageInput {
bucket: string;
region: string;
access_key: string;
access_secret: string;
acl?: string;
}
async fileCreate(key: string, file: XcFile): Promise<any> {
const fileStream = fs.createReadStream(file.path);
return this.fileCreateByStream(key, fileStream, {
mimetype: file?.mimetype,
});
}
export default class OvhCloud extends GenericS3 implements IStorageAdapterV2 {
protected input: OvhCloudStorageInput;
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
};
return new Promise((resolve, reject) => {
axios
.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer',
})
.then((response) => {
uploadParams.Body = response.data;
uploadParams.Key = key;
uploadParams.ContentType = response.headers['content-type'];
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
if (err1) {
console.log('Error', err1);
reject(err1);
}
if (data) {
resolve({
url: data.Location,
data: response.data,
});
}
});
})
.catch((error) => {
reject(error);
});
});
constructor(input: unknown) {
super(input as OvhCloudStorageInput);
}
async fileCreateByStream(
key: string,
stream: Readable,
options?: {
mimetype?: string;
},
): Promise<void> {
const uploadParams: any = {
ACL: 'public-read',
// ContentType: file.mimetype,
protected get defaultParams() {
return {
Bucket: this.input.bucket,
ACL: this.input?.acl || 'public-read',
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
uploadParams.Body = stream;
uploadParams.Key = key;
uploadParams.ContentType =
options?.mimetype || 'application/octet-stream';
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err, data) => {
if (err) {
console.log('Error', err);
reject(err);
}
if (data) {
resolve(data.Location);
}
});
});
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
}
// TODO - implement
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
public async fileRead(key: string): Promise<any> {
return new Promise((resolve, reject) => {
this.s3Client.getObject({ Key: key } as any, (err, data) => {
if (err) {
return reject(err);
}
if (!data?.Body) {
return reject(data);
}
return resolve(data.Body);
});
});
}
public async init(): Promise<any> {
const s3Options: any = {
params: { Bucket: this.input.bucket },
const s3Options: S3ClientConfig = {
region: this.input.region,
credentials: {
accessKeyId: this.input.access_key,
secretAccessKey: this.input.access_secret,
},
// TODO: Need to verify
// DOCS s3.<region_in_lowercase>.io.cloud.ovh.net
endpoint: `https://s3.${this.input.region}.cloud.ovh.net`,
};
s3Options.accessKeyId = this.input.access_key;
s3Options.secretAccessKey = this.input.access_secret;
s3Options.endpoint = new AWS.Endpoint(
`s3.${this.input.region}.cloud.ovh.net`,
);
this.s3Client = new AWS.S3(s3Options);
}
public async test(): Promise<boolean> {
try {
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});
await promisify(fs.unlink)(tempFile);
return true;
} catch (e) {
throw e;
}
this.s3Client = new S3Client(s3Options);
}
}

9
packages/nocodb/src/plugins/ovhCloud/index.ts

@ -5,7 +5,7 @@ import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: OvhCloud,
title: 'OvhCloud Object Storage',
version: '0.0.1',
version: '0.0.2',
logo: 'plugins/ovhCloud.png',
tags: 'Storage',
description:
@ -41,6 +41,13 @@ const config: XcPluginConfig = {
type: XcType.Password,
required: true,
},
{
key: 'acl',
label: 'Access Control Lists (ACL)',
placeholder: 'Default set to public-read',
type: XcType.SingleLineText,
required: false,
},
],
actions: [
{

42
packages/nocodb/src/plugins/r2/R2.ts

@ -0,0 +1,42 @@
import { S3 as S3Client } from '@aws-sdk/client-s3';
import type { S3ClientConfigType } from '@aws-sdk/client-s3';
import type { IStorageAdapterV2 } from 'nc-plugin';
import GenericS3 from '~/plugins/GenericS3/GenericS3';
interface R2ObjectStorageInput {
bucket: string;
access_key: string;
access_secret: string;
hostname: string;
region: string;
}
export default class R2 extends GenericS3 implements IStorageAdapterV2 {
protected input: R2ObjectStorageInput;
constructor(input: unknown) {
super(input as R2ObjectStorageInput);
}
protected get defaultParams() {
return {
Bucket: this.input.bucket,
// R2 does not support ACL
ACL: 'private',
};
}
public async init(): Promise<any> {
const s3Options: S3ClientConfigType = {
region: 'auto',
endpoint: this.input.hostname,
credentials: {
accessKeyId: this.input.access_key,
secretAccessKey: this.input.access_secret,
},
};
this.s3Client = new S3Client(s3Options);
}
}

18
packages/nocodb/src/plugins/r2/R2Plugin.ts

@ -0,0 +1,18 @@
import { XcStoragePlugin } from 'nc-plugin';
import R2 from './R2';
import type { IStorageAdapterV2 } from 'nc-plugin';
class R2Plugin extends XcStoragePlugin {
private static storageAdapter: R2;
public getAdapter(): IStorageAdapterV2 {
return R2Plugin.storageAdapter;
}
public async init(config: any): Promise<any> {
R2Plugin.storageAdapter = new R2(config);
await R2Plugin.storageAdapter.init();
}
}
export default R2Plugin;

68
packages/nocodb/src/plugins/r2/index.ts

@ -0,0 +1,68 @@
import { XcActionType, XcType } from 'nocodb-sdk';
import R2Plugin from './R2Plugin';
import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: R2Plugin,
title: 'Cloudflare R2 Storage',
version: '0.0.1',
logo: 'plugins/r2.png',
description:
'Cloudflare R2 is an S3-compatible, zero egress-fee, globally distributed object storage.',
tags: 'Storage',
inputs: {
title: 'Configure Cloudflare R2 Storage',
items: [
{
key: 'bucket',
label: 'Bucket Name',
placeholder: 'Bucket Name',
type: XcType.SingleLineText,
required: true,
},
{
key: 'hostname',
label: 'Host Name',
placeholder: 'e.g.: *****.r2.cloudflarestorage.com',
type: XcType.SingleLineText,
required: true,
},
{
key: 'access_key',
label: 'Access Key',
placeholder: 'Access Key',
type: XcType.SingleLineText,
required: true,
},
{
key: 'access_secret',
label: 'Access Secret',
placeholder: 'Access Secret',
type: XcType.Password,
required: true,
},
],
actions: [
{
label: 'Test',
placeholder: 'Test',
key: 'test',
actionType: XcActionType.TEST,
type: XcType.Button,
},
{
label: 'Save',
placeholder: 'Save',
key: 'save',
actionType: XcActionType.SUBMIT,
type: XcType.Button,
},
],
msgOnInstall:
'Successfully installed and attachment will be stored in Cloudflare R2 Storage',
msgOnUninstall: '',
},
category: 'Storage',
};
export default config;

183
packages/nocodb/src/plugins/s3/S3.ts

@ -1,163 +1,33 @@
import fs from 'fs';
import { promisify } from 'util';
import { GetObjectCommand, S3 as S3Client } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { S3 as S3Client } from '@aws-sdk/client-s3';
import { Upload } from '@aws-sdk/lib-storage';
import axios from 'axios';
import { useAgent } from 'request-filtering-agent';
import type { S3ClientConfig } from '@aws-sdk/client-s3';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
import { generateTempFilePath, waitForStreamClose } from '~/utils/pluginUtils';
import type { IStorageAdapterV2 } from 'nc-plugin';
import GenericS3 from '~/plugins/GenericS3/GenericS3';
export default class S3 implements IStorageAdapterV2 {
private s3Client: S3Client;
private input: any;
interface S3Input {
bucket: string;
region: string;
access_key: string;
access_secret: string;
endpoint?: string;
acl?: string;
}
export default class S3 extends GenericS3 implements IStorageAdapterV2 {
protected input: S3Input;
constructor(input: any) {
this.input = input;
super(input as S3Input);
}
get defaultParams() {
return {
ACL: 'private',
ACL: this.input.acl || 'private',
Bucket: this.input.bucket,
};
}
async fileCreate(key: string, file: XcFile): Promise<any> {
// create file stream
const fileStream = fs.createReadStream(file.path);
// upload using stream
return this.fileCreateByStream(key, fileStream, {
mimetype: file?.mimetype,
});
}
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
...this.defaultParams,
};
try {
const response = await axios.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO add an extra options argument to pass responseType & use stream for non-image files
responseType: 'arraybuffer',
});
uploadParams.Body = response.data;
uploadParams.Key = key;
uploadParams.ContentType = response.headers['content-type'];
const data = await this.upload(uploadParams);
return {
url: data,
data: response.data,
};
} catch (error) {
throw error;
}
}
fileCreateByStream(
key: string,
stream: Readable,
options?: {
mimetype?: string;
},
): Promise<void> {
const uploadParams: any = {
...this.defaultParams,
};
return new Promise((resolve, reject) => {
stream.on('error', (err) => {
console.log('File Error', err);
reject(err);
});
uploadParams.Body = stream;
uploadParams.Key = key;
uploadParams.ContentType =
options?.mimetype || 'application/octet-stream';
// call S3 to upload file to specified bucket
this.upload(uploadParams)
.then((data) => {
resolve(data);
})
.catch((err) => {
reject(err);
});
});
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
}
// TODO - implement
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
public async fileRead(key: string): Promise<any> {
const command = new GetObjectCommand({
Key: key,
Bucket: this.input.bucket,
});
const { Body } = await this.s3Client.send(command);
const fileStream = Body as Readable;
return new Promise((resolve, reject) => {
const chunks: any[] = [];
fileStream.on('data', (chunk) => {
chunks.push(chunk);
});
fileStream.on('end', () => {
const buffer = Buffer.concat(chunks);
resolve(buffer);
});
fileStream.on('error', (err) => {
reject(err);
});
});
}
public async getSignedUrl(
key,
expiresInSeconds = 7200,
pathParameters?: { [key: string]: string },
) {
const command = new GetObjectCommand({
Key: key,
Bucket: this.input.bucket,
...pathParameters,
});
return getSignedUrl(this.s3Client, command, {
expiresIn: expiresInSeconds,
});
}
public async init(): Promise<any> {
// const s3Options: any = {
// params: {Bucket: process.env.NC_S3_BUCKET},
// region: process.env.NC_S3_REGION
// };
//
// s3Options.accessKeyId = process.env.NC_S3_KEY;
// s3Options.secretAccessKey = process.env.NC_S3_SECRET;
const s3Options: S3ClientConfig = {
region: this.input.region,
credentials: {
@ -173,27 +43,8 @@ export default class S3 implements IStorageAdapterV2 {
this.s3Client = new S3Client(s3Options);
}
public async test(): Promise<boolean> {
try {
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});
await promisify(fs.unlink)(tempFile);
return true;
} catch (e) {
throw e;
}
}
private async upload(uploadParams): Promise<any> {
protected async upload(uploadParams): Promise<any> {
try {
// call S3 to retrieve upload file to specified bucket
const upload = new Upload({
client: this.s3Client,
params: { ...this.defaultParams, ...uploadParams },

9
packages/nocodb/src/plugins/s3/index.ts

@ -5,7 +5,7 @@ import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: S3Plugin,
title: 'S3',
version: '0.0.2',
version: '0.0.3',
logo: 'plugins/s3.png',
description:
'Amazon Simple Storage Service (Amazon S3) is an object storage service that offers industry-leading scalability, data availability, security, and performance.',
@ -47,6 +47,13 @@ const config: XcPluginConfig = {
type: XcType.Password,
required: true,
},
{
key: 'acl',
label: 'Access Control Lists (ACL)',
placeholder: 'Default set to private',
type: XcType.SingleLineText,
required: false,
},
],
actions: [
{

161
packages/nocodb/src/plugins/scaleway/ScalewayObjectStorage.ts

@ -1,149 +1,44 @@
import fs from 'fs';
import { promisify } from 'util';
import AWS from 'aws-sdk';
import axios from 'axios';
import { useAgent } from 'request-filtering-agent';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
import { generateTempFilePath, waitForStreamClose } from '~/utils/pluginUtils';
import { S3 as S3Client } from '@aws-sdk/client-s3';
import type { S3ClientConfig } from '@aws-sdk/client-s3';
export default class ScalewayObjectStorage implements IStorageAdapterV2 {
private s3Client: AWS.S3;
private input: any;
import type { IStorageAdapterV2 } from 'nc-plugin';
import GenericS3 from '~/plugins/GenericS3/GenericS3';
constructor(input: any) {
this.input = input;
interface ScalewayObjectStorageInput {
bucket: string;
region: string;
access_key: string;
access_secret: string;
acl?: string;
}
public async fileRead(key: string): Promise<any> {
return new Promise((resolve, reject) => {
this.s3Client.getObject({ Key: key } as any, (err, data) => {
if (err) {
return reject(err);
}
if (!data?.Body) {
return reject(data);
}
return resolve(data.Body);
});
});
}
export default class ScalewayObjectStorage
extends GenericS3
implements IStorageAdapterV2
{
protected input: ScalewayObjectStorageInput;
public async test(): Promise<boolean> {
try {
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});
await promisify(fs.unlink)(tempFile);
return true;
} catch (e) {
throw e;
}
constructor(input: unknown) {
super(input as ScalewayObjectStorageInput);
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
protected get defaultParams() {
return {
Bucket: this.input.bucket,
ACL: this.input?.acl || 'public-read',
};
}
public async init(): Promise<any> {
const s3Options: any = {
params: { Bucket: this.input.bucket },
const s3Options: S3ClientConfig = {
region: this.input.region,
};
s3Options.accessKeyId = this.input.access_key;
s3Options.secretAccessKey = this.input.access_secret;
s3Options.endpoint = new AWS.Endpoint(`s3.${this.input.region}.scw.cloud`);
this.s3Client = new AWS.S3(s3Options);
}
async fileCreate(key: string, file: XcFile): Promise<any> {
const fileStream = fs.createReadStream(file.path);
return this.fileCreateByStream(key, fileStream, {
mimetype: file?.mimetype,
});
}
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
};
return new Promise((resolve, reject) => {
axios
.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer',
})
.then((response) => {
uploadParams.Body = response.data;
uploadParams.Key = key;
uploadParams.ContentType = response.headers['content-type'];
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
if (err1) {
console.log('Error', err1);
reject(err1);
}
if (data) {
resolve({
url: data.Location,
data: response.data,
});
}
});
})
.catch((error) => {
reject(error);
});
});
}
async fileCreateByStream(
key: string,
stream: Readable,
options?: {
mimetype?: string;
credentials: {
accessKeyId: this.input.access_key,
secretAccessKey: this.input.access_secret,
},
): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
Body: stream,
Key: key,
ContentType: options?.mimetype || 'application/octet-stream',
endpoint: `https://s3.${this.input.region}.scw.cloud`,
};
return new Promise((resolve, reject) => {
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err, data) => {
if (err) {
console.log('Error', err);
reject(err);
}
if (data) {
resolve(data.Location);
}
});
});
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
}
// TODO - implement
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
this.s3Client = new S3Client(s3Options);
}
}

9
packages/nocodb/src/plugins/scaleway/index.ts

@ -5,7 +5,7 @@ import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: ScalewayObjectStoragePlugin,
title: 'Scaleway Object Storage',
version: '0.0.1',
version: '0.0.2',
logo: 'plugins/scaleway.png',
tags: 'Storage',
description:
@ -41,6 +41,13 @@ const config: XcPluginConfig = {
type: XcType.Password,
required: true,
},
{
key: 'acl',
label: 'Access Control Lists (ACL)',
placeholder: 'Default set to public-read',
type: XcType.SingleLineText,
required: false,
},
],
actions: [
{

17
packages/nocodb/src/plugins/ses/SES.ts

@ -1,5 +1,6 @@
import nodemailer from 'nodemailer';
import AWS from 'aws-sdk';
import { SendRawEmailCommand, SES as SESClient } from '@aws-sdk/client-ses';
import type { IEmailAdapter } from 'nc-plugin';
import type Mail from 'nodemailer/lib/mailer';
import type { XcEmail } from '~/interface/IEmailAdapter';
@ -13,14 +14,20 @@ export default class SES implements IEmailAdapter {
}
public async init(): Promise<any> {
const sesOptions: any = {
const ses = new SESClient({
apiVersion: '2006-03-01',
region: this.input.region,
credentials: {
accessKeyId: this.input.access_key,
secretAccessKey: this.input.access_secret,
region: this.input.region,
};
},
});
this.transporter = nodemailer.createTransport({
SES: new AWS.SES(sesOptions),
SES: {
ses,
aws: { SendRawEmailCommand },
},
});
}

172
packages/nocodb/src/plugins/spaces/Spaces.ts

@ -1,159 +1,41 @@
import fs from 'fs';
import { promisify } from 'util';
import AWS from 'aws-sdk';
import axios from 'axios';
import { useAgent } from 'request-filtering-agent';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
import { generateTempFilePath, waitForStreamClose } from '~/utils/pluginUtils';
import { S3 as S3Client } from '@aws-sdk/client-s3';
import type { S3ClientConfig } from '@aws-sdk/client-s3';
import type { IStorageAdapterV2 } from 'nc-plugin';
import GenericS3 from '~/plugins/GenericS3/GenericS3';
export default class Spaces implements IStorageAdapterV2 {
private s3Client: AWS.S3;
private input: any;
constructor(input: any) {
this.input = input;
interface SpacesObjectStorageInput {
bucket: string;
region: string;
access_key: string;
access_secret: string;
acl?: string;
}
async fileCreate(key: string, file: XcFile): Promise<any> {
const fileStream = fs.createReadStream(file.path);
return this.fileCreateByStream(key, fileStream, {
mimetype: file?.mimetype,
});
}
export default class Spaces extends GenericS3 implements IStorageAdapterV2 {
protected input: SpacesObjectStorageInput;
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
};
return new Promise((resolve, reject) => {
axios
.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer',
})
.then((response) => {
uploadParams.Body = response.data;
uploadParams.Key = key;
uploadParams.ContentType = response.headers['content-type'];
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
if (err1) {
console.log('Error', err1);
reject(err1);
}
if (data) {
resolve({
url: data.Location,
data: response.data,
});
}
});
})
.catch((error) => {
reject(error);
});
});
constructor(input: unknown) {
super(input as SpacesObjectStorageInput);
}
async fileCreateByStream(
key: string,
stream: Readable,
options?: {
mimetype?: string;
},
): Promise<void> {
const uploadParams: any = {
ACL: 'public-read',
Body: stream,
Key: key,
ContentType: options?.mimetype || 'application/octet-stream',
protected get defaultParams() {
return {
Bucket: this.input.bucket,
ACL: this.input?.acl || 'public-read',
};
return new Promise((resolve, reject) => {
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err, data) => {
if (err) {
console.log('Error', err);
reject(err);
}
if (data) {
resolve(data.Location);
}
});
});
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
}
// TODO - implement
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
public async fileRead(key: string): Promise<any> {
return new Promise((resolve, reject) => {
this.s3Client.getObject({ Key: key } as any, (err, data) => {
if (err) {
return reject(err);
}
if (!data?.Body) {
return reject(data);
}
return resolve(data.Body);
});
});
}
public async init(): Promise<any> {
// const s3Options: any = {
// params: {Bucket: process.env.NC_S3_BUCKET},
// region: process.env.NC_S3_REGION
// };
//
// s3Options.accessKeyId = process.env.NC_S3_KEY;
// s3Options.secretAccessKey = process.env.NC_S3_SECRET;
const s3Options: any = {
params: { Bucket: this.input.bucket },
region: this.input.region,
const s3Options: S3ClientConfig = {
region: 'us-east-1',
forcePathStyle: false,
credentials: {
accessKeyId: this.input.access_key,
secretAccessKey: this.input.access_secret,
},
endpoint: `https://${this.input.region || 'nyc3'}.digitaloceanspaces.com`,
};
s3Options.accessKeyId = this.input.access_key;
s3Options.secretAccessKey = this.input.access_secret;
s3Options.endpoint = new AWS.Endpoint(
`${this.input.region || 'nyc3'}.digitaloceanspaces.com`,
);
this.s3Client = new AWS.S3(s3Options);
}
public async test(): Promise<boolean> {
try {
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});
await promisify(fs.unlink)(tempFile);
return true;
} catch (e) {
throw e;
}
this.s3Client = new S3Client(s3Options);
}
}

7
packages/nocodb/src/plugins/spaces/index.ts

@ -43,6 +43,13 @@ const config: XcPluginConfig = {
type: XcType.Password,
required: true,
},
{
key: 'acl',
label: 'Access Control Lists (ACL)',
placeholder: 'Default set to public-read',
type: XcType.SingleLineText,
required: false,
},
],
actions: [
{

8
packages/nocodb/src/plugins/storage/Local.ts

@ -24,12 +24,16 @@ export default class Local implements IStorageAdapterV2 {
}
}
async fileCreateByUrl(key: string, url: string): Promise<any> {
async fileCreateByUrl(
key: string,
url: string,
{ fetchOptions: { buffer } = { buffer: false } },
): Promise<any> {
const destPath = validateAndNormaliseLocalPath(key);
return new Promise((resolve, reject) => {
axios
.get(url, {
responseType: 'arraybuffer',
responseType: buffer ? 'arraybuffer' : 'stream',
headers: {
accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',

164
packages/nocodb/src/plugins/upcloud/UpoCloud.ts

@ -1,149 +1,45 @@
import fs from 'fs';
import { promisify } from 'util';
import AWS from 'aws-sdk';
import axios from 'axios';
import { useAgent } from 'request-filtering-agent';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
import { generateTempFilePath, waitForStreamClose } from '~/utils/pluginUtils';
import { S3 as S3Client } from '@aws-sdk/client-s3';
import type { S3ClientConfig } from '@aws-sdk/client-s3';
import type { IStorageAdapterV2 } from 'nc-plugin';
import GenericS3 from '~/plugins/GenericS3/GenericS3';
export default class UpoCloud implements IStorageAdapterV2 {
private s3Client: AWS.S3;
private input: any;
constructor(input: any) {
this.input = input;
interface UpoCloudStorgeInput {
bucket: string;
region: string;
access_key: string;
access_secret: string;
endpoint: string;
acl?: string;
}
async fileCreate(key: string, file: XcFile): Promise<any> {
const fileStream = fs.createReadStream(file.path);
return this.fileCreateByStream(key, fileStream, {
mimetype: file?.mimetype,
});
}
export default class UpoCloud extends GenericS3 implements IStorageAdapterV2 {
protected input: UpoCloudStorgeInput;
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
};
return new Promise((resolve, reject) => {
axios
.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer',
})
.then((response) => {
uploadParams.Body = response.data;
uploadParams.Key = key;
uploadParams.ContentType = response.headers['content-type'];
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
if (err1) {
console.log('Error', err1);
reject(err1);
}
if (data) {
resolve({
url: data.Location,
data: response.data,
});
}
});
})
.catch((error) => {
reject(error);
});
});
constructor(input: unknown) {
super(input as UpoCloudStorgeInput);
}
async fileCreateByStream(
key: string,
stream: Readable,
options?: {
mimetype?: string;
},
): Promise<void> {
const uploadParams: any = {
ACL: 'public-read',
Key: key,
Body: stream,
ContentType: options?.mimetype || 'application/octet-stream',
protected get defaultParams() {
return {
Bucket: this.input.bucket,
ACL: this.input?.acl || 'public-read',
};
return new Promise((resolve, reject) => {
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err, data) => {
if (err) {
console.log('Error', err);
reject(err);
}
if (data) {
resolve(data.Location);
}
});
});
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
}
// TODO - implement
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
public async fileRead(key: string): Promise<any> {
return new Promise((resolve, reject) => {
this.s3Client.getObject({ Key: key } as any, (err, data) => {
if (err) {
return reject(err);
}
if (!data?.Body) {
return reject(data);
}
return resolve(data.Body);
});
});
}
public async init(): Promise<any> {
const s3Options: any = {
params: { Bucket: this.input.bucket },
const updatedEndpoint = this.input.endpoint.startsWith('https://')
? this.input.endpoint
: `https://${this.input.endpoint}`;
const s3Options: S3ClientConfig = {
region: this.input.region,
credentials: {
accessKeyId: this.input.access_key,
secretAccessKey: this.input.access_secret,
},
endpoint: updatedEndpoint,
};
s3Options.accessKeyId = this.input.access_key;
s3Options.secretAccessKey = this.input.access_secret;
s3Options.endpoint = new AWS.Endpoint(this.input.endpoint);
this.s3Client = new AWS.S3(s3Options);
}
public async test(): Promise<boolean> {
try {
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});
await promisify(fs.unlink)(tempFile);
return true;
} catch (e) {
throw e;
}
this.s3Client = new S3Client(s3Options);
}
}

9
packages/nocodb/src/plugins/upcloud/index.ts

@ -5,7 +5,7 @@ import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: UpCloudPlugin,
title: 'UpCloud Object Storage',
version: '0.0.1',
version: '0.0.2',
logo: 'plugins/upcloud.png',
description:
'The perfect home for your data. Thanks to the S3-compatible programmable interface,\n' +
@ -42,6 +42,13 @@ const config: XcPluginConfig = {
type: XcType.Password,
required: true,
},
{
key: 'acl',
label: 'Access Control Lists (ACL)',
placeholder: 'Default set to public-read',
type: XcType.SingleLineText,
required: false,
},
],
actions: [
{

160
packages/nocodb/src/plugins/vultr/Vultr.ts

@ -1,149 +1,41 @@
import fs from 'fs';
import { promisify } from 'util';
import AWS from 'aws-sdk';
import axios from 'axios';
import { useAgent } from 'request-filtering-agent';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
import { generateTempFilePath, waitForStreamClose } from '~/utils/pluginUtils';
import { S3 as S3Client } from '@aws-sdk/client-s3';
import type { S3ClientConfig } from '@aws-sdk/client-s3';
import type { IStorageAdapterV2 } from 'nc-plugin';
import GenericS3 from '~/plugins/GenericS3/GenericS3';
export default class Vultr implements IStorageAdapterV2 {
private s3Client: AWS.S3;
private input: any;
constructor(input: any) {
this.input = input;
interface VultrObjectStorageInput {
bucket: string;
region: string;
access_key: string;
hostname: string;
access_secret: string;
acl?: string;
}
async fileCreate(key: string, file: XcFile): Promise<any> {
const fileStream = fs.createReadStream(file.path);
export default class Vultr extends GenericS3 implements IStorageAdapterV2 {
protected input: VultrObjectStorageInput;
return this.fileCreateByStream(key, fileStream, {
mimetype: file?.mimetype,
});
constructor(input: unknown) {
super(input as VultrObjectStorageInput);
}
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
protected get defaultParams() {
return {
Bucket: this.input.bucket,
ACL: this.input?.acl || 'public-read',
};
return new Promise((resolve, reject) => {
axios
.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer',
})
.then((response) => {
uploadParams.Body = response.data;
uploadParams.Key = key;
uploadParams.ContentType = response.headers['content-type'];
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
if (err1) {
console.log('Error', err1);
reject(err1);
}
if (data) {
resolve({
url: data.Location,
data: response.data,
});
}
});
})
.catch((error) => {
reject(error);
});
});
}
async fileCreateByStream(
key: string,
stream: Readable,
options?: {
mimetype: string;
},
): Promise<void> {
const uploadParams: any = {
ACL: 'public-read',
Body: stream,
Key: key,
ContentType: options?.mimetype || 'application/octet-stream',
};
return new Promise((resolve, reject) => {
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err, data) => {
if (err) {
console.log('Error', err);
reject(err);
}
if (data) {
resolve(data.Location);
}
});
});
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
}
// TODO - implement
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
public async fileRead(key: string): Promise<any> {
return new Promise((resolve, reject) => {
this.s3Client.getObject({ Key: key } as any, (err, data) => {
if (err) {
return reject(err);
}
if (!data?.Body) {
return reject(data);
}
return resolve(data.Body);
});
});
}
public async init(): Promise<any> {
const s3Options: any = {
params: { Bucket: this.input.bucket },
const s3Options: S3ClientConfig = {
region: this.input.region,
credentials: {
accessKeyId: this.input.access_key,
secretAccessKey: this.input.access_secret,
},
endpoint: this.input.hostname,
};
s3Options.accessKeyId = this.input.access_key;
s3Options.secretAccessKey = this.input.access_secret;
s3Options.endpoint = new AWS.Endpoint(this.input.hostname);
this.s3Client = new AWS.S3(s3Options);
}
public async test(): Promise<boolean> {
try {
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});
await promisify(fs.unlink)(tempFile);
return true;
} catch (e) {
throw e;
}
this.s3Client = new S3Client(s3Options);
}
}

9
packages/nocodb/src/plugins/vultr/index.ts

@ -5,7 +5,7 @@ import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: VultrPlugin,
title: 'Vultr Object Storage',
version: '0.0.2',
version: '0.0.3',
logo: 'plugins/vultr.png',
description:
'Using Vultr Object Storage can give flexibility and cloud storage that allows applications greater flexibility and access worldwide.',
@ -41,6 +41,13 @@ const config: XcPluginConfig = {
type: XcType.Password,
required: true,
},
{
key: 'acl',
label: 'Access Control Lists (ACL)',
placeholder: 'Default set to public-read',
type: XcType.SingleLineText,
required: false,
},
],
actions: [
{

6
packages/nocodb/src/services/attachments.service.ts

@ -208,6 +208,12 @@ export class AttachmentsService {
await storageAdapter.fileCreateByUrl(
slash(path.join(destPath, fileName)),
finalUrl,
{
fetchOptions: {
// The sharp requires image to be passed as buffer.);
buffer: mimeType.includes('image'),
},
},
);
const tempMetadata: {

2220
pnpm-lock.yaml

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save