Просмотр исходного кода

Merge pull request #9033 from weseek/imprv/149843-152624-disable-bulk-export-when-fs-not-s3-or-gcs

Imprv/149843 152624 disable bulk export when fs not s3 or gcs
Futa Arai 1 год назад
Родитель
Сommit
6d5f66a1ae

+ 1 - 0
apps/app/public/static/locales/en_US/translation.json

@@ -642,6 +642,7 @@
     "bulk_export_started": "Please wait a moment...",
     "bulk_export_download_expired": "Download period has expired",
     "bulk_export_job_expired": "Export process was canceled because it took too long",
+    "bulk_export_only_available_for": "Only available for AWS or GCP",
     "export_in_progress": "Export in progress",
     "export_in_progress_explanation": "Export with the same format is already in progress. Would you like to restart to export the latest page contents?",
     "export_cancel_warning": "The export in progress will be canceled",

+ 1 - 0
apps/app/public/static/locales/fr_FR/translation.json

@@ -636,6 +636,7 @@
     "bulk_export_started": "Patientez s'il-vous-plait...",
     "bulk_export_download_expired": "La période de téléchargement a expiré",
     "bulk_export_job_expired": "Le traitement a été interrompu car le temps d'exportation était trop long",
+    "bulk_export_only_available_for": "Uniquement disponible pour AWS ou GCP",
     "export_in_progress": "Exportation en cours",
     "export_in_progress_explanation": "L'exportation avec le même format est déjà en cours. Souhaitez-vous redémarrer pour exporter le dernier contenu de la page ?",
     "export_cancel_warning": "L'export en cours sera annulé",

+ 1 - 0
apps/app/public/static/locales/ja_JP/translation.json

@@ -675,6 +675,7 @@
     "bulk_export_started": "ただいま準備中です...",
     "bulk_export_download_expired": "ダウンロード期限が切れました",
     "bulk_export_job_expired": "エクスポート時間が長すぎるため、処理が中断されました",
+    "bulk_export_only_available_for": "AWS と GCP のみ対応しています",
     "export_in_progress": "エクスポート進行中",
     "export_in_progress_explanation": "既に同じ形式でのエクスポートが進行中です。最新のページ内容でエクスポートを最初からやり直しますか?",
     "export_cancel_warning": "進行中のエクスポートはキャンセルされます",

+ 1 - 0
apps/app/public/static/locales/zh_CN/translation.json

@@ -645,6 +645,7 @@
     "bulk_export_started": "目前我们正在准备...",
     "bulk_export_download_expired": "下载期限已过",
     "bulk_export_job_expired": "由于导出时间太长,处理被中断",
+    "bulk_export_only_available_for": "仅适用于 AWS 或 GCP",
     "export_in_progress": "导出正在进行中",
     "export_in_progress_explanation": "已在进行相同格式的导出。您要重新启动以导出最新的页面内容吗?",
     "export_cancel_warning": "正在进行的导出将被取消",

+ 2 - 4
apps/app/src/client/components/Admin/App/FileUploadSetting.tsx

@@ -5,6 +5,7 @@ import { useTranslation } from 'next-i18next';
 
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 import { toastSuccess, toastError } from '~/client/util/toastr';
+import { FileUploadType } from '~/interfaces/file-uploader';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
@@ -16,9 +17,6 @@ import type { AzureSettingMoleculeProps } from './AzureSetting';
 import { GcsSettingMolecule } from './GcsSetting';
 import type { GcsSettingMoleculeProps } from './GcsSetting';
 
-
-const fileUploadTypes = ['aws', 'gcs', 'azure', 'gridfs', 'local'] as const;
-
 type FileUploadSettingMoleculeProps = {
   fileUploadType: string
   isFixedFileUploadByEnvVar: boolean
@@ -45,7 +43,7 @@ export const FileUploadSettingMolecule = React.memo((props: FileUploadSettingMol
         </label>
 
         <div className="col-md-6 py-2">
-          {fileUploadTypes.map((type) => {
+          {Object.values(FileUploadType).map((type) => {
             return (
               <div key={type} className="form-check form-check-inline">
                 <input

+ 23 - 8
apps/app/src/client/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -15,7 +15,7 @@ import dynamic from 'next/dynamic';
 import Link from 'next/link';
 import { useRouter } from 'next/router';
 import Sticky from 'react-stickynode';
-import { DropdownItem, UncontrolledTooltip } from 'reactstrap';
+import { Tooltip, DropdownItem, UncontrolledTooltip } from 'reactstrap';
 
 import { exportAsMarkdown, updateContentWidth, syncLatestRevisionBody } from '~/client/services/page-operation';
 import { toastSuccess, toastError, toastWarning } from '~/client/util/toastr';
@@ -25,7 +25,7 @@ import type { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from
 import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
 import {
   useCurrentPathname,
-  useCurrentUser, useIsGuestUser, useIsReadOnlyUser, useIsLocalAccountRegistrationEnabled, useIsSharedUser, useShareLinkId,
+  useCurrentUser, useIsGuestUser, useIsReadOnlyUser, useIsPageBulkExportEnabled, useIsLocalAccountRegistrationEnabled, useIsSharedUser, useShareLinkId,
 } from '~/stores-universal/context';
 import { useEditorMode } from '~/stores-universal/ui';
 import {
@@ -77,6 +77,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isSharedUser } = useIsSharedUser();
+  const { data: isPageBulkExportEnabled } = useIsPageBulkExportEnabled();
 
   const { open: openPresentationModal } = usePagePresentationModal();
   const { open: openAccessoriesModal } = usePageAccessoriesModal();
@@ -84,6 +85,8 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
 
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
 
+  const [isBulkExportTooltipOpen, setIsBulkExportTooltipOpen] = useState(false);
+
   const syncLatestRevisionBodyHandler = useCallback(async() => {
     // eslint-disable-next-line no-alert
     const answer = window.confirm(t('sync-latest-revision-body.confirm'));
@@ -141,13 +144,25 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
       </DropdownItem>
 
       {/* Bulk export */}
-      <DropdownItem
-        onClick={openPageBulkExportSelectModal}
-        className="grw-page-control-dropdown-item"
+      <span id="bulkExportDropdownItem">
+        <DropdownItem
+          disabled={!isPageBulkExportEnabled}
+          onClick={openPageBulkExportSelectModal}
+          className="grw-page-control-dropdown-item"
+        >
+          <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">cloud_download</span>
+          {t('page_export.bulk_export')}
+        </DropdownItem>
+      </span>
+      <Tooltip
+        placement="left"
+        isOpen={isBulkExportTooltipOpen}
+        // Tooltip cannot be activated when target is disabled so set the target to wrapper span
+        target="bulkExportDropdownItem"
+        toggle={() => setIsBulkExportTooltipOpen(!isBulkExportTooltipOpen)}
       >
-        <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">cloud_download</span>
-        {t('page_export.bulk_export')}
-      </DropdownItem>
+        {t('page_export.bulk_export_only_available_for')}
+      </Tooltip>
 
       <DropdownItem divider />
 

+ 4 - 0
apps/app/src/features/page-bulk-export/interfaces/page-bulk-export.ts

@@ -3,6 +3,8 @@ import type {
   IAttachment, IPage, IRevision, IUser, Ref,
 } from '@growi/core';
 
+import { FileUploadTypeForEnvVar } from '~/interfaces/file-uploader';
+
 export const PageBulkExportFormat = {
   md: 'md',
   pdf: 'pdf',
@@ -45,3 +47,5 @@ export interface IPageBulkExportPageSnapshot {
   path: string, // page path when export was stared
   revision: Ref<IRevision>, // page revision when export was stared
 }
+
+export const PageBulkExportEnabledFileUploadTypes = [FileUploadTypeForEnvVar.aws, FileUploadTypeForEnvVar.gcs, FileUploadTypeForEnvVar.gcp] as const;

+ 4 - 1
apps/app/src/features/page-bulk-export/server/service/page-bulk-export-job-cron.ts

@@ -4,7 +4,7 @@ import { configManager } from '~/server/service/config-manager';
 import CronService from '~/server/service/cron';
 import loggerFactory from '~/utils/logger';
 
-import { PageBulkExportJobInProgressStatus, PageBulkExportJobStatus } from '../../interfaces/page-bulk-export';
+import { PageBulkExportEnabledFileUploadTypes, PageBulkExportJobInProgressStatus, PageBulkExportJobStatus } from '../../interfaces/page-bulk-export';
 import type { PageBulkExportJobDocument } from '../models/page-bulk-export-job';
 import PageBulkExportJob from '../models/page-bulk-export-job';
 
@@ -29,6 +29,9 @@ class PageBulkExportJobCronService extends CronService {
   }
 
   override async executeJob(): Promise<void> {
+    const isPageBulkExportEnabled = PageBulkExportEnabledFileUploadTypes.includes(configManager.getConfig('crowi', 'app:fileUploadType'));
+    if (!isPageBulkExportEnabled) return;
+
     await this.deleteExpiredExportJobs();
     await this.deleteDownloadExpiredExportJobs();
     await this.deleteFailedExportJobs();

+ 28 - 0
apps/app/src/interfaces/file-uploader.ts

@@ -0,0 +1,28 @@
+// file upload types actually supported by the app
+export const FileUploadType = {
+  aws: 'aws',
+  gcs: 'gcs',
+  azure: 'azure',
+  gridfs: 'gridfs',
+  local: 'local',
+} as const;
+
+export type FileUploadType = typeof FileUploadType[keyof typeof FileUploadType]
+
+// file upload type strings you can specify in the env variable
+export const FileUploadTypeForEnvVar = {
+  ...FileUploadType,
+  mongo:   'mongo',
+  mongodb: 'mongodb',
+  gcp:     'gcp',
+} as const;
+
+export type FileUploadTypeForEnvVar = typeof FileUploadTypeForEnvVar[keyof typeof FileUploadTypeForEnvVar]
+
+// mapping from env variable to actual module name
+export const EnvToModuleMappings = {
+  ...FileUploadTypeForEnvVar,
+  mongo:   'gridfs',
+  mongodb: 'gridfs',
+  gcp:     'gcs',
+} as const;

+ 5 - 1
apps/app/src/pages/[[...path]].page.tsx

@@ -23,6 +23,7 @@ import superjson from 'superjson';
 import { BasicLayout } from '~/components/Layout/BasicLayout';
 import { PageView } from '~/components/PageView/PageView';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
+import { PageBulkExportEnabledFileUploadTypes } from '~/features/page-bulk-export/interfaces/page-bulk-export';
 import { SupportedAction, type SupportedActionType } from '~/interfaces/activity';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import { RegistrationMode } from '~/interfaces/registration-mode';
@@ -42,7 +43,7 @@ import {
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useIsEnabledMarp, useCurrentPathname,
   useIsSlackConfigured, useRendererConfig, useGrowiCloudUri,
   useIsAllReplyShown, useIsContainerFluid, useIsNotCreatable,
-  useIsUploadAllFileAllowed, useIsUploadEnabled,
+  useIsUploadAllFileAllowed, useIsUploadEnabled, useIsPageBulkExportEnabled,
   useElasticsearchMaxBodyLengthToIndex,
   useIsLocalAccountRegistrationEnabled,
 } from '~/stores-universal/context';
@@ -177,6 +178,7 @@ type Props = CommonProps & {
   isContainerFluid: boolean,
   isUploadEnabled: boolean,
   isUploadAllFileAllowed: boolean,
+  isPageBulkExportEnabled: boolean,
   isEnabledStaleNotification: boolean,
   isEnabledAttachTitleHeader: boolean,
   // isEnabledLinebreaks: boolean,
@@ -239,6 +241,7 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
 
   useIsUploadAllFileAllowed(props.isUploadAllFileAllowed);
   useIsUploadEnabled(props.isUploadEnabled);
+  useIsPageBulkExportEnabled(props.isPageBulkExportEnabled);
 
   useIsLocalAccountRegistrationEnabled(props.isLocalAccountRegistrationEnabled);
 
@@ -560,6 +563,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   props.disableLinkSharing = configManager.getConfig('crowi', 'security:disableLinkSharing');
   props.isUploadAllFileAllowed = crowi.fileUploadService.getFileUploadEnabled();
   props.isUploadEnabled = crowi.fileUploadService.getIsUploadable();
+  props.isPageBulkExportEnabled = PageBulkExportEnabledFileUploadTypes.includes(configManager.getConfig('crowi', 'app:fileUploadType'));
 
   props.isLocalAccountRegistrationEnabled = crowi.passportService.isLocalStrategySetup
   && configManager.getConfig('crowi', 'security:registrationMode') !== RegistrationMode.CLOSED;

+ 0 - 1
apps/app/src/server/routes/apiv3/app-settings.js

@@ -301,7 +301,6 @@ module.exports = (crowi) => {
 
   });
 
-
   /**
    * @swagger
    *

+ 2 - 12
apps/app/src/server/service/file-uploader/index.ts

@@ -1,3 +1,4 @@
+import { EnvToModuleMappings } from '~/interfaces/file-uploader';
 import type Crowi from '~/server/crowi';
 import loggerFactory from '~/utils/logger';
 
@@ -9,19 +10,8 @@ export type { FileUploader } from './file-uploader';
 
 const logger = loggerFactory('growi:service:FileUploaderServise');
 
-const envToModuleMappings = {
-  aws:     'aws',
-  local:   'local',
-  mongo:   'gridfs',
-  mongodb: 'gridfs',
-  gridfs:  'gridfs',
-  gcp:     'gcs',
-  gcs:     'gcs',
-  azure:   'azure',
-};
-
 export const getUploader = (crowi: Crowi): FileUploader => {
-  const method = envToModuleMappings[configManager.getConfig('crowi', 'app:fileUploadType')];
+  const method = EnvToModuleMappings[configManager.getConfig('crowi', 'app:fileUploadType')];
   const modulePath = `./${method}`;
   const uploader = require(modulePath)(crowi);
 

+ 4 - 0
apps/app/src/stores-universal/context.tsx

@@ -168,6 +168,10 @@ export const useIsUploadAllFileAllowed = (initialData?: boolean): SWRResponse<bo
   return useContextSWR('isUploadAllFileAllowed', initialData);
 };
 
+export const useIsPageBulkExportEnabled = (initialData?: boolean): SWRResponse<boolean, Error> => {
+  return useContextSWR('isPageBulkExportEnabled', initialData);
+};
+
 export const useShowPageLimitationL = (initialData?: number): SWRResponse<number, Error> => {
   return useContextSWR('showPageLimitationL', initialData);
 };