Compare commits

...
5 Commits
Author SHA1 Message Date
database-mysql 476946a85d reimplement 2026-10-04 18:51:20 +08:00
database-mysql 735d888d96 reimplement 2026-10-04 04:47:58 +08:00
database-mysql 155e5dc086 init 2026-10-04 04:33:37 +08:00
database-mysql 6dc7105b4c Merge branch 'main' of https://git.opensource-studio.info/database-mysql/TurboWarp_S3 2026-10-04 04:31:59 +08:00
database-mysql abb2b74468 first commit 2026-10-04 04:30:57 +08:00
8 changed files with 361 additions and 0 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
/.idea/
./.gitignore
./TurboWarp_S3.iml
+10
View File
@@ -0,0 +1,10 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 已忽略包含查询文件的默认文件夹
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
+5
View File
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/TurboWarp_S3.iml" filepath="$PROJECT_DIR$/TurboWarp_S3.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="GENERAL_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+315
View File
@@ -0,0 +1,315 @@
(function (Scratch) {
'use strict';
// 非沙箱守卫
if (!Scratch.extensions.unsandboxed) {
throw new Error('S3 扩展必须运行在非沙箱模式下(加载时请勾选 Run without sandbox)');
}
const enc = new TextEncoder();
// ---------- 工具函数 ----------
async function sha256Hex(data) {
const buf = typeof data === 'string' ? enc.encode(data) : data;
const hash = await crypto.subtle.digest('SHA-256', buf);
return [...new Uint8Array(hash)]
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
async function hmac(key, data) {
const cryptoKey = await crypto.subtle.importKey(
'raw',
typeof key === 'string' ? enc.encode(key) : key,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
return new Uint8Array(
await crypto.subtle.sign('HMAC', cryptoKey, enc.encode(data))
);
}
// RFC 3986 编码
function uriEncode(str, encodeSlash = true) {
return String(str).split('').map(ch => {
if (/[A-Za-z0-9\-_.~]/.test(ch)) return ch;
if (ch === '/' && !encodeSlash) return ch;
return '%' + ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0');
}).join('');
}
// 把 ArrayBuffer 转 hex
function bufToHex(buf) {
return [...new Uint8Array(buf)]
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
// ---------- SigV4 签名 ----------
async function signRequest({
method, host, path, query = {}, headers = {},
payloadHash, region, service, accessKey, secretKey
}) {
const now = new Date();
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '');
const dateStamp = amzDate.slice(0, 8);
// 规范化查询字符串
const canonicalQuery = Object.keys(query).sort()
.map(k => `${uriEncode(k)}=${uriEncode(query[k])}`)
.join('&');
// 规范化 URI(路径中的 / 不编码,但每段单独编码)
const canonicalUri = path.split('/').map(p => uriEncode(p)).join('/');
// 合并头,统一小写
const allHeaders = {
host,
'x-amz-date': amzDate,
'x-amz-content-sha256': payloadHash,
...headers
};
const lower = {};
Object.keys(allHeaders).forEach(k => {
lower[k.toLowerCase()] = String(allHeaders[k]).trim();
});
const sortedKeys = Object.keys(lower).sort();
const canonicalHeaders = sortedKeys.map(k => `${k}:${lower[k]}\n`).join('');
const signedHeaders = sortedKeys.join(';');
// Canonical Request
const canonicalRequest = [
method, canonicalUri, canonicalQuery,
canonicalHeaders, signedHeaders, payloadHash
].join('\n');
// String to Sign
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
const stringToSign = [
'AWS4-HMAC-SHA256',
amzDate,
credentialScope,
await sha256Hex(canonicalRequest)
].join('\n');
// 派生签名密钥
let k = await hmac('AWS4' + secretKey, dateStamp);
k = await hmac(k, region);
k = await hmac(k, service);
k = await hmac(k, 'aws4_request');
// 最终签名
const signature = bufToHex(await hmac(k, stringToSign));
const authorization =
`AWS4-HMAC-SHA256 Credential=${accessKey}/${credentialScope}, ` +
`SignedHeaders=${signedHeaders}, Signature=${signature}`;
return { authorization, amzDate };
}
// ---------- 扩展类 ----------
class S3Extension {
constructor() {
this.host = '';
this.port = 443;
this.isSSL = true;
this.accessKey = '';
this.secretKey = '';
this.region = 'us-east-1'; // 默认区域,可后续扩展块修改
}
getInfo() {
return {
id: 's3client',
name: 'S3 客户端',
color1: '#2e7d32',
color2: '#1b5e20',
blocks: [
// ========== 块 1:初始化 ==========
{
opcode: 'init',
blockType: Scratch.BlockType.COMMAND,
text: '初始化 S3 host [HOST] 端口 [PORT] SSL [SSL] AccessKey [AK] SecretKey [SK]',
arguments: {
HOST: {
type: Scratch.ArgumentType.STRING,
defaultValue: 's3.us-east-1.amazonaws.com'
},
PORT: {
type: Scratch.ArgumentType.NUMBER,
defaultValue: 443
},
SSL: {
type: Scratch.ArgumentType.STRING,
defaultValue: 'true',
menu: 'booleanMenu'
},
AK: {
type: Scratch.ArgumentType.STRING,
defaultValue: 'AKIA...'
},
SK: {
type: Scratch.ArgumentType.STRING,
defaultValue: 'secret...'
}
}
},
// ========== 块 2:上传文件 ==========
{
opcode: 'uploadFile',
blockType: Scratch.BlockType.COMMAND,
text: '上传文件 桶 [BUCKET] 本地路径 [SRC] 目标键 [DST]',
arguments: {
BUCKET: { type: Scratch.ArgumentType.STRING, defaultValue: 'my-bucket' },
SRC: { type: Scratch.ArgumentType.STRING, defaultValue: 'local/file.txt' },
DST: { type: Scratch.ArgumentType.STRING, defaultValue: 'remote/file.txt' }
}
},
// ========== 块 3:下载文件 ==========
{
opcode: 'downloadFile',
blockType: Scratch.BlockType.COMMAND,
text: '下载文件 桶 [BUCKET] 源键 [SRC] 本地路径 [DST]',
arguments: {
BUCKET: { type: Scratch.ArgumentType.STRING, defaultValue: 'my-bucket' },
SRC: { type: Scratch.ArgumentType.STRING, defaultValue: 'remote/file.txt' },
DST: { type: Scratch.ArgumentType.STRING, defaultValue: 'local/file.txt' }
}
}
],
menus: {
booleanMenu: {
items: ['true', 'false']
}
}
};
}
// ---------- 块 1 实现 ----------
init(args) {
this.host = String(args.HOST).trim();
this.port = Number(args.PORT) || 443;
this.isSSL = String(args.SSL).toLowerCase() === 'true';
this.accessKey = String(args.AK).trim();
this.secretKey = String(args.SK).trim();
// 如果 host 里带 region,尝试自动解析,例如 s3.us-west-2.amazonaws.com
const m = this.host.match(/s3[.-]([a-z0-9-]+)\.amazonaws\.com/);
if (m) this.region = m[1];
}
// ---------- 构造 endpoint ----------
_endpoint() {
const scheme = this.isSSL ? 'https' : 'http';
const defaultPort = this.isSSL ? 443 : 80;
const portPart = this.port && this.port !== defaultPort ? `:${this.port}` : '';
return `${scheme}://${this.host}${portPart}`;
}
// ---------- 通用请求 ----------
async _request(method, bucket, key, { body, headers = {} } = {}) {
if (!this.accessKey || !this.secretKey) {
throw new Error('请先调用“初始化 S3”块设置凭据');
}
// 路径风格:/bucket/key(兼容 MinIO、Ceph 等)
const path = '/' + uriEncode(bucket, false) + '/' + uriEncode(key, false);
let payloadHash;
let bodyBytes = null;
if (body === undefined || body === null) {
payloadHash = await sha256Hex('');
} else {
bodyBytes = body; // Uint8Array
payloadHash = await sha256Hex(bodyBytes);
}
const urlObj = new URL(this._endpoint());
const hostHeader = urlObj.port
? `${urlObj.hostname}:${urlObj.port}`
: urlObj.hostname;
const { authorization, amzDate } = await signRequest({
method,
host: hostHeader,
path,
query: {},
headers,
payloadHash,
region: this.region,
service: 's3',
accessKey: this.accessKey,
secretKey: this.secretKey
});
const finalHeaders = {
...headers,
'x-amz-date': amzDate,
'x-amz-content-sha256': payloadHash,
'Authorization': authorization
};
const res = await fetch(`${this._endpoint()}${path}`, {
method,
headers: finalHeaders,
body: bodyBytes
});
if (!res.ok) {
const text = await res.text();
throw new Error(`S3 ${res.status}: ${text}`);
}
return res;
}
// ---------- 块 2 实现:上传 ----------
async uploadFile(args) {
const bucket = String(args.BUCKET);
const src = String(args.SRC);
const dst = String(args.DST);
// 读取本地文件(TurboWarp 非沙箱下可用 fetch 读取同源/允许的文件)
let data;
try {
const fileRes = await fetch(src);
if (!fileRes.ok) throw new Error(`读取本地文件失败: ${fileRes.status}`);
data = new Uint8Array(await fileRes.arrayBuffer());
} catch (e) {
throw new Error(`无法读取本地文件 ${src}:${e.message}`);
}
await this._request('PUT', bucket, dst, {
body: data,
headers: { 'content-type': 'application/octet-stream' }
});
}
// ---------- 块 3 实现:下载 ----------
async downloadFile(args) {
const bucket = String(args.BUCKET);
const src = String(args.SRC);
const dst = String(args.DST);
const res = await this._request('GET', bucket, src);
const buf = new Uint8Array(await res.arrayBuffer());
// 触发浏览器下载
const blob = new Blob([buf], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
// 用目标路径的最后一段作为文件名
a.download = dst.split('/').pop() || 'download.bin';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
}
Scratch.extensions.register(new S3Extension());
})(Scratch);