From 8ca7c15861fb4b6cd5fad4d0ff6e9b0586d4bc9f Mon Sep 17 00:00:00 2001 From: MontaGhanmy Date: Tue, 12 Nov 2024 17:12:04 +0100 Subject: [PATCH] feat: added an option to trigger a manual rescan --- .../src/services/documents/services/index.ts | 67 +++++++++++++++++++ .../documents/web/controllers/documents.ts | 29 ++++++++ .../node/src/services/documents/web/routes.ts | 7 ++ tdrive/frontend/public/locales/en.json | 1 + tdrive/frontend/public/locales/fr.json | 1 + tdrive/frontend/public/locales/ru.json | 1 + tdrive/frontend/public/locales/vi.json | 1 + .../features/drive/api-client/api-client.ts | 7 ++ .../drive/hooks/use-drive-actions.tsx | 49 ++++++++++++-- .../views/client/body/drive/context-menu.tsx | 23 +++++-- .../body/drive/documents/document-row.tsx | 3 +- 11 files changed, 179 insertions(+), 10 deletions(-) diff --git a/tdrive/backend/node/src/services/documents/services/index.ts b/tdrive/backend/node/src/services/documents/services/index.ts index a2c20828..67ddb9d6 100644 --- a/tdrive/backend/node/src/services/documents/services/index.ts +++ b/tdrive/backend/node/src/services/documents/services/index.ts @@ -1028,6 +1028,73 @@ export class DocumentsService { } }; + /** + * Triggers an AV Rescan for the document. + * + * @param {string} id - the Drive item id to rescan. + * @param {DriveExecutionContext} context - the company execution context + * @returns {Promise} - the DriveFile after the rescan has been triggered + */ + reScan = async (id: string, context: DriveExecutionContext): Promise => { + if (!context) { + this.logger.error("invalid execution context"); + return null; + } + + try { + const hasAccess = await checkAccess(id, null, "write", this.repository, context); + if (!hasAccess) { + this.logger.error("user does not have access drive item ", id); + throw Error("user does not have access to this item"); + } + + const item = await this.repository.findOne( + { + id, + company_id: context.company.id, + }, + {}, + context, + ); + + if (!item) { + throw Error("Drive item not found"); + } + + if (item.is_directory) { + throw Error("cannot create version for a directory"); + } + + // If AV feature is enabled, scan the file + if (globalResolver.services.av?.avEnabled) { + try { + item.av_status = await globalResolver.services.av.scanDocument( + item, + item.last_version_cache, + async (av_status: AVStatus) => { + // Update the AV status of the file + await this.handleAVStatusUpdate(item, av_status, context); + }, + context, + ); + await this.repository.save(item); + if (item.av_status === "skipped") { + // Notify the user that the document has been skipped + await this.notifyAVScanAlert(item, context); + } + } catch (error) { + this.logger.error( + `Error scanning file ${item.last_version_cache.file_metadata.external_id}`, + ); + } + } + return item; + } catch (error) { + logger.error({ error: `${error}` }, "Failed to create Drive item version"); + CrudException.throwMe(error, new CrudException("Failed to create Drive item version", 500)); + } + }; + /** * If not already in an editing session, uses the `editing_session_key` of the * `DriveFile` entity to store a unique new value to expect an update later 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 348971fa..a3be4022 100644 --- a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts +++ b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts @@ -335,6 +335,35 @@ export class DocumentsController { } }; + /** + * Triggers an AV Rescan for the document. + * + * @param {FastifyRequest} request + * @returns {Promise} + */ + reScan = async ( + request: FastifyRequest<{ + Params: ItemRequestParams; + Body: Partial; + Querystring: { public_token?: string }; + }>, + ): Promise => { + try { + const context = getDriveExecutionContext(request); + const { id } = request.params; + + if (!id) throw new CrudException("Missing id", 400); + + return await globalResolver.services.documents.documents.reScan(id, context); + } catch (error) { + logger.error({ error: `${error}` }, "Failed to trigger AV rescan for Drive item"); + CrudException.throwMe( + error, + new CrudException("Failed to trigger AV rescan for Drive item", 500), + ); + } + }; + /** * Begin an editing session if none exists, or return the existing one * @returns The `editing_session_key` that was either set or already was there diff --git a/tdrive/backend/node/src/services/documents/web/routes.ts b/tdrive/backend/node/src/services/documents/web/routes.ts index 0b58856f..2f91cfc9 100644 --- a/tdrive/backend/node/src/services/documents/web/routes.ts +++ b/tdrive/backend/node/src/services/documents/web/routes.ts @@ -82,6 +82,13 @@ const routes: FastifyPluginCallback = (fastify: FastifyInstance, _options, next) handler: documentsController.beginEditing.bind(documentsController), }); + fastify.route({ + method: "POST", + url: `${serviceUrl}/:id/rescan`, + preValidation: [fastify.authenticateOptional], + handler: documentsController.reScan.bind(documentsController), + }); + fastify.route({ method: "GET", url: editingSessionBase, //TODO NONONO check authenticate*Optional* diff --git a/tdrive/frontend/public/locales/en.json b/tdrive/frontend/public/locales/en.json index dd7b17d5..4858ad10 100644 --- a/tdrive/frontend/public/locales/en.json +++ b/tdrive/frontend/public/locales/en.json @@ -80,6 +80,7 @@ "components.item_context_menu.move.modal_header": "Move", "components.item_context_menu.move_multiple": "Move selected items", "components.item_context_menu.move_multiple.modal_header": "Move selected items", + "components.item_context_menu.rescan_document": "Re-scan for viruses", "components.item_context_menu.move_to_trash": "Delete", "components.item_context_menu.open_new_window": "Open in new window", "components.item_context_menu.preview": "Preview", diff --git a/tdrive/frontend/public/locales/fr.json b/tdrive/frontend/public/locales/fr.json index 82bda248..8f8b350f 100644 --- a/tdrive/frontend/public/locales/fr.json +++ b/tdrive/frontend/public/locales/fr.json @@ -80,6 +80,7 @@ "components.item_context_menu.move.modal_header": "Déplacer", "components.item_context_menu.move_multiple": "Déplacer", "components.item_context_menu.move_multiple.modal_header": "Déplacer les éléments sélectionnés", + "components.item_context_menu.rescan_document": "Re-scanner pour les virus", "components.item_context_menu.move_to_trash": "Supprimer", "components.item_context_menu.open_new_window": "Ouvrir dans une nouvelle fenêtre", "components.item_context_menu.preview": "Aperçu", diff --git a/tdrive/frontend/public/locales/ru.json b/tdrive/frontend/public/locales/ru.json index faa07d8e..061ed3b0 100644 --- a/tdrive/frontend/public/locales/ru.json +++ b/tdrive/frontend/public/locales/ru.json @@ -80,6 +80,7 @@ "components.item_context_menu.move.modal_header": "Переместить", "components.item_context_menu.move_multiple": "Переместить все", "components.item_context_menu.move_multiple.modal_header": "Переместить выбранные елементы", + "components.item_context_menu.rescan_document": "Повторное сканирование на вирусы", "components.item_context_menu.move_to_trash": "Удалить", "components.item_context_menu.open_new_window": "Открыть в новом окне", "components.item_context_menu.preview": "Просмотр", diff --git a/tdrive/frontend/public/locales/vi.json b/tdrive/frontend/public/locales/vi.json index 8e4813ab..26078462 100644 --- a/tdrive/frontend/public/locales/vi.json +++ b/tdrive/frontend/public/locales/vi.json @@ -77,6 +77,7 @@ "components.item_context_menu.move.modal_header": "Di chuyển", "components.item_context_menu.move_multiple": "Di chuyển các mục đã chọn", "components.item_context_menu.move_multiple.modal_header": "Di chuyển các mục đã chọn", + "components.item_context_menu.rescan_document": "Quét lại để tìm virus", "components.item_context_menu.move_to_trash": "Xóa", "components.item_context_menu.open_new_window": "Mở trong cửa sổ mới", "components.item_context_menu.preview": "Xem trước", diff --git a/tdrive/frontend/src/app/features/drive/api-client/api-client.ts b/tdrive/frontend/src/app/features/drive/api-client/api-client.ts index b82244c2..1e8428c3 100644 --- a/tdrive/frontend/src/app/features/drive/api-client/api-client.ts +++ b/tdrive/frontend/src/app/features/drive/api-client/api-client.ts @@ -135,6 +135,13 @@ export class DriveApiClient { ); } + static async reScan(companyId: string, id: string) { + return await Api.post( + `/internal/services/documents/v1/companies/${companyId}/item/${id}/rescan${appendTdriveToken()}`, + {}, + ); + } + static getDownloadUrl(companyId: string, id: string, versionId?: string) { if (versionId) return Api.route(`/internal/services/documents/v1/companies/${companyId}/item/${id}/download?version_id=${versionId}`); diff --git a/tdrive/frontend/src/app/features/drive/hooks/use-drive-actions.tsx b/tdrive/frontend/src/app/features/drive/hooks/use-drive-actions.tsx index 337187f0..8dbb63f3 100644 --- a/tdrive/frontend/src/app/features/drive/hooks/use-drive-actions.tsx +++ b/tdrive/frontend/src/app/features/drive/hooks/use-drive-actions.tsx @@ -3,7 +3,12 @@ import useRouterCompany from '@features/router/hooks/use-router-company'; import { useCallback } from 'react'; import { useRecoilValue, useRecoilCallback, useRecoilState } from 'recoil'; import { DriveApiClient } from '../api-client/api-client'; -import { DriveItemAtom, DriveItemChildrenAtom, DriveItemPagination, DriveItemSort } from '../state/store'; +import { + DriveItemAtom, + DriveItemChildrenAtom, + DriveItemPagination, + DriveItemSort, +} from '../state/store'; import { BrowseFilter, DriveItem, DriveItemVersion } from '../types'; import { SharedWithMeFilterState } from '../state/shared-with-me-filter'; import Languages from 'features/global/services/languages-service'; @@ -35,7 +40,13 @@ export const useDriveActions = (inPublicSharing?: boolean) => { set(DriveItemPagination, pagination); } try { - const details = await DriveApiClient.browse(companyId, parentId, filter, sortItem, pagination); + const details = await DriveApiClient.browse( + companyId, + parentId, + filter, + sortItem, + pagination, + ); set(DriveItemChildrenAtom(parentId), details.children); set(DriveItemAtom(parentId), details); for (const child of details.children) { @@ -142,7 +153,12 @@ export const useDriveActions = (inPublicSharing?: boolean) => { try { const newItem = await DriveApiClient.update(companyId, id, update); if (previousName && previousName !== newItem.name && !update.name) - ToasterService.warn(Languages.t('hooks.use-drive-actions.update_caused_a_rename', [previousName, newItem.name])); + ToasterService.warn( + Languages.t('hooks.use-drive-actions.update_caused_a_rename', [ + previousName, + newItem.name, + ]), + ); await refresh(id || '', true); if (!inPublicSharing) await refresh(parentId || '', true); if (update?.parent_id !== parentId) await refresh(update?.parent_id || '', true); @@ -183,12 +199,35 @@ export const useDriveActions = (inPublicSharing?: boolean) => { parentId, filter, sortItem, - pagination + pagination, ); return details; }, [paginateItem, refresh], ); - return { create, refresh, download, downloadZip, remove, restore, update, updateLevel, nextPage }; + const reScan = useCallback( + async (item: Partial) => { + try { + await DriveApiClient.reScan(companyId, item.id || ''); + await refresh(item.parent_id || '', true); + } catch (e) { + ToasterService.error(Languages.t('hooks.use-drive-actions.unable_rescan_file')); + } + }, + [refresh], + ); + + return { + create, + refresh, + download, + downloadZip, + remove, + restore, + update, + updateLevel, + reScan, + nextPage, + }; }; diff --git a/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx b/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx index 9340bf22..f2bbdaf7 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx @@ -46,7 +46,7 @@ export const useOnBuildContextMenu = ( DriveCurrentFolderAtom({ initialFolderId: initialParentId || 'root' }), ); - const { download, downloadZip, update, restore } = useDriveActions(); + const { download, downloadZip, update, restore, reScan } = useDriveActions(); const setCreationModalState = useSetRecoilState(CreateModalAtom); const setUploadModalState = useSetRecoilState(UploadModelAtom); const setSelectorModalState = useSetRecoilState(SelectorModalAtom); @@ -102,7 +102,22 @@ export const useOnBuildContextMenu = ( hide: hideManageAccessItem || notSafe, onClick: () => setAccessModalState({ open: true, id: item.id }), }, - { type: 'separator', hide: inTrash || (hideShareItem && hideManageAccessItem) || notSafe }, + { + type: 'menu', + icon: 'shield-check', + text: Languages.t('components.item_context_menu.rescan_document'), + hide: !(item.av_status === 'scan_failed'), + onClick: () => { + reScan(item); + }, + }, + { + type: 'separator', + hide: + inTrash || + (hideShareItem && hideManageAccessItem) || + (notSafe && !(item.av_status === 'scan_failed')), + }, { type: 'menu', icon: 'download-alt', @@ -185,7 +200,7 @@ export const useOnBuildContextMenu = ( hide: item.is_directory || inTrash || notSafe, onClick: () => setVersionModal({ open: true, id: item.id }), }, - { type: 'separator', hide: (access !== 'manage') || inTrash || notSafe }, + { type: 'separator', hide: access !== 'manage' || inTrash || notSafe }, { type: 'menu', icon: 'trash', @@ -256,7 +271,7 @@ export const useOnBuildContextMenu = ( text: Languages.t('components.item_context_menu.clear_selection'), onClick: () => setChecked({}), }, - { type: 'separator', hide: (parent.access === 'read') || notSafe }, + { type: 'separator', hide: parent.access === 'read' || notSafe }, { type: 'menu', text: Languages.t('components.item_context_menu.delete_multiple'), diff --git a/tdrive/frontend/src/app/views/client/body/drive/documents/document-row.tsx b/tdrive/frontend/src/app/views/client/body/drive/documents/document-row.tsx index c17ab64f..e7ccb329 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/documents/document-row.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/documents/document-row.tsx @@ -2,7 +2,7 @@ import { DotsHorizontalIcon, ShieldCheckIcon, ShieldExclamationIcon, - BanIcon + BanIcon, } from '@heroicons/react/outline'; import { Button } from '@atoms/button/button'; import { Base, BaseSmall } from '@atoms/text'; @@ -97,6 +97,7 @@ export const DocumentRow = ({ )} {item?.av_status === 'skipped' && } + {item?.av_status === 'scan_failed' && } )}