From 12859cbb20d4d63dcd254ee676f6645f4ca87489 Mon Sep 17 00:00:00 2001
From: yuyx <237899745@qq.com>
Date: Thu, 12 Feb 2026 18:02:57 +0800
Subject: [PATCH] feat: apply UI/storage/share optimizations and quota
improvements
---
backend/auth.js | 11 +-
backend/database.js | 47 +-
backend/package-lock.json | 577 +++---
backend/package.json | 7 +-
backend/routes/index.js | 2 +-
backend/server.js | 667 +++++--
backend/storage.js | 6 +-
backend/test_share.js | 12 +-
backend/test_share_edge_cases.js | 2 +-
backend/utils/storage-cache.js | 17 +-
frontend/app.html | 3027 +++++++++++++++++++++++++++---
frontend/app.js | 508 ++++-
frontend/share.html | 421 ++++-
13 files changed, 4476 insertions(+), 828 deletions(-)
diff --git a/backend/auth.js b/backend/auth.js
index ce1edf8..e41ffb2 100644
--- a/backend/auth.js
+++ b/backend/auth.js
@@ -2,6 +2,8 @@ const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const { UserDB } = require('./database');
const { decryptSecret } = require('./utils/encryption');
+const DEFAULT_LOCAL_STORAGE_QUOTA_BYTES = 1024 * 1024 * 1024; // 1GB
+const DEFAULT_OSS_STORAGE_QUOTA_BYTES = 1024 * 1024 * 1024; // 1GB
// JWT密钥(必须在环境变量中设置)
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
@@ -169,6 +171,11 @@ function authMiddleware(req, res, next) {
});
}
+ const rawOssQuota = Number(user.oss_storage_quota);
+ const effectiveOssQuota = Number.isFinite(rawOssQuota) && rawOssQuota > 0
+ ? rawOssQuota
+ : DEFAULT_OSS_STORAGE_QUOTA_BYTES;
+
// 将用户信息附加到请求对象(包含所有存储相关字段)
req.user = {
id: user.id,
@@ -187,8 +194,10 @@ function authMiddleware(req, res, next) {
// 存储相关字段
storage_permission: user.storage_permission || 'oss_only',
current_storage_type: user.current_storage_type || 'oss',
- local_storage_quota: user.local_storage_quota || 1073741824,
+ local_storage_quota: user.local_storage_quota || DEFAULT_LOCAL_STORAGE_QUOTA_BYTES,
local_storage_used: user.local_storage_used || 0,
+ oss_storage_quota: effectiveOssQuota,
+ storage_used: user.storage_used || 0,
// 主题偏好
theme_preference: user.theme_preference || null
};
diff --git a/backend/database.js b/backend/database.js
index 1348b54..54f22f8 100644
--- a/backend/database.js
+++ b/backend/database.js
@@ -37,6 +37,8 @@ console.log(`[数据库] 路径: ${dbPath}`);
// 创建或连接数据库
const db = new Database(dbPath);
+const DEFAULT_LOCAL_STORAGE_QUOTA_BYTES = 1024 * 1024 * 1024; // 1GB
+const DEFAULT_OSS_STORAGE_QUOTA_BYTES = 1024 * 1024 * 1024; // 1GB
// ===== 性能优化配置(P0 优先级修复) =====
@@ -525,7 +527,8 @@ const UserDB = {
'has_oss_config': 'number',
'is_verified': 'number',
'local_storage_quota': 'number',
- 'local_storage_used': 'number'
+ 'local_storage_used': 'number',
+ 'oss_storage_quota': 'number'
};
const expectedType = FIELD_TYPES[fieldName];
@@ -585,6 +588,7 @@ const UserDB = {
'current_storage_type': 'current_storage_type',
'local_storage_quota': 'local_storage_quota',
'local_storage_used': 'local_storage_used',
+ 'oss_storage_quota': 'oss_storage_quota',
// 偏好设置
'theme_preference': 'theme_preference'
@@ -665,6 +669,7 @@ const UserDB = {
'current_storage_type': 'current_storage_type',
'local_storage_quota': 'local_storage_quota',
'local_storage_used': 'local_storage_used',
+ 'oss_storage_quota': 'oss_storage_quota',
// 偏好设置
'theme_preference': 'theme_preference'
@@ -717,7 +722,8 @@ const UserDB = {
'current_storage_type': 'string', 'theme_preference': 'string',
'is_admin': 'number', 'is_active': 'number', 'is_banned': 'number',
'has_oss_config': 'number', 'is_verified': 'number',
- 'local_storage_quota': 'number', 'local_storage_used': 'number'
+ 'local_storage_quota': 'number', 'local_storage_used': 'number',
+ 'oss_storage_quota': 'number'
}[key];
console.warn(`[类型检查] 字段 ${key} 值类型不符: 期望 ${expectedType}, 实际 ${typeof value}, 值: ${JSON.stringify(value)}`);
@@ -883,11 +889,14 @@ const ShareDB = {
// 根据分享码查找
// 增强: 检查分享者是否被封禁(被封禁用户的分享不可访问)
// ===== 性能优化(P0 优先级修复):只查询必要字段,避免 N+1 查询 =====
- // 移除了敏感字段:oss_access_key_id, oss_access_key_secret(不需要传递给分享访问者)
+ // 不返回 oss_access_key_id / oss_access_key_secret;
+ // share_password 仅用于服务端验证,不会透传给前端。
findByCode(shareCode) {
const result = db.prepare(`
SELECT
s.id, s.user_id, s.share_code, s.share_path, s.share_type,
+ s.storage_type,
+ s.share_password,
s.view_count, s.download_count, s.created_at, s.expires_at,
u.username,
-- OSS 配置(访问分享文件所需)
@@ -1172,8 +1181,9 @@ function migrateToV2() {
db.exec(`
ALTER TABLE users ADD COLUMN storage_permission TEXT DEFAULT 'sftp_only';
ALTER TABLE users ADD COLUMN current_storage_type TEXT DEFAULT 'sftp';
- ALTER TABLE users ADD COLUMN local_storage_quota INTEGER DEFAULT 1073741824;
+ ALTER TABLE users ADD COLUMN local_storage_quota INTEGER DEFAULT ${DEFAULT_LOCAL_STORAGE_QUOTA_BYTES};
ALTER TABLE users ADD COLUMN local_storage_used INTEGER DEFAULT 0;
+ ALTER TABLE users ADD COLUMN oss_storage_quota INTEGER DEFAULT ${DEFAULT_OSS_STORAGE_QUOTA_BYTES};
`);
console.log('[数据库迁移] ✓ 用户表已升级');
@@ -1247,6 +1257,34 @@ function migrateToOss() {
}
}
+
+// 数据库迁移 - OSS 存储配额字段
+function migrateOssQuotaField() {
+ try {
+ const columns = db.prepare("PRAGMA table_info(users)").all();
+ const hasOssStorageQuota = columns.some(col => col.name === 'oss_storage_quota');
+
+ if (!hasOssStorageQuota) {
+ console.log('[数据库迁移] 添加 oss_storage_quota 字段...');
+ db.exec(`ALTER TABLE users ADD COLUMN oss_storage_quota INTEGER DEFAULT ${DEFAULT_OSS_STORAGE_QUOTA_BYTES}`);
+ console.log('[数据库迁移] ✓ oss_storage_quota 字段已添加');
+ }
+
+ // 统一策略:未配置或无效值默认 1GB
+ const backfillResult = db.prepare(`
+ UPDATE users
+ SET oss_storage_quota = ?
+ WHERE oss_storage_quota IS NULL OR oss_storage_quota <= 0
+ `).run(DEFAULT_OSS_STORAGE_QUOTA_BYTES);
+
+ if (backfillResult.changes > 0) {
+ console.log(`[数据库迁移] ✓ OSS 配额默认值已回填: ${backfillResult.changes} 条记录`);
+ }
+ } catch (error) {
+ console.error('[数据库迁移] oss_storage_quota 迁移失败:', error);
+ }
+}
+
// 系统日志操作
const SystemLogDB = {
// 日志级别常量
@@ -1432,6 +1470,7 @@ initDefaultSettings();
migrateToV2(); // 执行数据库迁移
migrateThemePreference(); // 主题偏好迁移
migrateToOss(); // SFTP → OSS 迁移
+migrateOssQuotaField(); // OSS 配额字段迁移
module.exports = {
db,
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 5d63484..afec4cf 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -9,8 +9,8 @@
"version": "3.1.0",
"license": "MIT",
"dependencies": {
- "@aws-sdk/client-s3": "^3.600.0",
- "@aws-sdk/s3-request-presigner": "^3.600.0",
+ "@aws-sdk/client-s3": "^3.985.0",
+ "@aws-sdk/s3-request-presigner": "^3.985.0",
"archiver": "^7.0.1",
"bcryptjs": "^3.0.3",
"better-sqlite3": "^11.8.1",
@@ -21,8 +21,9 @@
"express-session": "^1.18.2",
"express-validator": "^7.3.0",
"jsonwebtoken": "^9.0.2",
+ "lodash": "^4.17.23",
"multer": "^2.0.2",
- "nodemailer": "^6.9.14",
+ "nodemailer": "^8.0.1",
"svg-captcha": "^1.4.0"
},
"devDependencies": {
@@ -232,34 +233,34 @@
}
},
"node_modules/@aws-sdk/client-s3": {
- "version": "3.971.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.971.0.tgz",
- "integrity": "sha512-BBUne390fKa4C4QvZlUZ5gKcu+Uyid4IyQ20N4jl0vS7SK2xpfXlJcgKqPW5ts6kx6hWTQBk6sH5Lf12RvuJxg==",
+ "version": "3.985.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.985.0.tgz",
+ "integrity": "sha512-S9TqjzzZEEIKBnC7yFpvqM7CG9ALpY5qhQ5BnDBJtdG20NoGpjKLGUUfD2wmZItuhbrcM4Z8c6m6Fg0XYIOVvw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha1-browser": "5.2.0",
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "3.970.0",
- "@aws-sdk/credential-provider-node": "3.971.0",
- "@aws-sdk/middleware-bucket-endpoint": "3.969.0",
- "@aws-sdk/middleware-expect-continue": "3.969.0",
- "@aws-sdk/middleware-flexible-checksums": "3.971.0",
- "@aws-sdk/middleware-host-header": "3.969.0",
- "@aws-sdk/middleware-location-constraint": "3.969.0",
- "@aws-sdk/middleware-logger": "3.969.0",
- "@aws-sdk/middleware-recursion-detection": "3.969.0",
- "@aws-sdk/middleware-sdk-s3": "3.970.0",
- "@aws-sdk/middleware-ssec": "3.971.0",
- "@aws-sdk/middleware-user-agent": "3.970.0",
- "@aws-sdk/region-config-resolver": "3.969.0",
- "@aws-sdk/signature-v4-multi-region": "3.970.0",
- "@aws-sdk/types": "3.969.0",
- "@aws-sdk/util-endpoints": "3.970.0",
- "@aws-sdk/util-user-agent-browser": "3.969.0",
- "@aws-sdk/util-user-agent-node": "3.971.0",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/credential-provider-node": "^3.972.6",
+ "@aws-sdk/middleware-bucket-endpoint": "^3.972.3",
+ "@aws-sdk/middleware-expect-continue": "^3.972.3",
+ "@aws-sdk/middleware-flexible-checksums": "^3.972.5",
+ "@aws-sdk/middleware-host-header": "^3.972.3",
+ "@aws-sdk/middleware-location-constraint": "^3.972.3",
+ "@aws-sdk/middleware-logger": "^3.972.3",
+ "@aws-sdk/middleware-recursion-detection": "^3.972.3",
+ "@aws-sdk/middleware-sdk-s3": "^3.972.7",
+ "@aws-sdk/middleware-ssec": "^3.972.3",
+ "@aws-sdk/middleware-user-agent": "^3.972.7",
+ "@aws-sdk/region-config-resolver": "^3.972.3",
+ "@aws-sdk/signature-v4-multi-region": "3.985.0",
+ "@aws-sdk/types": "^3.973.1",
+ "@aws-sdk/util-endpoints": "3.985.0",
+ "@aws-sdk/util-user-agent-browser": "^3.972.3",
+ "@aws-sdk/util-user-agent-node": "^3.972.5",
"@smithy/config-resolver": "^4.4.6",
- "@smithy/core": "^3.20.6",
+ "@smithy/core": "^3.22.1",
"@smithy/eventstream-serde-browser": "^4.2.8",
"@smithy/eventstream-serde-config-resolver": "^4.3.8",
"@smithy/eventstream-serde-node": "^4.2.8",
@@ -270,25 +271,25 @@
"@smithy/invalid-dependency": "^4.2.8",
"@smithy/md5-js": "^4.2.8",
"@smithy/middleware-content-length": "^4.2.8",
- "@smithy/middleware-endpoint": "^4.4.7",
- "@smithy/middleware-retry": "^4.4.23",
+ "@smithy/middleware-endpoint": "^4.4.13",
+ "@smithy/middleware-retry": "^4.4.30",
"@smithy/middleware-serde": "^4.2.9",
"@smithy/middleware-stack": "^4.2.8",
"@smithy/node-config-provider": "^4.3.8",
- "@smithy/node-http-handler": "^4.4.8",
+ "@smithy/node-http-handler": "^4.4.9",
"@smithy/protocol-http": "^5.3.8",
- "@smithy/smithy-client": "^4.10.8",
+ "@smithy/smithy-client": "^4.11.2",
"@smithy/types": "^4.12.0",
"@smithy/url-parser": "^4.2.8",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-body-length-browser": "^4.2.0",
"@smithy/util-body-length-node": "^4.2.1",
- "@smithy/util-defaults-mode-browser": "^4.3.22",
- "@smithy/util-defaults-mode-node": "^4.2.25",
+ "@smithy/util-defaults-mode-browser": "^4.3.29",
+ "@smithy/util-defaults-mode-node": "^4.2.32",
"@smithy/util-endpoints": "^3.2.8",
"@smithy/util-middleware": "^4.2.8",
"@smithy/util-retry": "^4.2.8",
- "@smithy/util-stream": "^4.5.10",
+ "@smithy/util-stream": "^4.5.11",
"@smithy/util-utf8": "^4.2.0",
"@smithy/util-waiter": "^4.2.8",
"tslib": "^2.6.2"
@@ -298,44 +299,44 @@
}
},
"node_modules/@aws-sdk/client-sso": {
- "version": "3.971.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.971.0.tgz",
- "integrity": "sha512-Xx+w6DQqJxDdymYyIxyKJnRzPvVJ4e/Aw0czO7aC9L/iraaV7AG8QtRe93OGW6aoHSh72CIiinnpJJfLsQqP4g==",
+ "version": "3.985.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.985.0.tgz",
+ "integrity": "sha512-81J8iE8MuXhdbMfIz4sWFj64Pe41bFi/uqqmqOC5SlGv+kwoyLsyKS/rH2tW2t5buih4vTUxskRjxlqikTD4oQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "3.970.0",
- "@aws-sdk/middleware-host-header": "3.969.0",
- "@aws-sdk/middleware-logger": "3.969.0",
- "@aws-sdk/middleware-recursion-detection": "3.969.0",
- "@aws-sdk/middleware-user-agent": "3.970.0",
- "@aws-sdk/region-config-resolver": "3.969.0",
- "@aws-sdk/types": "3.969.0",
- "@aws-sdk/util-endpoints": "3.970.0",
- "@aws-sdk/util-user-agent-browser": "3.969.0",
- "@aws-sdk/util-user-agent-node": "3.971.0",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/middleware-host-header": "^3.972.3",
+ "@aws-sdk/middleware-logger": "^3.972.3",
+ "@aws-sdk/middleware-recursion-detection": "^3.972.3",
+ "@aws-sdk/middleware-user-agent": "^3.972.7",
+ "@aws-sdk/region-config-resolver": "^3.972.3",
+ "@aws-sdk/types": "^3.973.1",
+ "@aws-sdk/util-endpoints": "3.985.0",
+ "@aws-sdk/util-user-agent-browser": "^3.972.3",
+ "@aws-sdk/util-user-agent-node": "^3.972.5",
"@smithy/config-resolver": "^4.4.6",
- "@smithy/core": "^3.20.6",
+ "@smithy/core": "^3.22.1",
"@smithy/fetch-http-handler": "^5.3.9",
"@smithy/hash-node": "^4.2.8",
"@smithy/invalid-dependency": "^4.2.8",
"@smithy/middleware-content-length": "^4.2.8",
- "@smithy/middleware-endpoint": "^4.4.7",
- "@smithy/middleware-retry": "^4.4.23",
+ "@smithy/middleware-endpoint": "^4.4.13",
+ "@smithy/middleware-retry": "^4.4.30",
"@smithy/middleware-serde": "^4.2.9",
"@smithy/middleware-stack": "^4.2.8",
"@smithy/node-config-provider": "^4.3.8",
- "@smithy/node-http-handler": "^4.4.8",
+ "@smithy/node-http-handler": "^4.4.9",
"@smithy/protocol-http": "^5.3.8",
- "@smithy/smithy-client": "^4.10.8",
+ "@smithy/smithy-client": "^4.11.2",
"@smithy/types": "^4.12.0",
"@smithy/url-parser": "^4.2.8",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-body-length-browser": "^4.2.0",
"@smithy/util-body-length-node": "^4.2.1",
- "@smithy/util-defaults-mode-browser": "^4.3.22",
- "@smithy/util-defaults-mode-node": "^4.2.25",
+ "@smithy/util-defaults-mode-browser": "^4.3.29",
+ "@smithy/util-defaults-mode-node": "^4.2.32",
"@smithy/util-endpoints": "^3.2.8",
"@smithy/util-middleware": "^4.2.8",
"@smithy/util-retry": "^4.2.8",
@@ -347,19 +348,19 @@
}
},
"node_modules/@aws-sdk/core": {
- "version": "3.970.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.970.0.tgz",
- "integrity": "sha512-klpzObldOq8HXzDjDlY6K8rMhYZU6mXRz6P9F9N+tWnjoYFfeBMra8wYApydElTUYQKP1O7RLHwH1OKFfKcqIA==",
+ "version": "3.973.7",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.7.tgz",
+ "integrity": "sha512-wNZZQQNlJ+hzD49cKdo+PY6rsTDElO8yDImnrI69p2PLBa7QomeUKAJWYp9xnaR38nlHqWhMHZuYLCQ3oSX+xg==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "3.969.0",
- "@aws-sdk/xml-builder": "3.969.0",
- "@smithy/core": "^3.20.6",
+ "@aws-sdk/types": "^3.973.1",
+ "@aws-sdk/xml-builder": "^3.972.4",
+ "@smithy/core": "^3.22.1",
"@smithy/node-config-provider": "^4.3.8",
"@smithy/property-provider": "^4.2.8",
"@smithy/protocol-http": "^5.3.8",
"@smithy/signature-v4": "^5.3.8",
- "@smithy/smithy-client": "^4.10.8",
+ "@smithy/smithy-client": "^4.11.2",
"@smithy/types": "^4.12.0",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-middleware": "^4.2.8",
@@ -371,9 +372,9 @@
}
},
"node_modules/@aws-sdk/crc64-nvme": {
- "version": "3.969.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.969.0.tgz",
- "integrity": "sha512-IGNkP54HD3uuLnrPCYsv3ZD478UYq+9WwKrIVJ9Pdi3hxPg8562CH3ZHf8hEgfePN31P9Kj+Zu9kq2Qcjjt61A==",
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.0.tgz",
+ "integrity": "sha512-ThlLhTqX68jvoIVv+pryOdb5coP1cX1/MaTbB9xkGDCbWbsqQcLqzPxuSoW1DCnAAIacmXCWpzUNOB9pv+xXQw==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.12.0",
@@ -384,13 +385,13 @@
}
},
"node_modules/@aws-sdk/credential-provider-env": {
- "version": "3.970.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.970.0.tgz",
- "integrity": "sha512-rtVzXzEtAfZBfh+lq3DAvRar4c3jyptweOAJR2DweyXx71QSMY+O879hjpMwES7jl07a3O1zlnFIDo4KP/96kQ==",
+ "version": "3.972.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.5.tgz",
+ "integrity": "sha512-LxJ9PEO4gKPXzkufvIESUysykPIdrV7+Ocb9yAhbhJLE4TiAYqbCVUE+VuKP1leGR1bBfjWjYgSV5MxprlX3mQ==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "3.970.0",
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
@@ -400,20 +401,20 @@
}
},
"node_modules/@aws-sdk/credential-provider-http": {
- "version": "3.970.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.970.0.tgz",
- "integrity": "sha512-CjDbWL7JxjLc9ZxQilMusWSw05yRvUJKRpz59IxDpWUnSMHC9JMMUUkOy5Izk8UAtzi6gupRWArp4NG4labt9Q==",
+ "version": "3.972.7",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.7.tgz",
+ "integrity": "sha512-L2uOGtvp2x3bTcxFTpSM+GkwFIPd8pHfGWO1764icMbo7e5xJh0nfhx1UwkXLnwvocTNEf8A7jISZLYjUSNaTg==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "3.970.0",
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/fetch-http-handler": "^5.3.9",
- "@smithy/node-http-handler": "^4.4.8",
+ "@smithy/node-http-handler": "^4.4.9",
"@smithy/property-provider": "^4.2.8",
"@smithy/protocol-http": "^5.3.8",
- "@smithy/smithy-client": "^4.10.8",
+ "@smithy/smithy-client": "^4.11.2",
"@smithy/types": "^4.12.0",
- "@smithy/util-stream": "^4.5.10",
+ "@smithy/util-stream": "^4.5.11",
"tslib": "^2.6.2"
},
"engines": {
@@ -421,20 +422,20 @@
}
},
"node_modules/@aws-sdk/credential-provider-ini": {
- "version": "3.971.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.971.0.tgz",
- "integrity": "sha512-c0TGJG4xyfTZz3SInXfGU8i5iOFRrLmy4Bo7lMyH+IpngohYMYGYl61omXqf2zdwMbDv+YJ9AviQTcCaEUKi8w==",
+ "version": "3.972.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.5.tgz",
+ "integrity": "sha512-SdDTYE6jkARzOeL7+kudMIM4DaFnP5dZVeatzw849k4bSXDdErDS188bgeNzc/RA2WGrlEpsqHUKP6G7sVXhZg==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "3.970.0",
- "@aws-sdk/credential-provider-env": "3.970.0",
- "@aws-sdk/credential-provider-http": "3.970.0",
- "@aws-sdk/credential-provider-login": "3.971.0",
- "@aws-sdk/credential-provider-process": "3.970.0",
- "@aws-sdk/credential-provider-sso": "3.971.0",
- "@aws-sdk/credential-provider-web-identity": "3.971.0",
- "@aws-sdk/nested-clients": "3.971.0",
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/credential-provider-env": "^3.972.5",
+ "@aws-sdk/credential-provider-http": "^3.972.7",
+ "@aws-sdk/credential-provider-login": "^3.972.5",
+ "@aws-sdk/credential-provider-process": "^3.972.5",
+ "@aws-sdk/credential-provider-sso": "^3.972.5",
+ "@aws-sdk/credential-provider-web-identity": "^3.972.5",
+ "@aws-sdk/nested-clients": "3.985.0",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/credential-provider-imds": "^4.2.8",
"@smithy/property-provider": "^4.2.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
@@ -446,14 +447,14 @@
}
},
"node_modules/@aws-sdk/credential-provider-login": {
- "version": "3.971.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.971.0.tgz",
- "integrity": "sha512-yhbzmDOsk0RXD3rTPhZra4AWVnVAC4nFWbTp+sUty1hrOPurUmhuz8bjpLqYTHGnlMbJp+UqkQONhS2+2LzW2g==",
+ "version": "3.972.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.5.tgz",
+ "integrity": "sha512-uYq1ILyTSI6ZDCMY5+vUsRM0SOCVI7kaW4wBrehVVkhAxC6y+e9rvGtnoZqCOWL1gKjTMouvsf4Ilhc5NCg1Aw==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "3.970.0",
- "@aws-sdk/nested-clients": "3.971.0",
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/nested-clients": "3.985.0",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/protocol-http": "^5.3.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
@@ -465,18 +466,18 @@
}
},
"node_modules/@aws-sdk/credential-provider-node": {
- "version": "3.971.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.971.0.tgz",
- "integrity": "sha512-epUJBAKivtJqalnEBRsYIULKYV063o/5mXNJshZfyvkAgNIzc27CmmKRXTN4zaNOZg8g/UprFp25BGsi19x3nQ==",
+ "version": "3.972.6",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.6.tgz",
+ "integrity": "sha512-DZ3CnAAtSVtVz+G+ogqecaErMLgzph4JH5nYbHoBMgBkwTUV+SUcjsjOJwdBJTHu3Dm6l5LBYekZoU2nDqQk2A==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/credential-provider-env": "3.970.0",
- "@aws-sdk/credential-provider-http": "3.970.0",
- "@aws-sdk/credential-provider-ini": "3.971.0",
- "@aws-sdk/credential-provider-process": "3.970.0",
- "@aws-sdk/credential-provider-sso": "3.971.0",
- "@aws-sdk/credential-provider-web-identity": "3.971.0",
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/credential-provider-env": "^3.972.5",
+ "@aws-sdk/credential-provider-http": "^3.972.7",
+ "@aws-sdk/credential-provider-ini": "^3.972.5",
+ "@aws-sdk/credential-provider-process": "^3.972.5",
+ "@aws-sdk/credential-provider-sso": "^3.972.5",
+ "@aws-sdk/credential-provider-web-identity": "^3.972.5",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/credential-provider-imds": "^4.2.8",
"@smithy/property-provider": "^4.2.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
@@ -488,13 +489,13 @@
}
},
"node_modules/@aws-sdk/credential-provider-process": {
- "version": "3.970.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.970.0.tgz",
- "integrity": "sha512-0XeT8OaT9iMA62DFV9+m6mZfJhrD0WNKf4IvsIpj2Z7XbaYfz3CoDDvNoALf3rPY9NzyMHgDxOspmqdvXP00mw==",
+ "version": "3.972.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.5.tgz",
+ "integrity": "sha512-HDKF3mVbLnuqGg6dMnzBf1VUOywE12/N286msI9YaK9mEIzdsGCtLTvrDhe3Up0R9/hGFbB+9l21/TwF5L1C6g==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "3.970.0",
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
"@smithy/types": "^4.12.0",
@@ -505,15 +506,15 @@
}
},
"node_modules/@aws-sdk/credential-provider-sso": {
- "version": "3.971.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.971.0.tgz",
- "integrity": "sha512-dY0hMQ7dLVPQNJ8GyqXADxa9w5wNfmukgQniLxGVn+dMRx3YLViMp5ZpTSQpFhCWNF0oKQrYAI5cHhUJU1hETw==",
+ "version": "3.972.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.5.tgz",
+ "integrity": "sha512-8urj3AoeNeQisjMmMBhFeiY2gxt6/7wQQbEGun0YV/OaOOiXrIudTIEYF8ZfD+NQI6X1FY5AkRsx6O/CaGiybA==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/client-sso": "3.971.0",
- "@aws-sdk/core": "3.970.0",
- "@aws-sdk/token-providers": "3.971.0",
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/client-sso": "3.985.0",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/token-providers": "3.985.0",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
"@smithy/types": "^4.12.0",
@@ -524,14 +525,14 @@
}
},
"node_modules/@aws-sdk/credential-provider-web-identity": {
- "version": "3.971.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.971.0.tgz",
- "integrity": "sha512-F1AwfNLr7H52T640LNON/h34YDiMuIqW/ZreGzhRR6vnFGaSPtNSKAKB2ssAMkLM8EVg8MjEAYD3NCUiEo+t/w==",
+ "version": "3.972.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.5.tgz",
+ "integrity": "sha512-OK3cULuJl6c+RcDZfPpaK5o3deTOnKZbxm7pzhFNGA3fI2hF9yDih17fGRazJzGGWaDVlR9ejZrpDef4DJCEsw==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "3.970.0",
- "@aws-sdk/nested-clients": "3.971.0",
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/nested-clients": "3.985.0",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
"@smithy/types": "^4.12.0",
@@ -542,13 +543,13 @@
}
},
"node_modules/@aws-sdk/middleware-bucket-endpoint": {
- "version": "3.969.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.969.0.tgz",
- "integrity": "sha512-MlbrlixtkTVhYhoasblKOkr7n2yydvUZjjxTnBhIuHmkyBS1619oGnTfq/uLeGYb4NYXdeQ5OYcqsRGvmWSuTw==",
+ "version": "3.972.3",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.3.tgz",
+ "integrity": "sha512-fmbgWYirF67YF1GfD7cg5N6HHQ96EyRNx/rDIrTF277/zTWVuPI2qS/ZHgofwR1NZPe/NWvoppflQY01LrbVLg==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "3.969.0",
- "@aws-sdk/util-arn-parser": "3.968.0",
+ "@aws-sdk/types": "^3.973.1",
+ "@aws-sdk/util-arn-parser": "^3.972.2",
"@smithy/node-config-provider": "^4.3.8",
"@smithy/protocol-http": "^5.3.8",
"@smithy/types": "^4.12.0",
@@ -560,12 +561,12 @@
}
},
"node_modules/@aws-sdk/middleware-expect-continue": {
- "version": "3.969.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.969.0.tgz",
- "integrity": "sha512-qXygzSi8osok7tH9oeuS3HoKw6jRfbvg5Me/X5RlHOvSSqQz8c5O9f3MjUApaCUSwbAU92KrbZWasw2PKiaVHg==",
+ "version": "3.972.3",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.3.tgz",
+ "integrity": "sha512-4msC33RZsXQpUKR5QR4HnvBSNCPLGHmB55oDiROqqgyOc+TOfVu2xgi5goA7ms6MdZLeEh2905UfWMnMMF4mRg==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/protocol-http": "^5.3.8",
"@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
@@ -575,23 +576,23 @@
}
},
"node_modules/@aws-sdk/middleware-flexible-checksums": {
- "version": "3.971.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.971.0.tgz",
- "integrity": "sha512-+hGUDUxeIw8s2kkjfeXym0XZxdh0cqkHkDpEanWYdS1gnWkIR+gf9u/DKbKqGHXILPaqHXhWpLTQTVlaB4sI7Q==",
+ "version": "3.972.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.972.5.tgz",
+ "integrity": "sha512-SF/1MYWx67OyCrLA4icIpWUfCkdlOi8Y1KecQ9xYxkL10GMjVdPTGPnYhAg0dw5U43Y9PVUWhAV2ezOaG+0BLg==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/crc32": "5.2.0",
"@aws-crypto/crc32c": "5.2.0",
"@aws-crypto/util": "5.2.0",
- "@aws-sdk/core": "3.970.0",
- "@aws-sdk/crc64-nvme": "3.969.0",
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/crc64-nvme": "3.972.0",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/is-array-buffer": "^4.2.0",
"@smithy/node-config-provider": "^4.3.8",
"@smithy/protocol-http": "^5.3.8",
"@smithy/types": "^4.12.0",
"@smithy/util-middleware": "^4.2.8",
- "@smithy/util-stream": "^4.5.10",
+ "@smithy/util-stream": "^4.5.11",
"@smithy/util-utf8": "^4.2.0",
"tslib": "^2.6.2"
},
@@ -600,12 +601,12 @@
}
},
"node_modules/@aws-sdk/middleware-host-header": {
- "version": "3.969.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.969.0.tgz",
- "integrity": "sha512-AWa4rVsAfBR4xqm7pybQ8sUNJYnjyP/bJjfAw34qPuh3M9XrfGbAHG0aiAfQGrBnmS28jlO6Kz69o+c6PRw1dw==",
+ "version": "3.972.3",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.3.tgz",
+ "integrity": "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/protocol-http": "^5.3.8",
"@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
@@ -615,12 +616,12 @@
}
},
"node_modules/@aws-sdk/middleware-location-constraint": {
- "version": "3.969.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.969.0.tgz",
- "integrity": "sha512-zH7pDfMLG/C4GWMOpvJEoYcSpj7XsNP9+irlgqwi667sUQ6doHQJ3yyDut3yiTk0maq1VgmriPFELyI9lrvH/g==",
+ "version": "3.972.3",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.3.tgz",
+ "integrity": "sha512-nIg64CVrsXp67vbK0U1/Is8rik3huS3QkRHn2DRDx4NldrEFMgdkZGI/+cZMKD9k4YOS110Dfu21KZLHrFA/1g==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
},
@@ -629,12 +630,12 @@
}
},
"node_modules/@aws-sdk/middleware-logger": {
- "version": "3.969.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.969.0.tgz",
- "integrity": "sha512-xwrxfip7Y2iTtCMJ+iifN1E1XMOuhxIHY9DreMCvgdl4r7+48x2S1bCYPWH3eNY85/7CapBWdJ8cerpEl12sQQ==",
+ "version": "3.972.3",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.3.tgz",
+ "integrity": "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
},
@@ -643,12 +644,12 @@
}
},
"node_modules/@aws-sdk/middleware-recursion-detection": {
- "version": "3.969.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.969.0.tgz",
- "integrity": "sha512-2r3PuNquU3CcS1Am4vn/KHFwLi8QFjMdA/R+CRDXT4AFO/0qxevF/YStW3gAKntQIgWgQV8ZdEtKAoJvLI4UWg==",
+ "version": "3.972.3",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.3.tgz",
+ "integrity": "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/types": "^3.973.1",
"@aws/lambda-invoke-store": "^0.2.2",
"@smithy/protocol-http": "^5.3.8",
"@smithy/types": "^4.12.0",
@@ -659,23 +660,23 @@
}
},
"node_modules/@aws-sdk/middleware-sdk-s3": {
- "version": "3.970.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.970.0.tgz",
- "integrity": "sha512-v/Y5F1lbFFY7vMeG5yYxuhnn0CAshz6KMxkz1pDyPxejNE9HtA0w8R6OTBh/bVdIm44QpjhbI7qeLdOE/PLzXQ==",
+ "version": "3.972.7",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.7.tgz",
+ "integrity": "sha512-VtZ7tMIw18VzjG+I6D6rh2eLkJfTtByiFoCIauGDtTTPBEUMQUiGaJ/zZrPlCY6BsvLLeFKz3+E5mntgiOWmIg==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "3.970.0",
- "@aws-sdk/types": "3.969.0",
- "@aws-sdk/util-arn-parser": "3.968.0",
- "@smithy/core": "^3.20.6",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/types": "^3.973.1",
+ "@aws-sdk/util-arn-parser": "^3.972.2",
+ "@smithy/core": "^3.22.1",
"@smithy/node-config-provider": "^4.3.8",
"@smithy/protocol-http": "^5.3.8",
"@smithy/signature-v4": "^5.3.8",
- "@smithy/smithy-client": "^4.10.8",
+ "@smithy/smithy-client": "^4.11.2",
"@smithy/types": "^4.12.0",
"@smithy/util-config-provider": "^4.2.0",
"@smithy/util-middleware": "^4.2.8",
- "@smithy/util-stream": "^4.5.10",
+ "@smithy/util-stream": "^4.5.11",
"@smithy/util-utf8": "^4.2.0",
"tslib": "^2.6.2"
},
@@ -684,12 +685,12 @@
}
},
"node_modules/@aws-sdk/middleware-ssec": {
- "version": "3.971.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.971.0.tgz",
- "integrity": "sha512-QGVhvRveYG64ZhnS/b971PxXM6N2NU79Fxck4EfQ7am8v1Br0ctoeDDAn9nXNblLGw87we9Z65F7hMxxiFHd3w==",
+ "version": "3.972.3",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.3.tgz",
+ "integrity": "sha512-dU6kDuULN3o3jEHcjm0c4zWJlY1zWVkjG9NPe9qxYLLpcbdj5kRYBS2DdWYD+1B9f910DezRuws7xDEqKkHQIg==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
},
@@ -698,15 +699,15 @@
}
},
"node_modules/@aws-sdk/middleware-user-agent": {
- "version": "3.970.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.970.0.tgz",
- "integrity": "sha512-dnSJGGUGSFGEX2NzvjwSefH+hmZQ347AwbLhAsi0cdnISSge+pcGfOFrJt2XfBIypwFe27chQhlfuf/gWdzpZg==",
+ "version": "3.972.7",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.7.tgz",
+ "integrity": "sha512-HUD+geASjXSCyL/DHPQc/Ua7JhldTcIglVAoCV8kiVm99IaFSlAbTvEnyhZwdE6bdFyTL+uIaWLaCFSRsglZBQ==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "3.970.0",
- "@aws-sdk/types": "3.969.0",
- "@aws-sdk/util-endpoints": "3.970.0",
- "@smithy/core": "^3.20.6",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/types": "^3.973.1",
+ "@aws-sdk/util-endpoints": "3.985.0",
+ "@smithy/core": "^3.22.1",
"@smithy/protocol-http": "^5.3.8",
"@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
@@ -716,44 +717,44 @@
}
},
"node_modules/@aws-sdk/nested-clients": {
- "version": "3.971.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.971.0.tgz",
- "integrity": "sha512-TWaILL8GyYlhGrxxnmbkazM4QsXatwQgoWUvo251FXmUOsiXDFDVX3hoGIfB3CaJhV2pJPfebHUNJtY6TjZ11g==",
+ "version": "3.985.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.985.0.tgz",
+ "integrity": "sha512-TsWwKzb/2WHafAY0CE7uXgLj0FmnkBTgfioG9HO+7z/zCPcl1+YU+i7dW4o0y+aFxFgxTMG+ExBQpqT/k2ao8g==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "3.970.0",
- "@aws-sdk/middleware-host-header": "3.969.0",
- "@aws-sdk/middleware-logger": "3.969.0",
- "@aws-sdk/middleware-recursion-detection": "3.969.0",
- "@aws-sdk/middleware-user-agent": "3.970.0",
- "@aws-sdk/region-config-resolver": "3.969.0",
- "@aws-sdk/types": "3.969.0",
- "@aws-sdk/util-endpoints": "3.970.0",
- "@aws-sdk/util-user-agent-browser": "3.969.0",
- "@aws-sdk/util-user-agent-node": "3.971.0",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/middleware-host-header": "^3.972.3",
+ "@aws-sdk/middleware-logger": "^3.972.3",
+ "@aws-sdk/middleware-recursion-detection": "^3.972.3",
+ "@aws-sdk/middleware-user-agent": "^3.972.7",
+ "@aws-sdk/region-config-resolver": "^3.972.3",
+ "@aws-sdk/types": "^3.973.1",
+ "@aws-sdk/util-endpoints": "3.985.0",
+ "@aws-sdk/util-user-agent-browser": "^3.972.3",
+ "@aws-sdk/util-user-agent-node": "^3.972.5",
"@smithy/config-resolver": "^4.4.6",
- "@smithy/core": "^3.20.6",
+ "@smithy/core": "^3.22.1",
"@smithy/fetch-http-handler": "^5.3.9",
"@smithy/hash-node": "^4.2.8",
"@smithy/invalid-dependency": "^4.2.8",
"@smithy/middleware-content-length": "^4.2.8",
- "@smithy/middleware-endpoint": "^4.4.7",
- "@smithy/middleware-retry": "^4.4.23",
+ "@smithy/middleware-endpoint": "^4.4.13",
+ "@smithy/middleware-retry": "^4.4.30",
"@smithy/middleware-serde": "^4.2.9",
"@smithy/middleware-stack": "^4.2.8",
"@smithy/node-config-provider": "^4.3.8",
- "@smithy/node-http-handler": "^4.4.8",
+ "@smithy/node-http-handler": "^4.4.9",
"@smithy/protocol-http": "^5.3.8",
- "@smithy/smithy-client": "^4.10.8",
+ "@smithy/smithy-client": "^4.11.2",
"@smithy/types": "^4.12.0",
"@smithy/url-parser": "^4.2.8",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-body-length-browser": "^4.2.0",
"@smithy/util-body-length-node": "^4.2.1",
- "@smithy/util-defaults-mode-browser": "^4.3.22",
- "@smithy/util-defaults-mode-node": "^4.2.25",
+ "@smithy/util-defaults-mode-browser": "^4.3.29",
+ "@smithy/util-defaults-mode-node": "^4.2.32",
"@smithy/util-endpoints": "^3.2.8",
"@smithy/util-middleware": "^4.2.8",
"@smithy/util-retry": "^4.2.8",
@@ -765,12 +766,12 @@
}
},
"node_modules/@aws-sdk/region-config-resolver": {
- "version": "3.969.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.969.0.tgz",
- "integrity": "sha512-scj9OXqKpcjJ4jsFLtqYWz3IaNvNOQTFFvEY8XMJXTv+3qF5I7/x9SJtKzTRJEBF3spjzBUYPtGFbs9sj4fisQ==",
+ "version": "3.972.3",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.3.tgz",
+ "integrity": "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/config-resolver": "^4.4.6",
"@smithy/node-config-provider": "^4.3.8",
"@smithy/types": "^4.12.0",
@@ -781,17 +782,17 @@
}
},
"node_modules/@aws-sdk/s3-request-presigner": {
- "version": "3.971.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.971.0.tgz",
- "integrity": "sha512-j4wCCoQ//xm03JQn7/Jq6BJ0HV3VzlI/HrIQSQupWWjZTrdxyqa9PXBhcYNNtvZtF1adA/cRpYTMS+2SUsZGRg==",
+ "version": "3.985.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.985.0.tgz",
+ "integrity": "sha512-lPnf977GFM4cMLJ7X+ThktKMe/0CXIfX+wz1z+sUT7yagPL2IRyiNUPFZ0VTEGBo1gRhHEDPWy6yzk8WWRFsvg==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/signature-v4-multi-region": "3.970.0",
- "@aws-sdk/types": "3.969.0",
- "@aws-sdk/util-format-url": "3.969.0",
- "@smithy/middleware-endpoint": "^4.4.7",
+ "@aws-sdk/signature-v4-multi-region": "3.985.0",
+ "@aws-sdk/types": "^3.973.1",
+ "@aws-sdk/util-format-url": "^3.972.3",
+ "@smithy/middleware-endpoint": "^4.4.13",
"@smithy/protocol-http": "^5.3.8",
- "@smithy/smithy-client": "^4.10.8",
+ "@smithy/smithy-client": "^4.11.2",
"@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
},
@@ -800,13 +801,13 @@
}
},
"node_modules/@aws-sdk/signature-v4-multi-region": {
- "version": "3.970.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.970.0.tgz",
- "integrity": "sha512-z3syXfuK/x/IsKf/AeYmgc2NT7fcJ+3fHaGO+fkghkV9WEba3fPyOwtTBX4KpFMNb2t50zDGZwbzW1/5ighcUQ==",
+ "version": "3.985.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.985.0.tgz",
+ "integrity": "sha512-W6hTSOPiSbh4IdTYVxN7xHjpCh0qvfQU1GKGBzGQm0ZEIOaMmWqiDEvFfyGYKmfBvumT8vHKxQRTX0av9omtIg==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/middleware-sdk-s3": "3.970.0",
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/middleware-sdk-s3": "^3.972.7",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/protocol-http": "^5.3.8",
"@smithy/signature-v4": "^5.3.8",
"@smithy/types": "^4.12.0",
@@ -817,14 +818,14 @@
}
},
"node_modules/@aws-sdk/token-providers": {
- "version": "3.971.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.971.0.tgz",
- "integrity": "sha512-4hKGWZbmuDdONMJV0HJ+9jwTDb0zLfKxcCLx2GEnBY31Gt9GeyIQ+DZ97Bb++0voawj6pnZToFikXTyrEq2x+w==",
+ "version": "3.985.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.985.0.tgz",
+ "integrity": "sha512-+hwpHZyEq8k+9JL2PkE60V93v2kNhUIv7STFt+EAez1UJsJOQDhc5LpzEX66pNjclI5OTwBROs/DhJjC/BtMjQ==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "3.970.0",
- "@aws-sdk/nested-clients": "3.971.0",
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/nested-clients": "3.985.0",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
"@smithy/types": "^4.12.0",
@@ -835,9 +836,9 @@
}
},
"node_modules/@aws-sdk/types": {
- "version": "3.969.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.969.0.tgz",
- "integrity": "sha512-7IIzM5TdiXn+VtgPdVLjmE6uUBUtnga0f4RiSEI1WW10RPuNvZ9U+pL3SwDiRDAdoGrOF9tSLJOFZmfuwYuVYQ==",
+ "version": "3.973.1",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.1.tgz",
+ "integrity": "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.12.0",
@@ -848,9 +849,9 @@
}
},
"node_modules/@aws-sdk/util-arn-parser": {
- "version": "3.968.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.968.0.tgz",
- "integrity": "sha512-gqqvYcitIIM2K4lrDX9de9YvOfXBcVdxfT/iLnvHJd4YHvSXlt+gs+AsL4FfPCxG4IG9A+FyulP9Sb1MEA75vw==",
+ "version": "3.972.2",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.2.tgz",
+ "integrity": "sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -860,12 +861,12 @@
}
},
"node_modules/@aws-sdk/util-endpoints": {
- "version": "3.970.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.970.0.tgz",
- "integrity": "sha512-TZNZqFcMUtjvhZoZRtpEGQAdULYiy6rcGiXAbLU7e9LSpIYlRqpLa207oMNfgbzlL2PnHko+eVg8rajDiSOYCg==",
+ "version": "3.985.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.985.0.tgz",
+ "integrity": "sha512-vth7UfGSUR3ljvaq8V4Rc62FsM7GUTH/myxPWkaEgOrprz1/Pc72EgTXxj+cPPPDAfHFIpjhkB7T7Td0RJx+BA==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/types": "^4.12.0",
"@smithy/url-parser": "^4.2.8",
"@smithy/util-endpoints": "^3.2.8",
@@ -876,12 +877,12 @@
}
},
"node_modules/@aws-sdk/util-format-url": {
- "version": "3.969.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.969.0.tgz",
- "integrity": "sha512-C7ZiE8orcrEF9In+XDlIKrZhMjp0HCPUH6u74pgadE3T2LRre5TmOQcTt785/wVS2G0we9cxkjlzMrfDsfPvFw==",
+ "version": "3.972.3",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.3.tgz",
+ "integrity": "sha512-n7F2ycckcKFXa01vAsT/SJdjFHfKH9s96QHcs5gn8AaaigASICeME8WdUL9uBp8XV/OVwEt8+6gzn6KFUgQa8g==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/querystring-builder": "^4.2.8",
"@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
@@ -903,25 +904,25 @@
}
},
"node_modules/@aws-sdk/util-user-agent-browser": {
- "version": "3.969.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.969.0.tgz",
- "integrity": "sha512-bpJGjuKmFr0rA6UKUCmN8D19HQFMLXMx5hKBXqBlPFdalMhxJSjcxzX9DbQh0Fn6bJtxCguFmRGOBdQqNOt49g==",
+ "version": "3.972.3",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.3.tgz",
+ "integrity": "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/types": "^4.12.0",
"bowser": "^2.11.0",
"tslib": "^2.6.2"
}
},
"node_modules/@aws-sdk/util-user-agent-node": {
- "version": "3.971.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.971.0.tgz",
- "integrity": "sha512-Eygjo9mFzQYjbGY3MYO6CsIhnTwAMd3WmuFalCykqEmj2r5zf0leWrhPaqvA5P68V5JdGfPYgj7vhNOd6CtRBQ==",
+ "version": "3.972.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.5.tgz",
+ "integrity": "sha512-GsUDF+rXyxDZkkJxUsDxnA67FG+kc5W1dnloCFLl6fWzceevsCYzJpASBzT+BPjwUgREE6FngfJYYYMQUY5fZQ==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/middleware-user-agent": "3.970.0",
- "@aws-sdk/types": "3.969.0",
+ "@aws-sdk/middleware-user-agent": "^3.972.7",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/node-config-provider": "^4.3.8",
"@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
@@ -939,13 +940,13 @@
}
},
"node_modules/@aws-sdk/xml-builder": {
- "version": "3.969.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.969.0.tgz",
- "integrity": "sha512-BSe4Lx/qdRQQdX8cSSI7Et20vqBspzAjBy8ZmXVoyLkol3y4sXBXzn+BiLtR+oh60ExQn6o2DU4QjdOZbXaKIQ==",
+ "version": "3.972.4",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.4.tgz",
+ "integrity": "sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.12.0",
- "fast-xml-parser": "5.2.5",
+ "fast-xml-parser": "5.3.4",
"tslib": "^2.6.2"
},
"engines": {
@@ -1044,9 +1045,9 @@
}
},
"node_modules/@smithy/core": {
- "version": "3.20.7",
- "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.7.tgz",
- "integrity": "sha512-aO7jmh3CtrmPsIJxUwYIzI5WVlMK8BMCPQ4D4nTzqTqBhbzvxHNzBMGcEg13yg/z9R2Qsz49NUFl0F0lVbTVFw==",
+ "version": "3.22.1",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.22.1.tgz",
+ "integrity": "sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/middleware-serde": "^4.2.9",
@@ -1055,7 +1056,7 @@
"@smithy/util-base64": "^4.3.0",
"@smithy/util-body-length-browser": "^4.2.0",
"@smithy/util-middleware": "^4.2.8",
- "@smithy/util-stream": "^4.5.10",
+ "@smithy/util-stream": "^4.5.11",
"@smithy/util-utf8": "^4.2.0",
"@smithy/uuid": "^1.1.0",
"tslib": "^2.6.2"
@@ -1264,12 +1265,12 @@
}
},
"node_modules/@smithy/middleware-endpoint": {
- "version": "4.4.8",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.8.tgz",
- "integrity": "sha512-TV44qwB/T0OMMzjIuI+JeS0ort3bvlPJ8XIH0MSlGADraXpZqmyND27ueuAL3E14optleADWqtd7dUgc2w+qhQ==",
+ "version": "4.4.13",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.13.tgz",
+ "integrity": "sha512-x6vn0PjYmGdNuKh/juUJJewZh7MoQ46jYaJ2mvekF4EesMuFfrl4LaW/k97Zjf8PTCPQmPgMvwewg7eNoH9n5w==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/core": "^3.20.7",
+ "@smithy/core": "^3.22.1",
"@smithy/middleware-serde": "^4.2.9",
"@smithy/node-config-provider": "^4.3.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
@@ -1283,15 +1284,15 @@
}
},
"node_modules/@smithy/middleware-retry": {
- "version": "4.4.24",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.24.tgz",
- "integrity": "sha512-yiUY1UvnbUFfP5izoKLtfxDSTRv724YRRwyiC/5HYY6vdsVDcDOXKSXmkJl/Hovcxt5r+8tZEUAdrOaCJwrl9Q==",
+ "version": "4.4.30",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.30.tgz",
+ "integrity": "sha512-CBGyFvN0f8hlnqKH/jckRDz78Snrp345+PVk8Ux7pnkUCW97Iinse59lY78hBt04h1GZ6hjBN94BRwZy1xC8Bg==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/node-config-provider": "^4.3.8",
"@smithy/protocol-http": "^5.3.8",
"@smithy/service-error-classification": "^4.2.8",
- "@smithy/smithy-client": "^4.10.9",
+ "@smithy/smithy-client": "^4.11.2",
"@smithy/types": "^4.12.0",
"@smithy/util-middleware": "^4.2.8",
"@smithy/util-retry": "^4.2.8",
@@ -1345,9 +1346,9 @@
}
},
"node_modules/@smithy/node-http-handler": {
- "version": "4.4.8",
- "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.8.tgz",
- "integrity": "sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg==",
+ "version": "4.4.9",
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.9.tgz",
+ "integrity": "sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/abort-controller": "^4.2.8",
@@ -1458,17 +1459,17 @@
}
},
"node_modules/@smithy/smithy-client": {
- "version": "4.10.9",
- "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.9.tgz",
- "integrity": "sha512-Je0EvGXVJ0Vrrr2lsubq43JGRIluJ/hX17aN/W/A0WfE+JpoMdI8kwk2t9F0zTX9232sJDGcoH4zZre6m6f/sg==",
+ "version": "4.11.2",
+ "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.2.tgz",
+ "integrity": "sha512-SCkGmFak/xC1n7hKRsUr6wOnBTJ3L22Qd4e8H1fQIuKTAjntwgU8lrdMe7uHdiT2mJAOWA/60qaW9tiMu69n1A==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/core": "^3.20.7",
- "@smithy/middleware-endpoint": "^4.4.8",
+ "@smithy/core": "^3.22.1",
+ "@smithy/middleware-endpoint": "^4.4.13",
"@smithy/middleware-stack": "^4.2.8",
"@smithy/protocol-http": "^5.3.8",
"@smithy/types": "^4.12.0",
- "@smithy/util-stream": "^4.5.10",
+ "@smithy/util-stream": "^4.5.11",
"tslib": "^2.6.2"
},
"engines": {
@@ -1565,13 +1566,13 @@
}
},
"node_modules/@smithy/util-defaults-mode-browser": {
- "version": "4.3.23",
- "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.23.tgz",
- "integrity": "sha512-mMg+r/qDfjfF/0psMbV4zd7F/i+rpyp7Hjh0Wry7eY15UnzTEId+xmQTGDU8IdZtDfbGQxuWNfgBZKBj+WuYbA==",
+ "version": "4.3.29",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.29.tgz",
+ "integrity": "sha512-nIGy3DNRmOjaYaaKcQDzmWsro9uxlaqUOhZDHQed9MW/GmkBZPtnU70Pu1+GT9IBmUXwRdDuiyaeiy9Xtpn3+Q==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/property-provider": "^4.2.8",
- "@smithy/smithy-client": "^4.10.9",
+ "@smithy/smithy-client": "^4.11.2",
"@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
},
@@ -1580,16 +1581,16 @@
}
},
"node_modules/@smithy/util-defaults-mode-node": {
- "version": "4.2.26",
- "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.26.tgz",
- "integrity": "sha512-EQqe/WkbCinah0h1lMWh9ICl0Ob4lyl20/10WTB35SC9vDQfD8zWsOT+x2FIOXKAoZQ8z/y0EFMoodbcqWJY/w==",
+ "version": "4.2.32",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.32.tgz",
+ "integrity": "sha512-7dtFff6pu5fsjqrVve0YMhrnzJtccCWDacNKOkiZjJ++fmjGExmmSu341x+WU6Oc1IccL7lDuaUj7SfrHpWc5Q==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/config-resolver": "^4.4.6",
"@smithy/credential-provider-imds": "^4.2.8",
"@smithy/node-config-provider": "^4.3.8",
"@smithy/property-provider": "^4.2.8",
- "@smithy/smithy-client": "^4.10.9",
+ "@smithy/smithy-client": "^4.11.2",
"@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
},
@@ -1651,13 +1652,13 @@
}
},
"node_modules/@smithy/util-stream": {
- "version": "4.5.10",
- "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.10.tgz",
- "integrity": "sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g==",
+ "version": "4.5.11",
+ "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.11.tgz",
+ "integrity": "sha512-lKmZ0S/3Qj2OF5H1+VzvDLb6kRxGzZHq6f3rAsoSu5cTLGsn3v3VQBA8czkNNXlLjoFEtVu3OQT2jEeOtOE2CA==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/fetch-http-handler": "^5.3.9",
- "@smithy/node-http-handler": "^4.4.8",
+ "@smithy/node-http-handler": "^4.4.9",
"@smithy/types": "^4.12.0",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-buffer-from": "^4.2.0",
@@ -1983,9 +1984,9 @@
}
},
"node_modules/bowser": {
- "version": "2.13.1",
- "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz",
- "integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==",
+ "version": "2.14.1",
+ "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
+ "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
"license": "MIT"
},
"node_modules/brace-expansion": {
@@ -2602,9 +2603,9 @@
"license": "MIT"
},
"node_modules/fast-xml-parser": {
- "version": "5.2.5",
- "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz",
- "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==",
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz",
+ "integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==",
"funding": [
{
"type": "github",
@@ -3114,9 +3115,9 @@
}
},
"node_modules/lodash": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"license": "MIT"
},
"node_modules/lodash.includes": {
@@ -3351,9 +3352,9 @@
}
},
"node_modules/nodemailer": {
- "version": "6.10.1",
- "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
- "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==",
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz",
+ "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
diff --git a/backend/package.json b/backend/package.json
index 917b632..a156713 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -18,6 +18,8 @@
"author": "玩玩云团队",
"license": "MIT",
"dependencies": {
+ "@aws-sdk/client-s3": "^3.985.0",
+ "@aws-sdk/s3-request-presigner": "^3.985.0",
"archiver": "^7.0.1",
"bcryptjs": "^3.0.3",
"better-sqlite3": "^11.8.1",
@@ -28,10 +30,9 @@
"express-session": "^1.18.2",
"express-validator": "^7.3.0",
"jsonwebtoken": "^9.0.2",
+ "lodash": "^4.17.23",
"multer": "^2.0.2",
- "nodemailer": "^6.9.14",
- "@aws-sdk/client-s3": "^3.600.0",
- "@aws-sdk/s3-request-presigner": "^3.600.0",
+ "nodemailer": "^8.0.1",
"svg-captcha": "^1.4.0"
},
"devDependencies": {
diff --git a/backend/routes/index.js b/backend/routes/index.js
index d70d588..e572cba 100644
--- a/backend/routes/index.js
+++ b/backend/routes/index.js
@@ -54,7 +54,7 @@
* - POST /api/share/:code/verify
* - POST /api/share/:code/list
* - POST /api/share/:code/download
- * - GET /api/share/:code/download-url
+ * - POST /api/share/:code/download-url
* - GET /api/share/:code/download-file
*
* 6. routes/admin.js - 管理员功能
diff --git a/backend/server.js b/backend/server.js
index b165455..c6ce666 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -64,7 +64,7 @@ function clearOssUsageCache(userId) {
const { db, UserDB, ShareDB, SettingsDB, VerificationDB, PasswordResetTokenDB, SystemLogDB, TransactionDB, WalManager } = require('./database');
const StorageUsageCache = require('./utils/storage-cache');
-const { generateToken, generateRefreshToken, refreshAccessToken, authMiddleware, adminMiddleware, requirePasswordConfirmation, isJwtSecretSecure } = require('./auth');
+const { JWT_SECRET, generateToken, generateRefreshToken, refreshAccessToken, authMiddleware, adminMiddleware, isJwtSecretSecure } = require('./auth');
const { StorageInterface, LocalStorageClient, OssStorageClient, formatFileSize, formatOssError } = require('./storage');
const { encryptSecret, decryptSecret } = require('./utils/encryption');
@@ -72,6 +72,8 @@ const app = express();
const PORT = process.env.PORT || 40001;
const USERNAME_REGEX = /^[A-Za-z0-9_.\u4e00-\u9fa5-]{3,20}$/u; // 允许中英文、数字、下划线、点和短横线
const ENFORCE_HTTPS = process.env.ENFORCE_HTTPS === 'true';
+const DEFAULT_LOCAL_STORAGE_QUOTA_BYTES = 1024 * 1024 * 1024; // 1GB
+const DEFAULT_OSS_STORAGE_QUOTA_BYTES = 1024 * 1024 * 1024; // 1GB
// ===== 安全配置:公开域名白名单(防止 Host Header 注入) =====
// 必须配置 PUBLIC_BASE_URL 环境变量,用于生成邮件链接和分享链接
@@ -225,14 +227,13 @@ function csrfProtection(req, res, next) {
return next();
}
- // 白名单:某些公开 API 不需要 CSRF 保护(如分享页面的密码验证)
- const csrfExemptPaths = [
- '/api/share/', // 分享相关的公开接口
- '/api/captcha', // 验证码
- '/api/health' // 健康检查
- ];
+ // 仅对基于 Cookie 的浏览器会话启用 CSRF(Bearer API 客户端不强制)
+ const hasCookieAuth = !!(
+ req.cookies?.token ||
+ req.cookies?.refreshToken
+ );
- if (csrfExemptPaths.some(path => req.path.startsWith(path))) {
+ if (!hasCookieAuth) {
return next();
}
@@ -250,11 +251,16 @@ function csrfProtection(req, res, next) {
next();
}
-// 注意:CSRF 保护将在 authMiddleware 后的路由中按需启用
-// 可以通过环境变量 ENABLE_CSRF=true 开启(默认关闭以保持向后兼容)
-const ENABLE_CSRF = process.env.ENABLE_CSRF === 'true';
+// CSRF 开关策略:
+// - 显式配置 ENABLE_CSRF 时按配置值
+// - 未配置时,生产环境默认开启
+const ENABLE_CSRF = process.env.ENABLE_CSRF !== undefined
+ ? process.env.ENABLE_CSRF === 'true'
+ : process.env.NODE_ENV === 'production';
+
if (ENABLE_CSRF) {
console.log('[安全] CSRF 保护已启用');
+ app.use(csrfProtection);
}
// 强制HTTPS(可通过环境变量控制,默认关闭以兼容本地环境)
@@ -303,7 +309,7 @@ if (DEFAULT_SESSION_SECRETS.includes(SESSION_SECRET)) {
app.use(session({
secret: SESSION_SECRET,
resave: false,
- saveUninitialized: true, // 改为true,确保验证码请求时创建session
+ saveUninitialized: false, // 仅在写入 session 时创建,减少无效会话
name: 'captcha.sid', // 自定义session cookie名称
cookie: {
secure: isSecureCookie,
@@ -555,6 +561,125 @@ function getProtocol(req) {
return req.protocol || (req.secure ? 'https' : 'http');
}
+function normalizeOssQuota(rawQuota) {
+ const parsedQuota = Number(rawQuota);
+ if (!Number.isFinite(parsedQuota) || parsedQuota <= 0) {
+ return DEFAULT_OSS_STORAGE_QUOTA_BYTES;
+ }
+ return parsedQuota;
+}
+
+// 构建用于存储客户端的用户对象(自动尝试解密 OSS Secret)
+function buildStorageUserContext(user, overrides = {}) {
+ if (!user) {
+ return user;
+ }
+
+ const storageUser = {
+ ...user,
+ ...overrides
+ };
+
+ if (typeof storageUser.oss_access_key_secret === 'string' && storageUser.oss_access_key_secret) {
+ try {
+ storageUser.oss_access_key_secret = decryptSecret(storageUser.oss_access_key_secret);
+ } catch {
+ // 已是明文或不是加密串,保持原值
+ }
+ }
+
+ return storageUser;
+}
+
+// 为指定用户创建 OSS 客户端(兼容加密存储的密钥)
+function createOssClientForUser(user, overrides = {}) {
+ const storageUser = buildStorageUserContext(user, {
+ current_storage_type: 'oss',
+ ...overrides
+ });
+ return new OssStorageClient(storageUser);
+}
+
+// 为指定用户创建可直接用于签名的 S3 客户端上下文
+function createS3ClientContextForUser(user, overrides = {}) {
+ const { S3Client } = require('@aws-sdk/client-s3');
+ const ossClient = createOssClientForUser(user, overrides);
+ const effectiveConfig = ossClient.getEffectiveConfig();
+
+ // 修复:未 connect() 时 getBucket() 会回退到用户个人 bucket,
+ // 在启用统一 OSS 配置时可能拿错 bucket(如误用 user_2)。
+ // 这里显式绑定当前有效配置,确保签名下载与上传使用同一 bucket。
+ ossClient.currentConfig = effectiveConfig;
+
+ return {
+ client: new S3Client(ossClient.buildConfig(effectiveConfig)),
+ bucket: effectiveConfig.oss_bucket,
+ ossClient
+ };
+}
+
+const EPHEMERAL_TOKEN_SECRET = JWT_SECRET;
+
+function signEphemeralToken(payload, expiresInSeconds = 900) {
+ const tokenPayload = {
+ ...payload,
+ exp: Math.floor(Date.now() / 1000) + expiresInSeconds
+ };
+
+ const encodedPayload = Buffer.from(JSON.stringify(tokenPayload)).toString('base64url');
+ const signature = crypto
+ .createHmac('sha256', EPHEMERAL_TOKEN_SECRET)
+ .update(encodedPayload)
+ .digest('base64url');
+
+ return `${encodedPayload}.${signature}`;
+}
+
+function verifyEphemeralToken(token, expectedType) {
+ if (!token || typeof token !== 'string') {
+ return { valid: false, error: 'token_missing' };
+ }
+
+ const parts = token.split('.');
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return { valid: false, error: 'token_format_invalid' };
+ }
+
+ const [encodedPayload, receivedSignature] = parts;
+ const expectedSignature = crypto
+ .createHmac('sha256', EPHEMERAL_TOKEN_SECRET)
+ .update(encodedPayload)
+ .digest('base64url');
+
+ const receivedBuffer = Buffer.from(receivedSignature);
+ const expectedBuffer = Buffer.from(expectedSignature);
+ if (
+ receivedBuffer.length !== expectedBuffer.length ||
+ !crypto.timingSafeEqual(receivedBuffer, expectedBuffer)
+ ) {
+ return { valid: false, error: 'token_signature_invalid' };
+ }
+
+ try {
+ const payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8'));
+ if (!payload || typeof payload !== 'object') {
+ return { valid: false, error: 'token_payload_invalid' };
+ }
+
+ if (expectedType && payload.type !== expectedType) {
+ return { valid: false, error: 'token_type_mismatch' };
+ }
+
+ if (!payload.exp || Math.floor(Date.now() / 1000) > payload.exp) {
+ return { valid: false, error: 'token_expired' };
+ }
+
+ return { valid: true, payload };
+ } catch {
+ return { valid: false, error: 'token_parse_failed' };
+ }
+}
+
// ===== 系统日志工具函数 =====
// 从请求中提取日志信息
@@ -1895,8 +2020,10 @@ app.post('/api/login',
// 存储相关字段
storage_permission: user.storage_permission || 'oss_only',
current_storage_type: user.current_storage_type || 'oss',
- local_storage_quota: user.local_storage_quota || 1073741824,
+ local_storage_quota: user.local_storage_quota || DEFAULT_LOCAL_STORAGE_QUOTA_BYTES,
local_storage_used: user.local_storage_used || 0,
+ oss_storage_quota: normalizeOssQuota(user.oss_storage_quota),
+ storage_used: user.storage_used || 0,
// OSS配置来源(重要:用于前端判断是否使用OSS直连上传)
oss_config_source: SettingsDB.hasUnifiedOssConfig() ? 'unified' : (user.has_oss_config ? 'personal' : 'none')
}
@@ -2215,7 +2342,10 @@ app.get('/api/user/oss-usage', authMiddleware, async (req, res) => {
// ===== P0 优化:优先使用数据库缓存 =====
// 从数据库 storage_used 字段读取(上传/删除时增量更新)
const user = UserDB.findById(req.user.id);
- const storageUsed = user.storage_used || 0;
+ const storageUsed = Number(user.storage_used || 0);
+ const ossQuota = normalizeOssQuota(user.oss_storage_quota);
+ const usagePercentage = Math.min(100, Math.round((storageUsed / ossQuota) * 100));
+ const remainingSize = Math.max(ossQuota - storageUsed, 0);
return res.json({
success: true,
@@ -2223,7 +2353,13 @@ app.get('/api/user/oss-usage', authMiddleware, async (req, res) => {
totalSize: storageUsed,
totalSizeFormatted: formatFileSize(storageUsed),
fileCount: null, // 缓存模式不提供文件数
- cached: true
+ cached: true,
+ quota: ossQuota,
+ quotaFormatted: formatFileSize(ossQuota),
+ quotaLimited: true,
+ remainingSize,
+ remainingSizeFormatted: formatFileSize(remainingSize),
+ usagePercentage
},
cached: true
});
@@ -2240,6 +2376,8 @@ app.get('/api/user/oss-usage', authMiddleware, async (req, res) => {
// 获取 OSS 存储空间详细统计(全量统计,仅限管理员或需要精确统计时使用)
// ===== P0 性能优化:此接口较慢,建议只在必要时调用 =====
app.get('/api/user/oss-usage-full', authMiddleware, async (req, res) => {
+ let ossClient;
+
try {
// 检查是否有可用的OSS配置(个人配置或系统级统一配置)
const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig();
@@ -2261,7 +2399,7 @@ app.get('/api/user/oss-usage-full', authMiddleware, async (req, res) => {
}
// 执行全量统计(较慢,仅在缓存未命中时执行)
- const ossClient = new OssStorageClient(req.user);
+ ossClient = createOssClientForUser(req.user);
await ossClient.connect();
let totalSize = 0;
@@ -2271,7 +2409,7 @@ app.get('/api/user/oss-usage-full', authMiddleware, async (req, res) => {
do {
const { ListObjectsV2Command } = require('@aws-sdk/client-s3');
const command = new ListObjectsV2Command({
- Bucket: req.user.oss_bucket,
+ Bucket: ossClient.getBucket(),
Prefix: `user_${req.user.id}/`,
ContinuationToken: continuationToken
});
@@ -2288,8 +2426,6 @@ app.get('/api/user/oss-usage-full', authMiddleware, async (req, res) => {
continuationToken = response.NextContinuationToken;
} while (continuationToken);
- await ossClient.end();
-
const usageData = {
totalSize,
totalSizeFormatted: formatFileSize(totalSize),
@@ -2312,6 +2448,10 @@ app.get('/api/user/oss-usage-full', authMiddleware, async (req, res) => {
success: false,
message: '获取OSS空间使用情况失败: ' + error.message
});
+ } finally {
+ if (ossClient) {
+ await ossClient.end();
+ }
}
});
@@ -2443,6 +2583,54 @@ app.post('/api/user/change-password',
}
);
+// 更新当前用户资料(目前支持邮箱)
+app.post('/api/user/update-profile',
+ authMiddleware,
+ [
+ body('email').isEmail().withMessage('邮箱格式不正确')
+ ],
+ (req, res) => {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ errors: errors.array()
+ });
+ }
+
+ try {
+ const { email } = req.body;
+
+ const existingUser = UserDB.findByEmail(email);
+ if (existingUser && existingUser.id !== req.user.id) {
+ return res.status(400).json({
+ success: false,
+ message: '邮箱已被使用'
+ });
+ }
+
+ UserDB.update(req.user.id, { email });
+
+ const updatedUser = UserDB.findById(req.user.id);
+ res.json({
+ success: true,
+ message: '资料更新成功',
+ user: {
+ id: updatedUser.id,
+ username: updatedUser.username,
+ email: updatedUser.email,
+ is_admin: updatedUser.is_admin
+ }
+ });
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ message: getSafeErrorMessage(error, '更新资料失败,请稍后重试', '更新资料失败')
+ });
+ }
+ }
+);
+
// 修改当前用户名
app.post('/api/user/update-username',
authMiddleware,
@@ -2841,7 +3029,7 @@ app.post('/api/files/folder-info', authMiddleware, async (req, res) => {
do {
const command = new ListObjectsV2Command({
- Bucket: req.user.oss_bucket,
+ Bucket: storage.getBucket(),
Prefix: prefix,
ContinuationToken: continuationToken
});
@@ -2986,6 +3174,7 @@ app.get('/api/files/upload-signature', authMiddleware, async (req, res) => {
const filename = req.query.filename;
const uploadPath = req.query.path || '/'; // 上传目标路径
const contentType = req.query.contentType || 'application/octet-stream';
+ const fileSize = Number(req.query.size);
if (!filename) {
return res.status(400).json({
@@ -3019,6 +3208,22 @@ app.get('/api/files/upload-signature', authMiddleware, async (req, res) => {
});
}
+ // 文件大小参数校验(用于 OSS 配额校验)
+ if (!Number.isFinite(fileSize) || fileSize <= 0) {
+ return res.status(400).json({
+ success: false,
+ message: '缺少或无效的文件大小参数'
+ });
+ }
+
+ const maxUploadSize = parseInt(SettingsDB.get('max_upload_size') || '10737418240', 10);
+ if (Number.isFinite(maxUploadSize) && maxUploadSize > 0 && fileSize > maxUploadSize) {
+ return res.status(400).json({
+ success: false,
+ message: `文件过大,最大允许 ${formatFileSize(maxUploadSize)}`
+ });
+ }
+
// 路径安全验证:防止目录遍历攻击
if (uploadPath.includes('..') || uploadPath.includes('\x00')) {
return res.status(400).json({
@@ -3037,15 +3242,10 @@ app.get('/api/files/upload-signature', authMiddleware, async (req, res) => {
}
try {
- const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
+ const { PutObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
- // 获取有效的 OSS 配置(系统配置优先)
- const unifiedConfig = SettingsDB.getUnifiedOssConfig();
- const effectiveBucket = unifiedConfig ? unifiedConfig.bucket : req.user.oss_bucket;
-
- // 构建 S3 客户端
- const client = new S3Client(buildS3Config(req.user));
+ const { client, bucket } = createS3ClientContextForUser(req.user);
// 构建对象 Key(与 OssStorageClient.getObjectKey 格式一致)
// 格式:user_${id}/${path}/${filename}
@@ -3066,9 +3266,49 @@ app.get('/api/files/upload-signature', authMiddleware, async (req, res) => {
objectKey = `user_${req.user.id}/${normalizedPath}/${sanitizedFilename}`;
}
+ let previousSize = 0;
+ try {
+ const headResponse = await client.send(new HeadObjectCommand({
+ Bucket: bucket,
+ Key: objectKey
+ }));
+ previousSize = Number(headResponse.ContentLength || 0);
+ if (!Number.isFinite(previousSize) || previousSize < 0) {
+ previousSize = 0;
+ }
+ } catch (headError) {
+ const statusCode = headError?.$metadata?.httpStatusCode;
+ if (headError?.name !== 'NotFound' && headError?.name !== 'NoSuchKey' && statusCode !== 404) {
+ throw headError;
+ }
+ }
+
+ // OSS 配额校验(未配置时默认 1GB)
+ const latestUser = UserDB.findById(req.user.id);
+ const ossQuota = normalizeOssQuota(latestUser?.oss_storage_quota);
+ const currentUsage = Number(latestUser?.storage_used || 0);
+ const baseUsage = Math.max(0, currentUsage - previousSize);
+ const projectedUsage = baseUsage + fileSize;
+
+ if (projectedUsage > ossQuota) {
+ const remainingBytes = Math.max(ossQuota - baseUsage, 0);
+ return res.status(400).json({
+ success: false,
+ message: `OSS 配额不足:文件 ${formatFileSize(fileSize)},剩余 ${formatFileSize(remainingBytes)}(总配额 ${formatFileSize(ossQuota)})`
+ });
+ }
+
+ const completionToken = signEphemeralToken({
+ type: 'upload_complete',
+ userId: req.user.id,
+ objectKey,
+ previousSize,
+ expectedSize: fileSize
+ }, 30 * 60);
+
// 创建 PutObject 命令
const command = new PutObjectCommand({
- Bucket: effectiveBucket,
+ Bucket: bucket,
Key: objectKey,
ContentType: contentType
});
@@ -3080,6 +3320,8 @@ app.get('/api/files/upload-signature', authMiddleware, async (req, res) => {
success: true,
uploadUrl: signedUrl,
objectKey: objectKey,
+ previousSize,
+ completionToken,
expiresIn: 900
});
} catch (error) {
@@ -3094,22 +3336,39 @@ app.get('/api/files/upload-signature', authMiddleware, async (req, res) => {
// OSS 上传完成通知(用于更新缓存和数据库)
// ===== P0 性能优化:使用增量更新替代全量统计 =====
app.post('/api/files/upload-complete', authMiddleware, async (req, res) => {
- const { objectKey, size, path } = req.body;
+ const { objectKey, size, completionToken } = req.body;
- if (!objectKey) {
+ const normalizedObjectKey = typeof objectKey === 'string'
+ ? objectKey.replace(/\\/g, '/').replace(/^\/+/, '')
+ : '';
+ const reportedSize = Number(size);
+
+ if (!normalizedObjectKey) {
return res.status(400).json({
success: false,
message: '缺少对象Key参数'
});
}
- if (!size || size < 0) {
+ if (!Number.isFinite(reportedSize) || reportedSize < 0) {
return res.status(400).json({
success: false,
message: '文件大小参数无效'
});
}
+ const expectedPrefix = `user_${req.user.id}/`;
+ if (
+ !normalizedObjectKey.startsWith(expectedPrefix) ||
+ normalizedObjectKey.includes('..') ||
+ normalizedObjectKey.includes('\x00')
+ ) {
+ return res.status(403).json({
+ success: false,
+ message: '对象Key不属于当前用户或格式非法'
+ });
+ }
+
// 安全检查:验证用户是否配置了OSS(个人配置或系统级统一配置)
const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig();
if (!req.user.has_oss_config && !hasUnifiedConfig) {
@@ -3119,17 +3378,94 @@ app.post('/api/files/upload-complete', authMiddleware, async (req, res) => {
});
}
+ const completionTokenResult = verifyEphemeralToken(completionToken, 'upload_complete');
+ if (!completionTokenResult.valid) {
+ return res.status(403).json({
+ success: false,
+ message: '上传完成凭证无效或已过期'
+ });
+ }
+
+ const completionPayload = completionTokenResult.payload || {};
+ if (
+ Number(completionPayload.userId) !== Number(req.user.id) ||
+ completionPayload.objectKey !== normalizedObjectKey
+ ) {
+ return res.status(403).json({
+ success: false,
+ message: '上传完成凭证与对象不匹配'
+ });
+ }
+
+ const previousObjectSize = Number.isFinite(Number(completionPayload.previousSize)) && Number(completionPayload.previousSize) >= 0
+ ? Number(completionPayload.previousSize)
+ : 0;
+
+ let ossClient;
+
try {
- // 更新存储使用量缓存(增量更新)
- await StorageUsageCache.updateUsage(req.user.id, size);
+ const { HeadObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3');
+
+ ossClient = createOssClientForUser(req.user);
+ await ossClient.connect();
+
+ const headResponse = await ossClient.s3Client.send(new HeadObjectCommand({
+ Bucket: ossClient.getBucket(),
+ Key: normalizedObjectKey
+ }));
+
+ const verifiedSize = Number(headResponse.ContentLength || 0);
+
+ if (!Number.isFinite(verifiedSize) || verifiedSize < 0) {
+ throw new Error('无法确认上传对象大小');
+ }
+
+ if (verifiedSize !== reportedSize) {
+ console.warn(`[上传完成] 用户 ${req.user.id} 上报大小(${reportedSize})与实际大小(${verifiedSize})不一致,已使用实际大小`);
+ }
+
+ const deltaSize = verifiedSize - previousObjectSize;
+
+ // 二次校验 OSS 配额(防止签名后并发上传导致超限)
+ const latestUser = UserDB.findById(req.user.id);
+ const ossQuota = normalizeOssQuota(latestUser?.oss_storage_quota);
+ const currentUsage = Number(latestUser?.storage_used || 0);
+ const projectedUsage = Math.max(0, currentUsage + deltaSize);
+
+ if (projectedUsage > ossQuota) {
+ // 回滚:删除刚上传的对象,避免超配额文件残留
+ await ossClient.s3Client.send(new DeleteObjectCommand({
+ Bucket: ossClient.getBucket(),
+ Key: normalizedObjectKey
+ }));
+
+ // 若为覆盖上传,旧对象已被替换,删除后需扣除旧对象体积
+ if (previousObjectSize > 0) {
+ await StorageUsageCache.updateUsage(req.user.id, -previousObjectSize);
+ }
+
+ clearOssUsageCache(req.user.id);
+
+ return res.status(400).json({
+ success: false,
+ message: `OSS 配额不足:已用 ${formatFileSize(currentUsage)},配额 ${formatFileSize(ossQuota)},上传后将超限`
+ });
+ }
+
+ // 更新存储使用量缓存(增量更新,覆盖上传只记录差值)
+ if (deltaSize !== 0) {
+ await StorageUsageCache.updateUsage(req.user.id, deltaSize);
+ }
// 同时更新旧的内存缓存(保持兼容性)
clearOssUsageCache(req.user.id);
- console.log(`[上传完成] 用户 ${req.user.id} 文件大小: ${size} 字节`);
+ console.log(`[上传完成] 用户 ${req.user.id} 文件大小: ${verifiedSize} 字节, 增量: ${deltaSize} 字节`);
res.json({
success: true,
- message: '上传完成已记录'
+ message: '上传完成已记录',
+ recordedSize: verifiedSize,
+ deltaSize
});
} catch (error) {
console.error('[OSS上传] 记录上传完成失败:', error);
@@ -3137,6 +3473,10 @@ app.post('/api/files/upload-complete', authMiddleware, async (req, res) => {
success: false,
message: '记录上传完成失败: ' + error.message
});
+ } finally {
+ if (ossClient) {
+ await ossClient.end();
+ }
}
});
@@ -3170,23 +3510,15 @@ app.get('/api/files/download-url', authMiddleware, async (req, res) => {
}
try {
- const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
+ const { GetObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
- // 获取有效的 OSS 配置(系统配置优先)
- const unifiedConfig = SettingsDB.getUnifiedOssConfig();
- const effectiveBucket = unifiedConfig ? unifiedConfig.bucket : req.user.oss_bucket;
-
- // 构建 S3 客户端
- const client = new S3Client(buildS3Config(req.user));
-
- // 构建对象 Key(复用 OssStorageClient 的 getObjectKey 方法,确保路径格式正确)
- const tempClient = new OssStorageClient(req.user);
- const objectKey = tempClient.getObjectKey(normalizedPath);
+ const { client, bucket, ossClient } = createS3ClientContextForUser(req.user);
+ const objectKey = ossClient.getObjectKey(normalizedPath);
// 创建 GetObject 命令
const command = new GetObjectCommand({
- Bucket: effectiveBucket,
+ Bucket: bucket,
Key: objectKey,
ResponseContentDisposition: `attachment; filename="${encodeURIComponent(filePath.split('/').pop())}"`
});
@@ -3208,15 +3540,6 @@ app.get('/api/files/download-url', authMiddleware, async (req, res) => {
}
});
-// 辅助函数:构建 S3 配置(复用 OssStorageClient.buildConfig)
-function buildS3Config(user) {
- // 创建临时 OssStorageClient 实例并复用其 buildConfig 方法
- // OssStorageClient 已在文件顶部导入
- const tempClient = new OssStorageClient(user);
- const config = tempClient.getEffectiveConfig(); // 先获取有效配置(系统配置优先)
- return tempClient.buildConfig(config); // 然后传递给buildConfig
-}
-
// 辅助函数:清理文件名(增强版安全处理)
function sanitizeFilename(filename) {
if (!filename || typeof filename !== 'string') {
@@ -3742,12 +4065,34 @@ app.post('/api/share/create', authMiddleware, (req, res) => {
}
}
- // 参数验证:密码长度限制
- if (password && (typeof password !== 'string' || password.length > 32)) {
- return res.status(400).json({
- success: false,
- message: '密码长度不能超过32个字符'
- });
+ const hasPasswordField = Object.prototype.hasOwnProperty.call(req.body || {}, 'password');
+ let normalizedPassword = null;
+
+ if (hasPasswordField) {
+ if (password !== null && password !== undefined && typeof password !== 'string') {
+ return res.status(400).json({
+ success: false,
+ message: '密码格式无效'
+ });
+ }
+
+ if (typeof password === 'string') {
+ normalizedPassword = password.trim();
+ }
+
+ if (password !== null && password !== undefined && !normalizedPassword) {
+ return res.status(400).json({
+ success: false,
+ message: '已启用密码保护时,访问密码不能为空'
+ });
+ }
+
+ if (normalizedPassword && normalizedPassword.length > 32) {
+ return res.status(400).json({
+ success: false,
+ message: '密码长度不能超过32个字符'
+ });
+ }
}
// 路径安全验证:防止路径遍历攻击
@@ -3770,7 +4115,7 @@ app.post('/api/share/create', authMiddleware, (req, res) => {
share_type: actualShareType,
file_path: file_path || '',
file_name: file_name || '',
- password: password || null,
+ password: normalizedPassword || null,
expiry_days: expiry_days || null
});
@@ -3783,7 +4128,7 @@ app.post('/api/share/create', authMiddleware, (req, res) => {
// 记录分享创建日志
logShare(req, 'create_share',
`用户创建分享: ${actualShareType === 'file' ? '文件' : '目录'} ${file_path}`,
- { shareCode: result.share_code, sharePath: file_path, shareType: actualShareType, hasPassword: !!password }
+ { shareCode: result.share_code, sharePath: file_path, shareType: actualShareType, hasPassword: !!normalizedPassword }
);
res.json({
@@ -3793,6 +4138,7 @@ app.post('/api/share/create', authMiddleware, (req, res) => {
share_url: shareUrl,
share_type: result.share_type,
expires_at: result.expires_at,
+ has_password: !!normalizedPassword
});
} catch (error) {
console.error('创建分享链接失败:', error);
@@ -3810,10 +4156,16 @@ app.get('/api/share/my', authMiddleware, (req, res) => {
res.json({
success: true,
- shares: shares.map(share => ({
- ...share,
- share_url: `${getSecureBaseUrl(req)}/s/${share.share_code}`
- }))
+ shares: shares.map(share => {
+ const hasPassword = !!share.share_password;
+ const { share_password, ...safeShare } = share;
+
+ return {
+ ...safeShare,
+ has_password: hasPassword,
+ share_url: `${getSecureBaseUrl(req)}/s/${share.share_code}`
+ };
+ })
});
} catch (error) {
console.error('获取分享列表失败:', error);
@@ -3953,7 +4305,8 @@ app.post('/api/share/:code/verify', shareRateLimitMiddleware, async (req, res) =
}
return res.status(401).json({
success: false,
- message: '密码错误'
+ message: '密码错误',
+ needPassword: true
});
}
}
@@ -4002,10 +4355,9 @@ app.post('/api/share/:code/verify', shareRateLimitMiddleware, async (req, res) =
// 使用统一存储接口
const { StorageInterface } = require('./storage');
- const userForStorage = {
- ...shareOwner,
+ const userForStorage = buildStorageUserContext(shareOwner, {
current_storage_type: storageType
- };
+ });
const storageInterface = new StorageInterface(userForStorage);
storage = await storageInterface.connect();
@@ -4097,15 +4449,20 @@ app.post('/api/share/:code/list', shareRateLimitMiddleware, async (req, res) =>
}
// 验证密码
- if (share.share_password && !ShareDB.verifyPassword(password, share.share_password)) {
- // 记录密码错误
- if (req.shareRateLimitKey) {
- shareLimiter.recordFailure(req.shareRateLimitKey);
+ if (share.share_password) {
+ const normalizedPassword = typeof password === 'string' ? password : '';
+
+ if (!normalizedPassword || !ShareDB.verifyPassword(normalizedPassword, share.share_password)) {
+ // 记录密码错误
+ if (req.shareRateLimitKey) {
+ shareLimiter.recordFailure(req.shareRateLimitKey);
+ }
+ return res.status(401).json({
+ success: false,
+ message: '密码错误或未提供密码',
+ needPassword: true
+ });
}
- return res.status(401).json({
- success: false,
- message: '密码错误'
- });
}
// 清除失败记录(密码验证成功或无密码)
@@ -4142,10 +4499,9 @@ app.post('/api/share/:code/list', shareRateLimitMiddleware, async (req, res) =>
console.log(`[分享列表] 存储类型: ${storageType}, 分享路径: ${share.share_path}`);
// 临时构造用户对象以使用存储接口
- const userForStorage = {
- ...shareOwner,
+ const userForStorage = buildStorageUserContext(shareOwner, {
current_storage_type: storageType
- };
+ });
const storageInterface = new StorageInterface(userForStorage);
storage = await storageInterface.connect();
@@ -4258,9 +4614,9 @@ app.post('/api/share/:code/download', shareRateLimitMiddleware, (req, res) => {
});
// 生成分享文件下载签名 URL(OSS 直连下载,公开 API,添加限流保护)
-app.get('/api/share/:code/download-url', shareRateLimitMiddleware, async (req, res) => {
+app.post('/api/share/:code/download-url', shareRateLimitMiddleware, async (req, res) => {
const { code } = req.params;
- const { path: filePath, password } = req.query;
+ const { path: filePath, password } = req.body || {};
// 参数验证:code 不能为空
if (!code || typeof code !== 'string' || code.length < 1 || code.length > 32) {
@@ -4270,13 +4626,20 @@ app.get('/api/share/:code/download-url', shareRateLimitMiddleware, async (req, r
});
}
- if (!filePath) {
+ if (!filePath || typeof filePath !== 'string') {
return res.status(400).json({
success: false,
message: '缺少文件路径参数'
});
}
+ if (filePath.includes('\x00')) {
+ return res.status(400).json({
+ success: false,
+ message: '文件路径非法'
+ });
+ }
+
try {
const share = ShareDB.findByCode(code);
@@ -4290,11 +4653,19 @@ app.get('/api/share/:code/download-url', shareRateLimitMiddleware, async (req, r
// 验证密码(如果需要)
if (share.share_password) {
if (!password || !ShareDB.verifyPassword(password, share.share_password)) {
+ if (req.shareRateLimitKey) {
+ shareLimiter.recordFailure(req.shareRateLimitKey);
+ }
return res.status(401).json({
success: false,
- message: '密码错误或未提供密码'
+ message: '密码错误或未提供密码',
+ needPassword: true
});
}
+
+ if (req.shareRateLimitKey) {
+ shareLimiter.recordSuccess(req.shareRateLimitKey);
+ }
}
// 安全验证:检查请求路径是否在分享范围内
@@ -4314,35 +4685,46 @@ app.get('/api/share/:code/download-url', shareRateLimitMiddleware, async (req, r
});
}
- // 检查是否使用 OSS(包括个人配置和系统级统一配置)
- const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig();
- if (!shareOwner.has_oss_config && !hasUnifiedConfig) {
- // 本地存储模式:返回后端下载 URL
+ const storageType = share.storage_type || 'oss';
+
+ // 本地存储模式:返回后端下载 URL(短期 token,避免在 URL 中传密码)
+ if (storageType !== 'oss') {
+ let downloadUrl = `${getSecureBaseUrl(req)}/api/share/${code}/download-file?path=${encodeURIComponent(filePath)}`;
+
+ if (share.share_password) {
+ const downloadToken = signEphemeralToken({
+ type: 'share_download',
+ code,
+ path: filePath
+ }, 15 * 60);
+ downloadUrl += `&token=${encodeURIComponent(downloadToken)}`;
+ }
+
return res.json({
success: true,
- downloadUrl: `${req.protocol}://${req.get('host')}/api/share/${code}/download-file?path=${encodeURIComponent(filePath)}${password ? '&password=' + encodeURIComponent(password) : ''}`,
+ downloadUrl,
direct: false
});
}
- // OSS 模式:生成签名 URL
- const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
+ // OSS 模式:检查配置并生成签名 URL
+ const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig();
+ if (!shareOwner.has_oss_config && !hasUnifiedConfig) {
+ return res.status(400).json({
+ success: false,
+ message: '分享者未配置 OSS 服务'
+ });
+ }
+
+ const { GetObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
- // 获取有效的 OSS 配置(系统配置优先)
- const unifiedConfig = SettingsDB.getUnifiedOssConfig();
- const effectiveBucket = unifiedConfig ? unifiedConfig.bucket : shareOwner.oss_bucket;
-
- // 构建 S3 客户端
- const client = new S3Client(buildS3Config(shareOwner));
-
- // 构建对象 Key(复用 OssStorageClient 的 getObjectKey 方法,确保路径格式正确)
- const tempClient = new OssStorageClient(shareOwner);
- const objectKey = tempClient.getObjectKey(filePath);
+ const { client, bucket, ossClient } = createS3ClientContextForUser(shareOwner);
+ const objectKey = ossClient.getObjectKey(filePath);
// 创建 GetObject 命令
const command = new GetObjectCommand({
- Bucket: effectiveBucket,
+ Bucket: bucket,
Key: objectKey,
ResponseContentDisposition: `attachment; filename="${encodeURIComponent(filePath.split('/').pop())}"`
});
@@ -4370,7 +4752,7 @@ app.get('/api/share/:code/download-url', shareRateLimitMiddleware, async (req, r
// 注意:OSS 模式请使用 /api/share/:code/download-url 获取直连下载链接
app.get('/api/share/:code/download-file', shareRateLimitMiddleware, async (req, res) => {
const { code } = req.params;
- const { path: filePath, password } = req.query;
+ const { path: filePath, password, token } = req.query;
let storage;
let storageEnded = false; // 防止重复关闭
@@ -4411,17 +4793,32 @@ app.get('/api/share/:code/download-file', shareRateLimitMiddleware, async (req,
});
}
- // 验证密码(如果需要)
+ // 验证密码(如果需要),支持短期下载 token(避免密码出现在 URL)
if (share.share_password) {
- if (!password || !ShareDB.verifyPassword(password, share.share_password)) {
- // 只在密码错误时记录失败
- if (req.shareRateLimitKey) {
- shareLimiter.recordFailure(req.shareRateLimitKey);
+ let verifiedByToken = false;
+
+ if (token) {
+ const tokenResult = verifyEphemeralToken(token, 'share_download');
+ if (tokenResult.valid) {
+ const tokenPayload = tokenResult.payload || {};
+ if (tokenPayload.code === code && tokenPayload.path === filePath) {
+ verifiedByToken = true;
+ }
+ }
+ }
+
+ if (!verifiedByToken) {
+ if (!password || !ShareDB.verifyPassword(password, share.share_password)) {
+ // 只在密码错误时记录失败
+ if (req.shareRateLimitKey) {
+ shareLimiter.recordFailure(req.shareRateLimitKey);
+ }
+ return res.status(401).json({
+ success: false,
+ message: '密码错误或未提供密码',
+ needPassword: true
+ });
}
- return res.status(401).json({
- success: false,
- message: '密码错误或未提供密码'
- });
}
// 密码验证成功,清除失败记录
@@ -4456,10 +4853,9 @@ app.get('/api/share/:code/download-file', shareRateLimitMiddleware, async (req,
console.log(`[分享下载] 存储类型: ${storageType} (分享记录), 文件路径: ${filePath}`);
// 临时构造用户对象以使用存储接口
- const userForStorage = {
- ...shareOwner,
+ const userForStorage = buildStorageUserContext(shareOwner, {
current_storage_type: storageType
- };
+ });
const storageInterface = new StorageInterface(userForStorage);
storage = await storageInterface.connect();
@@ -4675,7 +5071,6 @@ app.get('/api/admin/unified-oss-config', authMiddleware, adminMiddleware, (req,
app.post('/api/admin/unified-oss-config',
authMiddleware,
adminMiddleware,
- requirePasswordConfirmation, // 安全修复:添加密码二次验证
[
body('provider').isIn(['aliyun', 'tencent', 'aws']).withMessage('无效的OSS服务商'),
body('region').notEmpty().withMessage('地域不能为空'),
@@ -4817,7 +5212,6 @@ app.post('/api/admin/unified-oss-config/test',
app.delete('/api/admin/unified-oss-config',
authMiddleware,
adminMiddleware,
- requirePasswordConfirmation, // 安全修复:添加密码二次验证
(req, res) => {
try {
SettingsDB.clearUnifiedOssConfig();
@@ -5018,7 +5412,7 @@ app.get('/api/admin/health-check', authMiddleware, adminMiddleware, async (req,
});
// 11. CSRF 保护检查
- const csrfEnabled = process.env.ENABLE_CSRF === 'true';
+ const csrfEnabled = ENABLE_CSRF;
checks.push({
name: 'CSRF保护',
category: 'security',
@@ -5101,7 +5495,6 @@ app.get('/api/admin/wal-info', authMiddleware, adminMiddleware, (req, res) => {
app.post('/api/admin/wal-checkpoint',
authMiddleware,
adminMiddleware,
- requirePasswordConfirmation, // 安全修复:WAL 检查点是敏感操作
(req, res) => {
try {
const beforeSize = WalManager.getWalFileSize();
@@ -5253,8 +5646,10 @@ app.get('/api/admin/users', authMiddleware, adminMiddleware, (req, res) => {
// 新增:存储相关字段
storage_permission: u.storage_permission || 'oss_only',
current_storage_type: u.current_storage_type || 'oss',
- local_storage_quota: u.local_storage_quota || 1073741824,
- local_storage_used: u.local_storage_used || 0
+ local_storage_quota: u.local_storage_quota || DEFAULT_LOCAL_STORAGE_QUOTA_BYTES,
+ local_storage_used: u.local_storage_used || 0,
+ oss_storage_quota: normalizeOssQuota(u.oss_storage_quota),
+ storage_used: u.storage_used || 0
}))
});
} catch (error) {
@@ -5330,7 +5725,6 @@ app.get('/api/admin/logs/stats', authMiddleware, adminMiddleware, (req, res) =>
app.post('/api/admin/logs/cleanup',
authMiddleware,
adminMiddleware,
- requirePasswordConfirmation, // 安全修复:添加密码二次验证(日志清理影响审计追踪)
(req, res) => {
try {
const { keepDays = 90 } = req.body;
@@ -5428,7 +5822,6 @@ app.get('/api/admin/storage-cache/check/:userId',
app.post('/api/admin/storage-cache/rebuild/:userId',
authMiddleware,
adminMiddleware,
- requirePasswordConfirmation, // 安全修复:重建缓存是敏感操作
async (req, res) => {
try {
const { userId } = req.params;
@@ -5538,7 +5931,6 @@ app.get('/api/admin/storage-cache/check-all',
app.post('/api/admin/storage-cache/auto-fix',
authMiddleware,
adminMiddleware,
- requirePasswordConfirmation, // 安全修复:批量修复是敏感操作
async (req, res) => {
try {
const { threshold = 0 } = req.body; // 差异阈值(字节)
@@ -5614,7 +6006,6 @@ app.post('/api/admin/storage-cache/auto-fix',
app.post('/api/admin/users/:id/ban',
authMiddleware,
adminMiddleware,
- requirePasswordConfirmation, // 安全修复:添加密码二次验证(封禁用户是敏感操作)
(req, res) => {
try {
const { id } = req.params;
@@ -5687,7 +6078,6 @@ app.post('/api/admin/users/:id/ban',
app.delete('/api/admin/users/:id',
authMiddleware,
adminMiddleware,
- requirePasswordConfirmation, // 安全修复:添加密码二次验证
async (req, res) => {
try {
const { id } = req.params;
@@ -5841,9 +6231,10 @@ function getUserDirectorySize(dirPath) {
app.post('/api/admin/users/:id/storage-permission',
authMiddleware,
adminMiddleware,
- requirePasswordConfirmation, // 安全修复:添加密码二次验证(修改存储权限影响用户数据访问)
[
- body('storage_permission').isIn(['local_only', 'oss_only', 'user_choice']).withMessage('无效的存储权限')
+ body('storage_permission').isIn(['local_only', 'oss_only', 'user_choice']).withMessage('无效的存储权限'),
+ body('local_storage_quota').optional({ nullable: true }).isInt({ min: 1048576, max: 10995116277760 }).withMessage('本地配额必须在 1MB 到 10TB 之间'),
+ body('oss_storage_quota').optional({ nullable: true }).isInt({ min: 1048576, max: 10995116277760 }).withMessage('OSS配额必须在 1MB 到 10TB 之间')
],
(req, res) => {
const errors = validationResult(req);
@@ -5856,7 +6247,7 @@ app.post('/api/admin/users/:id/storage-permission',
try {
const { id } = req.params;
- const { storage_permission, local_storage_quota } = req.body;
+ const { storage_permission, local_storage_quota, oss_storage_quota } = req.body;
// 参数验证:验证 ID 格式
const userId = parseInt(id, 10);
@@ -5869,11 +6260,16 @@ app.post('/api/admin/users/:id/storage-permission',
const updates = { storage_permission };
- // 如果提供了配额,更新配额(单位:字节)
- if (local_storage_quota !== undefined) {
+ // 如果提供了本地配额,更新配额(单位:字节)
+ if (local_storage_quota !== undefined && local_storage_quota !== null) {
updates.local_storage_quota = parseInt(local_storage_quota, 10);
}
+ // 如果提供了 OSS 配额,更新配额(单位:字节)
+ if (oss_storage_quota !== undefined && oss_storage_quota !== null) {
+ updates.oss_storage_quota = parseInt(oss_storage_quota, 10);
+ }
+
// 根据权限设置自动调整存储类型
const user = UserDB.findById(userId);
if (!user) {
@@ -5947,7 +6343,7 @@ app.get('/api/admin/users/:id/files', authMiddleware, adminMiddleware, async (re
}
// OssStorageClient 已在文件顶部导入
- ossClient = new OssStorageClient(user);
+ ossClient = createOssClientForUser(user);
await ossClient.connect();
const list = await ossClient.list(dirPath);
@@ -6005,7 +6401,6 @@ app.get('/api/admin/shares', authMiddleware, adminMiddleware, (req, res) => {
app.delete('/api/admin/shares/:id',
authMiddleware,
adminMiddleware,
- requirePasswordConfirmation, // 安全修复:添加密码二次验证(删除用户分享是敏感操作)
(req, res) => {
try {
// 参数验证:验证 ID 格式
diff --git a/backend/storage.js b/backend/storage.js
index 6214736..b048f94 100644
--- a/backend/storage.js
+++ b/backend/storage.js
@@ -1534,7 +1534,7 @@ class OssStorageClient {
*/
async createReadStream(filePath) {
const key = this.getObjectKey(filePath);
- const bucket = this.user.oss_bucket;
+ const bucket = this.getBucket();
const command = new GetObjectCommand({
Bucket: bucket,
@@ -1589,7 +1589,7 @@ class OssStorageClient {
*/
async getPresignedUrl(filePath, expiresIn = 3600) {
const key = this.getObjectKey(filePath);
- const bucket = this.user.oss_bucket;
+ const bucket = this.getBucket();
try {
const command = new GetObjectCommand({
@@ -1646,7 +1646,7 @@ class OssStorageClient {
*/
async getUploadPresignedUrl(filePath, expiresIn = 900, contentType = 'application/octet-stream') {
const key = this.getObjectKey(filePath);
- const bucket = this.user.oss_bucket;
+ const bucket = this.getBucket();
try {
const command = new PutObjectCommand({
diff --git a/backend/test_share.js b/backend/test_share.js
index bad4ba5..06e8df7 100644
--- a/backend/test_share.js
+++ b/backend/test_share.js
@@ -448,7 +448,7 @@ async function testGetDownloadUrl() {
}
try {
- const res = await request('GET', `/api/share/${testShareCode}/download-url?path=/test-file.txt`);
+ const res = await request('POST', `/api/share/${testShareCode}/download-url`, { path: '/test-file.txt' });
// 如果文件存在
if (res.status === 200) {
@@ -481,7 +481,7 @@ async function testDownloadWithPassword() {
// 测试无密码
try {
- const res1 = await request('GET', `/api/share/${passwordShareCode}/download-url?path=/test-file-password.txt`);
+ const res1 = await request('POST', `/api/share/${passwordShareCode}/download-url`, { path: '/test-file-password.txt' });
assert(res1.status === 401, '无密码应返回 401');
} catch (error) {
console.log(` [ERROR] 测试无密码下载: ${error.message}`);
@@ -489,7 +489,7 @@ async function testDownloadWithPassword() {
// 测试带密码
try {
- const res2 = await request('GET', `/api/share/${passwordShareCode}/download-url?path=/test-file-password.txt&password=test123`);
+ const res2 = await request('POST', `/api/share/${passwordShareCode}/download-url`, { path: '/test-file-password.txt', password: 'test123' });
// 密码正确,根据文件是否存在返回不同结果
if (res2.status === 200) {
assert(res2.data.downloadUrl, '应返回下载链接');
@@ -535,7 +535,7 @@ async function testDownloadPathValidation() {
// 测试越权访问
try {
- const res = await request('GET', `/api/share/${testShareCode}/download-url?path=/other-file.txt`);
+ const res = await request('POST', `/api/share/${testShareCode}/download-url`, { path: '/other-file.txt' });
// 单文件分享应该禁止访问其他文件
assert(res.status === 403 || res.status === 404, '越权访问应被拒绝');
@@ -546,7 +546,7 @@ async function testDownloadPathValidation() {
// 测试路径遍历
try {
- const res2 = await request('GET', `/api/share/${testShareCode}/download-url?path=/../../../etc/passwd`);
+ const res2 = await request('POST', `/api/share/${testShareCode}/download-url`, { path: '/../../../etc/passwd' });
assert(res2.status === 403 || res2.status === 400, '路径遍历应被拒绝');
} catch (error) {
console.log(` [ERROR] 路径遍历测试: ${error.message}`);
@@ -682,7 +682,7 @@ async function testShareNotExists() {
// 下载
try {
- const res3 = await request('GET', `/api/share/${nonExistentCode}/download-url?path=/test.txt`);
+ const res3 = await request('POST', `/api/share/${nonExistentCode}/download-url`, { path: '/test.txt' });
assert(res3.status === 404, '下载不存在分享应返回 404');
} catch (error) {
console.log(` [ERROR] ${error.message}`);
diff --git a/backend/test_share_edge_cases.js b/backend/test_share_edge_cases.js
index 4b696b6..5edc381 100644
--- a/backend/test_share_edge_cases.js
+++ b/backend/test_share_edge_cases.js
@@ -225,7 +225,7 @@ async function testPathTraversalAttacks() {
let blocked = 0;
for (const attackPath of attackPaths) {
- const res = await request('GET', `/api/share/${shareCode}/download-url?path=${encodeURIComponent(attackPath)}`);
+ const res = await request('POST', `/api/share/${shareCode}/download-url`, { path: attackPath });
if (res.status === 403 || res.status === 400) {
blocked++;
diff --git a/backend/utils/storage-cache.js b/backend/utils/storage-cache.js
index df9702b..9c8e35c 100644
--- a/backend/utils/storage-cache.js
+++ b/backend/utils/storage-cache.js
@@ -53,21 +53,20 @@ class StorageUsageCache {
*/
static async updateUsage(userId, deltaSize) {
try {
- // 使用 SQL 原子操作,避免并发问题
- const result = UserDB.update(userId, {
- // 使用原始 SQL,因为 update 方法不支持表达式
- // 注意:这里需要在数据库层执行 UPDATE ... SET storage_used = storage_used + ?
- });
+ const numericDelta = Number(deltaSize);
+ if (!Number.isFinite(numericDelta)) {
+ throw new Error('deltaSize 必须是有效数字');
+ }
- // 直接执行 SQL 更新
+ // 直接执行 SQL 原子更新,并保证不小于 0
const { db } = require('../database');
db.prepare(`
UPDATE users
- SET storage_used = storage_used + ?
+ SET storage_used = MAX(COALESCE(storage_used, 0) + ?, 0)
WHERE id = ?
- `).run(deltaSize, userId);
+ `).run(numericDelta, userId);
- console.log(`[存储缓存] 用户 ${userId} 存储变化: ${deltaSize > 0 ? '+' : ''}${deltaSize} 字节`);
+ console.log(`[存储缓存] 用户 ${userId} 存储变化: ${numericDelta > 0 ? '+' : ''}${numericDelta} 字节`);
return true;
} catch (error) {
console.error('[存储缓存] 更新失败:', error);
diff --git a/frontend/app.html b/frontend/app.html
index ffda332..e9e2adc 100644
--- a/frontend/app.html
+++ b/frontend/app.html
@@ -521,8 +521,30 @@
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.15);
}
+
+ .file-grid-item,
+ .file-grid-item *,
+ .file-list-row,
+ .file-list-row *,
+ .file-list-name-wrap,
+ .file-list-name-text {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ user-select: none;
+ -webkit-tap-highlight-color: transparent;
+ }
+ .file-grid-item,
+ .file-list-row {
+ touch-action: pan-y;
+ }
+ .file-thumbnail,
+ .file-list-thumb {
+ -webkit-user-drag: none;
+ user-drag: none;
+ pointer-events: none;
+ }
.file-icon {
- margin-bottom: 10px;
+ margin-bottom: 8px;
}
.file-thumbnail {
width: 64px;
@@ -622,6 +644,17 @@
margin-bottom: 20px;
}
+
+ /* 管理端响应式增强 */
+ .admin-tabs-card {
+ overflow: hidden;
+ }
+ .admin-users-table-wrap,
+ .admin-log-list {
+ -webkit-overflow-scrolling: touch;
+ }
+
+
/* 移动端适配 */
@media (max-width: 768px) {
/* 导航栏移动端优化 */
@@ -736,6 +769,12 @@
.file-list .file-icon {
font-size: 16px !important;
}
+ .file-list-action-col {
+ display: table-cell !important;
+ }
+ .file-row-mobile-more {
+ display: inline-flex !important;
+ }
/* 列表视图视频图标容器移动端适配 */
.file-list div[style*="background: linear-gradient"] {
width: 28px !important;
@@ -791,6 +830,110 @@
.card table button i {
font-size: 10px;
}
+
+ /* 管理员页面移动端增强 */
+ .admin-tabs-nav {
+ display: grid !important;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 6px;
+ border-bottom: none !important;
+ padding: 8px;
+ }
+ .admin-tab-btn {
+ width: 100%;
+ padding: 10px 8px !important;
+ font-size: 13px !important;
+ border-radius: 8px !important;
+ }
+ .admin-debug-row,
+ .admin-health-header,
+ .admin-log-header {
+ flex-direction: column !important;
+ align-items: flex-start !important;
+ gap: 10px;
+ }
+ .admin-debug-toggle-btn {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ padding: 10px 12px !important;
+ font-size: 14px !important;
+ }
+ .admin-health-summary {
+ width: 100%;
+ flex-wrap: wrap;
+ gap: 8px !important;
+ }
+ .admin-log-actions {
+ width: 100%;
+ display: grid !important;
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+ }
+ .admin-log-actions .btn {
+ width: 100%;
+ }
+ .admin-log-filters {
+ gap: 10px !important;
+ padding: 12px !important;
+ }
+ .admin-log-filter-item {
+ width: 100%;
+ flex-direction: column;
+ align-items: stretch !important;
+ gap: 6px !important;
+ }
+ .admin-log-filter-item select,
+ .admin-log-filter-item input {
+ width: 100%;
+ min-width: 0;
+ }
+ .admin-log-filter-search {
+ min-width: 0 !important;
+ }
+ .admin-log-row {
+ display: grid !important;
+ grid-template-columns: 1fr auto;
+ gap: 8px 10px;
+ }
+ .admin-log-time {
+ width: auto !important;
+ }
+ .admin-log-level {
+ width: auto !important;
+ justify-self: end;
+ }
+ .admin-log-category {
+ display: none !important;
+ }
+ .admin-log-content {
+ grid-column: 1 / -1;
+ }
+ .admin-log-pager {
+ flex-wrap: wrap;
+ }
+ .admin-log-pager .btn {
+ min-width: 110px;
+ }
+ .admin-users-table {
+ min-width: 700px !important;
+ }
+ .admin-users-table th:nth-child(1),
+ .admin-users-table td:nth-child(1),
+ .admin-users-table th:nth-child(6),
+ .admin-users-table td:nth-child(6) {
+ display: none;
+ }
+ .admin-user-actions {
+ justify-content: flex-start !important;
+ gap: 4px !important;
+ }
+ .admin-user-action-btn {
+ padding: 4px 8px !important;
+ }
+
}
/* 超小屏幕优化 (手机竖屏) */
@@ -821,10 +964,40 @@
font-size: 11px;
}
+
+ /* 管理员页面超小屏优化 */
+ .admin-tabs-nav {
+ grid-template-columns: 1fr;
+ }
+ .admin-log-actions {
+ grid-template-columns: 1fr;
+ }
+ .admin-health-summary {
+ font-size: 12px !important;
+ }
+ .admin-log-row {
+ padding: 8px 4px !important;
+ }
+ .admin-log-time {
+ font-size: 11px !important;
+ }
+ .admin-users-table {
+ min-width: 620px !important;
+ }
+ .admin-user-action-btn {
+ font-size: 10px !important;
+ }
+ .admin-users-table th:nth-child(3),
+ .admin-users-table td:nth-child(3),
+ .admin-users-table th:nth-child(7),
+ .admin-users-table td:nth-child(7) {
+ display: none;
+ }
+
/* 列表视图在超小屏幕隐藏文件大小列 */
.file-list th:nth-child(2),
.file-list td:nth-child(2) {
- font-size: 11px;
+ display: none;
}
}
@@ -938,6 +1111,22 @@
background: rgba(0, 0, 0, 0.04);
}
+ .file-list-action-col {
+ display: none;
+ }
+ .file-row-mobile-more {
+ display: none;
+ border: 1px solid var(--glass-border);
+ background: rgba(255, 255, 255, 0.08);
+ color: var(--text-secondary);
+ padding: 6px 10px;
+ border-radius: 8px;
+ min-width: 36px;
+ justify-content: center;
+ align-items: center;
+ touch-action: manipulation;
+ }
+
/* ========== 危险按钮 ========== */
.btn-danger {
background: linear-gradient(135deg, #ef4444, #dc2626);
@@ -1138,15 +1327,154 @@
min-width: 140px;
cursor: pointer;
}
+
+ .share-password-toggle {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 12px;
+ border-radius: 10px;
+ border: 1px solid var(--glass-border);
+ background: rgba(255, 255, 255, 0.04);
+ color: var(--text-secondary);
+ font-size: 13px;
+ cursor: pointer;
+ user-select: none;
+ }
+ .share-password-toggle input {
+ accent-color: var(--accent-1);
+ width: 16px;
+ height: 16px;
+ }
+ .share-password-hint {
+ display: block;
+ margin-top: 8px;
+ color: var(--text-muted);
+ font-size: 12px;
+ line-height: 1.5;
+ }
+ .share-success-panel {
+ border: 1px solid rgba(34, 197, 94, 0.28);
+ border-radius: 12px;
+ padding: 14px;
+ background: rgba(34, 197, 94, 0.08);
+ color: var(--text-primary);
+ display: grid;
+ gap: 12px;
+ }
+ .share-success-head {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+ .share-success-head > i {
+ color: #22c55e;
+ font-size: 22px;
+ }
+ .share-success-title {
+ font-size: 15px;
+ font-weight: 700;
+ color: #22c55e;
+ }
+ .share-success-subtitle {
+ margin-top: 2px;
+ font-size: 13px;
+ color: var(--text-secondary);
+ word-break: break-all;
+ }
+ .share-success-link {
+ border-radius: 10px;
+ border: 1px dashed rgba(34, 197, 94, 0.35);
+ background: rgba(0, 0, 0, 0.12);
+ padding: 10px 12px;
+ font-size: 13px;
+ color: var(--text-primary);
+ word-break: break-all;
+ }
+ .share-success-actions {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+ }
+ .share-success-actions .btn {
+ flex: 1;
+ min-width: 96px;
+ padding: 9px 12px;
+ font-size: 13px;
+ }
+ .share-success-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+ .share-success-tip {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 12px;
+ padding: 10px 12px;
+ border-radius: 10px;
+ border: 1px solid transparent;
+ line-height: 1.5;
+ }
+ .share-success-tip.warn {
+ color: #f59e0b;
+ border-color: rgba(245, 158, 11, 0.28);
+ background: rgba(245, 158, 11, 0.12);
+ }
+ .share-success-tip.info {
+ color: #60a5fa;
+ border-color: rgba(59, 130, 246, 0.26);
+ background: rgba(59, 130, 246, 0.12);
+ }
body.light-theme .share-card {
box-shadow: 0 10px 30px rgba(0,0,0,0.08);
}
body.light-theme .share-card:hover {
box-shadow: 0 14px 36px rgba(0,0,0,0.12);
}
+
+ @media (max-width: 768px) {
+ .share-toolbar {
+ flex-direction: column;
+ align-items: stretch;
+ }
+ .share-toolbar input,
+ .share-toolbar select {
+ width: 100%;
+ min-width: 0 !important;
+ }
+ .share-card-grid {
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ }
+ .share-card__actions .btn {
+ flex: 1;
+ min-width: 108px;
+ }
+ .share-success-actions {
+ flex-direction: column;
+ }
+ .share-success-actions .btn {
+ width: 100%;
+ min-width: 0;
+ }
+ }
+
+ @media (max-width: 480px) {
+ .share-card-grid {
+ grid-template-columns: 1fr;
+ }
+ .share-card__meta {
+ grid-template-columns: 1fr;
+ }
+ .share-card__actions .btn {
+ width: 100%;
+ }
+ }
+
-
+
@@ -1266,76 +1594,29 @@
-
-
-
-
-
-
- 当前存储: {{ storageTypeText }}
-
-
-
-
- 配额使用情况
- {{ localUsedFormatted }} / {{ localQuotaFormatted }} ({{ quotaPercentage }}%)
-
-
-
-
+
-
-
-
-
文件夹是空的
+
+
+
文件视图 · {{ fileStats.totalCount }} 项
+
+
+
+ {{ storageTypeText }}
+
+
+
+
+ 本地 {{ localUsedFormatted }} / {{ localQuotaFormatted }} · {{ quotaPercentage }}%
+
+
+ OSS {{ ossUsedFormatted }} / {{ ossQuotaFormatted }} · {{ ossQuotaPercentage }}%
+
+
+
+
+
+ 上传文件
+
+
+ 新建文件夹
+
+
+
+ 大图标
+
+
+ 列表
+
+
+
+
+
+
+
+
当前目录暂无文件
+
你可以先上传文件,或新建文件夹整理资料
+
+
+ 上传文件
+
+
+ 新建文件夹
+
+
+
@@ -1359,7 +1690,7 @@
-
+
-
+ class="file-grid-video-icon">
+
-
+
-
-
-
-
-
-
+
+
+
+
+
+
{{ getFileDisplayName(file) }}
{{ file.isDirectory ? '文件夹' : file.sizeFormatted }}
@@ -1388,52 +1719,71 @@
-
+
-
- | 文件名 |
- 大小 |
- 修改时间 |
+
+ | 文件名 |
+ 大小 |
+ 修改时间 |
+ 操作 |
-
-
+ @touchend="handleLongPressEnd"
+ @touchcancel="handleLongPressEnd"
+ @selectstart.prevent
+ @dragstart.prevent>
+
+
![]()
+ class="file-list-thumb">
-
+ class="file-list-video-icon">
+
-
+
-
-
-
-
-
-
- {{ getFileDisplayName(file) }}
+
+
+
+
+
+
+
+ {{ getFileDisplayName(file) }}
+ {{ file.isDirectory ? '文件夹' : file.sizeFormatted }} · {{ formatDate(file.modifiedTime) }}
+
|
- {{ file.isDirectory ? '-' : file.sizeFormatted }} |
- {{ formatDate(file.modifiedTime) }} |
+ {{ file.isDirectory ? '-' : file.sizeFormatted }} |
+ {{ formatDate(file.modifiedTime) }} |
+
+
+
+
+ |
|
+
@@ -1523,58 +1873,22 @@
-
-
-
-
分享所有文件
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ creatingShare ? '创建中...' : '创建分享' }}
-
-
- 关闭
-
-
-
-
-
分享文件
文件: {{ shareFileForm.fileName }}
-
-
+
+
+
+
+
+
+ 请通过安全渠道单独发送密码,不要和链接放在一起。
@@ -1589,16 +1903,49 @@
-
-
分享链接:
-
{{ shareResult.share_url }}
-
-
到期时间:
-
{{ formatExpireTime(shareResult.expires_at) }}
+
+
+
+
+
分享创建成功
+
{{ shareResult.target_name || shareFileForm.fileName }} · {{ shareResult.has_password ? '密码访问' : '公开访问' }}
+
-
-
有效期:
-
永久有效
+
+
{{ shareResult.share_url }}
+
+
+
+ 复制链接
+
+
+ 打开链接
+
+
+ 复制密码
+
+
+
+
+
+
+ {{ shareResult.has_password ? '需要密码' : '无需密码' }}
+
+
+ 密码:{{ shareResult.share_password_plain }}
+
+
+ {{ shareResult.share_code }}
+
+
+
+ {{ shareResult.expires_at ? formatExpireTime(shareResult.expires_at) : '永久有效' }}
+
+
+
+
+
+ {{ shareResult.has_password ? '请单独发送访问密码,避免与链接一并泄露。' : '当前是公开分享,任何拿到链接的人都可以访问。' }}
@@ -1699,14 +2046,21 @@
+
+
+ 设置中心
+
+
管理存储策略、主题外观与账号安全
+
+
-
-
+
+
存储管理
-
-
+
+
当前模式
本地存储适合快速读写,OSS 适合云存储扩展
-
+
-
+
+
本地存储
@@ -1774,7 +2128,7 @@
-
+
OSS 存储
@@ -1809,7 +2163,9 @@
{{ ossUsage.totalSizeFormatted }}
- ({{ ossUsage.fileCount }} 文件)
+
+ / {{ ossUsage.quotaFormatted }}
+
点击刷新查看
@@ -1832,7 +2188,7 @@
-
+
本地存储速度快但受配额限制;OSS 支持多家云服务商,切换过程中可继续查看文件列表。
@@ -1840,12 +2196,12 @@
-
-
+
+
本地存储
-
+
存储方式:
本地存储
@@ -1867,7 +2223,7 @@
-
+
说明: 管理员已将您的存储权限设置为"仅本地存储",您的文件存储在服务器本地,速度快但有配额限制。如需使用OSS存储,请联系管理员修改权限设置。
@@ -1875,11 +2231,11 @@
-
-
+
+
OSS存储
-
+
@@ -1897,7 +2253,7 @@
-
+
云服务信息
@@ -1907,7 +2263,7 @@
-
+
空间使用统计
@@ -1935,14 +2291,29 @@
-
-
-
{{ ossUsage.totalSizeFormatted }}
-
总使用空间
+
+
+
+
{{ ossUsage.totalSizeFormatted }}
+
总使用空间
+
+
+
{{ ossUsage.quotaFormatted }}
+
OSS 配额
+
+
+
{{ ossUsage.usagePercentage + '%' }}
+
配额使用率
+
-
-
{{ ossUsage.fileCount }}
-
对象数
+
+
+ 剩余空间
+ {{ ossUsage.remainingSizeFormatted }}
+
+
@@ -1953,7 +2324,7 @@
-
+
数据存储在云服务上,安全可靠扩展性强。如需切换回本地请联系管理员调整权限。
@@ -1961,8 +2332,8 @@
-
界面设置
-
+
界面设置
+
主题模式
选择你喜欢的界面风格
@@ -1997,8 +2368,9 @@
-
账号设置
+
账号设置
+
@@ -2031,11 +2404,11 @@
-
-
+
+
我的分享
-
+
卡片
@@ -2049,7 +2422,7 @@
-