diff --git a/package.json b/package.json index 3d273e3d..f297ce55 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@trpc/server": "11.0.0-rc.553", "@types/mime-types": "^2.1.4", "adm-zip": "^0.5.16", + "archiver": "^7.0.1", "axios": "^1.7.7", "boring-avatars": "^1.11.2", "bowser": "^2.11.0", @@ -165,6 +166,7 @@ "usehooks-ts": "^3.1.0", "vanilla-tilt": "^1.8.1", "vditor": "^3.10.8", + "yauzl-promise": "^4.0.0", "zod": "^3.23.8", "zod-prisma-types": "^3.1.8" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7dbdbf9c..38098002 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,6 +107,9 @@ importers: adm-zip: specifier: ^0.5.16 version: 0.5.16 + archiver: + specifier: ^7.0.1 + version: 7.0.1 axios: specifier: ^1.7.7 version: 1.7.7 @@ -410,6 +413,9 @@ importers: vditor: specifier: ^3.10.8 version: 3.10.8 + yauzl-promise: + specifier: ^4.0.0 + version: 4.0.0 zod: specifier: ^3.23.8 version: 3.23.8 @@ -1363,9 +1369,15 @@ packages: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} + '@emnapi/core@1.3.1': + resolution: {integrity: sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==} + '@emnapi/runtime@1.3.1': resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + '@emnapi/wasi-threads@1.0.1': + resolution: {integrity: sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==} + '@emotion/is-prop-valid@1.2.2': resolution: {integrity: sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==} @@ -2013,6 +2025,9 @@ packages: '@mermaid-js/parser@0.3.0': resolution: {integrity: sha512-HsvL6zgE5sUPGgkIDlmAWR1HTNHz2Iy11BAWPTa4Jjabkpguy4Ze2gzfLrg6pdRuBvFwgUYyxiaNqZwrEEXepA==} + '@napi-rs/wasm-runtime@0.2.6': + resolution: {integrity: sha512-z8YVS3XszxFTO73iwvFDNpQIzdMmSDTP/mB3E/ucR37V3Sx57hSExcXyMoNwaucWxnsWf4xfbZv0iZ30jr0M4Q==} + '@next/bundle-analyzer@14.2.15': resolution: {integrity: sha512-W6iyrp/3G7WbIztDcNt+owYX1iv37m9f4RJs0fa/Ayw4EDdjNPX6qKQrC7gBrESHV3FuchED+8R+CNiw1i78eQ==} @@ -2727,6 +2742,93 @@ packages: react: '>=18 || >=19.0.0-rc.0' react-dom: '>=18 || >=19.0.0-rc.0' + '@node-rs/crc32-android-arm-eabi@1.10.6': + resolution: {integrity: sha512-vZAMuJXm3TpWPOkkhxdrofWDv+Q+I2oO7ucLRbXyAPmXFNDhHtBxbO1rk9Qzz+M3eep8ieS4/+jCL1Q0zacNMQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@node-rs/crc32-android-arm64@1.10.6': + resolution: {integrity: sha512-Vl/JbjCinCw/H9gEpZveWCMjxjcEChDcDBM8S4hKay5yyoRCUHJPuKr4sjVDBeOm+1nwU3oOm6Ca8dyblwp4/w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@node-rs/crc32-darwin-arm64@1.10.6': + resolution: {integrity: sha512-kARYANp5GnmsQiViA5Qu74weYQ3phOHSYQf0G+U5wB3NB5JmBHnZcOc46Ig21tTypWtdv7u63TaltJQE41noyg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@node-rs/crc32-darwin-x64@1.10.6': + resolution: {integrity: sha512-Q99bevJVMfLTISpkpKBlXgtPUItrvTWKFyiqoKH5IvscZmLV++NH4V13Pa17GTBmv9n18OwzgQY4/SRq6PQNVA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@node-rs/crc32-freebsd-x64@1.10.6': + resolution: {integrity: sha512-66hpawbNjrgnS9EDMErta/lpaqOMrL6a6ee+nlI2viduVOmRZWm9Rg9XdGTK/+c4bQLdtC6jOd+Kp4EyGRYkAg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@node-rs/crc32-linux-arm-gnueabihf@1.10.6': + resolution: {integrity: sha512-E8Z0WChH7X6ankbVm8J/Yym19Cq3otx6l4NFPS6JW/cWdjv7iw+Sps2huSug+TBprjbcEA+s4TvEwfDI1KScjg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@node-rs/crc32-linux-arm64-gnu@1.10.6': + resolution: {integrity: sha512-LmWcfDbqAvypX0bQjQVPmQGazh4dLiVklkgHxpV4P0TcQ1DT86H/SWpMBMs/ncF8DGuCQ05cNyMv1iddUDugoQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@node-rs/crc32-linux-arm64-musl@1.10.6': + resolution: {integrity: sha512-k8ra/bmg0hwRrIEE8JL1p32WfaN9gDlUUpQRWsbxd1WhjqvXea7kKO6K4DwVxyxlPhBS9Gkb5Urq7Y4mXANzaw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@node-rs/crc32-linux-x64-gnu@1.10.6': + resolution: {integrity: sha512-IfjtqcuFK7JrSZ9mlAFhb83xgium30PguvRjIMI45C3FJwu18bnLk1oR619IYb/zetQT82MObgmqfKOtgemEKw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@node-rs/crc32-linux-x64-musl@1.10.6': + resolution: {integrity: sha512-LbFYsA5M9pNunOweSt6uhxenYQF94v3bHDAQRPTQ3rnjn+mK6IC7YTAYoBjvoJP8lVzcvk9hRj8wp4Jyh6Y80g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@node-rs/crc32-wasm32-wasi@1.10.6': + resolution: {integrity: sha512-KaejdLgHMPsRaxnM+OG9L9XdWL2TabNx80HLdsCOoX9BVhEkfh39OeahBo8lBmidylKbLGMQoGfIKDjq0YMStw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@node-rs/crc32-win32-arm64-msvc@1.10.6': + resolution: {integrity: sha512-x50AXiSxn5Ccn+dCjLf1T7ZpdBiV1Sp5aC+H2ijhJO4alwznvXgWbopPRVhbp2nj0i+Gb6kkDUEyU+508KAdGQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@node-rs/crc32-win32-ia32-msvc@1.10.6': + resolution: {integrity: sha512-DpDxQLaErJF9l36aghe1Mx+cOnYLKYo6qVPqPL9ukJ5rAGLtCdU0C+Zoi3gs9ySm8zmbFgazq/LvmsZYU42aBw==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@node-rs/crc32-win32-x64-msvc@1.10.6': + resolution: {integrity: sha512-5B1vXosIIBw1m2Rcnw62IIfH7W9s9f7H7Ma0rRuhT8HR4Xh8QCgw6NJSI2S2MCngsGktYnAhyUvs81b7efTyQw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@node-rs/crc32@1.10.6': + resolution: {integrity: sha512-+llXfqt+UzgoDzT9of5vPQPGqTAVCohU74I9zIBkNo5TH6s2P31DFJOGsJQKN207f0GHnYv5pV3wh3BCY/un/A==} + engines: {node: '>= 10'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -4247,6 +4349,9 @@ packages: '@tweenjs/tween.js@23.1.3': resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + '@tybys/wasm-util@0.9.0': + resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + '@types/d3-array@3.2.1': resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} @@ -4683,6 +4788,14 @@ packages: aproba@2.0.0: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + + archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + are-we-there-yet@3.0.1: resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -4768,6 +4881,9 @@ packages: axios@1.7.7: resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} + b4a@1.6.7: + resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} + babel-loader@8.4.1: resolution: {integrity: sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA==} engines: {node: '>= 8.9'} @@ -4803,6 +4919,9 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + bare-events@2.5.4: + resolution: {integrity: sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -4863,6 +4982,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -5119,6 +5242,10 @@ packages: compare-func@2.0.0: resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + compute-scroll-into-view@3.1.0: resolution: {integrity: sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==} @@ -5232,6 +5359,15 @@ packages: typescript: optional: true + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -5916,6 +6052,9 @@ packages: fast-equals@4.0.3: resolution: {integrity: sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} @@ -6718,6 +6857,10 @@ packages: is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-it-type@5.1.2: + resolution: {integrity: sha512-q/gOZQTNYABAxaXWnBKZjTFH4yACvWEFtgVOj+LbgxYIgAJG1xVmUZOsECSrZPIemYUQvaQWVilSFVbh4Eyt8A==} + engines: {node: '>=12'} + is-lambda@1.0.1: resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} @@ -7033,6 +7176,10 @@ packages: layout-base@2.0.1: resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -8701,6 +8848,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} @@ -9073,6 +9223,13 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -9419,6 +9576,10 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + simple-invariant@2.0.1: + resolution: {integrity: sha512-1sbhsxqI+I2tqlmjbz99GXNmZtr6tKIyEgGGnJw/MKGblalqk/XoOYYFJlBzTKZCxx8kLaD3FD5s9BEEjx5Pyg==} + engines: {node: '>=10'} + simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} @@ -9546,6 +9707,9 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} + streamx@2.21.1: + resolution: {integrity: sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw==} + string-hash@1.1.3: resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==} @@ -9758,6 +9922,9 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} @@ -9799,6 +9966,9 @@ packages: engines: {node: '>=10'} hasBin: true + text-decoder@1.2.3: + resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -10592,6 +10762,10 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yauzl-promise@4.0.0: + resolution: {integrity: sha512-/HCXpyHXJQQHvFq9noqrjfa/WpQC2XYs3vI7tBiAi4QiIU1knvYhZGaO1QPjwIVMdqflxbmwgMXtYeaRiAE0CA==} + engines: {node: '>=16'} + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -10611,6 +10785,10 @@ packages: zenscroll@4.0.2: resolution: {integrity: sha512-jEA1znR7b4C/NnaycInCU6h/d15ZzCd1jmsruqOKnZP6WXQSMH3W2GL+OXbkruslU4h+Tzuos0HdswzRUk/Vgg==} + zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} + zod-openapi@2.19.0: resolution: {integrity: sha512-OUAAyBDPPwZ9u61i4k/LieXUzP2re8kFjqdNh2AvHjsyi/aRNz9leDAtMGcSoSzUT5xUeQoACJufBI6FzzZyxA==} engines: {node: '>=16.11'} @@ -12081,11 +12259,22 @@ snapshots: '@discoveryjs/json-ext@0.5.7': {} + '@emnapi/core@1.3.1': + dependencies: + '@emnapi/wasi-threads': 1.0.1 + tslib: 2.8.0 + optional: true + '@emnapi/runtime@1.3.1': dependencies: tslib: 2.8.0 optional: true + '@emnapi/wasi-threads@1.0.1': + dependencies: + tslib: 2.8.0 + optional: true + '@emotion/is-prop-valid@1.2.2': dependencies: '@emotion/memoize': 0.8.1 @@ -12448,6 +12637,13 @@ snapshots: dependencies: langium: 3.0.0 + '@napi-rs/wasm-runtime@0.2.6': + dependencies: + '@emnapi/core': 1.3.1 + '@emnapi/runtime': 1.3.1 + '@tybys/wasm-util': 0.9.0 + optional: true + '@next/bundle-analyzer@14.2.15': dependencies: webpack-bundle-analyzer: 4.10.1 @@ -13741,6 +13937,67 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@node-rs/crc32-android-arm-eabi@1.10.6': + optional: true + + '@node-rs/crc32-android-arm64@1.10.6': + optional: true + + '@node-rs/crc32-darwin-arm64@1.10.6': + optional: true + + '@node-rs/crc32-darwin-x64@1.10.6': + optional: true + + '@node-rs/crc32-freebsd-x64@1.10.6': + optional: true + + '@node-rs/crc32-linux-arm-gnueabihf@1.10.6': + optional: true + + '@node-rs/crc32-linux-arm64-gnu@1.10.6': + optional: true + + '@node-rs/crc32-linux-arm64-musl@1.10.6': + optional: true + + '@node-rs/crc32-linux-x64-gnu@1.10.6': + optional: true + + '@node-rs/crc32-linux-x64-musl@1.10.6': + optional: true + + '@node-rs/crc32-wasm32-wasi@1.10.6': + dependencies: + '@napi-rs/wasm-runtime': 0.2.6 + optional: true + + '@node-rs/crc32-win32-arm64-msvc@1.10.6': + optional: true + + '@node-rs/crc32-win32-ia32-msvc@1.10.6': + optional: true + + '@node-rs/crc32-win32-x64-msvc@1.10.6': + optional: true + + '@node-rs/crc32@1.10.6': + optionalDependencies: + '@node-rs/crc32-android-arm-eabi': 1.10.6 + '@node-rs/crc32-android-arm64': 1.10.6 + '@node-rs/crc32-darwin-arm64': 1.10.6 + '@node-rs/crc32-darwin-x64': 1.10.6 + '@node-rs/crc32-freebsd-x64': 1.10.6 + '@node-rs/crc32-linux-arm-gnueabihf': 1.10.6 + '@node-rs/crc32-linux-arm64-gnu': 1.10.6 + '@node-rs/crc32-linux-arm64-musl': 1.10.6 + '@node-rs/crc32-linux-x64-gnu': 1.10.6 + '@node-rs/crc32-linux-x64-musl': 1.10.6 + '@node-rs/crc32-wasm32-wasi': 1.10.6 + '@node-rs/crc32-win32-arm64-msvc': 1.10.6 + '@node-rs/crc32-win32-ia32-msvc': 1.10.6 + '@node-rs/crc32-win32-x64-msvc': 1.10.6 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -16166,6 +16423,11 @@ snapshots: '@tweenjs/tween.js@23.1.3': {} + '@tybys/wasm-util@0.9.0': + dependencies: + tslib: 2.8.0 + optional: true + '@types/d3-array@3.2.1': {} '@types/d3-axis@3.0.6': @@ -16646,6 +16908,26 @@ snapshots: aproba@2.0.0: optional: true + archiver-utils@5.0.2: + dependencies: + glob: 10.4.5 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.17.21 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + + archiver@7.0.1: + dependencies: + archiver-utils: 5.0.2 + async: 3.2.6 + buffer-crc32: 1.0.0 + readable-stream: 4.7.0 + readdir-glob: 1.1.3 + tar-stream: 3.1.7 + zip-stream: 6.0.1 + are-we-there-yet@3.0.1: dependencies: delegates: 1.0.0 @@ -16736,6 +17018,8 @@ snapshots: transitivePeerDependencies: - debug + b4a@1.6.7: {} + babel-loader@8.4.1(@babel/core@7.25.8)(webpack@5.95.0): dependencies: '@babel/core': 7.25.8 @@ -16780,6 +17064,9 @@ snapshots: balanced-match@1.0.2: {} + bare-events@2.5.4: + optional: true + base64-js@1.5.1: {} before-after-hook@3.0.2: {} @@ -16845,6 +17132,8 @@ snapshots: node-releases: 2.0.18 update-browserslist-db: 1.1.1(browserslist@4.24.2) + buffer-crc32@1.0.0: {} + buffer-equal-constant-time@1.0.1: {} buffer-from@1.1.2: {} @@ -17136,6 +17425,14 @@ snapshots: array-ify: 1.0.0 dot-prop: 5.3.0 + compress-commons@6.0.2: + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + compute-scroll-into-view@3.1.0: {} concat-map@0.0.1: {} @@ -17242,6 +17539,13 @@ snapshots: optionalDependencies: typescript: 5.6.3 + crc-32@1.2.2: {} + + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.7.0 + create-require@1.1.1: {} cron@3.1.7: @@ -18014,6 +18318,8 @@ snapshots: fast-equals@4.0.3: {} + fast-fifo@1.3.2: {} + fast-glob@3.3.2: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -18938,6 +19244,11 @@ snapshots: is-hexadecimal@2.0.1: {} + is-it-type@5.1.2: + dependencies: + '@babel/runtime': 7.26.0 + globalthis: 1.0.4 + is-lambda@1.0.1: optional: true @@ -19239,6 +19550,10 @@ snapshots: layout-base@2.0.1: {} + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + leven@3.1.0: {} lexical@0.17.1: {} @@ -21039,6 +21354,8 @@ snapshots: queue-microtask@1.2.3: {} + queue-tick@1.0.1: {} + radix3@1.1.2: {} raf-schd@4.0.3: {} @@ -21511,6 +21828,18 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.6 + readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -21549,7 +21878,7 @@ snapshots: regenerator-transform@0.15.2: dependencies: - '@babel/runtime': 7.25.7 + '@babel/runtime': 7.26.0 regexp.prototype.flags@1.5.3: dependencies: @@ -21961,6 +22290,8 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 + simple-invariant@2.0.1: {} + simple-swizzle@0.2.2: dependencies: is-arrayish: 0.3.2 @@ -22094,6 +22425,14 @@ snapshots: streamsearch@1.1.0: {} + streamx@2.21.1: + dependencies: + fast-fifo: 1.3.2 + queue-tick: 1.0.1 + text-decoder: 1.2.3 + optionalDependencies: + bare-events: 2.5.4 + string-hash@1.1.3: {} string-width@4.2.3: @@ -22397,6 +22736,12 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + tar-stream@3.1.7: + dependencies: + b4a: 1.6.7 + fast-fifo: 1.3.2 + streamx: 2.21.1 + tar@6.2.1: dependencies: chownr: 2.0.0 @@ -22440,6 +22785,10 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + text-decoder@1.2.3: + dependencies: + b4a: 1.6.7 + text-table@0.2.0: {} thenify-all@1.6.0: @@ -23296,6 +23645,12 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yauzl-promise@4.0.0: + dependencies: + '@node-rs/crc32': 1.10.6 + is-it-type: 5.1.2 + simple-invariant: 2.0.1 + yn@3.1.1: {} yocto-queue@0.1.0: {} @@ -23306,6 +23661,12 @@ snapshots: zenscroll@4.0.2: {} + zip-stream@6.0.1: + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.7.0 + zod-openapi@2.19.0(zod@3.23.8): dependencies: zod: 3.23.8 diff --git a/src/components/BlinkoSettings/TaskSetting.tsx b/src/components/BlinkoSettings/TaskSetting.tsx index 90f9568a..65a94992 100644 --- a/src/components/BlinkoSettings/TaskSetting.tsx +++ b/src/components/BlinkoSettings/TaskSetting.tsx @@ -1,5 +1,5 @@ import { observer } from "mobx-react-lite"; -import { Button, Card, Input, Select, SelectItem, Switch, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from "@nextui-org/react"; +import { Button, Card, Input, Select, SelectItem, Switch, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow, Progress } from "@nextui-org/react"; import { RootStore } from "@/store"; import { BlinkoStore } from "@/store/blinkoStore"; import { PromiseCall } from "@/store/standard/PromiseState"; @@ -20,12 +20,28 @@ const UpdateDebounceCall = _.debounce((v) => { export const TaskSetting = observer(() => { const blinko = RootStore.Get(BlinkoStore) const [autoArchivedDays, setAutoArchivedDays] = useState("90") + const [polling, setPolling] = useState(false); + useEffect(() => { if (blinko.config.value?.autoArchivedDays) { setAutoArchivedDays(String(blinko.config.value?.autoArchivedDays)) } }, [blinko.config.value?.autoArchivedDays]) + useEffect(() => { + let timer: NodeJS.Timeout; + if (polling) { + timer = setInterval(() => { + blinko.task.call(); + }, 1000); + } + return () => { + if (timer) { + clearInterval(timer); + } + }; + }, [polling]); + const { t } = useTranslation() return ( { isDisabled={blinko.updateDBTask.loading.value} isSelected={blinko.DBTask?.isRunning} onChange={async e => { - await blinko.updateDBTask.call(e.target.checked) + setPolling(true); + await blinko.updateDBTask.call(e.target.checked); + setPolling(false); }} />} /> { { blinko.task.value!.map(i => { + const progress = i.output?.progress; return {i.name} @@ -118,15 +137,33 @@ const TasksPanel = observer(() => { {/* @ts-ignore */} {i.output?.filePath} {/* @ts-ignore */} - helper.download.downloadByLink(i?.output?.filePath)} className="cursor-pointer" icon="tabler:download" width="24" height="24" /> + helper.download.downloadByLink(i?.output?.filePath)} icon="tabler:download" width="24" height="24" /> } + {progress && !i.output?.filePath && ( +
+ +
+ {`${(progress.processedBytes / (1024 * 1024)).toFixed(2)} MB`} +
+
+ )}
-
{i?.isRunning ? t('running') : t('stopped')}
+
+ {i?.isRunning ? ( + progress ? `${t('running')}` : t('running') + ) : t('stopped')} +
diff --git a/src/server/plugins/baseScheduleJob.ts b/src/server/plugins/baseScheduleJob.ts index dd92aa52..f26bf6ac 100644 --- a/src/server/plugins/baseScheduleJob.ts +++ b/src/server/plugins/baseScheduleJob.ts @@ -37,7 +37,7 @@ export abstract class BaseScheduleJob { static async Start(cronTime: string, immediate: boolean = true) { let success = false, output; - const hasTask = await prisma.scheduledTask.findFirst({ + let hasTask = await prisma.scheduledTask.findFirst({ where: { name: this.taskName } }); @@ -47,6 +47,9 @@ export abstract class BaseScheduleJob { if (immediate) { try { output = await this.RunTask(); + hasTask = await prisma.scheduledTask.findFirst({ + where: { name: this.taskName } + }); success = true; } catch (error) { output = error ?? (error.message ?? "internal error"); diff --git a/src/server/plugins/dbjob.ts b/src/server/plugins/dbjob.ts index 553d60a5..689876e7 100644 --- a/src/server/plugins/dbjob.ts +++ b/src/server/plugins/dbjob.ts @@ -13,6 +13,12 @@ import { Context } from "../context"; import { BaseScheduleJob } from "./baseScheduleJob"; import { CreateNotification } from "../routers/notification"; import { NotificationType } from "@/lib/prismaZodType"; +import archiver from 'archiver'; +import { createWriteStream } from 'fs'; +import yauzl from 'yauzl-promise'; +import { FileService } from "./files"; +import { PutObjectCommand } from "@aws-sdk/client-s3"; +import { getGlobalConfig } from "../routers/config"; export type RestoreResult = { type: 'success' | 'skip' | 'error'; @@ -32,6 +38,7 @@ export class DBJob extends BaseScheduleJob { protected static async RunTask() { try { + const config = await getGlobalConfig({ useAdmin: true }); const notes = await prisma.notes.findMany({ select: { id: true, @@ -63,28 +70,159 @@ export class DBJob extends BaseScheduleJob { const targetFile = UPLOAD_FILE_PATH + `/blinko_export.bko`; try { await unlink(targetFile); - } catch (error) { + } catch (error) { } + + const output = createWriteStream(targetFile); + const archive = archiver('zip', { + zlib: { level: 9 } + }); + + archive.on('error', (err) => { + throw err; + }); + + const archiveComplete = new Promise((resolve, reject) => { + output.on('close', resolve); + output.on('error', reject); + archive.on('error', reject); + }); + + archive.pipe(output); + + const addFilesRecursively = async (dirPath: string, basePath: string = '') => { + const files = fs.readdirSync(dirPath); + + for (const file of files) { + const fullPath = path.join(dirPath, file); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + await addFilesRecursively(fullPath, path.join(basePath, file)); + } else { + archive.file(fullPath, { + name: path.join(basePath, file) + }); + } + } + }; + + await addFilesRecursively(ROOT_PATH, ''); + + let lastUpdateTime = 0; + const updateInterval = 1000; + let finalProgress: any = null; + + const task = await prisma.scheduledTask.findFirst({ + where: { name: this.taskName } + }) + if (!task) { + await prisma.scheduledTask.create({ + data: { + name: this.taskName, + isRunning: true, + isSuccess: false, + lastRun: new Date(), + schedule: '0 0 * * *' + } + }); } - const zip = new AdmZip(); - zip.addLocalFolder(ROOT_PATH); - zip.writeZip(targetFile); + archive.on('progress', async (progress) => { + finalProgress = { + processed: progress.entries.processed, + total: progress.entries.total, + processedBytes: progress.fs.processedBytes, + percent: Math.floor((progress.entries.processed / progress.entries.total) * 100) + }; + + const now = Date.now(); + if (now - lastUpdateTime >= updateInterval) { + lastUpdateTime = now; + await prisma.scheduledTask.update({ + where: { name: this.taskName }, + data: { + output: { + progress: finalProgress + } + } + }); + } + }); + + archive.finalize(); + await archiveComplete; + await CreateNotification({ type: NotificationType.SYSTEM, title: 'system-notification', content: 'backup-success', useAdmin: true, - }) - return { filePath: `/api/file/blinko_export.bko` }; + }); + + if (config.objectStorage === 's3') { + const { s3ClientInstance } = await FileService.getS3Client(); + const fileStream = fs.createReadStream(targetFile); + await s3ClientInstance.send(new PutObjectCommand({ + Bucket: config.s3Bucket, + Key: `/BLINKO_BACKUP/blinko_export.bko`, + Body: fileStream + })); + return { + filePath: `/api/s3file/BLINKO_BACKUP/blinko_export.bko`, + progress: finalProgress + }; + } + + return { + filePath: `/api/file/blinko_export.bko`, + progress: finalProgress + }; } catch (error) { - throw new Error(error) + throw new Error(error); } } static async *RestoreDB(filePath: string, ctx: Context): AsyncGenerator { try { - const zip = new AdmZip(filePath); - zip.extractAllTo(ROOT_PATH, true); + const zipFile = await yauzl.open(filePath); + let processedBytes = 0; + let entryCount = 0; + const totalEntries = await (async () => { + let count = 0; + for await (const _ of zipFile) { + count++; + } + await zipFile.close(); + return count; + })(); + + const zipFileForExtract = await yauzl.open(filePath); + for await (const entry of zipFileForExtract) { + if (entry.filename.endsWith('/')) { + await fs.promises.mkdir(path.join(ROOT_PATH, entry.filename), { recursive: true }); + continue; + } + const targetPath = path.join(ROOT_PATH, entry.filename); + await fs.promises.mkdir(path.dirname(targetPath), { recursive: true }); + const readStream = await entry.openReadStream(); + const writeStream = fs.createWriteStream(targetPath); + await new Promise((resolve, reject) => { + readStream + .pipe(writeStream) + .on('finish', resolve) + .on('error', reject); + }); + processedBytes += entry.uncompressedSize; + entryCount++; + + yield { + type: 'success', + content: `extract: ${entry.filename}`, + progress: { current: entryCount, total: totalEntries } + }; + } + + await zipFileForExtract.close(); const backupData = JSON.parse( fs.readFileSync(`${DBBAKUP_PATH}/bak.json`, 'utf-8') @@ -213,7 +351,7 @@ export class DBJob extends BaseScheduleJob { yield { type: 'error', error, - content: error.message ?? 'internal error', + content: `extract failed: ${error.message}`, progress: { current: 0, total: 0 } }; }