From 22e30d20fb08f13715c9d6840f9526b6cdc9cd0d Mon Sep 17 00:00:00 2001 From: Anton Shepilov Date: Mon, 7 Apr 2025 14:25:04 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Support=20tool,=20zips=20folder=20a?= =?UTF-8?q?nd=20puts=20it=20to=20users=20home=20directory=20(#870)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨Support tool, zips folder and puts it in users home directory --- tdrive/backend/node/bin/config/cli.json | 261 ++++++++++++++++++ tdrive/backend/node/package-lock.json | 36 +++ tdrive/backend/node/package.json | 2 + .../node/src/cli/cmds/dev_cmds/zip_folder.ts | 141 ++++++++++ .../src/services/documents/services/index.ts | 2 +- .../documents/web/controllers/documents.ts | 2 +- .../node/src/services/files/services/index.ts | 6 +- .../services/files/web/controllers/files.ts | 2 +- tdrive/backend/node/yarn.lock | 24 ++ 9 files changed, 470 insertions(+), 6 deletions(-) create mode 100644 tdrive/backend/node/bin/config/cli.json create mode 100644 tdrive/backend/node/src/cli/cmds/dev_cmds/zip_folder.ts diff --git a/tdrive/backend/node/bin/config/cli.json b/tdrive/backend/node/bin/config/cli.json new file mode 100644 index 00000000..438f4772 --- /dev/null +++ b/tdrive/backend/node/bin/config/cli.json @@ -0,0 +1,261 @@ +{ + "general": { + "help_url": false, + "pricing_plan_url": "", + "app_download_url": "https://twake.app/download", + "mobile": { + "mobile_redirect": "mobile.twake.app", + "mobile_appstore": "https://apps.apple.com/fr/app/tdrive/id1588764852?l=en", + "mobile_googleplay": "https://play.google.com/store/apps/details?id=com.tdrive.tdrive&gl=FR" + }, + "accounts": { + "type": "internal", + "internal": { + "disable_account_creation": false + }, + "remote": { + "authority": "http://auth.example.com/", + "client_id": "tdriveweb", + "client_secret": "", + "issuer": "", + "audience": "", + "redirect_uris": [""], + "account_management_url": "http://web.tdrive-console.local/profile?company-code={company_id}", + "collaborators_management_url": "http://web.tdrive-console.local/compaies/{company_id}/users?company-code={company_id}", + "company_management_url": "http://web.tdrive-console.local/companies?company-code={company_id}" + } + } + }, + "console": { + "type": "internal" + }, + "sentry": { + "dsn": "" + }, + "logger": { + "level": "debug" + }, + "admin": { + "endpointSecret": "" + }, + "diagnostics": { + "skipKeys": [], + "probeSecret": "", + "statsLogPeriodMs": 120000, + "statsFullStatsLogPeriodMs": 600000 + }, + "tracker": { + "type": "segment", + "segment": { + "key": "" + } + }, + "webserver": { + "host": "localhost", + "port": 4000, + "logger": { + "level": "info" + }, + "cors": { + "origin": "*" + }, + "static": { + "root": "./public" + } + }, + "auth": { + "jwt": { + "secret": "supersecret", + "expiration": 3600, + "refresh_expiration": 2592000 + } + }, + "database": { + "secret": "", + "type": "mongodb", + "encryption": "aes-256-cbc", + "mongodb": { + "uri": "mongodb://localhost:27017", + "database": "tdrive" + } + }, + "message-queue": { + "type": "local", + "amqp": { + "urls": ["amqp://guest:guest@rabbitmq:5672"] + } + }, + "search": { + "type": "mongodb", + "elasticsearch": { + "endpoint": "http://elasticsearch:9200" + } + }, + "push": { + "type": false, + "fcm": { + "endpoint": "https://fcm.googleapis.com/fcm/send", + "key": "" + } + }, + "storage": { + "secret": "", + "iv": "1234abcd00000000", + "strategy": "local", + "type": "local", + "S3": { + "endPoint": "play.min.io", + "port": 9000, + "useSSL": false, + "accessKey": "ABCD", + "secretKey": "x1yz", + "disableRemove": false + }, + "local": { + "path": "/Users/shepilov/dev/linagora/TDrive/tdrive/docker-data/" + } + }, + "email-pusher": { + "endpoint": "https://api.smtp2go.com/v3/email/send", + "api_key": "secret", + "sender": "noreply@twake.app", + "debug": true + }, + "drive": { + "featureSharedDrive": true, + "featureDisplayEmail": true, + "featureUserQuota": false, + "featureManageAccess": true, + "defaultCompany": "00000000-0000-4000-0000-000000000000", + "defaultUserQuota": 200000000, + "featureAntivirus": false + }, + "av": { + "host": "av", + "port": 3310, + "debugMode": false, + "timeout": 2000, + "maxFileSize": 26214400 + }, + "applications": { + "grid": [ + { + "name": "Chat", + "logo": "/public/img/grid/twake.svg", + "url": "https://web.twake.app/" + }, + { + "name": "Mail", + "logo": "/public/img/grid/mail.svg", + "url": "https://tmail.linagora.com/" + }, + { + "name": "Drive", + "logo": "/public/img/grid/drive.svg", + "url": "https://tdrive.qa.lin-saas.com/" + }, + { + "name": "Calendar", + "logo": "/public/img/grid/calendar.svg", + "url": "https://openpaas.linagora.com/calendar/" + }, + { + "name": "Contacts", + "logo": "/public/img/grid/contacts.svg", + "url": "https://openpaas.linagora.com/contacts" + }, + { + "name": "Visio", + "logo": "/public/img/grid/visio.svg", + "url": "https://jitsi.linagora.com/" + } + ], + "plugins": [ + { + "api": { + "private_key": "" + }, + "display": { + "tdrive": { + "files": { + "editor": { + "edition_url": "http://localhost:5000/plugins/onlyoffice/", + "empty_files": [ + { + "filename": "Untitled.docx", + "name": "ONLYOFFICE Word Document", + "url": "/plugins/onlyoffice/assets/empty.docx" + }, + { + "filename": "Untitled.xlsx", + "name": "ONLYOFFICE Excel Document", + "url": "/plugins/onlyoffice/assets/empty.xlsx" + }, + { + "filename": "Untitled.pptx", + "name": "ONLYOFFICE PowerPoint Document", + "url": "/plugins/onlyoffice/assets/empty.pptx" + } + ], + "extensions": [ + "xlsx", + "pptx", + "docx", + "xls", + "ppt", + "doc", + "odt", + "ods", + "odp", + "txt", + "html", + "csv" + ], + "preview_url": "http://localhost:5000/plugins/onlyoffice/?preview=1" + } + }, + "version": 1 + } + }, + "external_prefix": "/plugins/onlyoffice/", + "id": "tdrive_onlyoffice", + "identity": { + "categories": [], + "code": "only_office", + "compatibility": ["tdrive"], + "description": null, + "icon": "/plugins/onlyoffice/assets/logo.png", + "name": "Only Office", + "website": "http://twake.app/" + }, + "internal_domain": "http://localhost:5000/" + } + ] + }, + "services": [ + "admin", + "auth", + "diagnostics", + "push", + "storage", + "webserver", + "database", + "cron", + "search", + "message-queue", + "tracker", + "general", + "user", + "files", + "workspaces", + "console", + "counter", + "statistics", + "cron", + "email-pusher", + "documents", + "applications", + "applications-api", + "tags" + ] +} diff --git a/tdrive/backend/node/package-lock.json b/tdrive/backend/node/package-lock.json index 2301f9c0..8baff1ad 100644 --- a/tdrive/backend/node/package-lock.json +++ b/tdrive/backend/node/package-lock.json @@ -36,6 +36,7 @@ "bcrypt": "^5.0.1", "clamscan": "^2.4.0", "class-transformer": "^0.3.1", + "cli-interact": "^0.1.9", "cli-table": "^0.3.6", "config": "^3.3.2", "deep-object-diff": "^1.1.0", @@ -96,6 +97,7 @@ "@types/analytics-node": "^3.1.5", "@types/archiver": "^6.0.3", "@types/bcrypt": "^5.0.0", + "@types/cli-interact": "^0.1.4", "@types/cli-table": "^0.3.0", "@types/config": "0.0.36", "@types/eslint": "^7.2.3", @@ -2637,6 +2639,15 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/cli-interact": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@types/cli-interact/-/cli-interact-0.1.4.tgz", + "integrity": "sha512-ApnXGFz+j6Yb6v6UvD3c9eEMQG+4YARy/+ZkwQIMBW+HTuwtAw6QknlE2rdkVHuPdt+dn4WsArQjS1K/LT6L/w==", + "dev": true, + "dependencies": { + "@types/readline-sync": "*" + } + }, "node_modules/@types/cli-table": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@types/cli-table/-/cli-table-0.3.4.tgz", @@ -2943,6 +2954,12 @@ "@types/node": "*" } }, + "node_modules/@types/readline-sync": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@types/readline-sync/-/readline-sync-1.4.8.tgz", + "integrity": "sha512-BL7xOf0yKLA6baAX6MMOnYkoflUyj/c7y3pqMRfU0va7XlwHAOTOIo4x55P/qLfMsuaYdJJKubToLqRVmRtRZA==", + "dev": true + }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -4560,6 +4577,17 @@ "node": ">=8" } }, + "node_modules/cli-interact": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/cli-interact/-/cli-interact-0.1.9.tgz", + "integrity": "sha512-QHrR2djQOfT1WfHSj9DidBGp+ImgV3t9dTP4cPWTnt9fnOcZqBzCoqd2RysWSIgUXuer5WpmKuT1jqjvwoxdCQ==", + "dependencies": { + "readline-sync": "~0.4.9" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/cli-spinners": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", @@ -10957,6 +10985,14 @@ "node": ">=8.10.0" } }, + "node_modules/readline-sync": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-0.4.10.tgz", + "integrity": "sha512-q/RyXCZ+49a7octyBMENisHi11JVcnNORLDKEdKEzUnBBiszElOtdCY+wJKPCnwHuh/8+yfDoptGL7D79yUOLw==", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/real-require": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", diff --git a/tdrive/backend/node/package.json b/tdrive/backend/node/package.json index b0e5ddec..5d312714 100644 --- a/tdrive/backend/node/package.json +++ b/tdrive/backend/node/package.json @@ -63,6 +63,7 @@ "@types/analytics-node": "^3.1.5", "@types/archiver": "^6.0.3", "@types/bcrypt": "^5.0.0", + "@types/cli-interact": "^0.1.4", "@types/cli-table": "^0.3.0", "@types/config": "0.0.36", "@types/eslint": "^7.2.3", @@ -133,6 +134,7 @@ "bcrypt": "^5.0.1", "clamscan": "^2.4.0", "class-transformer": "^0.3.1", + "cli-interact": "^0.1.9", "cli-table": "^0.3.6", "config": "^3.3.2", "deep-object-diff": "^1.1.0", diff --git a/tdrive/backend/node/src/cli/cmds/dev_cmds/zip_folder.ts b/tdrive/backend/node/src/cli/cmds/dev_cmds/zip_folder.ts new file mode 100644 index 00000000..a59c99db --- /dev/null +++ b/tdrive/backend/node/src/cli/cmds/dev_cmds/zip_folder.ts @@ -0,0 +1,141 @@ +import yargs from "yargs"; + +import runWithPlatform from "../../lib/run-with-platform"; +import gr from "../../../services/global-resolver"; +import { messageQueueLogger, platformLogger } from "../../../core/platform/framework"; +import User, { TYPE as UserTYPE } from "../../../services/user/entities/user"; +import config from "config"; +import globalResolver from "../../../services/global-resolver"; +import fs from "fs"; +import { CompanyExecutionContext, DriveItemDetails } from "../../../services/documents/types"; +import { randomUUID } from "crypto"; +import { getYesNo } from "cli-interact"; + +type ZipCommandArgs = { + verbose: boolean; + user: string; + folder: string; +}; + +/** + * + */ +export default { + command: "zip", + describe: "Zips specified folder and puts it to users root directory", + builder: { + user: { + alias: "u", + demandOption: true, + type: "string", + describe: "id or login or email of the user.", + }, + folder: { + alias: "f", + demandOption: true, + type: "string", + describe: "folder identifier to zip", + }, + }, + handler: async (argv: ZipCommandArgs) => { + if (!argv.verbose) { + platformLogger.level = "warn"; + messageQueueLogger.level = "warn"; + } + await runWithPlatform("zip", async ({ spinner: _spinner, platform: _platform }) => { + //create drive execution context for a specified user + const context = await getContext(argv); + //get folder to zip, and make all necessary checks + const toZip = await gr.services.documents.documents.get(argv.folder, {}, context); + //zip folder and pipe it to new file in storage + const tmpFilePath = `/tmp/${argv.folder}-${randomUUID()}.zip`; + await zipFolder(toZip, tmpFilePath, context); + _spinner.info(`Folder successfully zipped to "${tmpFilePath}"`); + //create a new document with this file in users root directory + if (getYesNo("Do you want to upload file?")) { + await saveFile(tmpFilePath, `${toZip.item.name}.zip`, context); + _spinner.info(`"${toZip.item.name}.zip" saved to users home directory`); + fs.rmSync(tmpFilePath); + _spinner.info("Tmp files cleaned"); + } + return 0; + }); + }, +} as yargs.CommandModule; + +async function saveFile(path: string, filename: string, context: CompanyExecutionContext) { + const file = await gr.services.files.save( + null, + fs.createReadStream(path), + { + totalChunks: 1, + totalSize: fs.statSync(path).size, + chunkNumber: 1, + filename: filename, + type: "application/zip", + waitForThumbnail: false, + ignoreThumbnails: true, + }, + context, + ); + + const driveItem = await gr.services.documents.documents.create( + file, + { + name: filename, + parent_id: `user_${context.user.id}`, + is_directory: false, + }, + { + file_metadata: { + source: "internal", + external_id: file.id, + name: filename, + mime: "application/zip", + size: file.upload_data.size, + }, + }, + context, + ); + console.log(driveItem); +} + +async function getContext(argv: ZipCommandArgs): Promise { + const user = await findUser(argv.user); + if (!user) { + throw new Error(`Error, unknown user ${JSON.stringify(argv.user)}`); + } + //create drive execution context for a specified user + const company = config.get("drive.defaultCompany"); + if (!company) { + throw new Error(`Error, unknown company in configuration ${company}`); + } + return { user, company: { id: company } }; +} + +async function findUser(identifier: string): Promise { + const userRepo = await gr.database.getRepository(UserTYPE, User); + let user = await userRepo.findOne({ id: identifier }); + if (!user) user = await userRepo.findOne({ email_canonical: identifier }); + if (!user) user = await userRepo.findOne({ username_canonical: identifier }); + return user; +} + +async function zipFolder( + toZip: DriveItemDetails, + outputPath: string, + context: CompanyExecutionContext, +) { + return new Promise(async (resolve, reject) => { + return await globalResolver.services.documents.documents.download( + toZip.item.id, + null, + archive => { + archive.pipe(fs.createWriteStream(outputPath)); + archive.on("finish", resolve); + archive.on("error", reject); + }, + context, + ); + }); +} diff --git a/tdrive/backend/node/src/services/documents/services/index.ts b/tdrive/backend/node/src/services/documents/services/index.ts index 29d39304..caa42cc5 100644 --- a/tdrive/backend/node/src/services/documents/services/index.ts +++ b/tdrive/backend/node/src/services/documents/services/index.ts @@ -1405,7 +1405,7 @@ export class DocumentsService { if (file) { const fileEntity = await globalResolver.services.files.save( null, - file, + file.file, { ...options, filename: options.filename ?? driveFile.name, diff --git a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts index 5f9b9830..e382bf78 100644 --- a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts +++ b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts @@ -67,7 +67,7 @@ export class DocumentsController { ignoreThumbnails: true, }; - createdFile = await globalResolver.services.files.save(null, file, options, context); + createdFile = await globalResolver.services.files.save(null, file?.file, options, context); } const { item, version } = request.body; diff --git a/tdrive/backend/node/src/services/files/services/index.ts b/tdrive/backend/node/src/services/files/services/index.ts index b5ac6f64..59a938c2 100644 --- a/tdrive/backend/node/src/services/files/services/index.ts +++ b/tdrive/backend/node/src/services/files/services/index.ts @@ -73,7 +73,7 @@ export class FileServiceImpl { async save( id: string, - file: MultipartFile, + file: Readable, options: UploadOptions, context: CompanyExecutionContext, ): Promise { @@ -136,10 +136,10 @@ export class FileServiceImpl { } let totalUploadedSize = 0; - file.file.on("data", function (chunk) { + file.on("data", function (chunk) { totalUploadedSize += chunk.length; }); - await gr.platformServices.storage.write(getFilePath(entity), file.file, { + await gr.platformServices.storage.write(getFilePath(entity), file, { chunkNumber: options.chunkNumber, encryptionAlgo: this.algorithm, encryptionKey: entity.encryption_key, diff --git a/tdrive/backend/node/src/services/files/web/controllers/files.ts b/tdrive/backend/node/src/services/files/web/controllers/files.ts index e3570bcf..de283b9a 100644 --- a/tdrive/backend/node/src/services/files/web/controllers/files.ts +++ b/tdrive/backend/node/src/services/files/web/controllers/files.ts @@ -34,7 +34,7 @@ export class FileController { }; const id = request.params.id; - const result = await gr.services.files.save(id, file, options, context); + const result = await gr.services.files.save(id, file?.file, options, context); return { resource: result.getPublicObject(), diff --git a/tdrive/backend/node/yarn.lock b/tdrive/backend/node/yarn.lock index fd9b0dff..00d29616 100644 --- a/tdrive/backend/node/yarn.lock +++ b/tdrive/backend/node/yarn.lock @@ -1170,6 +1170,13 @@ "@types/node" "*" "@types/responselike" "^1.0.0" +"@types/cli-interact@^0.1.4": + version "0.1.4" + resolved "https://registry.npmjs.org/@types/cli-interact/-/cli-interact-0.1.4.tgz" + integrity sha512-ApnXGFz+j6Yb6v6UvD3c9eEMQG+4YARy/+ZkwQIMBW+HTuwtAw6QknlE2rdkVHuPdt+dn4WsArQjS1K/LT6L/w== + dependencies: + "@types/readline-sync" "*" + "@types/cli-table@^0.3.0": version "0.3.4" resolved "https://registry.npmjs.org/@types/cli-table/-/cli-table-0.3.4.tgz" @@ -1440,6 +1447,11 @@ dependencies: "@types/node" "*" +"@types/readline-sync@*": + version "1.4.8" + resolved "https://registry.npmjs.org/@types/readline-sync/-/readline-sync-1.4.8.tgz" + integrity sha512-BL7xOf0yKLA6baAX6MMOnYkoflUyj/c7y3pqMRfU0va7XlwHAOTOIo4x55P/qLfMsuaYdJJKubToLqRVmRtRZA== + "@types/responselike@^1.0.0": version "1.0.3" resolved "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz" @@ -2380,6 +2392,13 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" +cli-interact@^0.1.9: + version "0.1.9" + resolved "https://registry.npmjs.org/cli-interact/-/cli-interact-0.1.9.tgz" + integrity sha512-QHrR2djQOfT1WfHSj9DidBGp+ImgV3t9dTP4cPWTnt9fnOcZqBzCoqd2RysWSIgUXuer5WpmKuT1jqjvwoxdCQ== + dependencies: + readline-sync "~0.4.9" + cli-spinners@^2.5.0: version "2.9.2" resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz" @@ -6248,6 +6267,11 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +readline-sync@~0.4.9: + version "0.4.10" + resolved "https://registry.npmjs.org/readline-sync/-/readline-sync-0.4.10.tgz" + integrity sha512-q/RyXCZ+49a7octyBMENisHi11JVcnNORLDKEdKEzUnBBiszElOtdCY+wJKPCnwHuh/8+yfDoptGL7D79yUOLw== + real-require@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz"