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: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true
allowNull: false
},
name: {
type: DataTypes.STRING,
-1
View File
@@ -14,7 +14,6 @@ const ConsumableLogArchive = sequelize.define('ConsumableLogArchive', {
archiveId: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
comment: '归档记录唯一标识'
},
consumableId: {
+1 -2
View File
@@ -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,
+493 -50
View File
@@ -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",
+8 -4
View File
@@ -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"
+306
View File
@@ -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
View File
@@ -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(),
+78 -1
View File
@@ -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');
+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,
};