Kaynağa Gözat

Merge pull request #2512 from weseek/feat/convert-md-file-to-pdf

Feat/export page as markdown file
Yuki Takei 5 yıl önce
ebeveyn
işleme
0b13bce0e7

+ 8 - 23
src/client/js/components/Page/PageShareManagement.jsx

@@ -2,14 +2,13 @@ import React, { useState } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
+import urljoin from 'url-join';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 import AppContainer from '../../services/AppContainer';
 import AppContainer from '../../services/AppContainer';
 import PageContainer from '../../services/PageContainer';
 import PageContainer from '../../services/PageContainer';
 import OutsideShareLinkModal from '../OutsideShareLinkModal';
 import OutsideShareLinkModal from '../OutsideShareLinkModal';
 
 
-import { toastError } from '../../util/apiNotification';
-
 const PageShareManagement = (props) => {
 const PageShareManagement = (props) => {
   const { t, appContainer, pageContainer } = props;
   const { t, appContainer, pageContainer } = props;
 
 
@@ -26,24 +25,13 @@ const PageShareManagement = (props) => {
     setIsOutsideShareLinkModalShown(false);
     setIsOutsideShareLinkModalShown(false);
   }
   }
 
 
-  async function getMarkdown() {
+  async function exportPageHandler(format) {
     const { pageId, revisionId } = pageContainer.state;
     const { pageId, revisionId } = pageContainer.state;
-    try {
-      const res = await appContainer.apiv3Get('/page/export', { pageId, revisionId });
-      return res.data.markdown;
-    }
-    catch (err) {
-      toastError(Error(t('export_bulk.failed_to_export')));
-    }
-  }
-
-  async function exportPage(markdown, type) {
-    // TODO: GW-3063
-  }
-
-  async function exportPageHundler(type) {
-    const markdown = await getMarkdown();
-    await exportPage(markdown, type);
+    const url = new URL(urljoin(window.location.origin, '_api/v3/page/export', pageId));
+    url.searchParams.append('_csrf', appContainer.csrfToken);
+    url.searchParams.append('format', format);
+    url.searchParams.append('revisionId', revisionId);
+    window.location.href = url.href;
   }
   }
 
 
   function renderModals() {
   function renderModals() {
@@ -101,12 +89,9 @@ const PageShareManagement = (props) => {
           <i className="icon-fw icon-link"></i>{t('share_links.Shere this page link to public')}
           <i className="icon-fw icon-link"></i>{t('share_links.Shere this page link to public')}
           <span className="ml-2 badge badge-info badge-pill">{pageContainer.state.shareLinksNumber}</span>
           <span className="ml-2 badge badge-info badge-pill">{pageContainer.state.shareLinksNumber}</span>
         </button>
         </button>
-        <button type="button" className="dropdown-item" onClick={() => { exportPageHundler('md') }}>
+        <button type="button" className="dropdown-item" onClick={() => { exportPageHandler('md') }}>
           <span>{t('export_bulk.export_page_markdown')}</span>
           <span>{t('export_bulk.export_page_markdown')}</span>
         </button>
         </button>
-        <button type="button" className="dropdown-item" onClick={() => { exportPageHundler('pdf') }}>
-          <span>{t('export_bulk.export_page_pdf')}</span>
-        </button>
       </div>
       </div>
       {renderModals()}
       {renderModals()}
     </>
     </>

+ 20 - 25
src/server/routes/apiv3/page.js

@@ -118,6 +118,7 @@ module.exports = (crowi) => {
 
 
   const globalNotificationService = crowi.getGlobalNotificationService();
   const globalNotificationService = crowi.getGlobalNotificationService();
   const { Page, GlobalNotificationSetting } = crowi.models;
   const { Page, GlobalNotificationSetting } = crowi.models;
+  const { exportService } = crowi;
 
 
   const validator = {
   const validator = {
     likes: [
     likes: [
@@ -125,7 +126,7 @@ module.exports = (crowi) => {
       body('bool').isBoolean(),
       body('bool').isBoolean(),
     ],
     ],
     export: [
     export: [
-      query('pageId').isString(),
+      query('format').isString().isIn(['md', 'pdf']),
       query('revisionId').isString(),
       query('revisionId').isString(),
     ],
     ],
   };
   };
@@ -197,41 +198,35 @@ module.exports = (crowi) => {
   *          200:
   *          200:
   *            description: Return page's markdown
   *            description: Return page's markdown
   */
   */
-  router.get('/export', validator.export, async(req, res) => {
+  router.get('/export/:pageId', validator.export, async(req, res) => {
     try {
     try {
-      const { pageId = null, revisionId = null } = req.query;
+      const { pageId } = req.params;
+      const { format, revisionId = null } = req.query;
 
 
-      if (pageId == null) {
-        return res.apiv3Err(new ErrorV3('Should provided pageId or both pageId and revisionId.'));
-      }
+      const Page = crowi.model('Page');
+      const page = await Page.findByIdAndViewer(pageId, req.user);
 
 
-      const isPageExist = await Page.count({ _id: pageId }) > 0;
-      if (!isPageExist) {
+      if (page == null) {
+        const isPageExist = await Page.count({ _id: pageId }) > 0;
+        if (isPageExist) {
+          // This page exists but req.user has not read permission
+          return res.apiv3Err(new ErrorV3(`Haven't the right to see the page ${pageId}.`), 403);
+        }
         return res.apiv3Err(new ErrorV3(`Page ${pageId} is not exist.`), 404);
         return res.apiv3Err(new ErrorV3(`Page ${pageId} is not exist.`), 404);
       }
       }
 
 
-      const isAccessible = await Page.isAccessiblePageByViewer(pageId, req.user);
-      if (!isAccessible) {
-        return res.apiv3Err(new ErrorV3(`Haven't the right to see the page ${pageId}.`), 403);
-      }
-
-
-      let revisionIdForFind;
-      if (revisionId == null) {
-        const Page = crowi.model('Page');
-        const page = await Page.findByIdAndViewer(pageId);
-        revisionIdForFind = page.revision;
-      }
-      else {
-        revisionIdForFind = revisionId;
-      }
+      const revisionIdForFind = revisionId || page.revision;
 
 
       const Revision = crowi.model('Revision');
       const Revision = crowi.model('Revision');
       const revision = await Revision.findById(revisionIdForFind);
       const revision = await Revision.findById(revisionIdForFind);
 
 
-      const markdown = revision.body;
+      const fileName = revisionIdForFind;
+      const stream = exportService.getReadStreamFromRevision(revision);
 
 
-      return res.apiv3({ markdown });
+      res.set({
+        'Content-Disposition': `attachment;filename*=UTF-8''${fileName}.${format}`,
+      });
+      return stream.pipe(res);
     }
     }
     catch (err) {
     catch (err) {
       logger.error('Failed to get markdown', err);
       logger.error('Failed to get markdown', err);

+ 12 - 0
src/server/service/export.js

@@ -350,6 +350,18 @@ class ExportService {
     return zipFile;
     return zipFile;
   }
   }
 
 
+  getReadStreamFromRevision(revision) {
+    const markdown = revision.body;
+
+    const Readable = require('stream').Readable;
+    const readable = new Readable();
+    readable._read = () => {};
+    readable.push(markdown);
+    readable.push(null);
+
+    return readable;
+  }
+
 }
 }
 
 
 module.exports = ExportService;
 module.exports = ExportService;