feat(backup): 添加远端备份功能并优化备份管理
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"targets": [],
|
||||
"globalSettings": {
|
||||
"enabled": false,
|
||||
"uploadAfterBackup": false,
|
||||
"deleteLocalAfterUpload": false,
|
||||
"retryCount": 3,
|
||||
"retryDelay": 5000,
|
||||
"timeout": 300000
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,7 @@ const Consumable = sequelize.define('Consumable', {
|
||||
consumableId: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
allowNull: false
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
|
||||
@@ -14,7 +14,6 @@ const ConsumableLogArchive = sequelize.define('ConsumableLogArchive', {
|
||||
archiveId: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '归档记录唯一标识'
|
||||
},
|
||||
consumableId: {
|
||||
|
||||
@@ -7,8 +7,7 @@ const ConsumableRecord = sequelize.define('ConsumableRecord', {
|
||||
type: DataTypes.UUID,
|
||||
primaryKey: true,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
allowNull: false
|
||||
},
|
||||
consumableId: {
|
||||
type: DataTypes.STRING,
|
||||
|
||||
Generated
+493
-50
@@ -8,7 +8,8 @@
|
||||
"name": "idc-backend",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.13.4",
|
||||
"axios": "^1.13.6",
|
||||
"basic-ftp": "^5.0.3",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"csv-parser": "^3.2.0",
|
||||
@@ -21,11 +22,14 @@
|
||||
"joi": "^18.0.2",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"multer": "^2.1.1",
|
||||
"mysql2": "^3.16.0",
|
||||
"mysql2": "^3.19.1",
|
||||
"node-cron": "^4.2.1",
|
||||
"sequelize": "^6.32.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
"sequelize": "^6.37.8",
|
||||
"smb2": "^0.2.2",
|
||||
"sqlite3": "^5.1.7",
|
||||
"ssh2-sftp-client": "^9.1.0",
|
||||
"three": "^0.182.0",
|
||||
"webdav": "^5.3.1",
|
||||
"winston": "^3.19.0",
|
||||
"winston-daily-rotate-file": "^5.0.0",
|
||||
"xlsx": "^0.18.5"
|
||||
@@ -627,6 +631,15 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.6.0",
|
||||
"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",
|
||||
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -2546,6 +2560,15 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
@@ -2594,13 +2617,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz",
|
||||
"integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==",
|
||||
"version": "1.13.6",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
|
||||
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
@@ -2707,7 +2730,12 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"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"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
@@ -2740,6 +2768,24 @@
|
||||
"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": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||
@@ -2923,6 +2969,15 @@
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"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": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
@@ -2934,6 +2989,12 @@
|
||||
"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": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -3125,6 +3186,15 @@
|
||||
"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": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
@@ -3433,6 +3503,20 @@
|
||||
"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": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
@@ -3460,6 +3544,15 @@
|
||||
"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": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz",
|
||||
@@ -3478,6 +3571,15 @@
|
||||
"integrity": "sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g==",
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
|
||||
@@ -3841,6 +3943,18 @@
|
||||
"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": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||
@@ -3855,8 +3969,7 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
|
||||
"integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.4",
|
||||
@@ -4620,6 +4733,40 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
|
||||
@@ -4636,6 +4783,29 @@
|
||||
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
|
||||
"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": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
@@ -4818,6 +4988,18 @@
|
||||
"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": {
|
||||
"version": "3.5.4",
|
||||
"resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
|
||||
@@ -5286,6 +5468,12 @@
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
@@ -5700,6 +5888,12 @@
|
||||
"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": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
||||
@@ -7137,6 +7331,12 @@
|
||||
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||
@@ -7292,9 +7492,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lru.min": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.3.tgz",
|
||||
"integrity": "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==",
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
|
||||
"integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"bun": ">=1.0.0",
|
||||
@@ -7369,6 +7569,17 @@
|
||||
"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": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@@ -7654,29 +7865,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mysql2": {
|
||||
"version": "3.16.0",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.16.0.tgz",
|
||||
"integrity": "sha512-AEGW7QLLSuSnjCS4pk3EIqOmogegmze9h8EyrndavUQnIUcfkVal/sK7QznE+a3bc6rzPbAiui9Jcb+96tPwYA==",
|
||||
"version": "3.19.1",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.19.1.tgz",
|
||||
"integrity": "sha512-yn4zh+Uxu5J3Zvi6Ao96lJ7BSBRkspHflWQAmOPND+htbpIKDQw99TTvPzgihKO/QyMickZopO4OsnixnpcUwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"aws-ssl-profiles": "^1.1.1",
|
||||
"aws-ssl-profiles": "^1.1.2",
|
||||
"denque": "^2.1.0",
|
||||
"generate-function": "^2.3.1",
|
||||
"iconv-lite": "^0.7.0",
|
||||
"long": "^5.2.1",
|
||||
"lru.min": "^1.0.0",
|
||||
"named-placeholders": "^1.1.3",
|
||||
"seq-queue": "^0.0.5",
|
||||
"sqlstring": "^2.3.2"
|
||||
"iconv-lite": "^0.7.2",
|
||||
"long": "^5.3.2",
|
||||
"lru.min": "^1.1.4",
|
||||
"named-placeholders": "^1.1.6",
|
||||
"sql-escaper": "^1.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/mysql2/node_modules/iconv-lite": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz",
|
||||
"integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==",
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
@@ -7701,6 +7914,13 @@
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||
@@ -7739,10 +7959,16 @@
|
||||
"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": {
|
||||
"version": "3.85.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz",
|
||||
"integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==",
|
||||
"version": "3.88.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.88.0.tgz",
|
||||
"integrity": "sha512-At6b4UqIEVudaqPsXjmUO1r/N5BUr4yhDGs5PkBE8/oG5+TfLPhFechiskFsnT6Ql0VfUXbalUUCbfXxtj7K+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
@@ -7766,6 +7992,44 @@
|
||||
"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": {
|
||||
"version": "8.4.1",
|
||||
"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_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": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -8241,6 +8513,21 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
@@ -8268,6 +8555,12 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||
@@ -8371,6 +8664,7 @@
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||
"deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
@@ -8473,7 +8767,6 @@
|
||||
"resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
|
||||
"integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"err-code": "^2.0.2",
|
||||
"retry": "^0.12.0"
|
||||
@@ -8579,6 +8872,12 @@
|
||||
"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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@@ -8718,6 +9017,12 @@
|
||||
"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": {
|
||||
"version": "2.0.0-next.5",
|
||||
"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",
|
||||
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
@@ -8924,15 +9228,10 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"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": {
|
||||
"version": "6.37.7",
|
||||
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz",
|
||||
"integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==",
|
||||
"version": "6.37.8",
|
||||
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.8.tgz",
|
||||
"integrity": "sha512-HJ0IQFqcTsTiqbEgiuioYFMSD00TP6Cz7zoTti+zVVBwVe9fEhev9cH6WnM3XU31+ABS356durAb99ZuOthnKw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -9281,6 +9580,17 @@
|
||||
"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": {
|
||||
"version": "2.8.7",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
|
||||
@@ -9364,6 +9674,21 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "5.1.7",
|
||||
"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": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||
@@ -9409,6 +9725,37 @@
|
||||
"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": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
|
||||
@@ -9683,6 +10030,18 @@
|
||||
"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": {
|
||||
"version": "10.2.3",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz",
|
||||
@@ -9802,6 +10161,7 @@
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||
"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",
|
||||
"dependencies": {
|
||||
"chownr": "^2.0.0",
|
||||
@@ -9959,6 +10319,12 @@
|
||||
"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": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@@ -10229,6 +10595,25 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@@ -10296,6 +10681,64 @@
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"format:check": "prettier --check \"**/*.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.4",
|
||||
"axios": "^1.13.6",
|
||||
"basic-ftp": "^5.0.3",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"csv-parser": "^3.2.0",
|
||||
@@ -27,11 +28,14 @@
|
||||
"joi": "^18.0.2",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"multer": "^2.1.1",
|
||||
"mysql2": "^3.16.0",
|
||||
"mysql2": "^3.19.1",
|
||||
"node-cron": "^4.2.1",
|
||||
"sequelize": "^6.32.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
"sequelize": "^6.37.8",
|
||||
"smb2": "^0.2.2",
|
||||
"sqlite3": "^5.1.7",
|
||||
"ssh2-sftp-client": "^9.1.0",
|
||||
"three": "^0.182.0",
|
||||
"webdav": "^5.3.1",
|
||||
"winston": "^3.19.0",
|
||||
"winston-daily-rotate-file": "^5.0.0",
|
||||
"xlsx": "^0.18.5"
|
||||
|
||||
@@ -22,6 +22,16 @@ const {
|
||||
updateAutoBackupSettings,
|
||||
executeBackupNow,
|
||||
} = 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');
|
||||
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;
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ async function syncConsumableModels() {
|
||||
const ConsumableLogArchive = require('./models/ConsumableLogArchive');
|
||||
|
||||
await Promise.all([
|
||||
Consumable.sync({ alter: true }),
|
||||
Consumable.sync(), // 暂时禁用 alter 模式
|
||||
ConsumableLog.sync(),
|
||||
ConsumableCategory.sync(),
|
||||
ConsumableRecord.sync(),
|
||||
|
||||
@@ -7,6 +7,8 @@ const cron = require('node-cron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { createBackup, createIncrementalBackup, getBackupPath } = require('./backup');
|
||||
const { uploadToRemote } = require('./remoteBackup');
|
||||
const { getEnabledTargets, getGlobalSettings } = require('./remoteBackupConfig');
|
||||
|
||||
// 全局调度器存储
|
||||
const schedulers = new Map();
|
||||
@@ -108,6 +110,10 @@ function createAutoBackupTask(settings) {
|
||||
if (result) {
|
||||
console.log('自动备份完成:', result.filename);
|
||||
console.log(`备份类型:${result.isIncremental ? '增量备份' : '全量备份'}`);
|
||||
|
||||
// 上传到远端
|
||||
await uploadToRemoteTargets(result.path, result.filename);
|
||||
|
||||
console.log('========================\n');
|
||||
} else {
|
||||
console.log('无数据变化,跳过备份');
|
||||
@@ -234,6 +240,69 @@ function updateAutoBackupSettings(newSettings) {
|
||||
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);
|
||||
|
||||
// 上传到远端
|
||||
const uploadResults = await uploadToRemoteTargets(result.path, result.filename);
|
||||
|
||||
console.log('====================\n');
|
||||
return { success: true, result };
|
||||
return {
|
||||
success: true,
|
||||
result,
|
||||
remoteUploads: uploadResults,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('手动备份失败:', error);
|
||||
console.error('====================\n');
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
Generated
+10
-10
@@ -12,7 +12,7 @@
|
||||
"@react-three/drei": "^9.122.0",
|
||||
"@react-three/fiber": "^8.18.0",
|
||||
"antd": "^5.8.6",
|
||||
"axios": "^1.5.0",
|
||||
"axios": "^1.13.6",
|
||||
"dayjs": "^1.11.19",
|
||||
"framer-motion": "^12.34.0",
|
||||
"papaparse": "^5.5.3",
|
||||
@@ -21,7 +21,7 @@
|
||||
"react-router-dom": "^6.15.0",
|
||||
"styled-components": "^6.3.9",
|
||||
"swr": "^2.4.0",
|
||||
"three": "^0.160.1",
|
||||
"three": "^0.183.2",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -3016,13 +3016,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"version": "1.13.6",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
|
||||
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
@@ -7802,9 +7802,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.160.1",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.160.1.tgz",
|
||||
"integrity": "sha512-Bgl2wPJypDOZ1stAxwfWAcJ0WQf7QzlptsxkjYiURPz+n5k4RBDLsq+6f9Y75TYxn6aHLcWz+JNmwTOXWrQTBQ==",
|
||||
"version": "0.183.2",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz",
|
||||
"integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"@react-three/drei": "^9.122.0",
|
||||
"@react-three/fiber": "^8.18.0",
|
||||
"antd": "^5.8.6",
|
||||
"axios": "^1.5.0",
|
||||
"axios": "^1.13.6",
|
||||
"dayjs": "^1.11.19",
|
||||
"framer-motion": "^12.34.0",
|
||||
"papaparse": "^5.5.3",
|
||||
@@ -26,7 +26,7 @@
|
||||
"react-router-dom": "^6.15.0",
|
||||
"styled-components": "^6.3.9",
|
||||
"swr": "^2.4.0",
|
||||
"three": "^0.160.1",
|
||||
"three": "^0.183.2",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -79,6 +79,7 @@ const InventoryTaskExecution = lazy(() => import('./pages/InventoryTaskExecution
|
||||
const PendingDeviceManagement = lazy(() => import('./pages/PendingDeviceManagement'));
|
||||
const BackupManagement = lazy(() => import('./pages/BackupManagement'));
|
||||
const AutoBackupSettings = lazy(() => import('./pages/AutoBackupSettings'));
|
||||
const RemoteBackupSettings = lazy(() => import('./pages/RemoteBackupSettings'));
|
||||
const ErrorBoundaryTest = lazy(() => import('./pages/ErrorBoundaryTest'));
|
||||
|
||||
const { Header, Content, Sider } = Layout;
|
||||
@@ -595,6 +596,7 @@ const routeConfig = [
|
||||
{ path: '/ports', component: PortManagement },
|
||||
{ path: '/backup', component: BackupManagement },
|
||||
{ path: '/auto-backup-settings', component: AutoBackupSettings },
|
||||
{ path: '/remote-backup-settings', component: RemoteBackupSettings },
|
||||
{ path: '/error-boundary-test', component: ErrorBoundaryTest },
|
||||
];
|
||||
|
||||
|
||||
@@ -18,10 +18,12 @@ import {
|
||||
Statistic,
|
||||
Row,
|
||||
Col,
|
||||
Dropdown,
|
||||
} from 'antd';
|
||||
import {
|
||||
CloudDownloadOutlined,
|
||||
CloudUploadOutlined,
|
||||
CloudOutlined,
|
||||
DeleteOutlined,
|
||||
DownloadOutlined,
|
||||
UploadOutlined,
|
||||
@@ -33,6 +35,11 @@ import {
|
||||
ExclamationCircleOutlined,
|
||||
InfoCircleOutlined,
|
||||
SettingOutlined,
|
||||
MoreOutlined,
|
||||
SafetyOutlined,
|
||||
PlusOutlined,
|
||||
ClearOutlined,
|
||||
EyeOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import api, { backupAPI } from '../api';
|
||||
import CloseButton from '../components/CloseButton';
|
||||
@@ -70,6 +77,7 @@ const designTokens = {
|
||||
warning: {
|
||||
main: '#f59e0b',
|
||||
light: '#fbbf24',
|
||||
gradient: 'linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%)',
|
||||
},
|
||||
danger: {
|
||||
main: '#ef4444',
|
||||
@@ -86,6 +94,11 @@ const designTokens = {
|
||||
secondary: '#64748b',
|
||||
muted: '#94a3b8',
|
||||
},
|
||||
border: {
|
||||
light: '#e2e8f0',
|
||||
medium: '#cbd5e1',
|
||||
dark: '#94a3b8',
|
||||
},
|
||||
},
|
||||
shadows: {
|
||||
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>,
|
||||
key: 'action',
|
||||
width: 320,
|
||||
width: 280,
|
||||
render: (_, record) => (
|
||||
<Space size="small">
|
||||
<Tooltip title="验证备份">
|
||||
<Space size={4}>
|
||||
<Tooltip title="验证备份完整性">
|
||||
<Button
|
||||
shape="circle"
|
||||
icon={<CheckCircleOutlined style={{ fontSize: 16 }} />}
|
||||
size="small"
|
||||
icon={<SafetyOutlined />}
|
||||
onClick={() => handleValidate(record.filename)}
|
||||
style={{
|
||||
border: 'none',
|
||||
background: 'linear-gradient(135deg, #10b981 0%, #34d399 100%)',
|
||||
color: '#fff',
|
||||
boxShadow: '0 2px 8px rgba(16, 185, 129, 0.3)',
|
||||
width: 36,
|
||||
height: 36,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
...buttonStyles.success.base,
|
||||
padding: '4px 12px',
|
||||
height: '32px',
|
||||
fontSize: '13px',
|
||||
background: record.invalid ? designTokens.colors.text.muted : designTokens.colors.success.gradient,
|
||||
}}
|
||||
disabled={record.invalid}
|
||||
/>
|
||||
>
|
||||
验证
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="下载备份">
|
||||
<Tooltip title="下载备份文件">
|
||||
<Button
|
||||
shape="circle"
|
||||
icon={<DownloadOutlined style={{ fontSize: 16 }} />}
|
||||
size="small"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={() => handleDownload(record.filename)}
|
||||
style={{
|
||||
border: 'none',
|
||||
background: designTokens.colors.primary.gradient,
|
||||
color: '#fff',
|
||||
boxShadow: '0 2px 8px rgba(102, 126, 234, 0.3)',
|
||||
width: 36,
|
||||
height: 36,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
...buttonStyles.secondary.base,
|
||||
padding: '4px 12px',
|
||||
height: '32px',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
disabled={record.invalid}
|
||||
/>
|
||||
>
|
||||
下载
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="恢复数据">
|
||||
<Tooltip title="恢复备份数据">
|
||||
<Button
|
||||
shape="circle"
|
||||
icon={<CloudUploadOutlined style={{ fontSize: 16 }} />}
|
||||
size="small"
|
||||
icon={<CloudUploadOutlined />}
|
||||
onClick={() => showRestoreConfirm(record)}
|
||||
style={{
|
||||
border: 'none',
|
||||
background: 'linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%)',
|
||||
color: '#fff',
|
||||
boxShadow: '0 2px 8px rgba(245, 158, 11, 0.3)',
|
||||
width: 36,
|
||||
height: 36,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
...buttonStyles.warning.base,
|
||||
padding: '4px 12px',
|
||||
height: '32px',
|
||||
fontSize: '13px',
|
||||
background: record.invalid ? designTokens.colors.text.muted : designTokens.colors.warning.gradient,
|
||||
}}
|
||||
disabled={record.invalid}
|
||||
/>
|
||||
>
|
||||
恢复
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Popconfirm
|
||||
title="确定要删除此备份文件吗?"
|
||||
description="此操作不可撤销"
|
||||
onConfirm={() => handleDelete(record.filename)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Tooltip title="删除备份">
|
||||
<Tooltip title="删除备份文件">
|
||||
<Button
|
||||
shape="circle"
|
||||
icon={<DeleteOutlined style={{ fontSize: 16 }} />}
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
style={{
|
||||
border: 'none',
|
||||
background: designTokens.colors.danger.gradient,
|
||||
color: '#fff',
|
||||
boxShadow: '0 2px 8px rgba(239, 68, 68, 0.3)',
|
||||
width: 36,
|
||||
height: 36,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
...buttonStyles.icon.base,
|
||||
background: designTokens.colors.background.accent,
|
||||
color: designTokens.colors.danger.main,
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -1094,22 +1096,26 @@ const BackupManagement = () => {
|
||||
padding: `${designTokens.spacing.lg} ${designTokens.spacing.lg}`,
|
||||
};
|
||||
|
||||
// 页面标题样式
|
||||
// 页面标题样式 - 响应式适配
|
||||
const pageHeaderStyle = {
|
||||
marginBottom: '32px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
alignItems: 'flex-start',
|
||||
flexWrap: 'wrap',
|
||||
gap: '20px',
|
||||
padding: `${designTokens.spacing.md} ${designTokens.spacing.lg}`,
|
||||
background: designTokens.colors.background.secondary,
|
||||
borderRadius: designTokens.borderRadius.xl,
|
||||
boxShadow: designTokens.shadows.lg,
|
||||
'@media (max-width: 768px)': {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'stretch',
|
||||
},
|
||||
};
|
||||
|
||||
const titleStyle = {
|
||||
fontSize: '28px',
|
||||
fontSize: 'clamp(20px, 4vw, 28px)',
|
||||
fontWeight: '800',
|
||||
margin: 0,
|
||||
background: designTokens.colors.primary.gradient,
|
||||
@@ -1142,44 +1148,129 @@ const BackupManagement = () => {
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
// 主按钮样式
|
||||
const primaryButtonStyle = {
|
||||
background: designTokens.colors.primary.gradient,
|
||||
border: 'none',
|
||||
borderRadius: designTokens.borderRadius.md,
|
||||
padding: '8px 20px',
|
||||
fontWeight: 600,
|
||||
boxShadow: designTokens.shadows.md,
|
||||
transition: 'all 0.3s ease',
|
||||
color: '#fff',
|
||||
textShadow: '0 1px 2px rgba(0, 0, 0, 0.1)',
|
||||
// 按钮设计系统 - 统一风格
|
||||
const buttonStyles = {
|
||||
primary: {
|
||||
base: {
|
||||
background: designTokens.colors.primary.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: designTokens.shadows.lg,
|
||||
},
|
||||
},
|
||||
secondary: {
|
||||
base: {
|
||||
background: designTokens.colors.background.secondary,
|
||||
border: `1px solid ${designTokens.colors.border.light}`,
|
||||
borderRadius: designTokens.borderRadius.md,
|
||||
padding: '8px 16px',
|
||||
fontWeight: 500,
|
||||
color: designTokens.colors.text.primary,
|
||||
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
height: '40px',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
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,
|
||||
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(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 secondaryButtonStyle = {
|
||||
background: designTokens.colors.background.secondary,
|
||||
border: `1px solid ${designTokens.colors.text.muted}`,
|
||||
borderRadius: designTokens.borderRadius.md,
|
||||
padding: '8px 20px',
|
||||
fontWeight: 500,
|
||||
color: designTokens.colors.text.primary,
|
||||
transition: 'all 0.3s ease',
|
||||
};
|
||||
|
||||
// 危险按钮样式
|
||||
const dangerButtonStyle = {
|
||||
background: designTokens.colors.danger.gradient,
|
||||
border: 'none',
|
||||
borderRadius: designTokens.borderRadius.md,
|
||||
padding: '8px 20px',
|
||||
fontWeight: 600,
|
||||
boxShadow: designTokens.shadows.md,
|
||||
transition: 'all 0.3s ease',
|
||||
color: '#fff',
|
||||
textShadow: '0 1px 2px rgba(0, 0, 0, 0.1)',
|
||||
};
|
||||
const primaryButtonStyle = buttonStyles.primary.base;
|
||||
const secondaryButtonStyle = buttonStyles.secondary.base;
|
||||
const dangerButtonStyle = buttonStyles.danger.base;
|
||||
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
@@ -1197,73 +1288,104 @@ const BackupManagement = () => {
|
||||
管理系统备份,确保数据安全与可恢复性
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={buttonGroupStyle}>
|
||||
<Button
|
||||
icon={<SettingOutlined />}
|
||||
onClick={() => navigate('/auto-backup-settings')}
|
||||
style={{
|
||||
...secondaryButtonStyle,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
自动备份设置
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => { fetchBackups(); fetchBackupInfo(); }}
|
||||
style={secondaryButtonStyle}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="清理旧备份"
|
||||
description={
|
||||
<div>
|
||||
<p>将删除超过 30 个或超过 90 天的旧备份</p>
|
||||
<p style={{ color: designTokens.colors.text.muted, fontSize: 12 }}>建议先点击"预览"查看将删除的文件</p>
|
||||
</div>
|
||||
}
|
||||
onConfirm={() => handleCleanBackups(30, 90, false)}
|
||||
okText="确认清理"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
style={dangerButtonStyle}
|
||||
<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>
|
||||
</Popconfirm>
|
||||
<Button
|
||||
onClick={() => handleCleanBackups(30, 90, true)}
|
||||
style={secondaryButtonStyle}
|
||||
>
|
||||
预览清理
|
||||
</Button>
|
||||
<Upload
|
||||
customRequest={handleUpload}
|
||||
showUploadList={false}
|
||||
accept=".json,.gz,.json.gz"
|
||||
>
|
||||
<Button
|
||||
icon={<SettingOutlined />}
|
||||
style={secondaryButtonStyle}
|
||||
>
|
||||
设置
|
||||
</Button>
|
||||
</Dropdown>
|
||||
|
||||
<Button
|
||||
icon={<UploadOutlined />}
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => { fetchBackups(); fetchBackupInfo(); }}
|
||||
style={secondaryButtonStyle}
|
||||
>
|
||||
上传备份
|
||||
刷新
|
||||
</Button>
|
||||
</Upload>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CloudDownloadOutlined />}
|
||||
loading={backupLoading}
|
||||
onClick={handleCreateBackup}
|
||||
style={primaryButtonStyle}
|
||||
>
|
||||
创建备份
|
||||
</Button>
|
||||
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'preview',
|
||||
icon: <EyeOutlined />,
|
||||
label: '预览清理',
|
||||
onClick: () => handleCleanBackups(30, 90, true),
|
||||
},
|
||||
{
|
||||
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
|
||||
icon={<ClearOutlined />}
|
||||
style={secondaryButtonStyle}
|
||||
>
|
||||
清理
|
||||
</Button>
|
||||
</Dropdown>
|
||||
|
||||
<Upload
|
||||
customRequest={handleUpload}
|
||||
showUploadList={false}
|
||||
accept=".json,.gz,.json.gz"
|
||||
>
|
||||
<Button
|
||||
icon={<UploadOutlined />}
|
||||
style={secondaryButtonStyle}
|
||||
>
|
||||
上传备份
|
||||
</Button>
|
||||
</Upload>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
loading={backupLoading}
|
||||
onClick={handleCreateBackup}
|
||||
style={primaryButtonStyle}
|
||||
>
|
||||
创建备份
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1638,7 +1760,12 @@ const BackupManagement = () => {
|
||||
|
||||
<Divider style={{ margin: '20px 0' }} />
|
||||
|
||||
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '12px',
|
||||
marginTop: '8px',
|
||||
}}>
|
||||
<Button
|
||||
onClick={() => setRestoreVisible(false)}
|
||||
style={secondaryButtonStyle}
|
||||
@@ -1649,15 +1776,16 @@ const BackupManagement = () => {
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
icon={<CloudUploadOutlined />}
|
||||
onClick={() => handleRestore(selectedBackup?.filename)}
|
||||
style={{
|
||||
...dangerButtonStyle,
|
||||
padding: '8px 32px',
|
||||
...buttonStyles.danger.base,
|
||||
padding: '8px 24px',
|
||||
}}
|
||||
>
|
||||
确认恢复
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user