Skip to content

Commit 9736bd1

Browse files
authored
Merge branch 'ling-drag0n:main' into main
2 parents 47ac0ad + a852353 commit 9736bd1

File tree

89 files changed

+10402
-1639
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+10402
-1639
lines changed

Api-doc.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1390,7 +1390,7 @@ X-Custom-Auth-Key: <api_key>
13901390
### 统一存储链接解析
13911391

13921392
- `POST /api/proxy/link`
1393-
- 描述:统一存储链接解析接口(OpenList Proxy 风格)
1393+
- 描述:统一存储链接解析接口
13941394
- 授权:支持管理员令牌、API 密钥或匿名访问
13951395
- 请求体:
13961396
```json

backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cloudpaste-api",
3-
"version": "1.2.0",
3+
"version": "1.3.0",
44
"description": "CloudPaste API基于Cloudflare Workers和D1数据库",
55
"main": "workers.js",
66
"scripts": {

backend/schema.sql

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,61 @@ CREATE INDEX idx_storage_mounts_is_active ON storage_mounts(is_active);
225225
CREATE INDEX idx_storage_mounts_sort_order ON storage_mounts(sort_order);
226226

227227

228+
-- 通用上传会话表:管理各存储驱动的前端分片/断点续传会话
229+
CREATE TABLE upload_sessions (
230+
id TEXT PRIMARY KEY, -- 内部会话ID(例如 upl_xxx),供前后端统一引用
231+
232+
-- 主体与目标信息
233+
user_id TEXT NOT NULL, -- 用户或 API Key 标识
234+
user_type TEXT NOT NULL, -- 'admin' | 'apikey' | 其他
235+
storage_type TEXT NOT NULL, -- 存储类型:S3 / ONEDRIVE / WEBDAV / LOCAL 等
236+
storage_config_id TEXT NOT NULL, -- 关联的存储配置 ID(storage_configs.id)
237+
mount_id TEXT, -- 关联挂载 ID(storage_mounts.id,可为空)
238+
fs_path TEXT NOT NULL, -- FS 视图路径,如 /onedrive/path/file.ext 或 /s3/bucket/key
239+
source TEXT NOT NULL, -- 会话来源:FS / SHARE / OTHER
240+
241+
-- 文件级元数据
242+
file_name TEXT NOT NULL, -- 文件名
243+
file_size INTEGER NOT NULL, -- 总大小(字节)
244+
mime_type TEXT, -- MIME 类型(可选)
245+
checksum TEXT, -- 校验和(可选,例如 SHA-256)
246+
247+
-- 文件指纹(用于跨驱动/跨会话识别同一逻辑文件)
248+
fingerprint_algo TEXT, -- 指纹算法标识,例如 meta-v1 / sha256 等
249+
fingerprint_value TEXT, -- 指纹值(算法自定义,可为元数据拼接或哈希)
250+
251+
-- 策略与进度
252+
strategy TEXT NOT NULL, -- 分片策略:per_part_url / single_session / ...
253+
part_size INTEGER NOT NULL, -- 分片大小(字节)
254+
total_parts INTEGER NOT NULL, -- 预估分片总数
255+
bytes_uploaded INTEGER NOT NULL DEFAULT 0, -- 已上传字节数(对于 single_session 可由 nextExpectedRanges 推导)
256+
uploaded_parts INTEGER NOT NULL DEFAULT 0, -- 已确认的分片数量(对于 per_part_url 有意义)
257+
next_expected_range TEXT, -- 对于 single_session:Graph nextExpectedRanges[0],如 \"5242880-35799947\"
258+
259+
-- provider 会话信息(驱动私有)
260+
provider_upload_id TEXT, -- 例如 S3 UploadId / 其他云 provider upload id
261+
provider_upload_url TEXT, -- 例如 OneDrive uploadSession.uploadUrl
262+
provider_meta TEXT, -- JSON 扩展字段(驱动私有)
263+
264+
-- 会话状态与错误
265+
status TEXT NOT NULL, -- active / completed / aborted / expired / error
266+
error_code TEXT,
267+
error_message TEXT,
268+
269+
-- 生命周期
270+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
271+
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
272+
expires_at DATETIME -- 会话过期时间(例如 Graph expirationDateTime)
273+
);
274+
275+
CREATE INDEX idx_upload_sessions_user ON upload_sessions(user_id, user_type);
276+
CREATE INDEX idx_upload_sessions_storage ON upload_sessions(storage_type, storage_config_id);
277+
CREATE INDEX idx_upload_sessions_mount_path ON upload_sessions(mount_id, fs_path);
278+
CREATE INDEX idx_upload_sessions_status ON upload_sessions(status, updated_at DESC);
279+
CREATE INDEX idx_upload_sessions_source ON upload_sessions(source);
280+
CREATE INDEX idx_upload_sessions_fingerprint ON upload_sessions(fingerprint_value);
281+
282+
228283
CREATE TABLE tasks (
229284
-- 核心标识
230285
task_id TEXT PRIMARY KEY,

backend/src/constants/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const DbTables = {
1717
STORAGE_MOUNTS: "storage_mounts", // 存储挂载表
1818
FS_META: "fs_meta", // 目录 Meta 配置表
1919
TASKS: "tasks", // 任务编排表
20+
UPLOAD_SESSIONS: "upload_sessions", // 通用上传会话表(前端分片/断点续传)
2021
};
2122

2223
// 默认的最大上传大小(MB)
@@ -36,7 +37,7 @@ export const ApiStatus = {
3637
INTERNAL_ERROR: 500,
3738
};
3839

39-
// alist风格的文件类型常量
40+
//文件类型常量
4041
export const FILE_TYPES = {
4142
UNKNOWN: 0, // 未知文件
4243
FOLDER: 1, // 文件夹

backend/src/repositories/StorageConfigRepository.js

Lines changed: 14 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -7,60 +7,14 @@
77
*/
88
import { BaseRepository } from "./BaseRepository.js";
99
import { DbTables } from "../constants/index.js";
10+
import { StorageFactory } from "../storage/factory/StorageFactory.js";
1011

1112
export class StorageConfigRepository extends BaseRepository {
1213
/**
13-
* 展开 S3 专用字段
14-
* @private
15-
* @param {object} cfg - config_json 解析后的对象
16-
* @param {object} merged - 合并目标对象
17-
* @param {object} options - 选项
18-
*/
19-
_inflateS3Fields(cfg, merged, { withSecrets = false }) {
20-
merged.provider_type = cfg?.provider_type;
21-
merged.endpoint_url = cfg?.endpoint_url;
22-
merged.bucket_name = cfg?.bucket_name;
23-
merged.region = cfg?.region;
24-
merged.path_style = cfg?.path_style;
25-
if (withSecrets) {
26-
merged.access_key_id = cfg?.access_key_id;
27-
merged.secret_access_key = cfg?.secret_access_key;
28-
}
29-
}
30-
31-
/**
32-
* 展开 WebDAV 专用字段
33-
* @private
34-
* @param {object} cfg - config_json 解析后的对象
35-
* @param {object} merged - 合并目标对象
36-
* @param {object} options - 选项
37-
*/
38-
_inflateWebDavFields(cfg, merged, { withSecrets = false }) {
39-
merged.endpoint_url = cfg?.endpoint_url;
40-
merged.username = cfg?.username;
41-
merged.tls_insecure_skip_verify = cfg?.tls_insecure_skip_verify;
42-
if (withSecrets) {
43-
merged.password = cfg?.password;
44-
}
45-
}
46-
47-
/**
48-
* 展开 LOCAL 专用字段
49-
* @private
50-
* @param {object} cfg - config_json 解析后的对象
51-
* @param {object} merged - 合并目标对象
52-
*/
53-
_inflateLocalFields(cfg, merged) {
54-
merged.root_path = cfg?.root_path;
55-
merged.auto_create_root = cfg?.auto_create_root;
56-
merged.readonly = cfg?.readonly;
57-
merged.trash_path = cfg?.trash_path;
58-
merged.dir_permission = cfg?.dir_permission;
59-
}
60-
61-
/**
62-
* 通用展开方法(策略模式)
63-
* 根据 storage_type 自动选择对应的字段展开策略
14+
* 通用展开方法
15+
* - 解析 config_json
16+
* - 委托 StorageFactory.projectConfig 完成类型特定的配置投影
17+
* - 将投影结果与行对象合并,并保留 __config_json__ 供上层复用
6418
* @param {object} row - 数据库原始行
6519
* @param {object} options - 选项
6620
* @returns {object|null} 展开后的配置对象
@@ -69,31 +23,19 @@ export class StorageConfigRepository extends BaseRepository {
6923
if (!row) return null;
7024
try {
7125
if (row.config_json) {
72-
const cfg = JSON.parse(row.config_json);
26+
const raw = row.config_json;
27+
const cfg = typeof raw === "string" ? JSON.parse(raw) : raw || {};
28+
29+
const projected = StorageFactory.projectConfig(row.storage_type, cfg, {
30+
withSecrets,
31+
row,
32+
});
33+
7334
const merged = {
7435
...row,
75-
// 通用字段(所有存储类型共享)
76-
default_folder: cfg?.default_folder,
77-
custom_host: cfg?.custom_host,
78-
signature_expires_in: cfg?.signature_expires_in,
79-
total_storage_bytes: cfg?.total_storage_bytes,
36+
...(projected && typeof projected === "object" ? projected : {}),
8037
};
8138

82-
// 根据存储类型展开专用字段
83-
switch (row.storage_type) {
84-
case "S3":
85-
this._inflateS3Fields(cfg, merged, { withSecrets });
86-
break;
87-
case "WEBDAV":
88-
this._inflateWebDavFields(cfg, merged, { withSecrets });
89-
break;
90-
case "LOCAL":
91-
this._inflateLocalFields(cfg, merged);
92-
break;
93-
default:
94-
console.warn(`Unknown storage_type: ${row.storage_type}`);
95-
}
96-
9739
// 保留原始 config_json 对象(非枚举属性,避免对外暴露)
9840
Object.defineProperty(merged, "__config_json__", {
9941
value: cfg,

backend/src/routes/fs/multipart.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,15 +172,19 @@ export const registerMultipartRoutes = (router, helpers) => {
172172

173173
const fileId = generateFileId();
174174

175-
return jsonOk(c, {
175+
return jsonOk(
176+
c,
177+
{
176178
presignedUrl: result.uploadUrl,
177179
fileId,
178180
storagePath: result.storagePath,
179181
publicUrl: result.publicUrl || null,
180182
mountId: mount.id,
181183
storageConfigId: mount.storage_config_id,
184+
storageType: mount.storage_type || null,
182185
targetPath,
183186
contentType: result.contentType,
187+
headers: result.headers || undefined,
184188
},
185189
{ success: true },
186190
);

backend/src/routes/proxyLinkRoutes.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* 统一存储链接解析接口(OpenList Proxy 风格)
2+
* 统一存储链接解析接口
33
* POST /api/proxy/link
44
*
55
* - Origin(CloudPaste)只负责权限与“如何访问源”的决策

backend/src/routes/storageConfigRoutes.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ import {
1212
deleteStorageConfig,
1313
setDefaultStorageConfig,
1414
testStorageConnection,
15-
getStorageConfigsWithUsage,
1615
} from "../services/storageConfigService.js";
17-
import { ApiStatus, UserType } from "../constants/index.js";
16+
import { UserType } from "../constants/index.js";
1817
import { getPagination, jsonOk, jsonCreated } from "../utils/common.js";
1918
import { getEncryptionSecret } from "../utils/environmentUtils.js";
2019
import { usePolicy } from "../security/policies/policies.js";
@@ -178,7 +177,19 @@ storageConfigRoutes.post("/api/storage/:id/test", requireAdmin, async (c) => {
178177
const requestOrigin = c.req.header("origin");
179178
const repositoryFactory = useRepositories(c);
180179
const testResult = await testStorageConnection(db, id, adminId, encryptionSecret, requestOrigin, repositoryFactory);
181-
return jsonOk(c, { success: testResult.success, result: testResult.result }, testResult.message);
180+
181+
// 根据测试结果返回正确的响应
182+
if (testResult.success) {
183+
return jsonOk(c, testResult, testResult.message);
184+
} else {
185+
// 测试失败时返回 200 状态码但 success: false(前端会根据 success 字段判断)
186+
return c.json({
187+
success: false,
188+
code: "TEST_FAILED",
189+
message: testResult.message,
190+
data: testResult
191+
}, 200);
192+
}
182193
});
183194

184195
export default storageConfigRoutes;

backend/src/routes/systemRoutes.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ systemRoutes.get("/api/version", async (c) => {
9797
const isDocker = runtimeEnv === "docker";
9898

9999
// 统一的默认版本配置
100-
const DEFAULT_VERSION = "1.2.0";
100+
const DEFAULT_VERSION = "1.3.0";
101101
const DEFAULT_NAME = "cloudpaste-api";
102102

103103
let version = DEFAULT_VERSION;

backend/src/services/ProxySignatureService.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { ensureRepositoryFactory } from "../utils/repositories.js";
33

44
/**
55
* 代理签名服务
6-
* 支持两层签名策略:全局签名所有 + 存储级签名
6+
* 支持两层签名策略:
7+
* - 全局签名所有:proxy_sign_all = true 时,所有代理流量都启用签名(与挂载配置无关)
8+
* - 挂载级签名:enable_sign = true 且挂载开启 web_proxy 时,对该挂载下的代理流量启用签名
79
*/
810
export class ProxySignatureService {
911
constructor(db, encryptionSecret, repositoryFactory = null) {
@@ -32,7 +34,28 @@ export class ProxySignatureService {
3234
};
3335
}
3436

35-
// 2. 检查存储级别的签名设置
37+
// 2. 挂载信息缺失时,不启用挂载级签名(仅全局策略生效)
38+
if (!mount || typeof mount !== "object") {
39+
return {
40+
required: false,
41+
reason: "mount_not_provided",
42+
level: "none",
43+
description: "未提供挂载配置,仅全局签名策略有效",
44+
};
45+
}
46+
47+
// 3. 仅在挂载开启 web_proxy 时才允许挂载级签名生效
48+
const webProxyEnabled = mount.web_proxy === 1 || mount.web_proxy === true;
49+
if (!webProxyEnabled) {
50+
return {
51+
required: false,
52+
reason: "web_proxy_disabled",
53+
level: "none",
54+
description: "挂载未开启 web_proxy,忽略挂载级签名配置",
55+
};
56+
}
57+
58+
// 4. 检查挂载级别的签名设置
3659
if (mount.enable_sign === 1 || mount.enable_sign === true) {
3760
return {
3861
required: true,
@@ -42,7 +65,7 @@ export class ProxySignatureService {
4265
};
4366
}
4467

45-
// 3. 不需要签名
68+
// 5. 不需要签名
4669
return {
4770
required: false,
4871
reason: "no_sign_required",

0 commit comments

Comments
 (0)