From 7e71c75e7e6613e66fd894b0104e16c23586eefa Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 16 Jan 2025 10:27:52 +0100 Subject: [PATCH 1/5] chore(deps): add sharp library for thumbnails generation --- package.json | 1 + yarn.lock | 163 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 161 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 6cecad9d..cca8abb8 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "selfsigned": "2.4.1", "sequelize": "6.37.5", "sequelize-typescript": "2.1.6", + "sharp": "0.33.5", "sqlite3": "5.1.7", "tty-table": "4.2.3", "winston": "3.17.0" diff --git a/yarn.lock b/yarn.lock index 82141fcf..405473a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -825,6 +825,13 @@ enabled "2.0.x" kuler "^2.0.0" +"@emnapi/runtime@^1.2.0": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.3.1.tgz#0fcaa575afc31f455fd33534c19381cfce6c6f60" + integrity sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw== + dependencies: + tslib "^2.4.0" + "@esbuild/aix-ppc64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" @@ -1039,6 +1046,119 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.1.tgz#9a96ce501bc62df46c4031fbd970e3cc6b10f07b" integrity sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA== +"@img/sharp-darwin-arm64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz#ef5b5a07862805f1e8145a377c8ba6e98813ca08" + integrity sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ== + optionalDependencies: + "@img/sharp-libvips-darwin-arm64" "1.0.4" + +"@img/sharp-darwin-x64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz#e03d3451cd9e664faa72948cc70a403ea4063d61" + integrity sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q== + optionalDependencies: + "@img/sharp-libvips-darwin-x64" "1.0.4" + +"@img/sharp-libvips-darwin-arm64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz#447c5026700c01a993c7804eb8af5f6e9868c07f" + integrity sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg== + +"@img/sharp-libvips-darwin-x64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz#e0456f8f7c623f9dbfbdc77383caa72281d86062" + integrity sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ== + +"@img/sharp-libvips-linux-arm64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz#979b1c66c9a91f7ff2893556ef267f90ebe51704" + integrity sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA== + +"@img/sharp-libvips-linux-arm@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz#99f922d4e15216ec205dcb6891b721bfd2884197" + integrity sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g== + +"@img/sharp-libvips-linux-s390x@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz#f8a5eb1f374a082f72b3f45e2fb25b8118a8a5ce" + integrity sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA== + +"@img/sharp-libvips-linux-x64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz#d4c4619cdd157774906e15770ee119931c7ef5e0" + integrity sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw== + +"@img/sharp-libvips-linuxmusl-arm64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz#166778da0f48dd2bded1fa3033cee6b588f0d5d5" + integrity sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA== + +"@img/sharp-libvips-linuxmusl-x64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz#93794e4d7720b077fcad3e02982f2f1c246751ff" + integrity sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw== + +"@img/sharp-linux-arm64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz#edb0697e7a8279c9fc829a60fc35644c4839bb22" + integrity sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA== + optionalDependencies: + "@img/sharp-libvips-linux-arm64" "1.0.4" + +"@img/sharp-linux-arm@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz#422c1a352e7b5832842577dc51602bcd5b6f5eff" + integrity sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ== + optionalDependencies: + "@img/sharp-libvips-linux-arm" "1.0.5" + +"@img/sharp-linux-s390x@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz#f5c077926b48e97e4a04d004dfaf175972059667" + integrity sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q== + optionalDependencies: + "@img/sharp-libvips-linux-s390x" "1.0.4" + +"@img/sharp-linux-x64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz#d806e0afd71ae6775cc87f0da8f2d03a7c2209cb" + integrity sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA== + optionalDependencies: + "@img/sharp-libvips-linux-x64" "1.0.4" + +"@img/sharp-linuxmusl-arm64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz#252975b915894fb315af5deea174651e208d3d6b" + integrity sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-arm64" "1.0.4" + +"@img/sharp-linuxmusl-x64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz#3f4609ac5d8ef8ec7dadee80b560961a60fd4f48" + integrity sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-x64" "1.0.4" + +"@img/sharp-wasm32@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz#6f44f3283069d935bb5ca5813153572f3e6f61a1" + integrity sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg== + dependencies: + "@emnapi/runtime" "^1.2.0" + +"@img/sharp-win32-ia32@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz#1a0c839a40c5351e9885628c85f2e5dfd02b52a9" + integrity sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ== + +"@img/sharp-win32-x64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz#56f00962ff0c4e0eb93d34a047d29fa995e3e342" + integrity sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg== + "@inquirer/checkbox@^4.0.6": version "4.0.6" resolved "https://registry.yarnpkg.com/@inquirer/checkbox/-/checkbox-4.0.6.tgz#e71401a7e1900332f17ed68c172a89fe20225f49" @@ -3268,7 +3388,7 @@ color-name@^1.0.0, color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.6.0: +color-string@^1.6.0, color-string@^1.9.0: version "1.9.1" resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== @@ -3289,6 +3409,14 @@ color@^3.1.3: color-convert "^1.9.3" color-string "^1.6.0" +color@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== + dependencies: + color-convert "^2.0.1" + color-string "^1.9.0" + colorette@^2.0.20: version "2.0.20" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" @@ -3601,7 +3729,7 @@ detect-indent@^7.0.1: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-7.0.1.tgz#cbb060a12842b9c4d333f1cac4aa4da1bb66bc25" integrity sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g== -detect-libc@^2.0.0: +detect-libc@^2.0.0, detect-libc@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== @@ -6963,6 +7091,35 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== +sharp@0.33.5: + version "0.33.5" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.5.tgz#13e0e4130cc309d6a9497596715240b2ec0c594e" + integrity sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw== + dependencies: + color "^4.2.3" + detect-libc "^2.0.3" + semver "^7.6.3" + optionalDependencies: + "@img/sharp-darwin-arm64" "0.33.5" + "@img/sharp-darwin-x64" "0.33.5" + "@img/sharp-libvips-darwin-arm64" "1.0.4" + "@img/sharp-libvips-darwin-x64" "1.0.4" + "@img/sharp-libvips-linux-arm" "1.0.5" + "@img/sharp-libvips-linux-arm64" "1.0.4" + "@img/sharp-libvips-linux-s390x" "1.0.4" + "@img/sharp-libvips-linux-x64" "1.0.4" + "@img/sharp-libvips-linuxmusl-arm64" "1.0.4" + "@img/sharp-libvips-linuxmusl-x64" "1.0.4" + "@img/sharp-linux-arm" "0.33.5" + "@img/sharp-linux-arm64" "0.33.5" + "@img/sharp-linux-s390x" "0.33.5" + "@img/sharp-linux-x64" "0.33.5" + "@img/sharp-linuxmusl-arm64" "0.33.5" + "@img/sharp-linuxmusl-x64" "0.33.5" + "@img/sharp-wasm32" "0.33.5" + "@img/sharp-win32-ia32" "0.33.5" + "@img/sharp-win32-x64" "0.33.5" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -7538,7 +7695,7 @@ tslib@1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== -tslib@^2.0.1, tslib@^2.0.3, tslib@^2.6.2: +tslib@^2.0.1, tslib@^2.0.3, tslib@^2.4.0, tslib@^2.6.2: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== From d4a0124e5d61b23d2fbbab34e42fe1dac3d9933d Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 16 Jan 2025 16:42:05 +0100 Subject: [PATCH 2/5] refactor(upload): streamline file upload process and rename methods for clarity --- src/commands/upload-file.ts | 43 +++++------------ .../network/network-facade.service.ts | 4 +- src/services/network/upload.service.ts | 48 ++++++++++++++++++- src/types/network.types.ts | 2 +- src/webdav/handlers/PUT.handler.ts | 36 ++++---------- test/services/network/upload.service.test.ts | 6 +-- 6 files changed, 73 insertions(+), 66 deletions(-) diff --git a/src/commands/upload-file.ts b/src/commands/upload-file.ts index feb85ae2..31ecf2b4 100644 --- a/src/commands/upload-file.ts +++ b/src/commands/upload-file.ts @@ -83,37 +83,18 @@ export default class UploadFile extends Command { }); progressBar.start(100, 0); - const minimumMultipartThreshold = 100 * 1024 * 1024; - const useMultipart = stats.size > minimumMultipartThreshold; - const partSize = 30 * 1024 * 1024; - const parts = Math.ceil(stats.size / partSize); - - let uploadOperation: Promise< - [ - Promise<{ - fileId: string; - hash: Buffer; - }>, - AbortController, - ] - >; - - if (useMultipart) { - uploadOperation = networkFacade.uploadMultipartFromStream(user.bucket, user.mnemonic, stats.size, fileStream, { - parts, - progressCallback: (progress) => { - progressBar.update(progress * 0.99); - }, - }); - } else { - uploadOperation = networkFacade.uploadFromStream(user.bucket, user.mnemonic, stats.size, fileStream, { - progressCallback: (progress) => { - progressBar.update(progress * 0.99); - }, - }); - } - - const [uploadPromise, abortable] = await uploadOperation; + const progressCallback = (progress: number) => { + progressBar.update(progress * 0.99); + }; + + const [uploadPromise, abortable] = await UploadService.instance.uploadFileStream( + fileStream, + user.bucket, + user.mnemonic, + stats.size, + networkFacade, + progressCallback, + ); process.on('SIGINT', () => { abortable.abort('SIGINT received'); diff --git a/src/services/network/network-facade.service.ts b/src/services/network/network-facade.service.ts index b2f56277..061ad02f 100644 --- a/src/services/network/network-facade.service.ts +++ b/src/services/network/network-facade.service.ts @@ -165,7 +165,7 @@ export class NetworkFacade { }; const uploadFile: UploadFileFunction = async (url) => { - await this.uploadService.uploadFile(url, encryptionTransform, { + await this.uploadService.uploadFileToNetwork(url, encryptionTransform, { abortController: abortable, progressCallback: onProgress, }); @@ -244,7 +244,7 @@ export class NetworkFacade { const limitConcurrency = 6; const uploadPart = async (upload: UploadTask) => { - const { etag } = await this.uploadService.uploadFile(upload.urlToUpload, upload.contentToUpload, { + const { etag } = await this.uploadService.uploadFileToNetwork(upload.urlToUpload, upload.contentToUpload, { abortController: abortable, progressCallback: (loadedBytes: number) => { onProgress(upload.index, loadedBytes); diff --git a/src/services/network/upload.service.ts b/src/services/network/upload.service.ts index c1ce0fb1..8ef51e3e 100644 --- a/src/services/network/upload.service.ts +++ b/src/services/network/upload.service.ts @@ -1,11 +1,16 @@ import { Readable } from 'node:stream'; import axios from 'axios'; import { UploadOptions } from '../../types/network.types'; +import { NetworkFacade } from './network-facade.service'; export class UploadService { public static readonly instance: UploadService = new UploadService(); - async uploadFile(url: string, from: Readable | Buffer, options: UploadOptions): Promise<{ etag: string }> { + public uploadFileToNetwork = async ( + url: string, + from: Readable | Buffer, + options: UploadOptions, + ): Promise<{ etag: string }> => { const response = await axios.put(url, from, { signal: options.abortController?.signal, onUploadProgress: (progressEvent) => { @@ -20,5 +25,44 @@ export class UploadService { throw new Error('Missing Etag in response when uploading file'); } return { etag }; - } + }; + + public uploadFileStream = async ( + fileStream: Readable, + userBucket: string, + userMnemonic: string, + fileSize: number, + networkFacade: NetworkFacade, + progressCallback?: (progress: number) => void, + ) => { + const minimumMultipartThreshold = 100 * 1024 * 1024; + const useMultipart = fileSize > minimumMultipartThreshold; + const partSize = 30 * 1024 * 1024; + const parts = Math.ceil(fileSize / partSize); + + let uploadOperation: Promise< + [ + Promise<{ + fileId: string; + hash: Buffer; + }>, + AbortController, + ] + >; + + if (useMultipart) { + uploadOperation = networkFacade.uploadMultipartFromStream(userBucket, userMnemonic, fileSize, fileStream, { + parts, + progressCallback, + }); + } else { + uploadOperation = networkFacade.uploadFromStream(userBucket, userMnemonic, fileSize, fileStream, { + progressCallback, + }); + } + + const uploadFileOperation = await uploadOperation; + + return uploadFileOperation; + }; } diff --git a/src/types/network.types.ts b/src/types/network.types.ts index 72c3869d..5c98b226 100644 --- a/src/types/network.types.ts +++ b/src/types/network.types.ts @@ -6,7 +6,7 @@ export interface NetworkCredentials { export type DownloadProgressCallback = (downloadedBytes: number) => void; export type UploadProgressCallback = (uploadedBytes: number) => void; export interface NetworkOperationBaseOptions { - progressCallback: UploadProgressCallback; + progressCallback?: UploadProgressCallback; abortController?: AbortController; } diff --git a/src/webdav/handlers/PUT.handler.ts b/src/webdav/handlers/PUT.handler.ts index 8e5d43aa..ab666181 100644 --- a/src/webdav/handlers/PUT.handler.ts +++ b/src/webdav/handlers/PUT.handler.ts @@ -12,6 +12,7 @@ import { DriveFolderService } from '../../services/drive/drive-folder.service'; import { TrashService } from '../../services/drive/trash.service'; import { EncryptionVersion } from '@internxt/sdk/dist/drive/storage/types'; import { CLIUtils } from '../../utils/cli.utils'; +import { UploadService } from '../../services/network/upload.service'; export class PUTRequestHandler implements WebDavMethodHandler { constructor( @@ -71,37 +72,18 @@ export class PUTRequestHandler implements WebDavMethodHandler { const timer = CLIUtils.timer(); - const minimumMultipartThreshold = 100 * 1024 * 1024; - const useMultipart = contentLength > minimumMultipartThreshold; - const partSize = 30 * 1024 * 1024; - const parts = Math.ceil(contentLength / partSize); - - let uploadOperation: Promise< - [ - Promise<{ - fileId: string; - hash: Buffer; - }>, - AbortController, - ] - >; - const progressCallback = (progress: number) => { webdavLogger.info(`[PUT] Upload progress for file ${resource.name}: ${progress}%`); }; - if (useMultipart) { - uploadOperation = networkFacade.uploadMultipartFromStream(user.bucket, user.mnemonic, contentLength, req, { - parts, - progressCallback, - }); - } else { - uploadOperation = networkFacade.uploadFromStream(user.bucket, user.mnemonic, contentLength, req, { - progressCallback, - }); - } - - const [uploadPromise, abortable] = await uploadOperation; + const [uploadPromise, abortable] = await UploadService.instance.uploadFileStream( + req, + user.bucket, + user.mnemonic, + contentLength, + networkFacade, + progressCallback, + ); let uploaded = false; res.on('close', () => { diff --git a/test/services/network/upload.service.test.ts b/test/services/network/upload.service.test.ts index 4999dfd7..b72de065 100644 --- a/test/services/network/upload.service.test.ts +++ b/test/services/network/upload.service.test.ts @@ -28,7 +28,7 @@ describe('Upload Service', () => { nock('https://example.com').put('/upload').reply(200, '', {}); try { - await sut.uploadFile(url, data, options); + await sut.uploadFileToNetwork(url, data, options); } catch (error) { expect((error as Error).message).to.contain('Missing Etag'); } @@ -52,7 +52,7 @@ describe('Upload Service', () => { etag: 'test-etag', }); - const result = await sut.uploadFile(url, data, options); + const result = await sut.uploadFileToNetwork(url, data, options); expect(result.etag).to.be.equal('test-etag'); }); @@ -74,7 +74,7 @@ describe('Upload Service', () => { etag: 'test-etag', }); - await sut.uploadFile(url, data, options); + await sut.uploadFileToNetwork(url, data, options); expect(options.progressCallback).toHaveBeenCalledWith(file.length); }); From 3ef60d6ec14ef964aac2577d05bc25bbaa85fae5 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 16 Jan 2025 17:30:45 +0100 Subject: [PATCH 3/5] feat(upload): add thumbnail generation for uploaded images --- src/commands/upload-file.ts | 38 ++++++++++++++++-- src/services/drive/drive-file.service.ts | 5 +++ src/services/thumbnail.service.ts | 46 ++++++++++++++++++++++ src/utils/stream.utils.ts | 29 +++++++++++++- src/utils/thumbnail.utils.ts | 50 ++++++++++++++++++++++++ src/webdav/handlers/PUT.handler.ts | 36 ++++++++++++++++- 6 files changed, 198 insertions(+), 6 deletions(-) create mode 100644 src/services/thumbnail.service.ts create mode 100644 src/utils/thumbnail.utils.ts diff --git a/src/commands/upload-file.ts b/src/commands/upload-file.ts index 31ecf2b4..41ed1f6a 100644 --- a/src/commands/upload-file.ts +++ b/src/commands/upload-file.ts @@ -15,6 +15,10 @@ import { ErrorUtils } from '../utils/errors.utils'; import { MissingCredentialsError, NotValidDirectoryError, NotValidFolderUuidError } from '../types/command.types'; import { ValidationService } from '../services/validation.service'; import { EncryptionVersion } from '@internxt/sdk/dist/drive/storage/types'; +import { ThumbnailService } from '../services/thumbnail.service'; +import { BufferStream } from '../utils/stream.utils'; +import { isFileThumbnailable } from '../utils/thumbnail.utils'; +import { Readable } from 'node:stream'; export default class UploadFile extends Command { static readonly args = {}; @@ -52,6 +56,9 @@ export default class UploadFile extends Command { throw new Error('The file is empty. Uploading empty files is not allowed.'); } + const fileInfo = path.parse(filePath); + const fileType = fileInfo.ext.replaceAll('.', ''); + let destinationFolderUuid = await this.getDestinationFolderUuid(flags['destination'], nonInteractive); if (destinationFolderUuid.trim().length === 0) { // destinationFolderUuid is empty from flags&prompt, which means we should use RootFolderUuid @@ -75,7 +82,7 @@ export default class UploadFile extends Command { CLIUtils.done(); // 2. Upload file to the Network - const fileStream = createReadStream(filePath); + const readStream = createReadStream(filePath); const timer = CLIUtils.timer(); const progressBar = CLIUtils.progress({ format: 'Uploading file [{bar}] {percentage}%', @@ -83,6 +90,14 @@ export default class UploadFile extends Command { }); progressBar.start(100, 0); + let bufferStream: BufferStream | undefined; + let fileStream: Readable = readStream; + const isThumbnailable = isFileThumbnailable(fileType); + if (isThumbnailable) { + bufferStream = new BufferStream(); + fileStream = readStream.pipe(bufferStream); + } + const progressCallback = (progress: number) => { progressBar.update(progress * 0.99); }; @@ -104,10 +119,9 @@ export default class UploadFile extends Command { const uploadResult = await uploadPromise; // 3. Create the file in Drive - const fileInfo = path.parse(filePath); const createdDriveFile = await DriveFileService.instance.createFile({ plain_name: fileInfo.name, - type: fileInfo.ext.replaceAll('.', ''), + type: fileType, size: stats.size, folder_id: destinationFolderUuid, id: uploadResult.fileId, @@ -116,6 +130,24 @@ export default class UploadFile extends Command { name: '', }); + try { + if (isThumbnailable && bufferStream) { + const thumbnailBuffer = bufferStream.getBuffer(); + + if (thumbnailBuffer) { + await ThumbnailService.instance.uploadThumbnail( + thumbnailBuffer, + user.bucket, + user.mnemonic, + createdDriveFile.id, + networkFacade, + ); + } + } + } catch (error) { + ErrorUtils.report(this.error.bind(this), error, { command: this.id }); + } + progressBar.update(100); progressBar.stop(); diff --git a/src/services/drive/drive-file.service.ts b/src/services/drive/drive-file.service.ts index c4dec48b..1dfe225a 100644 --- a/src/services/drive/drive-file.service.ts +++ b/src/services/drive/drive-file.service.ts @@ -51,4 +51,9 @@ export class DriveFileService { const fileMetadata = await storageClient.getFileByPath(encodeURIComponent(path)); return DriveUtils.driveFileMetaToItem(fileMetadata); }; + + public createThumbnail = (payload: StorageTypes.ThumbnailEntry): Promise => { + const storageClient = SdkManager.instance.getStorage(false); + return storageClient.createThumbnailEntry(payload); + }; } diff --git a/src/services/thumbnail.service.ts b/src/services/thumbnail.service.ts new file mode 100644 index 00000000..749ab6de --- /dev/null +++ b/src/services/thumbnail.service.ts @@ -0,0 +1,46 @@ +import { Readable } from 'node:stream'; +import { DriveFileService } from './drive/drive-file.service'; +import { StorageTypes } from '@internxt/sdk/dist/drive'; +import { NetworkFacade } from './network/network-facade.service'; +import { UploadService } from './network/upload.service'; + +export class ThumbnailService { + public static readonly instance: ThumbnailService = new ThumbnailService(); + + public static readonly MaxWidth = 300; + public static readonly MaxHeight = 300; + public static readonly Quality = 80; + public static readonly Type = 'png'; + + public uploadThumbnail = async ( + fileContent: Buffer, + userBucket: string, + userMnemonic: string, + file_id: number, + networkFacade: NetworkFacade, + ): Promise => { + const size = fileContent.length; + const [thumbnailPromise] = await UploadService.instance.uploadFileStream( + Readable.from(fileContent), + userBucket, + userMnemonic, + size, + networkFacade, + () => {}, + ); + + const thumbnailUploadResult = await thumbnailPromise; + + const createdThumbnailFile = await DriveFileService.instance.createThumbnail({ + file_id: file_id, + max_width: ThumbnailService.MaxWidth, + max_height: ThumbnailService.MaxHeight, + type: ThumbnailService.Type, + size: size, + bucket_id: userBucket, + bucket_file: thumbnailUploadResult.fileId, + encrypt_version: StorageTypes.EncryptionVersion.Aes03, + }); + return createdThumbnailFile; + }; +} diff --git a/src/utils/stream.utils.ts b/src/utils/stream.utils.ts index 769d49ed..e1eeb719 100644 --- a/src/utils/stream.utils.ts +++ b/src/utils/stream.utils.ts @@ -1,5 +1,5 @@ import { ReadStream, WriteStream } from 'node:fs'; -import { Readable, Transform, TransformCallback } from 'node:stream'; +import { Readable, Transform, TransformCallback, TransformOptions } from 'node:stream'; export class StreamUtils { static readStreamToReadableStream(readStream: ReadStream): ReadableStream { @@ -130,3 +130,30 @@ export class ProgressTransform extends Transform { callback(null); } } + +export class BufferStream extends Transform { + public buffer: Buffer | null; + + constructor(opts?: TransformOptions) { + super(opts); + this.buffer = null; + } + + _transform(chunk: Buffer, _: BufferEncoding, callback: TransformCallback) { + const currentBuffer = this.buffer ?? Buffer.alloc(0); + this.buffer = Buffer.concat([currentBuffer, chunk]); + callback(null, chunk); + } + + _flush(callback: TransformCallback) { + callback(); + } + + reset() { + this.buffer = null; + } + + getBuffer(): Buffer | null { + return this.buffer; + } +} diff --git a/src/utils/thumbnail.utils.ts b/src/utils/thumbnail.utils.ts new file mode 100644 index 00000000..c99bec13 --- /dev/null +++ b/src/utils/thumbnail.utils.ts @@ -0,0 +1,50 @@ +import sharp from 'sharp'; + +export const ThumbnailConfig = { + MaxWidth: 300, + MaxHeight: 300, + Quality: 100, + Type: 'png', +} as const; + +export const getThumbnailFromImageBuffer = (buffer: Buffer): Promise => { + return sharp(buffer) + .resize({ + height: ThumbnailConfig.MaxHeight, + width: ThumbnailConfig.MaxWidth, + fit: 'inside', + }) + .png({ + quality: ThumbnailConfig.Quality, + }) + .toBuffer(); +}; + +type FileExtensionMap = Record; +const imageExtensions: FileExtensionMap = { + tiff: ['tif', 'tiff'], + bmp: ['bmp'], + heic: ['heic'], + jpg: ['jpg', 'jpeg'], + gif: ['gif'], + png: ['png'], + eps: ['eps'], + raw: ['raw', 'cr2', 'nef', 'orf', 'sr2'], + webp: ['webp'], +}; +const thumbnailableImageExtension: string[] = [ + ...imageExtensions['jpg'], + ...imageExtensions['png'], + ...imageExtensions['webp'], + ...imageExtensions['gif'], + ...imageExtensions['tiff'], +]; +const pdfExtensions: FileExtensionMap = { + pdf: ['pdf'], +}; +const thumbnailablePdfExtension: string[] = pdfExtensions['pdf']; +const thumbnailableExtension: string[] = [...thumbnailableImageExtension]; + +export const isFileThumbnailable = (fileType: string) => { + return fileType.trim().length > 0 && thumbnailableExtension.includes(fileType); +}; diff --git a/src/webdav/handlers/PUT.handler.ts b/src/webdav/handlers/PUT.handler.ts index ab666181..8a6cb503 100644 --- a/src/webdav/handlers/PUT.handler.ts +++ b/src/webdav/handlers/PUT.handler.ts @@ -13,6 +13,10 @@ import { TrashService } from '../../services/drive/trash.service'; import { EncryptionVersion } from '@internxt/sdk/dist/drive/storage/types'; import { CLIUtils } from '../../utils/cli.utils'; import { UploadService } from '../../services/network/upload.service'; +import { BufferStream } from '../../utils/stream.utils'; +import { Readable } from 'node:stream'; +import { isFileThumbnailable } from '../../utils/thumbnail.utils'; +import { ThumbnailService } from '../../services/thumbnail.service'; export class PUTRequestHandler implements WebDavMethodHandler { constructor( @@ -70,14 +74,24 @@ export class PUTRequestHandler implements WebDavMethodHandler { const { user } = await authService.getAuthDetails(); + const fileType = resource.path.ext.replace('.', ''); + const timer = CLIUtils.timer(); + let bufferStream: BufferStream | undefined; + let fileStream: Readable = req; + const isThumbnailable = isFileThumbnailable(fileType); + if (isThumbnailable) { + bufferStream = new BufferStream(); + fileStream = req.pipe(bufferStream); + } + const progressCallback = (progress: number) => { webdavLogger.info(`[PUT] Upload progress for file ${resource.name}: ${progress}%`); }; const [uploadPromise, abortable] = await UploadService.instance.uploadFileStream( - req, + fileStream, user.bucket, user.mnemonic, contentLength, @@ -100,7 +114,7 @@ export class PUTRequestHandler implements WebDavMethodHandler { const file = await DriveFileService.instance.createFile({ plain_name: resource.path.name, - type: resource.path.ext.replace('.', ''), + type: fileType, size: contentLength, folder_id: parentFolderItem.uuid, id: uploadResult.fileId, @@ -109,6 +123,24 @@ export class PUTRequestHandler implements WebDavMethodHandler { name: '', }); + try { + if (isThumbnailable && bufferStream) { + const thumbnailBuffer = bufferStream.getBuffer(); + + if (thumbnailBuffer) { + await ThumbnailService.instance.uploadThumbnail( + thumbnailBuffer, + user.bucket, + user.mnemonic, + file.id, + networkFacade, + ); + } + } + } catch (error) { + webdavLogger.info(`[PUT] ❌ File thumbnail upload failed ${(error as Error).message}`); + } + const uploadTime = timer.stop(); webdavLogger.info(`[PUT] ✅ File uploaded in ${uploadTime}ms to Internxt Drive`); From 8b65858c5ddc3c318ee2d29feb04fa1925ce5a1c Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 16 Jan 2025 17:43:18 +0100 Subject: [PATCH 4/5] chore(deps): update @types/node and lint-staged to latest versions --- package.json | 4 ++-- yarn.lock | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index cca8abb8..710a5bdd 100644 --- a/package.json +++ b/package.json @@ -76,13 +76,13 @@ "@types/cli-progress": "3.11.6", "@types/express": "5.0.0", "@types/mime-types": "2.1.4", - "@types/node": "22.10.6", + "@types/node": "22.10.7", "@types/range-parser": "1.2.7", "@vitest/coverage-istanbul": "2.1.8", "@vitest/spy": "2.1.8", "eslint": "9.18.0", "husky": "9.1.7", - "lint-staged": "15.3.0", + "lint-staged": "15.4.0", "nock": "13.5.6", "nodemon": "3.1.9", "oclif": "4.17.13", diff --git a/yarn.lock b/yarn.lock index 405473a7..b48659f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2442,10 +2442,10 @@ dependencies: undici-types "~6.20.0" -"@types/node@22.10.6": - version "22.10.6" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.6.tgz#5c6795e71635876039f853cbccd59f523d9e4239" - integrity sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ== +"@types/node@22.10.7": + version "22.10.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.7.tgz#14a1ca33fd0ebdd9d63593ed8d3fbc882a6d28d7" + integrity sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg== dependencies: undici-types "~6.20.0" @@ -5535,10 +5535,10 @@ lilconfig@^3.1.3, lilconfig@~3.1.3: resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4" integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== -lint-staged@15.3.0: - version "15.3.0" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.3.0.tgz#32a0b3f2f2b8825950bd3b9fb093e045353bdfa3" - integrity sha512-vHFahytLoF2enJklgtOtCtIjZrKD/LoxlaUusd5nh7dWv/dkKQJY74ndFSzxCdv7g0ueGg1ORgTSt4Y9LPZn9A== +lint-staged@15.4.0: + version "15.4.0" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.4.0.tgz#ea2d096c35452ba7854f31431bb5d195260c9474" + integrity sha512-UdODqEZiQimd7rCzZ2vqFuELRNUda3mdv7M93jhE4SmDiqAj/w/msvwKgagH23jv2iCPw6Q5m+ltX4VlHvp2LQ== dependencies: chalk "~5.4.1" commander "~12.1.0" From 9f0867df3977e19f625db48aea40118c2d5ca7e0 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 16 Jan 2025 19:32:04 +0100 Subject: [PATCH 5/5] feat(thumbnail): enhance thumbnail upload process with file type handling --- src/commands/upload-file.ts | 1 + src/services/thumbnail.service.ts | 71 ++++++++++++++++++------------ src/utils/thumbnail.utils.ts | 29 +++++------- src/webdav/handlers/PUT.handler.ts | 1 + 4 files changed, 57 insertions(+), 45 deletions(-) diff --git a/src/commands/upload-file.ts b/src/commands/upload-file.ts index 41ed1f6a..76f8dbe6 100644 --- a/src/commands/upload-file.ts +++ b/src/commands/upload-file.ts @@ -137,6 +137,7 @@ export default class UploadFile extends Command { if (thumbnailBuffer) { await ThumbnailService.instance.uploadThumbnail( thumbnailBuffer, + fileType, user.bucket, user.mnemonic, createdDriveFile.id, diff --git a/src/services/thumbnail.service.ts b/src/services/thumbnail.service.ts index 749ab6de..98350848 100644 --- a/src/services/thumbnail.service.ts +++ b/src/services/thumbnail.service.ts @@ -3,44 +3,61 @@ import { DriveFileService } from './drive/drive-file.service'; import { StorageTypes } from '@internxt/sdk/dist/drive'; import { NetworkFacade } from './network/network-facade.service'; import { UploadService } from './network/upload.service'; +import { isImageThumbnailable, ThumbnailConfig } from '../utils/thumbnail.utils'; +import sharp from 'sharp'; export class ThumbnailService { public static readonly instance: ThumbnailService = new ThumbnailService(); - public static readonly MaxWidth = 300; - public static readonly MaxHeight = 300; - public static readonly Quality = 80; - public static readonly Type = 'png'; - public uploadThumbnail = async ( fileContent: Buffer, + fileType: string, userBucket: string, userMnemonic: string, file_id: number, networkFacade: NetworkFacade, - ): Promise => { - const size = fileContent.length; - const [thumbnailPromise] = await UploadService.instance.uploadFileStream( - Readable.from(fileContent), - userBucket, - userMnemonic, - size, - networkFacade, - () => {}, - ); + ): Promise => { + let thumbnailBuffer: Buffer | undefined; + if (isImageThumbnailable(fileType)) { + thumbnailBuffer = await this.getThumbnailFromImageBuffer(fileContent); + } + if (thumbnailBuffer) { + const size = thumbnailBuffer.length; + const [thumbnailPromise] = await UploadService.instance.uploadFileStream( + Readable.from(thumbnailBuffer), + userBucket, + userMnemonic, + size, + networkFacade, + () => {}, + ); + + const thumbnailUploadResult = await thumbnailPromise; - const thumbnailUploadResult = await thumbnailPromise; + const createdThumbnailFile = await DriveFileService.instance.createThumbnail({ + file_id: file_id, + max_width: ThumbnailConfig.MaxWidth, + max_height: ThumbnailConfig.MaxHeight, + type: ThumbnailConfig.Type, + size: size, + bucket_id: userBucket, + bucket_file: thumbnailUploadResult.fileId, + encrypt_version: StorageTypes.EncryptionVersion.Aes03, + }); + return createdThumbnailFile; + } + }; - const createdThumbnailFile = await DriveFileService.instance.createThumbnail({ - file_id: file_id, - max_width: ThumbnailService.MaxWidth, - max_height: ThumbnailService.MaxHeight, - type: ThumbnailService.Type, - size: size, - bucket_id: userBucket, - bucket_file: thumbnailUploadResult.fileId, - encrypt_version: StorageTypes.EncryptionVersion.Aes03, - }); - return createdThumbnailFile; + private getThumbnailFromImageBuffer = (buffer: Buffer): Promise => { + return sharp(buffer) + .resize({ + height: ThumbnailConfig.MaxHeight, + width: ThumbnailConfig.MaxWidth, + fit: 'inside', + }) + .png({ + quality: ThumbnailConfig.Quality, + }) + .toBuffer(); }; } diff --git a/src/utils/thumbnail.utils.ts b/src/utils/thumbnail.utils.ts index c99bec13..a051537e 100644 --- a/src/utils/thumbnail.utils.ts +++ b/src/utils/thumbnail.utils.ts @@ -1,5 +1,3 @@ -import sharp from 'sharp'; - export const ThumbnailConfig = { MaxWidth: 300, MaxHeight: 300, @@ -7,19 +5,6 @@ export const ThumbnailConfig = { Type: 'png', } as const; -export const getThumbnailFromImageBuffer = (buffer: Buffer): Promise => { - return sharp(buffer) - .resize({ - height: ThumbnailConfig.MaxHeight, - width: ThumbnailConfig.MaxWidth, - fit: 'inside', - }) - .png({ - quality: ThumbnailConfig.Quality, - }) - .toBuffer(); -}; - type FileExtensionMap = Record; const imageExtensions: FileExtensionMap = { tiff: ['tif', 'tiff'], @@ -32,6 +17,9 @@ const imageExtensions: FileExtensionMap = { raw: ['raw', 'cr2', 'nef', 'orf', 'sr2'], webp: ['webp'], }; +const pdfExtensions: FileExtensionMap = { + pdf: ['pdf'], +}; const thumbnailableImageExtension: string[] = [ ...imageExtensions['jpg'], ...imageExtensions['png'], @@ -39,12 +27,17 @@ const thumbnailableImageExtension: string[] = [ ...imageExtensions['gif'], ...imageExtensions['tiff'], ]; -const pdfExtensions: FileExtensionMap = { - pdf: ['pdf'], -}; const thumbnailablePdfExtension: string[] = pdfExtensions['pdf']; const thumbnailableExtension: string[] = [...thumbnailableImageExtension]; export const isFileThumbnailable = (fileType: string) => { return fileType.trim().length > 0 && thumbnailableExtension.includes(fileType); }; + +export const isPDFThumbnailable = (fileType: string) => { + return fileType.trim().length > 0 && thumbnailablePdfExtension.includes(fileType); +}; + +export const isImageThumbnailable = (fileType: string) => { + return fileType.trim().length > 0 && thumbnailableImageExtension.includes(fileType); +}; diff --git a/src/webdav/handlers/PUT.handler.ts b/src/webdav/handlers/PUT.handler.ts index 8a6cb503..fd1cb849 100644 --- a/src/webdav/handlers/PUT.handler.ts +++ b/src/webdav/handlers/PUT.handler.ts @@ -130,6 +130,7 @@ export class PUTRequestHandler implements WebDavMethodHandler { if (thumbnailBuffer) { await ThumbnailService.instance.uploadThumbnail( thumbnailBuffer, + fileType, user.bucket, user.mnemonic, file.id,