From 3610d8fdc6f7913f59f17ae66d1a863aa733294e Mon Sep 17 00:00:00 2001 From: louis <869322496@qq.com> Date: Fri, 8 Mar 2024 16:24:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20minio=E5=8A=9F=E8=83=BD=E5=BC=80?= =?UTF-8?q?=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 15 +- .env.development | 1 - .env.production | 3 +- minio.js | 14 + package.json | 1 + pnpm-lock.yaml | 169 +- src/config/oss.config.ts | 4 +- .../netdisk/manager/manage-qiniu.service.ts | 836 +++++++++ src/modules/netdisk/manager/manage.class.ts | 4 +- .../netdisk/manager/manage.controller.ts | 114 +- src/modules/netdisk/manager/manage.service.ts | 1524 ++++++++--------- src/modules/netdisk/minio/minio.service.ts | 8 + src/modules/netdisk/netdisk.module.ts | 5 +- 13 files changed, 1853 insertions(+), 845 deletions(-) create mode 100644 minio.js create mode 100644 src/modules/netdisk/manager/manage-qiniu.service.ts create mode 100644 src/modules/netdisk/minio/minio.service.ts diff --git a/.env b/.env index 6a2b67a..43a592f 100644 --- a/.env +++ b/.env @@ -11,13 +11,17 @@ LOGGER_MAX_FILES = 31 TZ = Asia/Shanghai # OSS(minio) -OSS_ACCESSKEY=xxx -OSS_SECRETKEY=xxx -OSS_DOMAIN=https://cdn.louis.site -OSS_BUCKET=hxoa +OSS_ACCESSKEY=8Zttvx4ZbF2ikFRb +OSS_SECRETKEY=SCgOJEJXM5vMNQL4fF8opXA1wmpACRfw +OSS_PORT=8021 +OSS_DOMAIN=144.123.43.138 +OSS_DOMAIN_USE_SSL=false +OSS_BUCKET=tes1 OSS_ZONE=Zone_z2 # Zone_as0 | Zone_na0 | Zone_z0 | Zone_z1 | Zone_z2 OSS_ACCESS_TYPE=public # or private + + DB_HOST = host.docker.internal DB_PORT = 13307 DB_DATABASE = hxoa @@ -29,4 +33,5 @@ DB_LOGGING = ["error"] REDIS_PORT = 6379 REDIS_HOST = host.docker.internal REDIS_PASSWORD = 123456 -REDIS_DB = 0 \ No newline at end of file +REDIS_DB = 0 + diff --git a/.env.development b/.env.development index c330ac4..89c8c66 100644 --- a/.env.development +++ b/.env.development @@ -33,4 +33,3 @@ SMTP_PORT = 465 SMTP_USER = nest_admin@163.com SMTP_PASS = VIPLLOIPMETTROYU - \ No newline at end of file diff --git a/.env.production b/.env.production index 0a4e6bd..8dc6113 100644 --- a/.env.production +++ b/.env.production @@ -34,4 +34,5 @@ SMTP_USER = nest_admin@163.com SMTP_PASS = VIPLLOIPMETTROYU # 是否为演示模式(在演示模式下,会拦截除 GET 方法以外的所有请求) -IS_DEMO = false \ No newline at end of file +IS_DEMO = false + diff --git a/minio.js b/minio.js new file mode 100644 index 0000000..8d0410f --- /dev/null +++ b/minio.js @@ -0,0 +1,14 @@ +const Minio = require('minio'); + +const minioClient = new Minio.Client({ + endPoint: '144.123.43.138', + port: 8021, + useSSL: false, + accessKey: '8Zttvx4ZbF2ikFRb', + secretKey: 'SCgOJEJXM5vMNQL4fF8opXA1wmpACRfw' +}); + +minioClient.listBuckets((err, buckets) => { + if (err) return console.log(err); + console.log('Buckets:', buckets); +}); diff --git a/package.json b/package.json index 9cb7ada..7c66bd9 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "helmet": "^7.1.0", "ioredis": "^5.3.2", "lodash": "^4.17.21", + "minio": "^7.1.3", "mysql2": "^3.9.1", "nanoid": "^3.3.7", "nodemailer": "^6.9.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9528f32..71071f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,6 +131,9 @@ dependencies: lodash: specifier: ^4.17.21 version: 4.17.21 + minio: + specifier: ^7.1.3 + version: 7.1.3 mysql2: specifier: ^3.9.1 version: 3.9.1 @@ -3663,6 +3666,12 @@ packages: resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} dev: true + /@zxing/text-encoding@0.9.0: + resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} + requiresBuild: true + dev: false + optional: true + /JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -4061,6 +4070,13 @@ packages: engines: {node: '>=8.0.0'} dev: false + /available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + dependencies: + possible-typed-array-names: 1.0.0 + dev: false + /avvio@8.3.0: resolution: {integrity: sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==} dependencies: @@ -4348,6 +4364,10 @@ packages: base64-js: 1.5.1 dev: true + /browser-or-node@2.1.1: + resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==} + dev: false + /browser-resolve@1.11.3: resolution: {integrity: sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==} dependencies: @@ -5409,6 +5429,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + dev: false + /dedent@0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} dev: true @@ -6340,6 +6365,13 @@ packages: resolution: {integrity: sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==} dev: false + /fast-xml-parser@4.3.5: + resolution: {integrity: sha512-sWvP1Pl8H03B8oFJpFR3HE31HUfwtX7Rlf9BNsvdpujD4n7WMhfmu8h9wOV2u+c1k0ZilTADhPqypzx2J690ZQ==} + hasBin: true + dependencies: + strnum: 1.0.5 + dev: false + /fastify-plugin@4.5.1: resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} dev: false @@ -6438,6 +6470,11 @@ packages: dependencies: to-regex-range: 5.0.1 + /filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + dev: false + /finalhandler@1.1.2: resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} engines: {node: '>= 0.8'} @@ -6559,6 +6596,12 @@ packages: optional: true dev: false + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: false + /foreground-child@3.1.1: resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} engines: {node: '>=14'} @@ -7359,13 +7402,17 @@ packages: engines: {node: '>= 0.10'} dev: false + /ipaddr.js@2.1.0: + resolution: {integrity: sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==} + engines: {node: '>= 10'} + dev: false + /is-arguments@1.1.1: resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.7 has-tostringtag: 1.0.2 - dev: true /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -7380,6 +7427,11 @@ packages: dependencies: binary-extensions: 2.2.0 + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: false + /is-core-module@2.13.1: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} dependencies: @@ -7428,6 +7480,13 @@ packages: engines: {node: '>=6'} dev: true + /is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: false + /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -7490,6 +7549,13 @@ packages: text-extensions: 1.9.0 dev: true + /is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.14 + dev: false + /is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -8104,6 +8170,10 @@ packages: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: true + /json-stream@1.0.0: + resolution: {integrity: sha512-H/ZGY0nIAg3QcOwE1QN/rK/Fa7gJn7Ii5obwp6zyPO4xiPNwpIMjqy2gwjBEGqzkF/vSWEIBQCBuN19hYiL6Qg==} + dev: false + /json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} dev: true @@ -8797,6 +8867,26 @@ packages: /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + /minio@7.1.3: + resolution: {integrity: sha512-xPrLjWkTT5E7H7VnzOjF//xBp9I40jYB4aWhb2xTFopXXfw+Wo82DDWngdUju7Doy3Wk7R8C4LAgwhLHHnf0wA==} + engines: {node: ^16 || ^18 || >=20} + dependencies: + async: 3.2.5 + block-stream2: 2.1.0 + browser-or-node: 2.1.1 + buffer-crc32: 0.2.13 + fast-xml-parser: 4.3.5 + ipaddr.js: 2.1.0 + json-stream: 1.0.0 + lodash: 4.17.21 + mime-types: 2.1.35 + query-string: 7.1.3 + through2: 4.0.2 + web-encoding: 1.1.5 + xml: 1.0.1 + xml2js: 0.5.0 + dev: false + /minipass@3.3.6: resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} engines: {node: '>=8'} @@ -10056,6 +10146,11 @@ packages: resolution: {integrity: sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==} dev: true + /possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + dev: false + /prelude-ls@1.1.2: resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} engines: {node: '>= 0.8.0'} @@ -10328,6 +10423,16 @@ packages: dependencies: side-channel: 1.0.5 + /query-string@7.1.3: + resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} + engines: {node: '>=6'} + dependencies: + decode-uri-component: 0.2.2 + filter-obj: 1.1.0 + split-on-first: 1.1.0 + strict-uri-encode: 2.0.0 + dev: false + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true @@ -10723,7 +10828,6 @@ packages: /sax@1.3.0: resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} - dev: true /saxes@5.0.1: resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} @@ -11060,6 +11164,11 @@ packages: resolution: {integrity: sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==} dev: true + /split-on-first@1.1.0: + resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} + engines: {node: '>=6'} + dev: false + /split2@3.2.2: resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} dependencies: @@ -11192,6 +11301,11 @@ packages: engines: {node: '>=4.0.0'} dev: false + /strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + dev: false + /string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -11302,6 +11416,10 @@ packages: engines: {node: '>=8'} dev: true + /strnum@1.0.5: + resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + dev: false + /superagent@8.1.2: resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} engines: {node: '>=6.4.0 <13 || >=14'} @@ -11513,7 +11631,6 @@ packages: resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} dependencies: readable-stream: 3.6.2 - dev: true /through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -12026,6 +12143,16 @@ packages: /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + /util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + dependencies: + inherits: 2.0.4 + is-arguments: 1.1.1 + is-generator-function: 1.0.10 + is-typed-array: 1.1.13 + which-typed-array: 1.1.14 + dev: false + /utility@1.18.0: resolution: {integrity: sha512-PYxZDA+6QtvRvm//++aGdmKG/cI07jNwbROz0Ql+VzFV1+Z0Dy55NI4zZ7RHc9KKpBePNFwoErqIuqQv/cjiTA==} engines: {node: '>= 0.12.0'} @@ -12118,6 +12245,14 @@ packages: defaults: 1.0.4 dev: true + /web-encoding@1.1.5: + resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} + dependencies: + util: 0.12.5 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + dev: false + /web-resource-inliner@6.0.1: resolution: {integrity: sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==} engines: {node: '>=10.0.0'} @@ -12205,6 +12340,17 @@ packages: tr46: 0.0.3 webidl-conversions: 3.0.1 + /which-typed-array@1.1.14: + resolution: {integrity: sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + dev: false + /which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true @@ -12353,6 +12499,23 @@ packages: utf-8-validate: optional: true + /xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + dependencies: + sax: 1.3.0 + xmlbuilder: 11.0.1 + dev: false + + /xml@1.0.1: + resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} + dev: false + + /xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + dev: false + /xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} dev: false diff --git a/src/config/oss.config.ts b/src/config/oss.config.ts index 10ffda8..42f46c0 100644 --- a/src/config/oss.config.ts +++ b/src/config/oss.config.ts @@ -1,7 +1,7 @@ import { ConfigType, registerAs } from '@nestjs/config'; import * as qiniu from 'qiniu'; -import { env } from '~/global/env'; +import { env, envBoolean, envNumber } from '~/global/env'; function parseZone(zone: string) { switch (zone) { @@ -24,6 +24,8 @@ export const OssConfig = registerAs(ossRegToken, () => ({ accessKey: env('OSS_ACCESSKEY'), secretKey: env('OSS_SECRETKEY'), domain: env('OSS_DOMAIN'), + port: envNumber('OSS_PORT'), + useSSL: envBoolean('OSS_USE_SSL'), bucket: env('OSS_BUCKET'), zone: parseZone(env('OSS_ZONE') || 'Zone_z2'), access: (env('OSS_ACCESS_TYPE') as any) || 'public' diff --git a/src/modules/netdisk/manager/manage-qiniu.service.ts b/src/modules/netdisk/manager/manage-qiniu.service.ts new file mode 100644 index 0000000..3554b7b --- /dev/null +++ b/src/modules/netdisk/manager/manage-qiniu.service.ts @@ -0,0 +1,836 @@ +import { basename, extname } from 'node:path'; + +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { isEmpty } from 'lodash'; +import * as qiniu from 'qiniu'; +import { auth, conf, rs } from 'qiniu'; + +import { ConfigKeyPaths } from '~/config'; +import { + NETDISK_COPY_SUFFIX, + NETDISK_DELIMITER, + NETDISK_HANDLE_MAX_ITEM, + NETDISK_LIMIT +} from '~/constants/oss.constant'; + +import { AccountInfo } from '~/modules/user/user.model'; +import { UserService } from '~/modules/user/user.service'; + +import { generateRandomValue } from '~/utils'; + +import { SFileInfo, SFileInfoDetail, SFileList } from './manage.class'; +import { FileOpItem } from './manage.dto'; + +@Injectable() +export class QiNiuNetDiskManageService { + private config: conf.ConfigOptions; + private mac: auth.digest.Mac; + private bucketManager: rs.BucketManager; + + private get qiniuConfig() { + return this.configService.get('oss', { infer: true }); + } + + constructor( + private configService: ConfigService, + private userService: UserService + ) { + this.mac = new qiniu.auth.digest.Mac(this.qiniuConfig.accessKey, this.qiniuConfig.secretKey); + this.config = new qiniu.conf.Config({ + zone: this.qiniuConfig.zone + }); + // bucket manager + this.bucketManager = new qiniu.rs.BucketManager(this.mac, this.config); + } + + /** + * 获取文件列表 + * @param prefix 当前文件夹路径,搜索模式下会被忽略 + * @param marker 下一页标识 + * @returns iFileListResult + */ + async getFileList(prefix = '', marker = '', skey = ''): Promise { + // 是否需要搜索 + const searching = !isEmpty(skey); + return new Promise((resolve, reject) => { + this.bucketManager.listPrefix( + this.qiniuConfig.bucket, + { + prefix: searching ? '' : prefix, + limit: NETDISK_LIMIT, + delimiter: searching ? '' : NETDISK_DELIMITER, + marker + }, + (err, respBody, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + // 如果这个nextMarker不为空,那么还有未列举完毕的文件列表,下次调用listPrefix的时候, + // 指定options里面的marker为这个值 + const fileList: SFileInfo[] = []; + // 处理目录,但只有非搜索模式下可用 + if (!searching && !isEmpty(respBody.commonPrefixes)) { + // dir + for (const dirPath of respBody.commonPrefixes) { + const name = (dirPath as string).substr(0, dirPath.length - 1).replace(prefix, ''); + if (isEmpty(skey) || name.includes(skey)) { + fileList.push({ + name: (dirPath as string).substr(0, dirPath.length - 1).replace(prefix, ''), + type: 'dir', + id: generateRandomValue(10) + }); + } + } + } + // handle items + if (!isEmpty(respBody.items)) { + // file + for (const item of respBody.items) { + // 搜索模式下处理 + if (searching) { + const pathList: string[] = item.key.split(NETDISK_DELIMITER); + // dir is empty stirng, file is key string + const name = pathList.pop(); + if ( + item.key.endsWith(NETDISK_DELIMITER) && + pathList[pathList.length - 1].includes(skey) + ) { + // 结果是目录 + const ditName = pathList.pop(); + fileList.push({ + id: generateRandomValue(10), + name: ditName, + type: 'dir', + belongTo: pathList.join(NETDISK_DELIMITER) + }); + } else if (name.includes(skey)) { + // 文件 + fileList.push({ + id: generateRandomValue(10), + name, + type: 'file', + fsize: item.fsize, + mimeType: item.mimeType, + putTime: new Date(Number.parseInt(item.putTime) / 10000), + belongTo: pathList.join(NETDISK_DELIMITER) + }); + } + } else { + // 正常获取列表 + const fileKey = item.key.replace(prefix, '') as string; + if (!isEmpty(fileKey)) { + fileList.push({ + id: generateRandomValue(10), + name: fileKey, + type: 'file', + fsize: item.fsize, + mimeType: item.mimeType, + putTime: new Date(Number.parseInt(item.putTime) / 10000) + }); + } + } + } + } + resolve({ + list: fileList, + marker: respBody.marker || null + }); + } else { + reject( + new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + ); + } + } + ); + }); + } + + /** + * 获取文件信息 + */ + async getFileInfo(name: string, path: string): Promise { + return new Promise((resolve, reject) => { + this.bucketManager.stat( + this.qiniuConfig.bucket, + `${path}${name}`, + (err, respBody, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + const detailInfo: SFileInfoDetail = { + fsize: respBody.fsize, + hash: respBody.hash, + md5: respBody.md5, + mimeType: respBody.mimeType.split('/x-qn-meta')[0], + putTime: new Date(Number.parseInt(respBody.putTime) / 10000), + type: respBody.type, + uploader: '', + mark: respBody?.['x-qn-meta']?.['!mark'] ?? '' + }; + if (!respBody.endUser) { + resolve(detailInfo); + } else { + this.userService + .getAccountInfo(Number.parseInt(respBody.endUser)) + .then((user: AccountInfo) => { + if (isEmpty(user)) { + resolve(detailInfo); + } else { + detailInfo.uploader = user.username; + resolve(detailInfo); + } + }); + } + } else { + reject( + new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + ); + } + } + ); + }); + } + + /** + * 修改文件MimeType + */ + async changeFileHeaders( + name: string, + path: string, + headers: { [k: string]: string } + ): Promise { + return new Promise((resolve, reject) => { + this.bucketManager.changeHeaders( + this.qiniuConfig.bucket, + `${path}${name}`, + headers, + (err, _, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + resolve(); + } else { + reject( + new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + ); + } + } + ); + }); + } + + /** + * 创建文件夹 + * @returns true创建成功 + */ + async createDir(dirName: string): Promise { + const safeDirName = dirName.endsWith('/') ? dirName : `${dirName}/`; + return new Promise((resolve, reject) => { + // 上传一个空文件以用于显示文件夹效果 + const formUploader = new qiniu.form_up.FormUploader(this.config); + const putExtra = new qiniu.form_up.PutExtra(); + formUploader.put( + this.createUploadToken(''), + safeDirName, + ' ', + putExtra, + (respErr, respBody, respInfo) => { + if (respErr) { + reject(respErr); + return; + } + if (respInfo.statusCode === 200) { + resolve(); + } else { + reject( + new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + ); + } + } + ); + }); + } + + /** + * 检查文件是否存在,同可检查目录 + */ + async checkFileExist(filePath: string): Promise { + return new Promise((resolve, reject) => { + // fix path end must a / + + // 检测文件夹是否存在 + this.bucketManager.stat(this.qiniuConfig.bucket, filePath, (respErr, respBody, respInfo) => { + if (respErr) { + reject(respErr); + return; + } + if (respInfo.statusCode === 200) { + // 文件夹存在 + resolve(true); + } else if (respInfo.statusCode === 612) { + // 文件夹不存在 + resolve(false); + } else { + reject( + new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + ); + } + }); + }); + } + + /** + * 创建Upload Token, 默认过期时间一小时 + * @returns upload token + */ + createUploadToken(endUser: string): string { + const policy = new qiniu.rs.PutPolicy({ + scope: this.qiniuConfig.bucket, + insertOnly: 1, + fsizeLimit: 1024 ** 2 * 10, + endUser + }); + const uploadToken = policy.uploadToken(this.mac); + return uploadToken; + } + + /** + * 重命名文件 + * @param dir 文件路径 + * @param name 文件名称 + */ + async renameFile(dir: string, name: string, toName: string): Promise { + const fileName = `${dir}${name}`; + const toFileName = `${dir}${toName}`; + const op = { + force: true + }; + return new Promise((resolve, reject) => { + this.bucketManager.move( + this.qiniuConfig.bucket, + fileName, + this.qiniuConfig.bucket, + toFileName, + op, + (err, respBody, respInfo) => { + if (err) { + reject(err); + } else { + if (respInfo.statusCode === 200) { + resolve(); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + ) + ); + } + } + } + ); + }); + } + + /** + * 移动文件 + */ + async moveFile(dir: string, toDir: string, name: string): Promise { + const fileName = `${dir}${name}`; + const toFileName = `${toDir}${name}`; + const op = { + force: true + }; + return new Promise((resolve, reject) => { + this.bucketManager.move( + this.qiniuConfig.bucket, + fileName, + this.qiniuConfig.bucket, + toFileName, + op, + (err, respBody, respInfo) => { + if (err) { + reject(err); + } else { + if (respInfo.statusCode === 200) { + resolve(); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + ) + ); + } + } + } + ); + }); + } + + /** + * 复制文件 + */ + async copyFile(dir: string, toDir: string, name: string): Promise { + const fileName = `${dir}${name}`; + // 拼接文件名 + const ext = extname(name); + const bn = basename(name, ext); + const toFileName = `${toDir}${bn}${NETDISK_COPY_SUFFIX}${ext}`; + const op = { + force: true + }; + return new Promise((resolve, reject) => { + this.bucketManager.copy( + this.qiniuConfig.bucket, + fileName, + this.qiniuConfig.bucket, + toFileName, + op, + (err, respBody, respInfo) => { + if (err) { + reject(err); + } else { + if (respInfo.statusCode === 200) { + resolve(); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + ) + ); + } + } + } + ); + }); + } + + /** + * 重命名文件夹 + */ + async renameDir(path: string, name: string, toName: string): Promise { + const dirName = `${path}${name}`; + const toDirName = `${path}${toName}`; + let hasFile = true; + let marker = ''; + const op = { + force: true + }; + const bucketName = this.qiniuConfig.bucket; + while (hasFile) { + await new Promise((resolve, reject) => { + // 列举当前目录下的所有文件 + this.bucketManager.listPrefix( + this.qiniuConfig.bucket, + { + prefix: dirName, + limit: NETDISK_HANDLE_MAX_ITEM, + marker + }, + (err, respBody, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + const moveOperations = respBody.items.map(item => { + const { key } = item; + const destKey = key.replace(dirName, toDirName); + return qiniu.rs.moveOp(bucketName, key, bucketName, destKey, op); + }); + this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => { + if (err2) { + reject(err2); + return; + } + if (respInfo2.statusCode === 200) { + if (isEmpty(respBody.marker)) hasFile = false; + else marker = respBody.marker; + + resolve(); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}` + ) + ); + } + }); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + ) + ); + } + } + ); + }); + } + } + + /** + * 获取七牛下载的文件url链接 + * @param key 文件路径 + * @returns 连接 + */ + getDownloadLink(key: string): string { + if (this.qiniuConfig.access === 'public') { + return this.bucketManager.publicDownloadUrl(this.qiniuConfig.domain, key); + } else if (this.qiniuConfig.access === 'private') { + return this.bucketManager.privateDownloadUrl( + this.qiniuConfig.domain, + key, + Date.now() / 1000 + 36000 + ); + } + throw new Error('qiniu config access type not support'); + } + + /** + * 删除文件 + * @param dir 删除的文件夹目录 + * @param name 文件名 + */ + async deleteFile(dir: string, name: string): Promise { + return new Promise((resolve, reject) => { + this.bucketManager.delete( + this.qiniuConfig.bucket, + `${dir}${name}`, + (err, respBody, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + resolve(); + } else { + reject( + new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + ); + } + } + ); + }); + } + + /** + * 删除文件夹 + * @param dir 文件夹所在的上级目录 + * @param name 文件目录名称 + */ + async deleteMultiFileOrDir(fileList: FileOpItem[], dir: string): Promise { + const files = fileList.filter(item => item.type === 'file'); + if (files.length > 0) { + // 批处理文件 + const copyOperations = files.map(item => { + const fileName = `${dir}${item.name}`; + return qiniu.rs.deleteOp(this.qiniuConfig.bucket, fileName); + }); + await new Promise((resolve, reject) => { + this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + resolve(); + } else if (respInfo.statusCode === 298) { + reject(new Error('操作异常,但部分文件夹删除成功')); + } else { + reject( + new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + ); + } + }); + }); + } + // 处理文件夹 + const dirs = fileList.filter(item => item.type === 'dir'); + if (dirs.length > 0) { + // 处理文件夹的复制 + for (let i = 0; i < dirs.length; i++) { + const dirName = `${dir}${dirs[i].name}/`; + let hasFile = true; + let marker = ''; + while (hasFile) { + await new Promise((resolve, reject) => { + // 列举当前目录下的所有文件 + this.bucketManager.listPrefix( + this.qiniuConfig.bucket, + { + prefix: dirName, + limit: NETDISK_HANDLE_MAX_ITEM, + marker + }, + (err, respBody, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + const moveOperations = respBody.items.map(item => { + const { key } = item; + return qiniu.rs.deleteOp(this.qiniuConfig.bucket, key); + }); + this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => { + if (err2) { + reject(err2); + return; + } + if (respInfo2.statusCode === 200) { + if (isEmpty(respBody.marker)) hasFile = false; + else marker = respBody.marker; + + resolve(); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}` + ) + ); + } + }); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + ) + ); + } + } + ); + }); + } + } + } + } + + /** + * 复制文件,含文件夹 + */ + async copyMultiFileOrDir(fileList: FileOpItem[], dir: string, toDir: string): Promise { + const files = fileList.filter(item => item.type === 'file'); + const op = { + force: true + }; + if (files.length > 0) { + // 批处理文件 + const copyOperations = files.map(item => { + const fileName = `${dir}${item.name}`; + // 拼接文件名 + const ext = extname(item.name); + const bn = basename(item.name, ext); + const toFileName = `${toDir}${bn}${NETDISK_COPY_SUFFIX}${ext}`; + return qiniu.rs.copyOp( + this.qiniuConfig.bucket, + fileName, + this.qiniuConfig.bucket, + toFileName, + op + ); + }); + await new Promise((resolve, reject) => { + this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + resolve(); + } else if (respInfo.statusCode === 298) { + reject(new Error('操作异常,但部分文件夹删除成功')); + } else { + reject( + new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + ); + } + }); + }); + } + // 处理文件夹 + const dirs = fileList.filter(item => item.type === 'dir'); + if (dirs.length > 0) { + // 处理文件夹的复制 + for (let i = 0; i < dirs.length; i++) { + const dirName = `${dir}${dirs[i].name}/`; + const copyDirName = `${toDir}${dirs[i].name}${NETDISK_COPY_SUFFIX}/`; + let hasFile = true; + let marker = ''; + while (hasFile) { + await new Promise((resolve, reject) => { + // 列举当前目录下的所有文件 + this.bucketManager.listPrefix( + this.qiniuConfig.bucket, + { + prefix: dirName, + limit: NETDISK_HANDLE_MAX_ITEM, + marker + }, + (err, respBody, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + const moveOperations = respBody.items.map(item => { + const { key } = item; + const destKey = key.replace(dirName, copyDirName); + return qiniu.rs.copyOp( + this.qiniuConfig.bucket, + key, + this.qiniuConfig.bucket, + destKey, + op + ); + }); + this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => { + if (err2) { + reject(err2); + return; + } + if (respInfo2.statusCode === 200) { + if (isEmpty(respBody.marker)) hasFile = false; + else marker = respBody.marker; + + resolve(); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}` + ) + ); + } + }); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + ) + ); + } + } + ); + }); + } + } + } + } + + /** + * 移动文件,含文件夹 + */ + async moveMultiFileOrDir(fileList: FileOpItem[], dir: string, toDir: string): Promise { + const files = fileList.filter(item => item.type === 'file'); + const op = { + force: true + }; + if (files.length > 0) { + // 批处理文件 + const copyOperations = files.map(item => { + const fileName = `${dir}${item.name}`; + const toFileName = `${toDir}${item.name}`; + return qiniu.rs.moveOp( + this.qiniuConfig.bucket, + fileName, + this.qiniuConfig.bucket, + toFileName, + op + ); + }); + await new Promise((resolve, reject) => { + this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + resolve(); + } else if (respInfo.statusCode === 298) { + reject(new Error('操作异常,但部分文件夹删除成功')); + } else { + reject( + new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + ); + } + }); + }); + } + // 处理文件夹 + const dirs = fileList.filter(item => item.type === 'dir'); + if (dirs.length > 0) { + // 处理文件夹的复制 + for (let i = 0; i < dirs.length; i++) { + const dirName = `${dir}${dirs[i].name}/`; + const toDirName = `${toDir}${dirs[i].name}/`; + // 移动的目录不是是自己 + if (toDirName.startsWith(dirName)) continue; + + let hasFile = true; + let marker = ''; + while (hasFile) { + await new Promise((resolve, reject) => { + // 列举当前目录下的所有文件 + this.bucketManager.listPrefix( + this.qiniuConfig.bucket, + { + prefix: dirName, + limit: NETDISK_HANDLE_MAX_ITEM, + marker + }, + (err, respBody, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + const moveOperations = respBody.items.map(item => { + const { key } = item; + const destKey = key.replace(dirName, toDirName); + return qiniu.rs.moveOp( + this.qiniuConfig.bucket, + key, + this.qiniuConfig.bucket, + destKey, + op + ); + }); + this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => { + if (err2) { + reject(err2); + return; + } + if (respInfo2.statusCode === 200) { + if (isEmpty(respBody.marker)) hasFile = false; + else marker = respBody.marker; + + resolve(); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}` + ) + ); + } + }); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + ) + ); + } + } + ); + }); + } + } + } + } +} diff --git a/src/modules/netdisk/manager/manage.class.ts b/src/modules/netdisk/manager/manage.class.ts index 1c099d8..3f236f6 100644 --- a/src/modules/netdisk/manager/manage.class.ts +++ b/src/modules/netdisk/manager/manage.class.ts @@ -51,7 +51,7 @@ export class SFileInfoDetail { @ApiProperty({ description: '文件存储类型,2 表示归档存储,1 表示低频存储,0表示普通存储。' }) - type: number; + type?: number; @ApiProperty({ description: '文件上传时间', type: Date }) putTime: Date; @@ -60,7 +60,7 @@ export class SFileInfoDetail { md5: string; @ApiProperty({ description: '上传人' }) - uploader: string; + uploader?: string; @ApiProperty({ description: '文件备注' }) mark?: string; diff --git a/src/modules/netdisk/manager/manage.controller.ts b/src/modules/netdisk/manager/manage.controller.ts index 8a6d2eb..f3e092c 100644 --- a/src/modules/netdisk/manager/manage.controller.ts +++ b/src/modules/netdisk/manager/manage.controller.ts @@ -49,27 +49,27 @@ export class NetDiskManageController { return await this.manageService.getFileList(dto.path, dto.marker, dto.key); } - @Post('mkdir') - @ApiOperation({ summary: '创建文件夹,支持多级' }) - @Perm(permissions.MKDIR) - async mkdir(@Body() dto: MKDirDto): Promise { - const result = await this.manageService.checkFileExist(`${dto.path}${dto.dirName}/`); - if (result) throw new BusinessException(ErrorEnum.OSS_FILE_OR_DIR_EXIST); + // @Post('mkdir') + // @ApiOperation({ summary: '创建文件夹,支持多级' }) + // @Perm(permissions.MKDIR) + // async mkdir(@Body() dto: MKDirDto): Promise { + // const result = await this.manageService.checkFileExist(`${dto.path}${dto.dirName}/`); + // if (result) throw new BusinessException(ErrorEnum.OSS_FILE_OR_DIR_EXIST); - await this.manageService.createDir(`${dto.path}${dto.dirName}`); - } + // await this.manageService.createDir(`${dto.path}${dto.dirName}`); + // } - @Get('token') - @ApiOperation({ summary: '获取上传Token,无Token前端无法上传' }) - @ApiOkResponse({ type: UploadToken }) - @Perm(permissions.TOKEN) - async token(@AuthUser() user: IAuthUser): Promise { - checkIsDemoMode(); + // @Get('token') + // @ApiOperation({ summary: '获取上传Token,无Token前端无法上传' }) + // @ApiOkResponse({ type: UploadToken }) + // @Perm(permissions.TOKEN) + // async token(@AuthUser() user: IAuthUser): Promise { + // checkIsDemoMode(); - return { - token: this.manageService.createUploadToken(`${user.uid}`) - }; - } + // return { + // token: this.manageService.createUploadToken(`${user.uid}`) + // }; + // } @Get('info') @ApiOperation({ summary: '获取文件详细信息' }) @@ -79,14 +79,14 @@ export class NetDiskManageController { return await this.manageService.getFileInfo(dto.name, dto.path); } - @Post('mark') - @ApiOperation({ summary: '添加文件备注' }) - @Perm(permissions.MARK) - async mark(@Body() dto: MarkFileDto): Promise { - await this.manageService.changeFileHeaders(dto.name, dto.path, { - mark: dto.mark - }); - } + // @Post('mark') + // @ApiOperation({ summary: '添加文件备注' }) + // @Perm(permissions.MARK) + // async mark(@Body() dto: MarkFileDto): Promise { + // await this.manageService.changeFileHeaders(dto.name, dto.path, { + // mark: dto.mark + // }); + // } @Get('download') @ApiOperation({ summary: '获取下载链接,不支持下载文件夹' }) @@ -96,40 +96,40 @@ export class NetDiskManageController { return this.manageService.getDownloadLink(`${dto.path}${dto.name}`); } - @Post('rename') - @ApiOperation({ summary: '重命名文件或文件夹' }) - @Perm(permissions.RENAME) - async rename(@Body() dto: RenameDto): Promise { - const result = await this.manageService.checkFileExist( - `${dto.path}${dto.toName}${dto.type === 'dir' ? '/' : ''}` - ); - if (result) throw new BusinessException(ErrorEnum.OSS_FILE_OR_DIR_EXIST); + // @Post('rename') + // @ApiOperation({ summary: '重命名文件或文件夹' }) + // @Perm(permissions.RENAME) + // async rename(@Body() dto: RenameDto): Promise { + // const result = await this.manageService.checkFileExist( + // `${dto.path}${dto.toName}${dto.type === 'dir' ? '/' : ''}` + // ); + // if (result) throw new BusinessException(ErrorEnum.OSS_FILE_OR_DIR_EXIST); - if (dto.type === 'file') await this.manageService.renameFile(dto.path, dto.name, dto.toName); - else await this.manageService.renameDir(dto.path, dto.name, dto.toName); - } + // if (dto.type === 'file') await this.manageService.renameFile(dto.path, dto.name, dto.toName); + // else await this.manageService.renameDir(dto.path, dto.name, dto.toName); + // } - @Post('delete') - @ApiOperation({ summary: '删除文件或文件夹' }) - @Perm(permissions.DELETE) - async delete(@Body() dto: DeleteDto): Promise { - await this.manageService.deleteMultiFileOrDir(dto.files, dto.path); - } + // @Post('delete') + // @ApiOperation({ summary: '删除文件或文件夹' }) + // @Perm(permissions.DELETE) + // async delete(@Body() dto: DeleteDto): Promise { + // await this.manageService.deleteMultiFileOrDir(dto.files, dto.path); + // } - @Post('cut') - @ApiOperation({ summary: '剪切文件或文件夹,支持批量' }) - @Perm(permissions.CUT) - async cut(@Body() dto: FileOpDto): Promise { - if (dto.originPath === dto.toPath) - throw new BusinessException(ErrorEnum.OSS_NO_OPERATION_REQUIRED); + // @Post('cut') + // @ApiOperation({ summary: '剪切文件或文件夹,支持批量' }) + // @Perm(permissions.CUT) + // async cut(@Body() dto: FileOpDto): Promise { + // if (dto.originPath === dto.toPath) + // throw new BusinessException(ErrorEnum.OSS_NO_OPERATION_REQUIRED); - await this.manageService.moveMultiFileOrDir(dto.files, dto.originPath, dto.toPath); - } + // await this.manageService.moveMultiFileOrDir(dto.files, dto.originPath, dto.toPath); + // } - @Post('copy') - @ApiOperation({ summary: '复制文件或文件夹,支持批量' }) - @Perm(permissions.COPY) - async copy(@Body() dto: FileOpDto): Promise { - await this.manageService.copyMultiFileOrDir(dto.files, dto.originPath, dto.toPath); - } + // @Post('copy') + // @ApiOperation({ summary: '复制文件或文件夹,支持批量' }) + // @Perm(permissions.COPY) + // async copy(@Body() dto: FileOpDto): Promise { + // await this.manageService.copyMultiFileOrDir(dto.files, dto.originPath, dto.toPath); + // } } diff --git a/src/modules/netdisk/manager/manage.service.ts b/src/modules/netdisk/manager/manage.service.ts index 50f4d8e..b6d0c74 100644 --- a/src/modules/netdisk/manager/manage.service.ts +++ b/src/modules/netdisk/manager/manage.service.ts @@ -3,8 +3,6 @@ import { basename, extname } from 'node:path'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { isEmpty } from 'lodash'; -import * as qiniu from 'qiniu'; -import { auth, conf, rs } from 'qiniu'; import { ConfigKeyPaths } from '~/config'; import { @@ -17,31 +15,25 @@ import { import { AccountInfo } from '~/modules/user/user.model'; import { UserService } from '~/modules/user/user.service'; -import { generateRandomValue } from '~/utils'; +import { fileRename, generateRandomValue } from '~/utils'; import { SFileInfo, SFileInfoDetail, SFileList } from './manage.class'; import { FileOpItem } from './manage.dto'; - +import * as Minio from 'Minio'; @Injectable() export class NetDiskManageService { - private config: conf.ConfigOptions; - private mac: auth.digest.Mac; - private bucketManager: rs.BucketManager; - - private get qiniuConfig() { + private get ossConfig() { return this.configService.get('oss', { infer: true }); } - - constructor( - private configService: ConfigService, - private userService: UserService - ) { - this.mac = new qiniu.auth.digest.Mac(this.qiniuConfig.accessKey, this.qiniuConfig.secretKey); - this.config = new qiniu.conf.Config({ - zone: this.qiniuConfig.zone + private minioClient: Minio.Client; + constructor(private configService: ConfigService) { + this.minioClient = new Minio.Client({ + endPoint: this.ossConfig.domain, + port: this.ossConfig.port, + useSSL: this.ossConfig.useSSL, + accessKey: this.ossConfig.accessKey, + secretKey: this.ossConfig.secretKey }); - // bucket manager - this.bucketManager = new qiniu.rs.BucketManager(this.mac, this.config); } /** @@ -54,97 +46,86 @@ export class NetDiskManageService { // 是否需要搜索 const searching = !isEmpty(skey); return new Promise((resolve, reject) => { - this.bucketManager.listPrefix( - this.qiniuConfig.bucket, - { - prefix: searching ? '' : prefix, - limit: NETDISK_LIMIT, - delimiter: searching ? '' : NETDISK_DELIMITER, - marker - }, - (err, respBody, respInfo) => { - if (err) { - reject(err); - return; - } - if (respInfo.statusCode === 200) { - // 如果这个nextMarker不为空,那么还有未列举完毕的文件列表,下次调用listPrefix的时候, - // 指定options里面的marker为这个值 - const fileList: SFileInfo[] = []; - // 处理目录,但只有非搜索模式下可用 - if (!searching && !isEmpty(respBody.commonPrefixes)) { - // dir - for (const dirPath of respBody.commonPrefixes) { - const name = (dirPath as string).substr(0, dirPath.length - 1).replace(prefix, ''); - if (isEmpty(skey) || name.includes(skey)) { - fileList.push({ - name: (dirPath as string).substr(0, dirPath.length - 1).replace(prefix, ''), - type: 'dir', - id: generateRandomValue(10) - }); - } + try { + const fileStream = this.minioClient.listObjects(this.ossConfig.bucket, prefix, false); + this.readStreamData(fileStream).then(respBody => { + console.log(respBody); + const dirs = respBody.filter(item => 'prefix' in item).map(item => item.prefix); + const files = respBody.filter(item => !('prefix' in item)); + // 如果这个nextMarker不为空,那么还有未列举完毕的文件列表,下次调用listPrefix的时候, + // 指定options里面的marker为这个值 + const fileList: SFileInfo[] = []; + // 处理目录,但只有非搜索模式下可用 + if (!searching && !isEmpty(dirs)) { + // dir + for (const dirPath of dirs) { + const name = (dirPath as string).substr(0, dirPath.length - 1).replace(prefix, ''); + if (isEmpty(skey) || name.includes(skey)) { + fileList.push({ + name: (dirPath as string).substr(0, dirPath.length - 1).replace(prefix, ''), + type: 'dir', + id: generateRandomValue(10) + }); } } - // handle items - if (!isEmpty(respBody.items)) { - // file - for (const item of respBody.items) { - // 搜索模式下处理 - if (searching) { - const pathList: string[] = item.key.split(NETDISK_DELIMITER); - // dir is empty stirng, file is key string - const name = pathList.pop(); - if ( - item.key.endsWith(NETDISK_DELIMITER) && - pathList[pathList.length - 1].includes(skey) - ) { - // 结果是目录 - const ditName = pathList.pop(); - fileList.push({ - id: generateRandomValue(10), - name: ditName, - type: 'dir', - belongTo: pathList.join(NETDISK_DELIMITER) - }); - } else if (name.includes(skey)) { - // 文件 - fileList.push({ - id: generateRandomValue(10), - name, - type: 'file', - fsize: item.fsize, - mimeType: item.mimeType, - putTime: new Date(Number.parseInt(item.putTime) / 10000), - belongTo: pathList.join(NETDISK_DELIMITER) - }); - } - } else { - // 正常获取列表 - const fileKey = item.key.replace(prefix, '') as string; - if (!isEmpty(fileKey)) { - fileList.push({ - id: generateRandomValue(10), - name: fileKey, - type: 'file', - fsize: item.fsize, - mimeType: item.mimeType, - putTime: new Date(Number.parseInt(item.putTime) / 10000) - }); - } - } + } + // handle items + if (!isEmpty(files)) { + // file + for (const item of files) { + // 搜索模式下处理 + // if (searching) { + // const pathList: string[] = item.key.split(NETDISK_DELIMITER); + // // dir is empty stirng, file is key string + // const name = pathList.pop(); + // if ( + // item.key.endsWith(NETDISK_DELIMITER) && + // pathList[pathList.length - 1].includes(skey) + // ) { + // // 结果是目录 + // const ditName = pathList.pop(); + // fileList.push({ + // id: generateRandomValue(10), + // name: ditName, + // type: 'dir', + // belongTo: pathList.join(NETDISK_DELIMITER) + // }); + // } else if (name.includes(skey)) { + // // 文件 + // fileList.push({ + // id: generateRandomValue(10), + // name, + // type: 'file', + // fsize: item.fsize, + // mimeType: item.mimeType, + // putTime: new Date(Number.parseInt(item.putTime) / 10000), + // belongTo: pathList.join(NETDISK_DELIMITER) + // }); + // } + // } else { + // 正常获取列表 + const fileKey = item.name.replace(prefix, '') as string; + if (!isEmpty(fileKey)) { + fileList.push({ + id: generateRandomValue(10), + name: fileKey, + type: 'file', + fsize: `${item.size || 0}`, + mimeType: item.name.split('.').pop(), + putTime: new Date(item.lastModified) + }); + // } } } - resolve({ - list: fileList, - marker: respBody.marker || null - }); - } else { - reject( - new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) - ); } - } - ); + resolve({ + list: fileList + // marker: respBody.marker || null + }); + }); + } catch (e) { + reject(e); + } }); } @@ -152,685 +133,682 @@ export class NetDiskManageService { * 获取文件信息 */ async getFileInfo(name: string, path: string): Promise { - return new Promise((resolve, reject) => { - this.bucketManager.stat( - this.qiniuConfig.bucket, - `${path}${name}`, - (err, respBody, respInfo) => { - if (err) { - reject(err); - return; - } - if (respInfo.statusCode === 200) { - const detailInfo: SFileInfoDetail = { - fsize: respBody.fsize, - hash: respBody.hash, - md5: respBody.md5, - mimeType: respBody.mimeType.split('/x-qn-meta')[0], - putTime: new Date(Number.parseInt(respBody.putTime) / 10000), - type: respBody.type, - uploader: '', - mark: respBody?.['x-qn-meta']?.['!mark'] ?? '' - }; - if (!respBody.endUser) { - resolve(detailInfo); - } else { - this.userService - .getAccountInfo(Number.parseInt(respBody.endUser)) - .then((user: AccountInfo) => { - if (isEmpty(user)) { - resolve(detailInfo); - } else { - detailInfo.uploader = user.username; - resolve(detailInfo); - } - }); - } - } else { - reject( - new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) - ); - } - } - ); - }); - } - - /** - * 修改文件MimeType - */ - async changeFileHeaders( - name: string, - path: string, - headers: { [k: string]: string } - ): Promise { - return new Promise((resolve, reject) => { - this.bucketManager.changeHeaders( - this.qiniuConfig.bucket, - `${path}${name}`, - headers, - (err, _, respInfo) => { - if (err) { - reject(err); - return; - } - if (respInfo.statusCode === 200) { - resolve(); - } else { - reject( - new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) - ); - } - } - ); - }); - } - - /** - * 创建文件夹 - * @returns true创建成功 - */ - async createDir(dirName: string): Promise { - const safeDirName = dirName.endsWith('/') ? dirName : `${dirName}/`; - return new Promise((resolve, reject) => { - // 上传一个空文件以用于显示文件夹效果 - const formUploader = new qiniu.form_up.FormUploader(this.config); - const putExtra = new qiniu.form_up.PutExtra(); - formUploader.put( - this.createUploadToken(''), - safeDirName, - ' ', - putExtra, - (respErr, respBody, respInfo) => { - if (respErr) { - reject(respErr); - return; - } - if (respInfo.statusCode === 200) { - resolve(); - } else { - reject( - new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) - ); - } - } - ); - }); - } - - /** - * 检查文件是否存在,同可检查目录 - */ - async checkFileExist(filePath: string): Promise { - return new Promise((resolve, reject) => { - // fix path end must a / - - // 检测文件夹是否存在 - this.bucketManager.stat(this.qiniuConfig.bucket, filePath, (respErr, respBody, respInfo) => { - if (respErr) { - reject(respErr); - return; - } - if (respInfo.statusCode === 200) { - // 文件夹存在 - resolve(true); - } else if (respInfo.statusCode === 612) { - // 文件夹不存在 - resolve(false); - } else { - reject( - new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) - ); - } - }); - }); - } - - /** - * 创建Upload Token, 默认过期时间一小时 - * @returns upload token - */ - createUploadToken(endUser: string): string { - const policy = new qiniu.rs.PutPolicy({ - scope: this.qiniuConfig.bucket, - insertOnly: 1, - fsizeLimit: 1024 ** 2 * 10, - endUser - }); - const uploadToken = policy.uploadToken(this.mac); - return uploadToken; - } - - /** - * 重命名文件 - * @param dir 文件路径 - * @param name 文件名称 - */ - async renameFile(dir: string, name: string, toName: string): Promise { - const fileName = `${dir}${name}`; - const toFileName = `${dir}${toName}`; - const op = { - force: true + const respBody = await this.minioClient.statObject(this.ossConfig.bucket, `${path}${name}`); + const detailInfo: SFileInfoDetail = { + fsize: respBody.size, + hash: respBody.metaData.hash || '', + md5: respBody.metaData.md5 || '', + mimeType: respBody.metaData['content-type'], + putTime: new Date(respBody.lastModified) }; - return new Promise((resolve, reject) => { - this.bucketManager.move( - this.qiniuConfig.bucket, - fileName, - this.qiniuConfig.bucket, - toFileName, - op, - (err, respBody, respInfo) => { - if (err) { - reject(err); - } else { - if (respInfo.statusCode === 200) { - resolve(); - } else { - reject( - new Error( - `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` - ) - ); - } - } - } - ); - }); + return detailInfo; } - /** - * 移动文件 - */ - async moveFile(dir: string, toDir: string, name: string): Promise { - const fileName = `${dir}${name}`; - const toFileName = `${toDir}${name}`; - const op = { - force: true - }; + // /** + // * 修改文件MimeType + // */ + // async changeFileHeaders( + // name: string, + // path: string, + // headers: { [k: string]: string } + // ): Promise { + // return new Promise((resolve, reject) => { + // this.bucketManager.changeHeaders( + // this.qiniuConfig.bucket, + // `${path}${name}`, + // headers, + // (err, _, respInfo) => { + // if (err) { + // reject(err); + // return; + // } + // if (respInfo.statusCode === 200) { + // resolve(); + // } else { + // reject( + // new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + // ); + // } + // } + // ); + // }); + // } + + // /** + // * 创建文件夹 + // * @returns true创建成功 + // */ + // async createDir(dirName: string): Promise { + // const safeDirName = dirName.endsWith('/') ? dirName : `${dirName}/`; + // return new Promise((resolve, reject) => { + // // 上传一个空文件以用于显示文件夹效果 + // const formUploader = new qiniu.form_up.FormUploader(this.config); + // const putExtra = new qiniu.form_up.PutExtra(); + // formUploader.put( + // this.createUploadToken(''), + // safeDirName, + // ' ', + // putExtra, + // (respErr, respBody, respInfo) => { + // if (respErr) { + // reject(respErr); + // return; + // } + // if (respInfo.statusCode === 200) { + // resolve(); + // } else { + // reject( + // new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + // ); + // } + // } + // ); + // }); + // } + + // /** + // * 检查文件是否存在,同可检查目录 + // */ + // async checkFileExist(filePath: string): Promise { + // return new Promise((resolve, reject) => { + // // fix path end must a / + + // // 检测文件夹是否存在 + // this.bucketManager.stat(this.qiniuConfig.bucket, filePath, (respErr, respBody, respInfo) => { + // if (respErr) { + // reject(respErr); + // return; + // } + // if (respInfo.statusCode === 200) { + // // 文件夹存在 + // resolve(true); + // } else if (respInfo.statusCode === 612) { + // // 文件夹不存在 + // resolve(false); + // } else { + // reject( + // new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + // ); + // } + // }); + // }); + // } + + // /** + // * 创建Upload Token, 默认过期时间一小时 + // * @returns upload token + // */ + // createUploadToken(endUser: string): string { + // const policy = new qiniu.rs.PutPolicy({ + // scope: this.qiniuConfig.bucket, + // insertOnly: 1, + // fsizeLimit: 1024 ** 2 * 10, + // endUser + // }); + // const uploadToken = policy.uploadToken(this.mac); + // return uploadToken; + // } + + // /** + // * 重命名文件 + // * @param dir 文件路径 + // * @param name 文件名称 + // */ + // async renameFile(dir: string, name: string, toName: string): Promise { + // const fileName = `${dir}${name}`; + // const toFileName = `${dir}${toName}`; + // const op = { + // force: true + // }; + // return new Promise((resolve, reject) => { + // this.bucketManager.move( + // this.qiniuConfig.bucket, + // fileName, + // this.qiniuConfig.bucket, + // toFileName, + // op, + // (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // } else { + // if (respInfo.statusCode === 200) { + // resolve(); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + // ) + // ); + // } + // } + // } + // ); + // }); + // } + + // /** + // * 移动文件 + // */ + // async moveFile(dir: string, toDir: string, name: string): Promise { + // const fileName = `${dir}${name}`; + // const toFileName = `${toDir}${name}`; + // const op = { + // force: true + // }; + // return new Promise((resolve, reject) => { + // this.bucketManager.move( + // this.qiniuConfig.bucket, + // fileName, + // this.qiniuConfig.bucket, + // toFileName, + // op, + // (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // } else { + // if (respInfo.statusCode === 200) { + // resolve(); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + // ) + // ); + // } + // } + // } + // ); + // }); + // } + + // /** + // * 复制文件 + // */ + // async copyFile(dir: string, toDir: string, name: string): Promise { + // const fileName = `${dir}${name}`; + // // 拼接文件名 + // const ext = extname(name); + // const bn = basename(name, ext); + // const toFileName = `${toDir}${bn}${NETDISK_COPY_SUFFIX}${ext}`; + // const op = { + // force: true + // }; + // return new Promise((resolve, reject) => { + // this.bucketManager.copy( + // this.qiniuConfig.bucket, + // fileName, + // this.qiniuConfig.bucket, + // toFileName, + // op, + // (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // } else { + // if (respInfo.statusCode === 200) { + // resolve(); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + // ) + // ); + // } + // } + // } + // ); + // }); + // } + + // /** + // * 重命名文件夹 + // */ + // async renameDir(path: string, name: string, toName: string): Promise { + // const dirName = `${path}${name}`; + // const toDirName = `${path}${toName}`; + // let hasFile = true; + // let marker = ''; + // const op = { + // force: true + // }; + // const bucketName = this.qiniuConfig.bucket; + // while (hasFile) { + // await new Promise((resolve, reject) => { + // // 列举当前目录下的所有文件 + // this.bucketManager.listPrefix( + // this.qiniuConfig.bucket, + // { + // prefix: dirName, + // limit: NETDISK_HANDLE_MAX_ITEM, + // marker + // }, + // (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // return; + // } + // if (respInfo.statusCode === 200) { + // const moveOperations = respBody.items.map(item => { + // const { key } = item; + // const destKey = key.replace(dirName, toDirName); + // return qiniu.rs.moveOp(bucketName, key, bucketName, destKey, op); + // }); + // this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => { + // if (err2) { + // reject(err2); + // return; + // } + // if (respInfo2.statusCode === 200) { + // if (isEmpty(respBody.marker)) hasFile = false; + // else marker = respBody.marker; + + // resolve(); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}` + // ) + // ); + // } + // }); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + // ) + // ); + // } + // } + // ); + // }); + // } + // } + + // /** + // * 获取七牛下载的文件url链接 + // * @param key 文件路径 + // * @returns 连接 + // */ + getDownloadLink(key: string): Promise { return new Promise((resolve, reject) => { - this.bucketManager.move( - this.qiniuConfig.bucket, - fileName, - this.qiniuConfig.bucket, - toFileName, - op, - (err, respBody, respInfo) => { - if (err) { - reject(err); - } else { - if (respInfo.statusCode === 200) { - resolve(); - } else { - reject( - new Error( - `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` - ) - ); - } - } - } - ); - }); - } - - /** - * 复制文件 - */ - async copyFile(dir: string, toDir: string, name: string): Promise { - const fileName = `${dir}${name}`; - // 拼接文件名 - const ext = extname(name); - const bn = basename(name, ext); - const toFileName = `${toDir}${bn}${NETDISK_COPY_SUFFIX}${ext}`; - const op = { - force: true - }; - return new Promise((resolve, reject) => { - this.bucketManager.copy( - this.qiniuConfig.bucket, - fileName, - this.qiniuConfig.bucket, - toFileName, - op, - (err, respBody, respInfo) => { - if (err) { - reject(err); - } else { - if (respInfo.statusCode === 200) { - resolve(); - } else { - reject( - new Error( - `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` - ) - ); - } - } - } - ); - }); - } - - /** - * 重命名文件夹 - */ - async renameDir(path: string, name: string, toName: string): Promise { - const dirName = `${path}${name}`; - const toDirName = `${path}${toName}`; - let hasFile = true; - let marker = ''; - const op = { - force: true - }; - const bucketName = this.qiniuConfig.bucket; - while (hasFile) { - await new Promise((resolve, reject) => { - // 列举当前目录下的所有文件 - this.bucketManager.listPrefix( - this.qiniuConfig.bucket, - { - prefix: dirName, - limit: NETDISK_HANDLE_MAX_ITEM, - marker - }, - (err, respBody, respInfo) => { - if (err) { - reject(err); - return; - } - if (respInfo.statusCode === 200) { - const moveOperations = respBody.items.map(item => { - const { key } = item; - const destKey = key.replace(dirName, toDirName); - return qiniu.rs.moveOp(bucketName, key, bucketName, destKey, op); - }); - this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => { - if (err2) { - reject(err2); - return; - } - if (respInfo2.statusCode === 200) { - if (isEmpty(respBody.marker)) hasFile = false; - else marker = respBody.marker; - - resolve(); - } else { - reject( - new Error( - `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}` - ) - ); - } - }); - } else { - reject( - new Error( - `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` - ) - ); - } + if (this.ossConfig.access === 'public') { + this.minioClient.presignedUrl( + 'GET', + this.ossConfig.bucket, + key, + 24 * 60 * 60, + (err, presignedUrl) => { + if (err) reject(); + resolve(presignedUrl); } ); - }); - } - } - - /** - * 获取七牛下载的文件url链接 - * @param key 文件路径 - * @returns 连接 - */ - getDownloadLink(key: string): string { - if (this.qiniuConfig.access === 'public') { - return this.bucketManager.publicDownloadUrl(this.qiniuConfig.domain, key); - } else if (this.qiniuConfig.access === 'private') { - return this.bucketManager.privateDownloadUrl( - this.qiniuConfig.domain, - key, - Date.now() / 1000 + 36000 - ); - } - throw new Error('qiniu config access type not support'); - } - - /** - * 删除文件 - * @param dir 删除的文件夹目录 - * @param name 文件名 - */ - async deleteFile(dir: string, name: string): Promise { - return new Promise((resolve, reject) => { - this.bucketManager.delete( - this.qiniuConfig.bucket, - `${dir}${name}`, - (err, respBody, respInfo) => { - if (err) { - reject(err); - return; - } - if (respInfo.statusCode === 200) { - resolve(); - } else { - reject( - new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) - ); - } - } - ); + } else if (this.ossConfig.access === 'private') { + // return this.bucketManager.privateDownloadUrl( + // this.qiniuConfig.domain, + // key, + // Date.now() / 1000 + 36000 + // ); + } }); } + // /** + // * 删除文件 + // * @param dir 删除的文件夹目录 + // * @param name 文件名 + // */ + // async deleteFile(dir: string, name: string): Promise { + // return new Promise((resolve, reject) => { + // this.bucketManager.delete( + // this.qiniuConfig.bucket, + // `${dir}${name}`, + // (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // return; + // } + // if (respInfo.statusCode === 200) { + // resolve(); + // } else { + // reject( + // new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + // ); + // } + // } + // ); + // }); + // } + + // /** + // * 删除文件夹 + // * @param dir 文件夹所在的上级目录 + // * @param name 文件目录名称 + // */ + // async deleteMultiFileOrDir(fileList: FileOpItem[], dir: string): Promise { + // const files = fileList.filter(item => item.type === 'file'); + // if (files.length > 0) { + // // 批处理文件 + // const copyOperations = files.map(item => { + // const fileName = `${dir}${item.name}`; + // return qiniu.rs.deleteOp(this.qiniuConfig.bucket, fileName); + // }); + // await new Promise((resolve, reject) => { + // this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // return; + // } + // if (respInfo.statusCode === 200) { + // resolve(); + // } else if (respInfo.statusCode === 298) { + // reject(new Error('操作异常,但部分文件夹删除成功')); + // } else { + // reject( + // new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + // ); + // } + // }); + // }); + // } + // // 处理文件夹 + // const dirs = fileList.filter(item => item.type === 'dir'); + // if (dirs.length > 0) { + // // 处理文件夹的复制 + // for (let i = 0; i < dirs.length; i++) { + // const dirName = `${dir}${dirs[i].name}/`; + // let hasFile = true; + // let marker = ''; + // while (hasFile) { + // await new Promise((resolve, reject) => { + // // 列举当前目录下的所有文件 + // this.bucketManager.listPrefix( + // this.qiniuConfig.bucket, + // { + // prefix: dirName, + // limit: NETDISK_HANDLE_MAX_ITEM, + // marker + // }, + // (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // return; + // } + // if (respInfo.statusCode === 200) { + // const moveOperations = respBody.items.map(item => { + // const { key } = item; + // return qiniu.rs.deleteOp(this.qiniuConfig.bucket, key); + // }); + // this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => { + // if (err2) { + // reject(err2); + // return; + // } + // if (respInfo2.statusCode === 200) { + // if (isEmpty(respBody.marker)) hasFile = false; + // else marker = respBody.marker; + + // resolve(); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}` + // ) + // ); + // } + // }); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + // ) + // ); + // } + // } + // ); + // }); + // } + // } + // } + // } + + // /** + // * 复制文件,含文件夹 + // */ + // async copyMultiFileOrDir(fileList: FileOpItem[], dir: string, toDir: string): Promise { + // const files = fileList.filter(item => item.type === 'file'); + // const op = { + // force: true + // }; + // if (files.length > 0) { + // // 批处理文件 + // const copyOperations = files.map(item => { + // const fileName = `${dir}${item.name}`; + // // 拼接文件名 + // const ext = extname(item.name); + // const bn = basename(item.name, ext); + // const toFileName = `${toDir}${bn}${NETDISK_COPY_SUFFIX}${ext}`; + // return qiniu.rs.copyOp( + // this.qiniuConfig.bucket, + // fileName, + // this.qiniuConfig.bucket, + // toFileName, + // op + // ); + // }); + // await new Promise((resolve, reject) => { + // this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // return; + // } + // if (respInfo.statusCode === 200) { + // resolve(); + // } else if (respInfo.statusCode === 298) { + // reject(new Error('操作异常,但部分文件夹删除成功')); + // } else { + // reject( + // new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + // ); + // } + // }); + // }); + // } + // // 处理文件夹 + // const dirs = fileList.filter(item => item.type === 'dir'); + // if (dirs.length > 0) { + // // 处理文件夹的复制 + // for (let i = 0; i < dirs.length; i++) { + // const dirName = `${dir}${dirs[i].name}/`; + // const copyDirName = `${toDir}${dirs[i].name}${NETDISK_COPY_SUFFIX}/`; + // let hasFile = true; + // let marker = ''; + // while (hasFile) { + // await new Promise((resolve, reject) => { + // // 列举当前目录下的所有文件 + // this.bucketManager.listPrefix( + // this.qiniuConfig.bucket, + // { + // prefix: dirName, + // limit: NETDISK_HANDLE_MAX_ITEM, + // marker + // }, + // (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // return; + // } + // if (respInfo.statusCode === 200) { + // const moveOperations = respBody.items.map(item => { + // const { key } = item; + // const destKey = key.replace(dirName, copyDirName); + // return qiniu.rs.copyOp( + // this.qiniuConfig.bucket, + // key, + // this.qiniuConfig.bucket, + // destKey, + // op + // ); + // }); + // this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => { + // if (err2) { + // reject(err2); + // return; + // } + // if (respInfo2.statusCode === 200) { + // if (isEmpty(respBody.marker)) hasFile = false; + // else marker = respBody.marker; + + // resolve(); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}` + // ) + // ); + // } + // }); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + // ) + // ); + // } + // } + // ); + // }); + // } + // } + // } + // } + + // /** + // * 移动文件,含文件夹 + // */ + // async moveMultiFileOrDir(fileList: FileOpItem[], dir: string, toDir: string): Promise { + // const files = fileList.filter(item => item.type === 'file'); + // const op = { + // force: true + // }; + // if (files.length > 0) { + // // 批处理文件 + // const copyOperations = files.map(item => { + // const fileName = `${dir}${item.name}`; + // const toFileName = `${toDir}${item.name}`; + // return qiniu.rs.moveOp( + // this.qiniuConfig.bucket, + // fileName, + // this.qiniuConfig.bucket, + // toFileName, + // op + // ); + // }); + // await new Promise((resolve, reject) => { + // this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // return; + // } + // if (respInfo.statusCode === 200) { + // resolve(); + // } else if (respInfo.statusCode === 298) { + // reject(new Error('操作异常,但部分文件夹删除成功')); + // } else { + // reject( + // new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + // ); + // } + // }); + // }); + // } + // // 处理文件夹 + // const dirs = fileList.filter(item => item.type === 'dir'); + // if (dirs.length > 0) { + // // 处理文件夹的复制 + // for (let i = 0; i < dirs.length; i++) { + // const dirName = `${dir}${dirs[i].name}/`; + // const toDirName = `${toDir}${dirs[i].name}/`; + // // 移动的目录不是是自己 + // if (toDirName.startsWith(dirName)) continue; + + // let hasFile = true; + // let marker = ''; + // while (hasFile) { + // await new Promise((resolve, reject) => { + // // 列举当前目录下的所有文件 + // this.bucketManager.listPrefix( + // this.qiniuConfig.bucket, + // { + // prefix: dirName, + // limit: NETDISK_HANDLE_MAX_ITEM, + // marker + // }, + // (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // return; + // } + // if (respInfo.statusCode === 200) { + // const moveOperations = respBody.items.map(item => { + // const { key } = item; + // const destKey = key.replace(dirName, toDirName); + // return qiniu.rs.moveOp( + // this.qiniuConfig.bucket, + // key, + // this.qiniuConfig.bucket, + // destKey, + // op + // ); + // }); + // this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => { + // if (err2) { + // reject(err2); + // return; + // } + // if (respInfo2.statusCode === 200) { + // if (isEmpty(respBody.marker)) hasFile = false; + // else marker = respBody.marker; + + // resolve(); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}` + // ) + // ); + // } + // }); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + // ) + // ); + // } + // } + // ); + // }); + // } + // } + // } + // } /** - * 删除文件夹 - * @param dir 文件夹所在的上级目录 - * @param name 文件目录名称 + * + * @param stream 文件流 + * @returns [] */ - async deleteMultiFileOrDir(fileList: FileOpItem[], dir: string): Promise { - const files = fileList.filter(item => item.type === 'file'); - if (files.length > 0) { - // 批处理文件 - const copyOperations = files.map(item => { - const fileName = `${dir}${item.name}`; - return qiniu.rs.deleteOp(this.qiniuConfig.bucket, fileName); - }); - await new Promise((resolve, reject) => { - this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => { - if (err) { - reject(err); - return; - } - if (respInfo.statusCode === 200) { - resolve(); - } else if (respInfo.statusCode === 298) { - reject(new Error('操作异常,但部分文件夹删除成功')); - } else { - reject( - new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) - ); - } + readStreamData(stream): Promise { + return new Promise((resolve, reject) => { + const result: T[] = []; + stream + .on('data', function (row) { + result.push(row); + }) + .on('end', function () { + resolve(result); + }) + .on('error', function (error) { + reject(error); }); - }); - } - // 处理文件夹 - const dirs = fileList.filter(item => item.type === 'dir'); - if (dirs.length > 0) { - // 处理文件夹的复制 - for (let i = 0; i < dirs.length; i++) { - const dirName = `${dir}${dirs[i].name}/`; - let hasFile = true; - let marker = ''; - while (hasFile) { - await new Promise((resolve, reject) => { - // 列举当前目录下的所有文件 - this.bucketManager.listPrefix( - this.qiniuConfig.bucket, - { - prefix: dirName, - limit: NETDISK_HANDLE_MAX_ITEM, - marker - }, - (err, respBody, respInfo) => { - if (err) { - reject(err); - return; - } - if (respInfo.statusCode === 200) { - const moveOperations = respBody.items.map(item => { - const { key } = item; - return qiniu.rs.deleteOp(this.qiniuConfig.bucket, key); - }); - this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => { - if (err2) { - reject(err2); - return; - } - if (respInfo2.statusCode === 200) { - if (isEmpty(respBody.marker)) hasFile = false; - else marker = respBody.marker; - - resolve(); - } else { - reject( - new Error( - `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}` - ) - ); - } - }); - } else { - reject( - new Error( - `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` - ) - ); - } - } - ); - }); - } - } - } - } - - /** - * 复制文件,含文件夹 - */ - async copyMultiFileOrDir(fileList: FileOpItem[], dir: string, toDir: string): Promise { - const files = fileList.filter(item => item.type === 'file'); - const op = { - force: true - }; - if (files.length > 0) { - // 批处理文件 - const copyOperations = files.map(item => { - const fileName = `${dir}${item.name}`; - // 拼接文件名 - const ext = extname(item.name); - const bn = basename(item.name, ext); - const toFileName = `${toDir}${bn}${NETDISK_COPY_SUFFIX}${ext}`; - return qiniu.rs.copyOp( - this.qiniuConfig.bucket, - fileName, - this.qiniuConfig.bucket, - toFileName, - op - ); - }); - await new Promise((resolve, reject) => { - this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => { - if (err) { - reject(err); - return; - } - if (respInfo.statusCode === 200) { - resolve(); - } else if (respInfo.statusCode === 298) { - reject(new Error('操作异常,但部分文件夹删除成功')); - } else { - reject( - new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) - ); - } - }); - }); - } - // 处理文件夹 - const dirs = fileList.filter(item => item.type === 'dir'); - if (dirs.length > 0) { - // 处理文件夹的复制 - for (let i = 0; i < dirs.length; i++) { - const dirName = `${dir}${dirs[i].name}/`; - const copyDirName = `${toDir}${dirs[i].name}${NETDISK_COPY_SUFFIX}/`; - let hasFile = true; - let marker = ''; - while (hasFile) { - await new Promise((resolve, reject) => { - // 列举当前目录下的所有文件 - this.bucketManager.listPrefix( - this.qiniuConfig.bucket, - { - prefix: dirName, - limit: NETDISK_HANDLE_MAX_ITEM, - marker - }, - (err, respBody, respInfo) => { - if (err) { - reject(err); - return; - } - if (respInfo.statusCode === 200) { - const moveOperations = respBody.items.map(item => { - const { key } = item; - const destKey = key.replace(dirName, copyDirName); - return qiniu.rs.copyOp( - this.qiniuConfig.bucket, - key, - this.qiniuConfig.bucket, - destKey, - op - ); - }); - this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => { - if (err2) { - reject(err2); - return; - } - if (respInfo2.statusCode === 200) { - if (isEmpty(respBody.marker)) hasFile = false; - else marker = respBody.marker; - - resolve(); - } else { - reject( - new Error( - `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}` - ) - ); - } - }); - } else { - reject( - new Error( - `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` - ) - ); - } - } - ); - }); - } - } - } - } - - /** - * 移动文件,含文件夹 - */ - async moveMultiFileOrDir(fileList: FileOpItem[], dir: string, toDir: string): Promise { - const files = fileList.filter(item => item.type === 'file'); - const op = { - force: true - }; - if (files.length > 0) { - // 批处理文件 - const copyOperations = files.map(item => { - const fileName = `${dir}${item.name}`; - const toFileName = `${toDir}${item.name}`; - return qiniu.rs.moveOp( - this.qiniuConfig.bucket, - fileName, - this.qiniuConfig.bucket, - toFileName, - op - ); - }); - await new Promise((resolve, reject) => { - this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => { - if (err) { - reject(err); - return; - } - if (respInfo.statusCode === 200) { - resolve(); - } else if (respInfo.statusCode === 298) { - reject(new Error('操作异常,但部分文件夹删除成功')); - } else { - reject( - new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) - ); - } - }); - }); - } - // 处理文件夹 - const dirs = fileList.filter(item => item.type === 'dir'); - if (dirs.length > 0) { - // 处理文件夹的复制 - for (let i = 0; i < dirs.length; i++) { - const dirName = `${dir}${dirs[i].name}/`; - const toDirName = `${toDir}${dirs[i].name}/`; - // 移动的目录不是是自己 - if (toDirName.startsWith(dirName)) continue; - - let hasFile = true; - let marker = ''; - while (hasFile) { - await new Promise((resolve, reject) => { - // 列举当前目录下的所有文件 - this.bucketManager.listPrefix( - this.qiniuConfig.bucket, - { - prefix: dirName, - limit: NETDISK_HANDLE_MAX_ITEM, - marker - }, - (err, respBody, respInfo) => { - if (err) { - reject(err); - return; - } - if (respInfo.statusCode === 200) { - const moveOperations = respBody.items.map(item => { - const { key } = item; - const destKey = key.replace(dirName, toDirName); - return qiniu.rs.moveOp( - this.qiniuConfig.bucket, - key, - this.qiniuConfig.bucket, - destKey, - op - ); - }); - this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => { - if (err2) { - reject(err2); - return; - } - if (respInfo2.statusCode === 200) { - if (isEmpty(respBody.marker)) hasFile = false; - else marker = respBody.marker; - - resolve(); - } else { - reject( - new Error( - `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}` - ) - ); - } - }); - } else { - reject( - new Error( - `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` - ) - ); - } - } - ); - }); - } - } - } + }); } } diff --git a/src/modules/netdisk/minio/minio.service.ts b/src/modules/netdisk/minio/minio.service.ts new file mode 100644 index 0000000..294f16f --- /dev/null +++ b/src/modules/netdisk/minio/minio.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import * as Minio from 'minio'; +import { ConfigKeyPaths } from '~/config'; +@Injectable() +export class MinioService { + +} diff --git a/src/modules/netdisk/netdisk.module.ts b/src/modules/netdisk/netdisk.module.ts index dc14031..0ea272f 100644 --- a/src/modules/netdisk/netdisk.module.ts +++ b/src/modules/netdisk/netdisk.module.ts @@ -5,9 +5,10 @@ import { RouterModule } from '@nestjs/core'; import { UserModule } from '../user/user.module'; import { NetDiskManageController } from './manager/manage.controller'; -import { NetDiskManageService } from './manager/manage.service'; import { NetDiskOverviewController } from './overview/overview.controller'; import { NetDiskOverviewService } from './overview/overview.service'; +import { MinioService } from './minio/minio.service'; +import { NetDiskManageService } from './manager/manage.service'; @Module({ imports: [ @@ -20,6 +21,6 @@ import { NetDiskOverviewService } from './overview/overview.service'; ]) ], controllers: [NetDiskManageController, NetDiskOverviewController], - providers: [NetDiskManageService, NetDiskOverviewService] + providers: [NetDiskManageService, NetDiskOverviewService, MinioService] }) export class NetdiskModule {}