feat(backup): 添加远端备份功能并优化备份管理

This commit is contained in:
zhang1106
2026-03-13 22:11:34 +08:00
parent 7094c8be49
commit 7aa09ac9e1
16 changed files with 3344 additions and 228 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"targets": [],
"globalSettings": {
"enabled": false,
"uploadAfterBackup": false,
"deleteLocalAfterUpload": false,
"retryCount": 3,
"retryDelay": 5000,
"timeout": 300000
}
}
+1 -2
View File
@@ -5,8 +5,7 @@ const Consumable = sequelize.define('Consumable', {
consumableId: { consumableId: {
type: DataTypes.STRING, type: DataTypes.STRING,
primaryKey: true, primaryKey: true,
allowNull: false, allowNull: false
unique: true
}, },
name: { name: {
type: DataTypes.STRING, type: DataTypes.STRING,
-1
View File
@@ -14,7 +14,6 @@ const ConsumableLogArchive = sequelize.define('ConsumableLogArchive', {
archiveId: { archiveId: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
unique: true,
comment: '归档记录唯一标识' comment: '归档记录唯一标识'
}, },
consumableId: { consumableId: {
+1 -2
View File
@@ -7,8 +7,7 @@ const ConsumableRecord = sequelize.define('ConsumableRecord', {
type: DataTypes.UUID, type: DataTypes.UUID,
primaryKey: true, primaryKey: true,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
allowNull: false, allowNull: false
unique: true
}, },
consumableId: { consumableId: {
type: DataTypes.STRING, type: DataTypes.STRING,
+493 -50
View File
@@ -8,7 +8,8 @@
"name": "idc-backend", "name": "idc-backend",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"axios": "^1.13.4", "axios": "^1.13.6",
"basic-ftp": "^5.0.3",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"csv-parser": "^3.2.0", "csv-parser": "^3.2.0",
@@ -21,11 +22,14 @@
"joi": "^18.0.2", "joi": "^18.0.2",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"multer": "^2.1.1", "multer": "^2.1.1",
"mysql2": "^3.16.0", "mysql2": "^3.19.1",
"node-cron": "^4.2.1", "node-cron": "^4.2.1",
"sequelize": "^6.32.1", "sequelize": "^6.37.8",
"sqlite3": "^5.1.6", "smb2": "^0.2.2",
"sqlite3": "^5.1.7",
"ssh2-sftp-client": "^9.1.0",
"three": "^0.182.0", "three": "^0.182.0",
"webdav": "^5.3.1",
"winston": "^3.19.0", "winston": "^3.19.0",
"winston-daily-rotate-file": "^5.0.0", "winston-daily-rotate-file": "^5.0.0",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
@@ -627,6 +631,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@buttercup/fetch": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@buttercup/fetch/-/fetch-0.2.1.tgz",
"integrity": "sha512-sCgECOx8wiqY8NN1xN22BqqKzXYIG2AicNLlakOAI4f0WgyLVUbAigMf8CZhBtJxdudTcB1gD5lciqi44jwJvg==",
"license": "MIT",
"optionalDependencies": {
"node-fetch": "^3.3.0"
}
},
"node_modules/@colors/colors": { "node_modules/@colors/colors": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
@@ -1850,6 +1863,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
@@ -2546,6 +2560,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": "~2.1.0"
}
},
"node_modules/async": { "node_modules/async": {
"version": "3.2.6", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@@ -2594,13 +2617,13 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.13.4", "version": "1.13.6",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.11",
"form-data": "^4.0.4", "form-data": "^4.0.5",
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
@@ -2707,7 +2730,12 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"devOptional": true, "license": "MIT"
},
"node_modules/base-64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz",
"integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64-js": { "node_modules/base64-js": {
@@ -2740,6 +2768,24 @@
"baseline-browser-mapping": "dist/cli.js" "baseline-browser-mapping": "dist/cli.js"
} }
}, },
"node_modules/basic-ftp": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz",
"integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
"license": "BSD-3-Clause",
"dependencies": {
"tweetnacl": "^0.14.3"
}
},
"node_modules/bcryptjs": { "node_modules/bcryptjs": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
@@ -2923,6 +2969,15 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/buildcheck": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz",
"integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==",
"optional": true,
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/busboy": { "node_modules/busboy": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -2934,6 +2989,12 @@
"node": ">=10.16.0" "node": ">=10.16.0"
} }
}, },
"node_modules/byte-length": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/byte-length/-/byte-length-1.0.2.tgz",
"integrity": "sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q==",
"license": "MIT"
},
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -3125,6 +3186,15 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
"license": "BSD-3-Clause",
"engines": {
"node": "*"
}
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -3433,6 +3503,20 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/cpu-features": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
"integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
"buildcheck": "~0.0.6",
"nan": "^2.19.0"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/crc-32": { "node_modules/crc-32": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
@@ -3460,6 +3544,15 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
"license": "BSD-3-Clause",
"engines": {
"node": "*"
}
},
"node_modules/csv-parser": { "node_modules/csv-parser": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz", "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz",
@@ -3478,6 +3571,15 @@
"integrity": "sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g==", "integrity": "sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/data-view-buffer": { "node_modules/data-view-buffer": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
@@ -3841,6 +3943,18 @@
"once": "^1.4.0" "once": "^1.4.0"
} }
}, },
"node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/env-paths": { "node_modules/env-paths": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
@@ -3855,8 +3969,7 @@
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
"integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
"license": "MIT", "license": "MIT"
"optional": true
}, },
"node_modules/error-ex": { "node_modules/error-ex": {
"version": "1.3.4", "version": "1.3.4",
@@ -4620,6 +4733,40 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-xml-builder": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.3.tgz",
"integrity": "sha512-1o60KoFw2+LWKQu3IdcfcFlGTW4dpqEWmjhYec6H82AYZU2TVBXep6tMl8Z1Y+wM+ZrzCwe3BZ9Vyd9N2rIvmg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"path-expression-matcher": "^1.1.3"
}
},
"node_modules/fast-xml-parser": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz",
"integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"fast-xml-builder": "^1.0.0",
"strnum": "^2.1.2"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/fb-watchman": { "node_modules/fb-watchman": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
@@ -4636,6 +4783,29 @@
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/file-entry-cache": { "node_modules/file-entry-cache": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -4818,6 +4988,18 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"license": "MIT",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/formidable": { "node_modules/formidable": {
"version": "3.5.4", "version": "3.5.4",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
@@ -5286,6 +5468,12 @@
"hermes-estree": "0.25.1" "hermes-estree": "0.25.1"
} }
}, },
"node_modules/hot-patcher": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-2.0.1.tgz",
"integrity": "sha512-ECg1JFG0YzehicQaogenlcs2qg6WsXQsxtnbr1i696u5tLUjtJdQAh0u2g0Q5YV45f263Ta1GnUJsc8WIfJf4Q==",
"license": "MIT"
},
"node_modules/html-escaper": { "node_modules/html-escaper": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -5700,6 +5888,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
"license": "MIT"
},
"node_modules/is-callable": { "node_modules/is-callable": {
"version": "1.2.7", "version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
@@ -7137,6 +7331,12 @@
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/layerr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/layerr/-/layerr-3.0.0.tgz",
"integrity": "sha512-tv754Ki2dXpPVApOrjTyRo4/QegVb9eVFq4mjqp4+NM5NaX7syQvN5BBNfV/ZpAHCEHV24XdUVrBAoka4jt3pA==",
"license": "MIT"
},
"node_modules/leven": { "node_modules/leven": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -7292,9 +7492,9 @@
} }
}, },
"node_modules/lru.min": { "node_modules/lru.min": {
"version": "1.1.3", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.3.tgz", "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
"integrity": "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==", "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"bun": ">=1.0.0", "bun": ">=1.0.0",
@@ -7369,6 +7569,17 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/md5": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
"license": "BSD-3-Clause",
"dependencies": {
"charenc": "0.0.2",
"crypt": "0.0.2",
"is-buffer": "~1.1.6"
}
},
"node_modules/media-typer": { "node_modules/media-typer": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -7654,29 +7865,31 @@
} }
}, },
"node_modules/mysql2": { "node_modules/mysql2": {
"version": "3.16.0", "version": "3.19.1",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.16.0.tgz", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.19.1.tgz",
"integrity": "sha512-AEGW7QLLSuSnjCS4pk3EIqOmogegmze9h8EyrndavUQnIUcfkVal/sK7QznE+a3bc6rzPbAiui9Jcb+96tPwYA==", "integrity": "sha512-yn4zh+Uxu5J3Zvi6Ao96lJ7BSBRkspHflWQAmOPND+htbpIKDQw99TTvPzgihKO/QyMickZopO4OsnixnpcUwA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"aws-ssl-profiles": "^1.1.1", "aws-ssl-profiles": "^1.1.2",
"denque": "^2.1.0", "denque": "^2.1.0",
"generate-function": "^2.3.1", "generate-function": "^2.3.1",
"iconv-lite": "^0.7.0", "iconv-lite": "^0.7.2",
"long": "^5.2.1", "long": "^5.3.2",
"lru.min": "^1.0.0", "lru.min": "^1.1.4",
"named-placeholders": "^1.1.3", "named-placeholders": "^1.1.6",
"seq-queue": "^0.0.5", "sql-escaper": "^1.3.3"
"sqlstring": "^2.3.2"
}, },
"engines": { "engines": {
"node": ">= 8.0" "node": ">= 8.0"
},
"peerDependencies": {
"@types/node": ">= 8"
} }
}, },
"node_modules/mysql2/node_modules/iconv-lite": { "node_modules/mysql2/node_modules/iconv-lite": {
"version": "0.7.1", "version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
"integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0" "safer-buffer": ">= 2.1.2 < 3.0.0"
@@ -7701,6 +7914,13 @@
"node": ">=8.0.0" "node": ">=8.0.0"
} }
}, },
"node_modules/nan": {
"version": "2.25.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz",
"integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==",
"license": "MIT",
"optional": true
},
"node_modules/napi-build-utils": { "node_modules/napi-build-utils": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
@@ -7739,10 +7959,16 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/nested-property": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/nested-property/-/nested-property-4.0.0.tgz",
"integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==",
"license": "MIT"
},
"node_modules/node-abi": { "node_modules/node-abi": {
"version": "3.85.0", "version": "3.88.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.88.0.tgz",
"integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", "integrity": "sha512-At6b4UqIEVudaqPsXjmUO1r/N5BUr4yhDGs5PkBE8/oG5+TfLPhFechiskFsnT6Ql0VfUXbalUUCbfXxtj7K+w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"semver": "^7.3.5" "semver": "^7.3.5"
@@ -7766,6 +7992,44 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"deprecated": "Use your platform's native DOMException instead",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/node-gyp": { "node_modules/node-gyp": {
"version": "8.4.1", "version": "8.4.1",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz",
@@ -7915,6 +8179,14 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0" "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
} }
}, },
"node_modules/ntlm": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/ntlm/-/ntlm-0.1.3.tgz",
"integrity": "sha512-pPlHxhAegZP4QAaOYd51vRd6VXTGfF7VLKJwuwN0iEB1aIi3SnqXYuS/bH/6wWBOq+Ehdil49mHm1Nseon085w==",
"engines": [
"node"
]
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -8241,6 +8513,21 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/path-expression-matcher": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz",
"integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/path-is-absolute": { "node_modules/path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -8268,6 +8555,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/path-posix": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz",
"integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==",
"license": "ISC"
},
"node_modules/path-scurry": { "node_modules/path-scurry": {
"version": "1.11.1", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
@@ -8371,6 +8664,7 @@
"version": "7.1.3", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"detect-libc": "^2.0.0", "detect-libc": "^2.0.0",
@@ -8473,7 +8767,6 @@
"resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
"integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"err-code": "^2.0.2", "err-code": "^2.0.2",
"retry": "^0.12.0" "retry": "^0.12.0"
@@ -8579,6 +8872,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"license": "MIT"
},
"node_modules/range-parser": { "node_modules/range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -8718,6 +9017,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"license": "MIT"
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "2.0.0-next.5", "version": "2.0.0-next.5",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
@@ -8764,7 +9069,6 @@
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">= 4" "node": ">= 4"
} }
@@ -8924,15 +9228,10 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/seq-queue": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
},
"node_modules/sequelize": { "node_modules/sequelize": {
"version": "6.37.7", "version": "6.37.8",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz", "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.8.tgz",
"integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==", "integrity": "sha512-HJ0IQFqcTsTiqbEgiuioYFMSD00TP6Cz7zoTti+zVVBwVe9fEhev9cH6WnM3XU31+ABS356durAb99ZuOthnKw==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -9281,6 +9580,17 @@
"npm": ">= 3.0.0" "npm": ">= 3.0.0"
} }
}, },
"node_modules/smb2": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/smb2/-/smb2-0.2.11.tgz",
"integrity": "sha512-p9Lx72JBuQ+7+UQPDIzBywQ9x0QBk7Z77cJ3lJ3vW2mhr/U1NppSDTU9Abc+dZn9ie13CVh3z54Aa3zx8N+Aeg==",
"engines": [
"node"
],
"dependencies": {
"ntlm": "^0.1.3"
}
},
"node_modules/socks": { "node_modules/socks": {
"version": "2.8.7", "version": "2.8.7",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
@@ -9364,6 +9674,21 @@
"dev": true, "dev": true,
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/sql-escaper": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz",
"integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==",
"license": "MIT",
"engines": {
"bun": ">=1.0.0",
"deno": ">=2.0.0",
"node": ">=12.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
}
},
"node_modules/sqlite3": { "node_modules/sqlite3": {
"version": "5.1.7", "version": "5.1.7",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz",
@@ -9388,15 +9713,6 @@
} }
} }
}, },
"node_modules/sqlstring": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
"integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ssf": { "node_modules/ssf": {
"version": "0.11.2", "version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
@@ -9409,6 +9725,37 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/ssh2": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz",
"integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==",
"hasInstallScript": true,
"dependencies": {
"asn1": "^0.2.6",
"bcrypt-pbkdf": "^1.0.2"
},
"engines": {
"node": ">=10.16.0"
},
"optionalDependencies": {
"cpu-features": "~0.0.10",
"nan": "^2.23.0"
}
},
"node_modules/ssh2-sftp-client": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/ssh2-sftp-client/-/ssh2-sftp-client-9.1.0.tgz",
"integrity": "sha512-Hzdr9OE6GxZjcmyM9tgBSIFVyrHAp9c6U2Y4yBkmYOHoQvZ7pIm27dmltvcmRfxcWiIcg8HBvG5iAikDf+ZuzQ==",
"license": "Apache-2.0",
"dependencies": {
"concat-stream": "^2.0.0",
"promise-retry": "^2.0.1",
"ssh2": "^1.12.0"
},
"engines": {
"node": ">=10.24.1"
}
},
"node_modules/ssri": { "node_modules/ssri": {
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
@@ -9683,6 +10030,18 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/strnum": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz",
"integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
},
"node_modules/superagent": { "node_modules/superagent": {
"version": "10.2.3", "version": "10.2.3",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz",
@@ -9802,6 +10161,7 @@
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"chownr": "^2.0.0", "chownr": "^2.0.0",
@@ -9959,6 +10319,12 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
"license": "Unlicense"
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -10229,6 +10595,25 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/url-join": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz",
"integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"license": "MIT",
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -10296,6 +10681,64 @@
"makeerror": "1.0.12" "makeerror": "1.0.12"
} }
}, },
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/webdav": {
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/webdav/-/webdav-5.9.0.tgz",
"integrity": "sha512-OMJ6wtK1WvCO++aOLoQgE96S8KT4e5aaClWHmHXfFU369r4eyELN569B7EqT4OOUb99mmO58GkyuiCv/Ag6J0Q==",
"license": "MIT",
"dependencies": {
"@buttercup/fetch": "^0.2.1",
"base-64": "^1.0.0",
"byte-length": "^1.0.2",
"entities": "^6.0.1",
"fast-xml-parser": "^5.3.4",
"hot-patcher": "^2.0.1",
"layerr": "^3.0.0",
"md5": "^2.3.0",
"minimatch": "^9.0.5",
"nested-property": "^4.0.0",
"node-fetch": "^3.3.2",
"path-posix": "^1.0.0",
"url-join": "^5.0.0",
"url-parse": "^1.5.10"
},
"engines": {
"node": ">=14"
}
},
"node_modules/webdav/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/webdav/node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+8 -4
View File
@@ -14,7 +14,8 @@
"format:check": "prettier --check \"**/*.js\"" "format:check": "prettier --check \"**/*.js\""
}, },
"dependencies": { "dependencies": {
"axios": "^1.13.4", "axios": "^1.13.6",
"basic-ftp": "^5.0.3",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"csv-parser": "^3.2.0", "csv-parser": "^3.2.0",
@@ -27,11 +28,14 @@
"joi": "^18.0.2", "joi": "^18.0.2",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"multer": "^2.1.1", "multer": "^2.1.1",
"mysql2": "^3.16.0", "mysql2": "^3.19.1",
"node-cron": "^4.2.1", "node-cron": "^4.2.1",
"sequelize": "^6.32.1", "sequelize": "^6.37.8",
"sqlite3": "^5.1.6", "smb2": "^0.2.2",
"sqlite3": "^5.1.7",
"ssh2-sftp-client": "^9.1.0",
"three": "^0.182.0", "three": "^0.182.0",
"webdav": "^5.3.1",
"winston": "^3.19.0", "winston": "^3.19.0",
"winston-daily-rotate-file": "^5.0.0", "winston-daily-rotate-file": "^5.0.0",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
+306
View File
@@ -22,6 +22,16 @@ const {
updateAutoBackupSettings, updateAutoBackupSettings,
executeBackupNow, executeBackupNow,
} = require('../utils/autoBackupScheduler'); } = require('../utils/autoBackupScheduler');
const {
getAllTargets,
getTarget,
addTarget,
updateTarget,
deleteTarget,
getGlobalSettings,
updateGlobalSettings,
} = require('../utils/remoteBackupConfig');
const { testRemoteConnection, PROTOCOL_TYPES, PROTOCOL_LABELS } = require('../utils/remoteBackup');
const tempDir = path.join(__dirname, '..', 'temp'); const tempDir = path.join(__dirname, '..', 'temp');
if (!fs.existsSync(tempDir)) { if (!fs.existsSync(tempDir)) {
@@ -548,4 +558,300 @@ router.post('/auto/test-cron', (req, res) => {
} }
}); });
// ==================== 远端备份配置路由 ====================
// 获取所有远端备份目标
router.get('/remote/targets', (req, res) => {
try {
const targets = getAllTargets();
res.json({
success: true,
data: { targets },
});
} catch (error) {
console.error('获取远端备份目标失败:', error);
res.status(500).json({
success: false,
message: '获取远端备份目标失败',
error: error.message,
});
}
});
// 获取单个远端备份目标
router.get('/remote/targets/:id', (req, res) => {
try {
const target = getTarget(req.params.id);
if (!target) {
return res.status(404).json({
success: false,
message: '目标不存在',
});
}
res.json({
success: true,
data: { target },
});
} catch (error) {
console.error('获取远端备份目标失败:', error);
res.status(500).json({
success: false,
message: '获取远端备份目标失败',
error: error.message,
});
}
});
// 添加远端备份目标
router.post('/remote/targets', (req, res) => {
try {
const targetData = req.body;
if (!targetData.name || !targetData.protocol) {
return res.status(400).json({
success: false,
message: '请提供目标名称和协议类型',
});
}
const target = addTarget(targetData);
res.status(201).json({
success: true,
message: '远端备份目标已添加',
data: { target },
});
} catch (error) {
console.error('添加远端备份目标失败:', error);
res.status(500).json({
success: false,
message: '添加远端备份目标失败',
error: error.message,
});
}
});
// 更新远端备份目标
router.put('/remote/targets/:id', (req, res) => {
try {
const updates = req.body;
const target = updateTarget(req.params.id, updates);
res.json({
success: true,
message: '远端备份目标已更新',
data: { target },
});
} catch (error) {
console.error('更新远端备份目标失败:', error);
res.status(500).json({
success: false,
message: '更新远端备份目标失败',
error: error.message,
});
}
});
// 删除远端备份目标
router.delete('/remote/targets/:id', (req, res) => {
try {
const deleted = deleteTarget(req.params.id);
if (!deleted) {
return res.status(404).json({
success: false,
message: '目标不存在',
});
}
res.json({
success: true,
message: '远端备份目标已删除',
});
} catch (error) {
console.error('删除远端备份目标失败:', error);
res.status(500).json({
success: false,
message: '删除远端备份目标失败',
error: error.message,
});
}
});
// 测试远端连接
router.post('/remote/targets/:id/test', async (req, res) => {
try {
const target = getTarget(req.params.id);
if (!target) {
return res.status(404).json({
success: false,
message: '目标不存在',
});
}
const result = await testRemoteConnection(target);
if (result.success) {
res.json({
success: true,
message: '连接测试成功',
data: result,
});
} else {
res.status(500).json({
success: false,
message: result.message,
error: result.error,
});
}
} catch (error) {
console.error('测试远端连接失败:', error);
res.status(500).json({
success: false,
message: '测试远端连接失败',
error: error.message,
});
}
});
// 获取远端备份全局设置
router.get('/remote/settings', (req, res) => {
try {
const settings = getGlobalSettings();
res.json({
success: true,
data: { settings },
});
} catch (error) {
console.error('获取远端备份设置失败:', error);
res.status(500).json({
success: false,
message: '获取远端备份设置失败',
error: error.message,
});
}
});
// 更新远端备份全局设置
router.put('/remote/settings', (req, res) => {
try {
const settings = updateGlobalSettings(req.body);
res.json({
success: true,
message: '远端备份设置已更新',
data: { settings },
});
} catch (error) {
console.error('更新远端备份设置失败:', error);
res.status(500).json({
success: false,
message: '更新远端备份设置失败',
error: error.message,
});
}
});
// 获取支持的协议列表
router.get('/remote/protocols', (req, res) => {
try {
const protocols = Object.entries(PROTOCOL_TYPES).map(([key, value]) => ({
key,
value,
label: PROTOCOL_LABELS[value],
}));
res.json({
success: true,
data: { protocols },
});
} catch (error) {
console.error('获取协议列表失败:', error);
res.status(500).json({
success: false,
message: '获取协议列表失败',
error: error.message,
});
}
});
// 手动上传备份到远端
router.post('/remote/upload', async (req, res) => {
try {
const { filename, targetIds } = req.body;
if (!filename) {
return res.status(400).json({
success: false,
message: '请提供备份文件名',
});
}
const backupPath = getBackupPath();
const filePath = path.join(backupPath, filename);
if (!fs.existsSync(filePath)) {
return res.status(404).json({
success: false,
message: '备份文件不存在',
});
}
const { uploadToRemote } = require('../utils/remoteBackup');
const { getTarget } = require('../utils/remoteBackupConfig');
const targets = targetIds
? targetIds.map(id => getTarget(id)).filter(Boolean)
: getEnabledTargets();
if (targets.length === 0) {
return res.status(400).json({
success: false,
message: '没有可用的远端目标',
});
}
const uploadResults = [];
for (const target of targets) {
try {
const remotePath = `${target.prefix || 'backups/'}${filename}`;
const result = await uploadToRemote(target, filePath, remotePath);
uploadResults.push({
targetId: target.id,
targetName: target.name,
success: true,
...result,
});
} catch (error) {
uploadResults.push({
targetId: target.id,
targetName: target.name,
success: false,
error: error.message,
});
}
}
res.json({
success: true,
message: '上传完成',
data: {
filename,
results: uploadResults,
},
});
} catch (error) {
console.error('手动上传备份失败:', error);
res.status(500).json({
success: false,
message: '手动上传备份失败',
error: error.message,
});
}
});
module.exports = router; module.exports = router;
+1 -1
View File
@@ -56,7 +56,7 @@ async function syncConsumableModels() {
const ConsumableLogArchive = require('./models/ConsumableLogArchive'); const ConsumableLogArchive = require('./models/ConsumableLogArchive');
await Promise.all([ await Promise.all([
Consumable.sync({ alter: true }), Consumable.sync(), // 暂时禁用 alter 模式
ConsumableLog.sync(), ConsumableLog.sync(),
ConsumableCategory.sync(), ConsumableCategory.sync(),
ConsumableRecord.sync(), ConsumableRecord.sync(),
+78 -1
View File
@@ -7,6 +7,8 @@ const cron = require('node-cron');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const { createBackup, createIncrementalBackup, getBackupPath } = require('./backup'); const { createBackup, createIncrementalBackup, getBackupPath } = require('./backup');
const { uploadToRemote } = require('./remoteBackup');
const { getEnabledTargets, getGlobalSettings } = require('./remoteBackupConfig');
// 全局调度器存储 // 全局调度器存储
const schedulers = new Map(); const schedulers = new Map();
@@ -108,6 +110,10 @@ function createAutoBackupTask(settings) {
if (result) { if (result) {
console.log('自动备份完成:', result.filename); console.log('自动备份完成:', result.filename);
console.log(`备份类型:${result.isIncremental ? '增量备份' : '全量备份'}`); console.log(`备份类型:${result.isIncremental ? '增量备份' : '全量备份'}`);
// 上传到远端
await uploadToRemoteTargets(result.path, result.filename);
console.log('========================\n'); console.log('========================\n');
} else { } else {
console.log('无数据变化,跳过备份'); console.log('无数据变化,跳过备份');
@@ -234,6 +240,69 @@ function updateAutoBackupSettings(newSettings) {
return false; return false;
} }
/**
* 上传备份到所有启用的远端目标
*/
async function uploadToRemoteTargets(localFilePath, filename) {
const globalSettings = getGlobalSettings();
if (!globalSettings.enabled || !globalSettings.uploadAfterBackup) {
console.log('远端备份已禁用');
return [];
}
const enabledTargets = getEnabledTargets();
if (enabledTargets.length === 0) {
console.log('没有启用的远端备份目标');
return [];
}
const uploadResults = [];
for (const target of enabledTargets) {
try {
console.log(`开始上传到目标:${target.name} (${target.protocol})`);
const remotePath = `${target.prefix || 'backups/'}${filename}`;
const result = await uploadToRemote(target, localFilePath, remotePath);
uploadResults.push({
targetId: target.id,
targetName: target.name,
protocol: target.protocol,
success: true,
...result,
});
console.log(`上传到 ${target.name} 成功`);
} catch (error) {
console.error(`上传到 ${target.name} 失败:`, error.message);
uploadResults.push({
targetId: target.id,
targetName: target.name,
protocol: target.protocol,
success: false,
error: error.message,
});
}
}
// 检查是否需要删除本地文件
const settings = getGlobalSettings();
if (settings.deleteLocalAfterUpload && uploadResults.every(r => r.success)) {
try {
fs.unlinkSync(localFilePath);
console.log('本地备份文件已删除');
} catch (error) {
console.error('删除本地备份文件失败:', error.message);
}
}
return uploadResults;
}
/** /**
* 立即执行一次备份 * 立即执行一次备份
*/ */
@@ -251,8 +320,16 @@ async function executeBackupNow(options = {}) {
}); });
console.log('手动备份完成:', result.filename); console.log('手动备份完成:', result.filename);
// 上传到远端
const uploadResults = await uploadToRemoteTargets(result.path, result.filename);
console.log('====================\n'); console.log('====================\n');
return { success: true, result }; return {
success: true,
result,
remoteUploads: uploadResults,
};
} catch (error) { } catch (error) {
console.error('手动备份失败:', error); console.error('手动备份失败:', error);
console.error('====================\n'); console.error('====================\n');
+300
View File
@@ -0,0 +1,300 @@
/**
* 远端备份上传工具模块
* 支持 FTP、SFTP、WebDAV、SMB 等协议
*/
const fs = require('fs');
const path = require('path');
const axios = require('axios');
// 协议类型枚举
const PROTOCOL_TYPES = {
FTP: 'ftp',
SFTP: 'sftp',
WEBDAV: 'webdav',
SMB: 'smb',
};
// 协议显示名称映射
const PROTOCOL_LABELS = {
[PROTOCOL_TYPES.FTP]: 'FTP',
[PROTOCOL_TYPES.SFTP]: 'SFTP (SSH 文件传输)',
[PROTOCOL_TYPES.WEBDAV]: 'WebDAV',
[PROTOCOL_TYPES.SMB]: 'SMB/CIFS 网络共享',
};
/**
* FTP 上传实现
*/
async function uploadViaFTP(config, localFilePath, remotePath) {
const { Client } = require('basic-ftp');
const client = new Client();
try {
await client.access({
host: config.host,
port: config.port || 21,
user: config.username,
password: config.password,
secure: config.secure || false,
secureOptions: {
rejectUnauthorized: config.rejectUnauthorized !== false,
},
});
await client.cd(config.rootPath || '/');
const dirPath = path.dirname(remotePath);
if (dirPath !== '.') {
await ensureRemoteDir(client, dirPath, 'ftp');
}
await client.uploadFrom(localFilePath, path.basename(remotePath));
return {
success: true,
message: `FTP 上传成功:${remotePath}`,
};
} catch (error) {
throw new Error(`FTP 上传失败:${error.message}`);
} finally {
client.close();
}
}
/**
* SFTP 上传实现
*/
async function uploadViaSFTP(config, localFilePath, remotePath) {
const Client = require('ssh2-sftp-client');
const client = new Client();
try {
await client.connect({
host: config.host,
port: config.port || 22,
username: config.username,
password: config.password,
privateKey: config.privateKey ? fs.readFileSync(config.privateKey) : undefined,
passphrase: config.passphrase,
readyTimeout: config.timeout || 10000,
});
const dirPath = path.dirname(remotePath);
if (dirPath !== '.') {
await ensureRemoteDir(client, dirPath, 'sftp');
}
await client.put(fs.createReadStream(localFilePath), remotePath);
return {
success: true,
message: `SFTP 上传成功:${remotePath}`,
};
} catch (error) {
throw new Error(`SFTP 上传失败:${error.message}`);
} finally {
await client.end();
}
}
/**
* WebDAV 上传实现
*/
async function uploadViaWebDAV(config, localFilePath, remotePath) {
const { createClient } = require('webdav');
const client = createClient(config.url, {
username: config.username,
password: config.password,
authType: config.authType || 'password',
headers: {
'User-Agent': 'IDC-Backup-Client/1.0',
},
});
try {
const dirPath = path.dirname(remotePath);
if (dirPath !== '/') {
await ensureRemoteDir(client, dirPath, 'webdav');
}
const fileContent = fs.readFileSync(localFilePath);
await client.putFileContents(remotePath, fileContent, {
overwrite: true,
});
return {
success: true,
message: `WebDAV 上传成功:${remotePath}`,
};
} catch (error) {
throw new Error(`WebDAV 上传失败:${error.message}`);
}
}
/**
* SMB/CIFS 网络共享上传实现
*/
async function uploadViaSMB(config, localFilePath, remotePath) {
const smb = require('smb2');
const client = new smb({
share: `\\\\${config.host}\\${config.share}`,
domain: config.domain || '',
username: config.username,
password: config.password,
});
try {
const dirPath = path.dirname(remotePath);
if (dirPath !== '.') {
await ensureRemoteDir(client, dirPath, 'smb');
}
await client.writeFile(remotePath, fs.readFileSync(localFilePath));
return {
success: true,
message: `SMB 上传成功:${remotePath}`,
};
} catch (error) {
throw new Error(`SMB 上传失败:${error.message}`);
}
}
/**
* 确保远程目录存在
*/
async function ensureRemoteDir(client, dirPath, protocol) {
try {
if (protocol === 'ftp') {
const dirs = dirPath.split('/').filter(d => d);
for (const dir of dirs) {
try {
await client.cd(dir);
} catch {
await client.mkdir(dir);
await client.cd(dir);
}
}
} else if (protocol === 'sftp') {
await client.mkdir(dirPath, true);
} else if (protocol === 'webdav') {
const parts = dirPath.split('/').filter(p => p);
let currentPath = '';
for (const part of parts) {
currentPath += '/' + part;
try {
await client.createDirectory(currentPath);
} catch (error) {
if (!error.status || error.status !== 405) {
throw error;
}
}
}
} else if (protocol === 'smb') {
const parts = dirPath.split(path.sep).filter(p => p);
let currentPath = '';
for (const part of parts) {
currentPath += part + path.sep;
try {
await client.exists(currentPath);
} catch {
await client.mkdir(currentPath);
}
}
}
} catch (error) {
console.warn(`创建远程目录失败 [${protocol}]:`, error.message);
}
}
/**
* 主上传函数 - 根据配置选择对应协议
*/
async function uploadToRemote(config, localFilePath, remotePath) {
console.log(`开始上传到远端 [${config.protocol}]: ${remotePath}`);
const startTime = Date.now();
let result;
switch (config.protocol) {
case PROTOCOL_TYPES.FTP:
result = await uploadViaFTP(config, localFilePath, remotePath);
break;
case PROTOCOL_TYPES.SFTP:
result = await uploadViaSFTP(config, localFilePath, remotePath);
break;
case PROTOCOL_TYPES.WEBDAV:
result = await uploadViaWebDAV(config, localFilePath, remotePath);
break;
case PROTOCOL_TYPES.SMB:
result = await uploadViaSMB(config, localFilePath, remotePath);
break;
default:
throw new Error(`不支持的协议类型:${config.protocol}`);
}
const duration = Date.now() - startTime;
const fileSize = fs.statSync(localFilePath).size;
console.log(`远端上传完成 [${config.protocol}] - 耗时:${duration}ms, 文件大小:${(fileSize / 1024).toFixed(2)}KB`);
return {
...result,
protocol: config.protocol,
protocolLabel: PROTOCOL_LABELS[config.protocol],
duration,
fileSize,
uploadedAt: new Date().toISOString(),
};
}
/**
* 测试远端连接
*/
async function testRemoteConnection(config) {
console.log(`测试远端连接 [${config.protocol}]...`);
try {
const testContent = `IDC Backup Connection Test - ${new Date().toISOString()}`;
const testFile = path.join(require('os').tmpdir(), `backup-test-${Date.now()}.txt`);
fs.writeFileSync(testFile, testContent);
const testRemotePath = `test/backup-connection-test-${Date.now()}.txt`;
const result = await uploadToRemote(config, testFile, testRemotePath);
try {
fs.unlinkSync(testFile);
} catch (e) {
console.warn('删除测试文件失败:', e.message);
}
return {
success: true,
message: '连接测试成功',
details: result,
};
} catch (error) {
return {
success: false,
message: `连接测试失败:${error.message}`,
error: error.message,
};
}
}
module.exports = {
PROTOCOL_TYPES,
PROTOCOL_LABELS,
uploadToRemote,
testRemoteConnection,
uploadViaFTP,
uploadViaSFTP,
uploadViaWebDAV,
uploadViaSMB,
};
+297
View File
@@ -0,0 +1,297 @@
/**
* 远端备份配置管理模块
* 管理多个远端备份目标的配置
*/
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { PROTOCOL_TYPES } = require('./remoteBackup');
// 配置文件路径
const CONFIG_FILE = path.join(__dirname, '..', 'config', 'remote-backup-configs.json');
// 默认配置
const DEFAULT_CONFIG = {
targets: [],
globalSettings: {
enabled: false,
uploadAfterBackup: true,
deleteLocalAfterUpload: false,
retryCount: 3,
retryDelay: 5000,
timeout: 300000,
},
};
/**
* 加密敏感信息
*/
function encrypt(text) {
if (!text) return '';
const algorithm = 'aes-256-cbc';
const key = crypto.scryptSync(process.env.JWT_SECRET || 'default-secret', 'salt', 32);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
}
/**
* 解密敏感信息
*/
function decrypt(text) {
if (!text) return '';
try {
const algorithm = 'aes-256-cbc';
const key = crypto.scryptSync(process.env.JWT_SECRET || 'default-secret', 'salt', 32);
const parts = text.split(':');
const iv = Buffer.from(parts[0], 'hex');
const encrypted = parts[1];
const decipher = crypto.createDecipheriv(algorithm, key, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
} catch (error) {
console.error('解密失败:', error.message);
return text;
}
}
/**
* 加载配置
*/
function loadConfig() {
try {
if (fs.existsSync(CONFIG_FILE)) {
const content = fs.readFileSync(CONFIG_FILE, 'utf8');
const config = JSON.parse(content);
return { ...DEFAULT_CONFIG, ...config };
}
} catch (error) {
console.error('加载远端备份配置失败:', error);
}
return { ...DEFAULT_CONFIG };
}
/**
* 保存配置
*/
function saveConfig(config) {
try {
const configDir = path.dirname(CONFIG_FILE);
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
return true;
} catch (error) {
console.error('保存远端备份配置失败:', error);
return false;
}
}
/**
* 获取所有远端备份目标(隐藏敏感信息)
*/
function getAllTargets() {
const config = loadConfig();
return config.targets.map(target => ({
...target,
password: target.password ? '********' : undefined,
accessKeySecret: target.accessKeySecret ? '********' : undefined,
secretKey: target.secretKey ? '********' : undefined,
privateKey: target.privateKey ? '********' : undefined,
passphrase: target.passphrase ? '********' : undefined,
}));
}
/**
* 获取单个目标(包含解密后的敏感信息)
*/
function getTarget(id) {
const config = loadConfig();
const target = config.targets.find(t => t.id === id);
if (!target) {
return null;
}
return {
...target,
password: target.password ? decrypt(target.password) : undefined,
accessKeySecret: target.accessKeySecret ? decrypt(target.accessKeySecret) : undefined,
secretKey: target.secretKey ? decrypt(target.secretKey) : undefined,
privateKey: target.privateKey ? decrypt(target.privateKey) : undefined,
passphrase: target.passphrase ? decrypt(target.passphrase) : undefined,
};
}
/**
* 添加远端备份目标
*/
function addTarget(targetData) {
const config = loadConfig();
const newTarget = {
id: `target_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
name: targetData.name,
protocol: targetData.protocol,
enabled: targetData.enabled !== false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
switch (targetData.protocol) {
case PROTOCOL_TYPES.FTP:
case PROTOCOL_TYPES.SFTP:
Object.assign(newTarget, {
host: targetData.host,
port: targetData.port,
username: targetData.username,
password: targetData.password ? encrypt(targetData.password) : undefined,
rootPath: targetData.rootPath || '/',
secure: targetData.secure,
});
break;
case PROTOCOL_TYPES.WEBDAV:
Object.assign(newTarget, {
url: targetData.url,
username: targetData.username,
password: targetData.password ? encrypt(targetData.password) : undefined,
authType: targetData.authType || 'password',
rootPath: targetData.rootPath || '/',
});
break;
case PROTOCOL_TYPES.SMB:
Object.assign(newTarget, {
host: targetData.host,
share: targetData.share,
domain: targetData.domain,
username: targetData.username,
password: targetData.password ? encrypt(targetData.password) : undefined,
rootPath: targetData.rootPath || '/',
});
break;
default:
throw new Error(`不支持的协议类型:${targetData.protocol}`);
}
config.targets.push(newTarget);
if (saveConfig(config)) {
return newTarget;
}
throw new Error('保存配置失败');
}
/**
* 更新远端备份目标
*/
function updateTarget(id, updates) {
const config = loadConfig();
const targetIndex = config.targets.findIndex(t => t.id === id);
if (targetIndex === -1) {
throw new Error('目标不存在');
}
const existingTarget = config.targets[targetIndex];
const updatedTarget = { ...existingTarget, ...updates, updatedAt: new Date().toISOString() };
if (updates.password) {
updatedTarget.password = encrypt(updates.password);
}
if (updates.accessKeySecret) {
updatedTarget.accessKeySecret = encrypt(updates.accessKeySecret);
}
if (updates.secretKey) {
updatedTarget.secretKey = encrypt(updates.secretKey);
}
if (updates.privateKey) {
updatedTarget.privateKey = encrypt(updates.privateKey);
}
if (updates.passphrase) {
updatedTarget.passphrase = encrypt(updates.passphrase);
}
config.targets[targetIndex] = updatedTarget;
if (saveConfig(config)) {
return updatedTarget;
}
throw new Error('保存配置失败');
}
/**
* 删除远端备份目标
*/
function deleteTarget(id) {
const config = loadConfig();
const initialLength = config.targets.length;
config.targets = config.targets.filter(t => t.id !== id);
if (config.targets.length < initialLength) {
if (saveConfig(config)) {
return true;
}
throw new Error('保存配置失败');
}
return false;
}
/**
* 获取全局设置
*/
function getGlobalSettings() {
const config = loadConfig();
return config.globalSettings;
}
/**
* 更新全局设置
*/
function updateGlobalSettings(settings) {
const config = loadConfig();
config.globalSettings = { ...config.globalSettings, ...settings };
if (saveConfig(config)) {
return config.globalSettings;
}
throw new Error('保存配置失败');
}
/**
* 获取启用的目标列表
*/
function getEnabledTargets() {
const config = loadConfig();
if (!config.globalSettings.enabled) {
return [];
}
return config.targets.filter(t => t.enabled);
}
module.exports = {
loadConfig,
saveConfig,
getAllTargets,
getTarget,
addTarget,
updateTarget,
deleteTarget,
getGlobalSettings,
updateGlobalSettings,
getEnabledTargets,
encrypt,
decrypt,
};
+10 -10
View File
@@ -12,7 +12,7 @@
"@react-three/drei": "^9.122.0", "@react-three/drei": "^9.122.0",
"@react-three/fiber": "^8.18.0", "@react-three/fiber": "^8.18.0",
"antd": "^5.8.6", "antd": "^5.8.6",
"axios": "^1.5.0", "axios": "^1.13.6",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"framer-motion": "^12.34.0", "framer-motion": "^12.34.0",
"papaparse": "^5.5.3", "papaparse": "^5.5.3",
@@ -21,7 +21,7 @@
"react-router-dom": "^6.15.0", "react-router-dom": "^6.15.0",
"styled-components": "^6.3.9", "styled-components": "^6.3.9",
"swr": "^2.4.0", "swr": "^2.4.0",
"three": "^0.160.1", "three": "^0.183.2",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
@@ -3016,13 +3016,13 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.13.2", "version": "1.13.6",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.11",
"form-data": "^4.0.4", "form-data": "^4.0.5",
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
@@ -7802,9 +7802,9 @@
} }
}, },
"node_modules/three": { "node_modules/three": {
"version": "0.160.1", "version": "0.183.2",
"resolved": "https://registry.npmjs.org/three/-/three-0.160.1.tgz", "resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz",
"integrity": "sha512-Bgl2wPJypDOZ1stAxwfWAcJ0WQf7QzlptsxkjYiURPz+n5k4RBDLsq+6f9Y75TYxn6aHLcWz+JNmwTOXWrQTBQ==", "integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==",
"license": "MIT", "license": "MIT",
"peer": true "peer": true
}, },
+2 -2
View File
@@ -17,7 +17,7 @@
"@react-three/drei": "^9.122.0", "@react-three/drei": "^9.122.0",
"@react-three/fiber": "^8.18.0", "@react-three/fiber": "^8.18.0",
"antd": "^5.8.6", "antd": "^5.8.6",
"axios": "^1.5.0", "axios": "^1.13.6",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"framer-motion": "^12.34.0", "framer-motion": "^12.34.0",
"papaparse": "^5.5.3", "papaparse": "^5.5.3",
@@ -26,7 +26,7 @@
"react-router-dom": "^6.15.0", "react-router-dom": "^6.15.0",
"styled-components": "^6.3.9", "styled-components": "^6.3.9",
"swr": "^2.4.0", "swr": "^2.4.0",
"three": "^0.160.1", "three": "^0.183.2",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
+2
View File
@@ -79,6 +79,7 @@ const InventoryTaskExecution = lazy(() => import('./pages/InventoryTaskExecution
const PendingDeviceManagement = lazy(() => import('./pages/PendingDeviceManagement')); const PendingDeviceManagement = lazy(() => import('./pages/PendingDeviceManagement'));
const BackupManagement = lazy(() => import('./pages/BackupManagement')); const BackupManagement = lazy(() => import('./pages/BackupManagement'));
const AutoBackupSettings = lazy(() => import('./pages/AutoBackupSettings')); const AutoBackupSettings = lazy(() => import('./pages/AutoBackupSettings'));
const RemoteBackupSettings = lazy(() => import('./pages/RemoteBackupSettings'));
const ErrorBoundaryTest = lazy(() => import('./pages/ErrorBoundaryTest')); const ErrorBoundaryTest = lazy(() => import('./pages/ErrorBoundaryTest'));
const { Header, Content, Sider } = Layout; const { Header, Content, Sider } = Layout;
@@ -595,6 +596,7 @@ const routeConfig = [
{ path: '/ports', component: PortManagement }, { path: '/ports', component: PortManagement },
{ path: '/backup', component: BackupManagement }, { path: '/backup', component: BackupManagement },
{ path: '/auto-backup-settings', component: AutoBackupSettings }, { path: '/auto-backup-settings', component: AutoBackupSettings },
{ path: '/remote-backup-settings', component: RemoteBackupSettings },
{ path: '/error-boundary-test', component: ErrorBoundaryTest }, { path: '/error-boundary-test', component: ErrorBoundaryTest },
]; ];
+235 -107
View File
@@ -18,10 +18,12 @@ import {
Statistic, Statistic,
Row, Row,
Col, Col,
Dropdown,
} from 'antd'; } from 'antd';
import { import {
CloudDownloadOutlined, CloudDownloadOutlined,
CloudUploadOutlined, CloudUploadOutlined,
CloudOutlined,
DeleteOutlined, DeleteOutlined,
DownloadOutlined, DownloadOutlined,
UploadOutlined, UploadOutlined,
@@ -33,6 +35,11 @@ import {
ExclamationCircleOutlined, ExclamationCircleOutlined,
InfoCircleOutlined, InfoCircleOutlined,
SettingOutlined, SettingOutlined,
MoreOutlined,
SafetyOutlined,
PlusOutlined,
ClearOutlined,
EyeOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import api, { backupAPI } from '../api'; import api, { backupAPI } from '../api';
import CloseButton from '../components/CloseButton'; import CloseButton from '../components/CloseButton';
@@ -70,6 +77,7 @@ const designTokens = {
warning: { warning: {
main: '#f59e0b', main: '#f59e0b',
light: '#fbbf24', light: '#fbbf24',
gradient: 'linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%)',
}, },
danger: { danger: {
main: '#ef4444', main: '#ef4444',
@@ -86,6 +94,11 @@ const designTokens = {
secondary: '#64748b', secondary: '#64748b',
muted: '#94a3b8', muted: '#94a3b8',
}, },
border: {
light: '#e2e8f0',
medium: '#cbd5e1',
dark: '#94a3b8',
},
}, },
shadows: { shadows: {
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
@@ -998,86 +1011,75 @@ const BackupManagement = () => {
{ {
title: <span style={{ fontWeight: 600, color: designTokens.colors.text.primary }}>操作</span>, title: <span style={{ fontWeight: 600, color: designTokens.colors.text.primary }}>操作</span>,
key: 'action', key: 'action',
width: 320, width: 280,
render: (_, record) => ( render: (_, record) => (
<Space size="small"> <Space size={4}>
<Tooltip title="验证备份"> <Tooltip title="验证备份完整性">
<Button <Button
shape="circle" size="small"
icon={<CheckCircleOutlined style={{ fontSize: 16 }} />} icon={<SafetyOutlined />}
onClick={() => handleValidate(record.filename)} onClick={() => handleValidate(record.filename)}
style={{ style={{
border: 'none', ...buttonStyles.success.base,
background: 'linear-gradient(135deg, #10b981 0%, #34d399 100%)', padding: '4px 12px',
color: '#fff', height: '32px',
boxShadow: '0 2px 8px rgba(16, 185, 129, 0.3)', fontSize: '13px',
width: 36, background: record.invalid ? designTokens.colors.text.muted : designTokens.colors.success.gradient,
height: 36,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}} }}
disabled={record.invalid} disabled={record.invalid}
/> >
验证
</Button>
</Tooltip> </Tooltip>
<Tooltip title="下载备份"> <Tooltip title="下载备份文件">
<Button <Button
shape="circle" size="small"
icon={<DownloadOutlined style={{ fontSize: 16 }} />} icon={<DownloadOutlined />}
onClick={() => handleDownload(record.filename)} onClick={() => handleDownload(record.filename)}
style={{ style={{
border: 'none', ...buttonStyles.secondary.base,
background: designTokens.colors.primary.gradient, padding: '4px 12px',
color: '#fff', height: '32px',
boxShadow: '0 2px 8px rgba(102, 126, 234, 0.3)', fontSize: '13px',
width: 36,
height: 36,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}} }}
disabled={record.invalid} disabled={record.invalid}
/> >
下载
</Button>
</Tooltip> </Tooltip>
<Tooltip title="恢复数据"> <Tooltip title="恢复备份数据">
<Button <Button
shape="circle" size="small"
icon={<CloudUploadOutlined style={{ fontSize: 16 }} />} icon={<CloudUploadOutlined />}
onClick={() => showRestoreConfirm(record)} onClick={() => showRestoreConfirm(record)}
style={{ style={{
border: 'none', ...buttonStyles.warning.base,
background: 'linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%)', padding: '4px 12px',
color: '#fff', height: '32px',
boxShadow: '0 2px 8px rgba(245, 158, 11, 0.3)', fontSize: '13px',
width: 36, background: record.invalid ? designTokens.colors.text.muted : designTokens.colors.warning.gradient,
height: 36,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}} }}
disabled={record.invalid} disabled={record.invalid}
/> >
恢复
</Button>
</Tooltip> </Tooltip>
<Popconfirm <Popconfirm
title="确定要删除此备份文件吗?" title="确定要删除此备份文件吗?"
description="此操作不可撤销"
onConfirm={() => handleDelete(record.filename)} onConfirm={() => handleDelete(record.filename)}
okText="确定" okText="确定"
cancelText="取消" cancelText="取消"
okButtonProps={{ danger: true }}
> >
<Tooltip title="删除备份"> <Tooltip title="删除备份文件">
<Button <Button
shape="circle" size="small"
icon={<DeleteOutlined style={{ fontSize: 16 }} />} icon={<DeleteOutlined />}
style={{ style={{
border: 'none', ...buttonStyles.icon.base,
background: designTokens.colors.danger.gradient, background: designTokens.colors.background.accent,
color: '#fff', color: designTokens.colors.danger.main,
boxShadow: '0 2px 8px rgba(239, 68, 68, 0.3)',
width: 36,
height: 36,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}} }}
/> />
</Tooltip> </Tooltip>
@@ -1094,22 +1096,26 @@ const BackupManagement = () => {
padding: `${designTokens.spacing.lg} ${designTokens.spacing.lg}`, padding: `${designTokens.spacing.lg} ${designTokens.spacing.lg}`,
}; };
// 页面标题样式 // 页面标题样式 - 响应式适配
const pageHeaderStyle = { const pageHeaderStyle = {
marginBottom: '32px', marginBottom: '32px',
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'flex-start',
flexWrap: 'wrap', flexWrap: 'wrap',
gap: '20px', gap: '20px',
padding: `${designTokens.spacing.md} ${designTokens.spacing.lg}`, padding: `${designTokens.spacing.md} ${designTokens.spacing.lg}`,
background: designTokens.colors.background.secondary, background: designTokens.colors.background.secondary,
borderRadius: designTokens.borderRadius.xl, borderRadius: designTokens.borderRadius.xl,
boxShadow: designTokens.shadows.lg, boxShadow: designTokens.shadows.lg,
'@media (max-width: 768px)': {
flexDirection: 'column',
alignItems: 'stretch',
},
}; };
const titleStyle = { const titleStyle = {
fontSize: '28px', fontSize: 'clamp(20px, 4vw, 28px)',
fontWeight: '800', fontWeight: '800',
margin: 0, margin: 0,
background: designTokens.colors.primary.gradient, background: designTokens.colors.primary.gradient,
@@ -1142,45 +1148,130 @@ const BackupManagement = () => {
display: 'flex', display: 'flex',
gap: '12px', gap: '12px',
flexWrap: 'wrap', flexWrap: 'wrap',
alignItems: 'center',
}; };
// 按钮样式 // 按钮设计系统 - 统一风格
const primaryButtonStyle = { const buttonStyles = {
primary: {
base: {
background: designTokens.colors.primary.gradient, background: designTokens.colors.primary.gradient,
border: 'none', border: 'none',
borderRadius: designTokens.borderRadius.md, borderRadius: designTokens.borderRadius.md,
padding: '8px 20px', padding: '8px 20px',
fontWeight: 600, fontWeight: 600,
boxShadow: designTokens.shadows.md, boxShadow: designTokens.shadows.md,
transition: 'all 0.3s ease', transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
color: '#fff', color: '#fff',
textShadow: '0 1px 2px rgba(0, 0, 0, 0.1)', height: '40px',
}; display: 'inline-flex',
alignItems: 'center',
// 次要按钮样式 gap: '8px',
const secondaryButtonStyle = { },
hover: {
transform: 'translateY(-1px)',
boxShadow: designTokens.shadows.lg,
},
},
secondary: {
base: {
background: designTokens.colors.background.secondary, background: designTokens.colors.background.secondary,
border: `1px solid ${designTokens.colors.text.muted}`, border: `1px solid ${designTokens.colors.border.light}`,
borderRadius: designTokens.borderRadius.md, borderRadius: designTokens.borderRadius.md,
padding: '8px 20px', padding: '8px 16px',
fontWeight: 500, fontWeight: 500,
color: designTokens.colors.text.primary, color: designTokens.colors.text.primary,
transition: 'all 0.3s ease', transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
}; height: '40px',
display: 'inline-flex',
// 危险按钮样式 alignItems: 'center',
const dangerButtonStyle = { gap: '8px',
},
hover: {
borderColor: designTokens.colors.primary.main,
color: designTokens.colors.primary.main,
background: `${designTokens.colors.primary.main}08`,
},
},
danger: {
base: {
background: designTokens.colors.danger.gradient, background: designTokens.colors.danger.gradient,
border: 'none', border: 'none',
borderRadius: designTokens.borderRadius.md, borderRadius: designTokens.borderRadius.md,
padding: '8px 20px', padding: '8px 20px',
fontWeight: 600, fontWeight: 600,
boxShadow: designTokens.shadows.md, boxShadow: designTokens.shadows.md,
transition: 'all 0.3s ease', transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
color: '#fff', color: '#fff',
textShadow: '0 1px 2px rgba(0, 0, 0, 0.1)', height: '40px',
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
},
hover: {
transform: 'translateY(-1px)',
boxShadow: '0 8px 16px rgba(239, 68, 68, 0.3)',
},
},
success: {
base: {
background: designTokens.colors.success.gradient,
border: 'none',
borderRadius: designTokens.borderRadius.md,
padding: '8px 20px',
fontWeight: 600,
boxShadow: designTokens.shadows.md,
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
color: '#fff',
height: '40px',
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
},
hover: {
transform: 'translateY(-1px)',
boxShadow: '0 8px 16px rgba(16, 185, 129, 0.3)',
},
},
warning: {
base: {
background: designTokens.colors.warning.gradient,
border: 'none',
borderRadius: designTokens.borderRadius.md,
padding: '8px 20px',
fontWeight: 600,
boxShadow: designTokens.shadows.md,
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
color: '#fff',
height: '40px',
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
},
hover: {
transform: 'translateY(-1px)',
boxShadow: '0 8px 16px rgba(245, 158, 11, 0.3)',
},
},
icon: {
base: {
border: 'none',
borderRadius: designTokens.borderRadius.md,
width: '36px',
height: '36px',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
cursor: 'pointer',
},
},
}; };
const primaryButtonStyle = buttonStyles.primary.base;
const secondaryButtonStyle = buttonStyles.secondary.base;
const dangerButtonStyle = buttonStyles.danger.base;
return ( return (
<div style={containerStyle}> <div style={containerStyle}>
<div style={pageHeaderStyle}> <div style={pageHeaderStyle}>
@@ -1197,19 +1288,36 @@ const BackupManagement = () => {
管理系统备份确保数据安全与可恢复性 管理系统备份确保数据安全与可恢复性
</p> </p>
</div> </div>
<div style={buttonGroupStyle}> <div style={buttonGroupStyle}>
<Space size="middle" wrap>
<Dropdown
menu={{
items: [
{
key: 'auto',
icon: <SettingOutlined />,
label: '自动备份设置',
onClick: () => navigate('/auto-backup-settings'),
},
{
key: 'remote',
icon: <CloudOutlined />,
label: '远端备份配置',
onClick: () => navigate('/remote-backup-settings'),
},
],
}}
placement="bottomRight"
>
<Button <Button
icon={<SettingOutlined />} icon={<SettingOutlined />}
onClick={() => navigate('/auto-backup-settings')} style={secondaryButtonStyle}
style={{
...secondaryButtonStyle,
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
> >
自动备份设置 设置
</Button> </Button>
</Dropdown>
<Button <Button
icon={<ReloadOutlined />} icon={<ReloadOutlined />}
onClick={() => { fetchBackups(); fetchBackupInfo(); }} onClick={() => { fetchBackups(); fetchBackupInfo(); }}
@@ -1217,32 +1325,44 @@ const BackupManagement = () => {
> >
刷新 刷新
</Button> </Button>
<Popconfirm
title="清理旧备份" <Dropdown
description={ menu={{
<div> items: [
<p>将删除超过 30 个或超过 90 天的旧备份</p> {
<p style={{ color: designTokens.colors.text.muted, fontSize: 12 }}>建议先点击"预览"查看将删除的文件</p> key: 'preview',
</div> icon: <EyeOutlined />,
} label: '预览清理',
onConfirm={() => handleCleanBackups(30, 90, false)} onClick: () => handleCleanBackups(30, 90, true),
okText="确认清理" },
cancelText="取消" {
key: 'clean',
icon: <ClearOutlined />,
label: '清理旧备份',
danger: true,
onClick: () => {
Modal.confirm({
title: '确认清理旧备份',
content: '将删除超过 30 个或超过 90 天的旧备份文件,此操作不可撤销',
okText: '确认清理',
cancelText: '取消',
okButtonProps: { style: dangerButtonStyle },
onOk: () => handleCleanBackups(30, 90, false),
});
},
},
],
}}
placement="bottomRight"
> >
<Button <Button
icon={<DeleteOutlined />} icon={<ClearOutlined />}
danger
style={dangerButtonStyle}
>
清理旧备份
</Button>
</Popconfirm>
<Button
onClick={() => handleCleanBackups(30, 90, true)}
style={secondaryButtonStyle} style={secondaryButtonStyle}
> >
预览清理 清理
</Button> </Button>
</Dropdown>
<Upload <Upload
customRequest={handleUpload} customRequest={handleUpload}
showUploadList={false} showUploadList={false}
@@ -1255,15 +1375,17 @@ const BackupManagement = () => {
上传备份 上传备份
</Button> </Button>
</Upload> </Upload>
<Button <Button
type="primary" type="primary"
icon={<CloudDownloadOutlined />} icon={<PlusOutlined />}
loading={backupLoading} loading={backupLoading}
onClick={handleCreateBackup} onClick={handleCreateBackup}
style={primaryButtonStyle} style={primaryButtonStyle}
> >
创建备份 创建备份
</Button> </Button>
</Space>
</div> </div>
</div> </div>
@@ -1638,7 +1760,12 @@ const BackupManagement = () => {
<Divider style={{ margin: '20px 0' }} /> <Divider style={{ margin: '20px 0' }} />
<Space style={{ width: '100%', justifyContent: 'space-between' }}> <div style={{
display: 'flex',
justifyContent: 'flex-end',
gap: '12px',
marginTop: '8px',
}}>
<Button <Button
onClick={() => setRestoreVisible(false)} onClick={() => setRestoreVisible(false)}
style={secondaryButtonStyle} style={secondaryButtonStyle}
@@ -1649,15 +1776,16 @@ const BackupManagement = () => {
<Button <Button
type="primary" type="primary"
danger danger
icon={<CloudUploadOutlined />}
onClick={() => handleRestore(selectedBackup?.filename)} onClick={() => handleRestore(selectedBackup?.filename)}
style={{ style={{
...dangerButtonStyle, ...buttonStyles.danger.base,
padding: '8px 32px', padding: '8px 24px',
}} }}
> >
确认恢复 确认恢复
</Button> </Button>
</Space> </div>
</> </>
)} )}
</div> </div>
File diff suppressed because it is too large Load Diff