Skip to content

Commit d44694b

Browse files
authoredSep 27, 2024··
Merge pull request #744 from encorearon/feature/aliyun-oss
supporting aliyun oss data source
2 parents 5cbb9eb + 1b05a0e commit d44694b

File tree

9 files changed

+389
-0
lines changed

9 files changed

+389
-0
lines changed
 

‎server/node-service/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"build": "rm -rf build/ && yarn test && tsc && yarn copy"
1717
},
1818
"devDependencies": {
19+
"@types/ali-oss": "^6.16.11",
1920
"@types/jest": "^29.2.4",
2021
"commander": "^10.0.0",
2122
"copyfiles": "^2.4.1",
@@ -46,6 +47,7 @@
4647
"@types/morgan": "^1.9.3",
4748
"@types/node": "^20.1.1",
4849
"@types/node-fetch": "^2.6.2",
50+
"ali-oss": "^6.20.0",
4951
"axios": "^1.7.7",
5052
"base64-arraybuffer": "^1.0.2",
5153
"bluebird": "^3.7.2",
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { ConfigToType } from "lowcoder-sdk/dataSource";
2+
import { AliyunOssI18nTranslator } from "./i18n";
3+
4+
const getDataSourceConfig = (i18n: AliyunOssI18nTranslator) => {
5+
const dataSourceConfig = {
6+
type: "dataSource",
7+
params: [
8+
{
9+
key: "accessKeyId",
10+
label: "Access key ID",
11+
type: "textInput",
12+
placeholder: "<Your Access key ID>",
13+
rules: [{ required: true, message: i18n.trans("akRequiredMessage") }],
14+
},
15+
{
16+
key: "accessKeySecret",
17+
label: "Secret key",
18+
type: "password",
19+
placeholder: "<Your Access key Secrect>",
20+
rules: [{ required: true, message: i18n.trans("skRequiredMessage") }],
21+
},
22+
{
23+
key: "arn",
24+
label: "ARN",
25+
type: "password",
26+
tooltip: i18n.trans("arnTooltip"),
27+
placeholder: "<Your Aliyun ARN>",
28+
rules: [{ required: true, message: i18n.trans("arnRequiredMessage") }],
29+
},
30+
{
31+
key: "endpointUrl",
32+
label: "STS Endpoint",
33+
type: "textInput",
34+
tooltip: i18n.trans("endpointUrlTooltip"),
35+
default: "sts.cn-hangzhou.aliyuncs.com",
36+
rules: [{ required: true }],
37+
},
38+
{
39+
key: "region",
40+
type: "textInput",
41+
label: i18n.trans("region"),
42+
defaultValue: "oss-cn-hangzhou",
43+
rules: [{ required: true }],
44+
},
45+
],
46+
} as const;
47+
return dataSourceConfig;
48+
};
49+
50+
export default getDataSourceConfig;
51+
52+
export type DataSourceDataType = ConfigToType<ReturnType<typeof getDataSourceConfig>>;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export const en = {
2+
name: "OSS",
3+
description: "Supports Aliyun Object Storage Service (Based on STS)",
4+
skRequiredMessage: "Please input the SecretKey",
5+
akRequiredMessage: "Please input the AccessKey",
6+
arnRequiredMessage: "Please input the ARN",
7+
endpointUrlTooltip: "Endpoint url of STS service",
8+
arnTooltip: "The global resource descriptor for the role",
9+
bucket: "Bucket",
10+
returnSignedUrl: "Return signed url",
11+
actions: "Actions",
12+
prefix: "Prefix to filter",
13+
delimiter: "Delimiter",
14+
limit: "Limit",
15+
fileName: "File name",
16+
dataType: "Data type",
17+
data: "Data",
18+
dataTooltip:"The content of the data only supports BASE64 encoding, for example: window.btoa(xxx).",
19+
region: "OSS Region",
20+
messages: {
21+
bucketRequired: "Bucket is required",
22+
},
23+
actionName: {
24+
listBuckets: "List buckets",
25+
listObjects: "List files",
26+
uploadFile: "Upload file",
27+
readFile: "Read file",
28+
deleteFile: "Delete file",
29+
},
30+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { en } from "./en";
2+
import { zh } from "./zh";
3+
import { I18n } from "../../../common/i18n";
4+
5+
export default function getI18nTranslator(languages: string[]) {
6+
return new I18n<typeof en>({ zh, en }, languages);
7+
}
8+
9+
export type AliyunOssI18nTranslator = ReturnType<typeof getI18nTranslator>;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { en } from "./en";
2+
3+
export const zh: typeof en = {
4+
name: "阿里云对象存储",
5+
description: "支持OSS对象存储服务(基于 STS 身份认证)",
6+
skRequiredMessage: "请输入 SecretKey",
7+
akRequiredMessage: "请输入 AccessKey",
8+
arnRequiredMessage: "请输入阿里云ARN",
9+
endpointUrlTooltip: "STS 服务接入点",
10+
arnTooltip: "角色的全局资源描述符",
11+
bucket: "存储桶",
12+
region: "OSS 区域",
13+
returnSignedUrl: "返回文件签名地址",
14+
actions: "方法",
15+
prefix: "前缀",
16+
delimiter: "分隔符",
17+
limit: "最大文件数",
18+
fileName: "文件名",
19+
dataType: "数据类型",
20+
data: "数据",
21+
dataTooltip:"数据内容仅支持 BASE64 编码,例:window.btoa(xxx)",
22+
messages: {
23+
bucketRequired: "需要提供存储桶名称",
24+
},
25+
actionName: {
26+
listBuckets: "查询桶列表",
27+
listObjects: "获取文件列表",
28+
uploadFile: "上传文件",
29+
readFile: "读取文件",
30+
deleteFile: "删除文件",
31+
},
32+
};
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { DataSourcePluginFactory, PluginContext } from "lowcoder-sdk/dataSource";
2+
import getI18nTranslator from "./i18n";
3+
import getDataSourceConfig, { DataSourceDataType } from "./dataSourceConfig";
4+
import run, { validateDataSourceConfig } from "./run";
5+
import getQueryConfig, { ActionDataType } from "./queryConfig";
6+
import { ServiceError } from "../../common/error";
7+
8+
const ossPlugin: DataSourcePluginFactory = (context: PluginContext) => {
9+
const i18n = getI18nTranslator(context.languages);
10+
return {
11+
id: "oss",
12+
name: i18n.trans("name"),
13+
icon: "https://img.alicdn.com/tfs/TB1_ZXuNcfpK1RjSZFOXXa6nFXa-32-32.ico",
14+
description: i18n.trans("description"),
15+
category: "api",
16+
dataSourceConfig: getDataSourceConfig(i18n),
17+
queryConfig: getQueryConfig(i18n),
18+
19+
validateDataSourceConfig: async (dataSourceConfig: DataSourceDataType) => {
20+
return validateDataSourceConfig(dataSourceConfig);
21+
},
22+
23+
run: async (
24+
action: ActionDataType,
25+
dataSourceConfig: DataSourceDataType,
26+
ctx: PluginContext
27+
) => {
28+
const i18n = getI18nTranslator(ctx.languages);
29+
try {
30+
return await run(action, dataSourceConfig, i18n);
31+
} catch (e:any) {
32+
throw new ServiceError(e.message, 400);
33+
}
34+
},
35+
};
36+
};
37+
38+
export default ossPlugin;
39+
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { ActionParamConfig, Config, ConfigToType, QueryConfig } from "lowcoder-sdk/dataSource";
2+
import { AliyunOssI18nTranslator } from "./i18n";
3+
4+
function getQueryConfig(i18n: AliyunOssI18nTranslator) {
5+
const bucketActionParam = {
6+
key: "bucket",
7+
type: "textInput",
8+
label: i18n.trans("bucket"),
9+
} as const;
10+
11+
const queryConfig = {
12+
type: "query",
13+
label: i18n.trans("actions"),
14+
actions: [
15+
// {
16+
// actionName: "listBuckets",
17+
// label: i18n.trans("actionName.listBuckets"),
18+
// params: [],
19+
// },
20+
{
21+
actionName: "listObjects",
22+
label: i18n.trans("actionName.listObjects"),
23+
params: [
24+
bucketActionParam,
25+
{
26+
key: "prefix",
27+
type: "textInput",
28+
label: i18n.trans("prefix"),
29+
},
30+
{
31+
key: "delimiter",
32+
type: "textInput",
33+
label: i18n.trans("delimiter"),
34+
},
35+
{
36+
key: "limit",
37+
type: "numberInput",
38+
defaultValue: 10,
39+
label: i18n.trans("limit"),
40+
}
41+
],
42+
},
43+
{
44+
actionName: "uploadData",
45+
label: i18n.trans("actionName.uploadFile"),
46+
params: [
47+
bucketActionParam,
48+
{
49+
key: "fileName",
50+
type: "textInput",
51+
label: i18n.trans("fileName"),
52+
},
53+
{
54+
key: "data",
55+
type: "textInput",
56+
label: i18n.trans("data"),
57+
tooltip: i18n.trans("dataTooltip"),
58+
},
59+
],
60+
},
61+
// {
62+
// actionName: "deleteFile",
63+
// label: i18n.trans("actionName.deleteFile"),
64+
// params: [
65+
// bucketActionParam,
66+
// {
67+
// key: "fileName",
68+
// type: "textInput",
69+
// label: i18n.trans("fileName"),
70+
// },
71+
// ],
72+
// },
73+
],
74+
} as const;
75+
return queryConfig;
76+
}
77+
78+
export type ActionDataType = ConfigToType<ReturnType<typeof getQueryConfig>>;
79+
80+
export default getQueryConfig;
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// import {
2+
// OSS
3+
// } from "ali-oss";
4+
import OSS from "ali-oss";
5+
import { STS } from 'ali-oss';
6+
import { ServiceError } from "../../common/error";
7+
import { DataSourceDataType } from "./dataSourceConfig";
8+
import { AliyunOssI18nTranslator } from "./i18n";
9+
import { ActionDataType } from "./queryConfig";
10+
import { P } from "pino";
11+
import { query } from "express";
12+
import { Readable } from "stream";
13+
14+
interface StsCredential {
15+
AccessKeyId: string;
16+
AccessKeySecret: string;
17+
SecurityToken: string;
18+
Expiration: Date;
19+
}
20+
var stsCredential: StsCredential;
21+
async function loginWithSts(params: DataSourceDataType): Promise<StsCredential> {
22+
if (stsCredential && new Date().getTime() < stsCredential.Expiration.getTime()) {
23+
return stsCredential;
24+
}
25+
let sts = new STS({
26+
// 填写步骤1创建的RAM用户AccessKey。
27+
accessKeyId: params.accessKeyId,
28+
accessKeySecret: params.accessKeySecret,
29+
});
30+
let res = await sts.assumeRole(params.arn, ``, 3000, 'lowcoder');
31+
var { AccessKeyId, AccessKeySecret, SecurityToken, Expiration } = res.credentials;
32+
stsCredential = {
33+
AccessKeyId,
34+
AccessKeySecret,
35+
SecurityToken,
36+
Expiration: new Date(Expiration)
37+
};
38+
return stsCredential;
39+
}
40+
41+
async function getClient(params: DataSourceDataType) {
42+
var stsCredential = await loginWithSts(params);
43+
return new OSS({
44+
region: params.region,
45+
accessKeyId: stsCredential.AccessKeyId,
46+
accessKeySecret: stsCredential.AccessKeySecret,
47+
stsToken: stsCredential.SecurityToken,
48+
refreshSTSToken: async () => {
49+
var res = await loginWithSts(params);
50+
return {
51+
accessKeyId: res.AccessKeyId,
52+
accessKeySecret: res.AccessKeySecret,
53+
stsToken: res.SecurityToken,
54+
}
55+
}
56+
});
57+
}
58+
59+
function getBucket(actionConfig: ActionDataType, dataSourceConfig: DataSourceDataType) {
60+
if ("bucket" in actionConfig) {
61+
return actionConfig.bucket;
62+
}
63+
return "";
64+
}
65+
66+
export async function validateDataSourceConfig(dataSourceConfig: DataSourceDataType) {
67+
try {
68+
const client = getClient(dataSourceConfig);
69+
return{
70+
success:true
71+
};
72+
} catch (e) {
73+
if (e) {
74+
return {
75+
success: false,
76+
message: String(e),
77+
};
78+
}
79+
throw e;
80+
}
81+
}
82+
83+
export default async function run(
84+
action: ActionDataType,
85+
dataSourceConfig: DataSourceDataType,
86+
i18n: AliyunOssI18nTranslator
87+
) {
88+
const client = await getClient(dataSourceConfig);
89+
const bucket = getBucket(action, dataSourceConfig);
90+
client.useBucket(bucket);
91+
92+
// list
93+
if (action.actionName === "listObjects") {
94+
if (!bucket) {
95+
throw new ServiceError(i18n.trans("messages.bucketRequired"), 400);
96+
}
97+
const res = await client.listV2({
98+
prefix: action.prefix,
99+
delimiter: action.delimiter,
100+
"max-keys": String(action.limit ?? 100),
101+
}, {});
102+
103+
const files = [];
104+
for (const i of res.objects || []) {
105+
files.push({
106+
name: i.name || "",
107+
size: i.size,
108+
lastModified: i.lastModified,
109+
etag: i.etag,
110+
url: i.url,
111+
});
112+
}
113+
return files;
114+
}
115+
116+
// upload
117+
if (action.actionName === "uploadData") {
118+
const buf = Buffer.from(action.data, ("base64") as BufferEncoding);
119+
const r = new Readable();
120+
r.push(buf);
121+
r.push(null);
122+
let result = await client.putStream(action.fileName, r);
123+
return {
124+
fileName: action.fileName,
125+
url: getUrl(action.fileName, client),
126+
}
127+
128+
// if (action.actionName === "deleteFile") {
129+
// await client.send(
130+
// new DeleteObjectCommand({
131+
// Bucket: bucket,
132+
// Key: action.fileName,
133+
// })
134+
// );
135+
// return {
136+
// success: true,
137+
// };
138+
// }
139+
}
140+
function getUrl(fileName: string, client: OSS) {
141+
return client.signatureUrl(fileName);
142+
}
143+
}

‎server/node-service/src/plugins/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import faunaPlugin from "./fauna";
3232
import huggingFaceInferencePlugin from "./huggingFaceInference";
3333
import didPlugin from "./did";
3434
import bigQueryPlugin from "./bigQuery";
35+
import ossPlugin from "./aliyunOss";
3536
import appConfigPlugin from "./appconfig";
3637
import tursoPlugin from "./turso";
3738
import postmanEchoPlugin from "./postmanEcho";
@@ -87,6 +88,7 @@ let plugins: (DataSourcePlugin | DataSourcePluginFactory)[] = [
8788
googleCloudStorage,
8889
supabasePlugin,
8990
cloudinaryPlugin,
91+
ossPlugin,
9092

9193
// Project Management
9294
asanaPlugin,

0 commit comments

Comments
 (0)
Please sign in to comment.