Преглед изворни кода

Merge pull request #8279 from weseek/imprv/131756-131772-apiv3-file-upload

imprv: Upload handler use apiv3 post
reiji-h пре 2 година
родитељ
комит
2592320943

+ 6 - 21
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -18,7 +18,7 @@ import { throttle, debounce } from 'throttle-debounce';
 
 import { useShouldExpandContent } from '~/client/services/layout';
 import { useUpdateStateAfterSave, useSaveOrUpdate } from '~/client/services/page-operation';
-import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
+import { apiv3Get, apiv3PostForm } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { SocketEventName } from '~/interfaces/websocket';
@@ -312,10 +312,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   const uploadHandler = useCallback((files: File[]) => {
     files.forEach(async(file) => {
       try {
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        const resLimit: any = await apiGet('/attachments.limit', {
-          fileSize: file.size,
-        });
+        const { data: resLimit } = await apiv3Get('/attachment/limit', { fileSize: file.size });
 
         if (!resLimit.isUploadable) {
           throw new Error(resLimit.errorMessage);
@@ -323,17 +320,12 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
         const formData = new FormData();
         formData.append('file', file);
-        if (currentPagePath != null) {
-          formData.append('path', currentPagePath);
-        }
         if (pageId != null) {
           formData.append('page_id', pageId);
         }
-        if (pageId == null) {
-          formData.append('page_body', codeMirrorEditor?.getDoc() ?? '');
-        }
 
-        const resAdd: any = await apiPostForm('/attachments.add', formData);
+        const { data: resAdd } = await apiv3PostForm('/attachment', formData);
+
         const attachment = resAdd.attachment;
         const fileName = attachment.originalName;
 
@@ -343,23 +335,16 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
           // modify to "![fileName](url)" syntax
           insertText = `!${insertText}`;
         }
-        // TODO: implement
-        // refs: https://redmine.weseek.co.jp/issues/126528
-        // editorRef.current.insertText(insertText);
+
         codeMirrorEditor?.insertText(insertText);
       }
       catch (e) {
         logger.error('failed to upload', e);
         toastError(e);
       }
-      finally {
-        // TODO: implement
-        // refs: https://redmine.weseek.co.jp/issues/126528
-        // editorRef.current.terminateUploadingState();
-      }
     });
 
-  }, [codeMirrorEditor, currentPagePath, pageId]);
+  }, [codeMirrorEditor, pageId]);
 
   const acceptedFileType = useMemo(() => {
     if (!isUploadEnabled) {

+ 246 - 1
apps/app/src/server/routes/apiv3/attachment.js

@@ -1,17 +1,28 @@
 import { ErrorV3 } from '@growi/core/dist/models';
+import multer from 'multer';
+import autoReap from 'multer-autoreap';
 
+import { SupportedAction } from '~/interfaces/activity';
+import { AttachmentType } from '~/server/interfaces/attachment';
 import { Attachment } from '~/server/models';
 import loggerFactory from '~/utils/logger';
 
+import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 import { certifySharedPageAttachmentMiddleware } from '../../middlewares/certify-shared-page-attachment';
+import { excludeReadOnlyUser } from '../../middlewares/exclude-read-only-user';
+
 
 const logger = loggerFactory('growi:routes:apiv3:attachment'); // eslint-disable-line no-unused-vars
 const express = require('express');
 
 const router = express.Router();
-const { query, param } = require('express-validator');
+const {
+  query, param, body,
+} = require('express-validator');
 
+const { serializePageSecurely } = require('../../models/serializers/page-serializer');
+const { serializeRevisionSecurely } = require('../../models/serializers/revision-serializer');
 const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
 
 /**
@@ -20,11 +31,75 @@ const { serializeUserSecurely } = require('../../models/serializers/user-seriali
  *    name: Attachment
  */
 
+
+/**
+ * @swagger
+ *
+ *  components:
+ *    schemas:
+ *      Attachment:
+ *        description: Attachment
+ *        type: object
+ *        properties:
+ *          _id:
+ *            type: string
+ *            description: attachment ID
+ *            example: 5e0734e072560e001761fa67
+ *          __v:
+ *            type: number
+ *            description: attachment version
+ *            example: 0
+ *          fileFormat:
+ *            type: string
+ *            description: file format in MIME
+ *            example: text/plain
+ *          fileName:
+ *            type: string
+ *            description: file name
+ *            example: 601b7c59d43a042c0117e08dd37aad0aimage.txt
+ *          originalName:
+ *            type: string
+ *            description: original file name
+ *            example: file.txt
+ *          creator:
+ *            $ref: '#/components/schemas/User'
+ *          page:
+ *            type: string
+ *            description: page ID attached at
+ *            example: 5e07345972560e001761fa63
+ *          createdAt:
+ *            type: string
+ *            description: date created at
+ *            example: 2010-01-01T00:00:00.000Z
+ *          fileSize:
+ *            type: number
+ *            description: file size
+ *            example: 3494332
+ *          url:
+ *            type: string
+ *            description: attachment URL
+ *            example: http://localhost/files/5e0734e072560e001761fa67
+ *          filePathProxied:
+ *            type: string
+ *            description: file path proxied
+ *            example: "/attachment/5e0734e072560e001761fa67"
+ *          downloadPathProxied:
+ *            type: string
+ *            description: download path proxied
+ *            example: "/download/5e0734e072560e001761fa67"
+ */
+
 module.exports = (crowi) => {
   const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const loginRequired = require('../../middlewares/login-required')(crowi, true);
+  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const Page = crowi.model('Page');
   const User = crowi.model('User');
+  const { attachmentService } = crowi;
+  const uploads = multer({ dest: `${crowi.tmpDir}uploads` });
+  const addActivity = generateAddActivityMiddleware(crowi);
+
+  const activityEvent = crowi.event('activity');
 
   const validator = {
     retrieveAttachment: [
@@ -35,6 +110,12 @@ module.exports = (crowi) => {
       query('pageNumber').optional().isInt().withMessage('pageNumber must be a number'),
       query('limit').optional().isInt({ max: 100 }).withMessage('You should set less than 100 or not to set limit.'),
     ],
+    retrieveFileLimit: [
+      query('fileSize').isNumeric().exists({ checkNull: true }).withMessage('fileSize is required'),
+    ],
+    retrieveAddAttachment: [
+      body('page_id').isString().exists({ checkNull: true }).withMessage('page_id is required'),
+    ],
   };
 
   /**
@@ -95,6 +176,170 @@ module.exports = (crowi) => {
     }
   });
 
+
+  /**
+   * @swagger
+   *
+   *    /attachment/limit:
+   *      get:
+   *        tags: [Attachment]
+   *        operationId: getAttachmentLimit
+   *        summary: /attachment/limit
+   *        description: Get available capacity of uploaded file with GridFS
+   *        parameters:
+   *          - in: query
+   *            name: fileSize
+   *            schema:
+   *              type: number
+   *              description: file size
+   *              example: 23175
+   *            required: true
+   *        responses:
+   *          200:
+   *            description: Succeeded to get available capacity of uploaded file with GridFS.
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    isUploadable:
+   *                      type: boolean
+   *                      description: uploadable
+   *                      example: true
+   *          403:
+   *            $ref: '#/components/responses/403'
+   *          500:
+   *            $ref: '#/components/responses/500'
+   */
+  /**
+   * @api {get} /attachment/limit get available capacity of uploaded file with GridFS
+   * @apiName AddAttachment
+   * @apiGroup Attachment
+   */
+  router.get('/limit', accessTokenParser, loginRequiredStrictly, validator.retrieveFileLimit, apiV3FormValidator, async(req, res) => {
+    const { fileUploadService } = crowi;
+    const fileSize = Number(req.query.fileSize);
+    try {
+      return res.apiv3(await fileUploadService.checkLimit(fileSize));
+    }
+    catch (err) {
+      logger.error('File limit retrieval failed', err);
+      return res.apiv3Err(err, 500);
+    }
+  });
+
+  /**
+   * @swagger
+   *
+   *    /attachment:
+   *      post:
+   *        tags: [Attachment, CrowiCompatibles]
+   *        operationId: addAttachment
+   *        summary: /attachment
+   *        description: Add attachment to the page
+   *        requestBody:
+   *          content:
+   *            "multipart/form-data":
+   *              schema:
+   *                properties:
+   *                  page_id:
+   *                    nullable: true
+   *                    type: string
+   *                  path:
+   *                    nullable: true
+   *                    type: string
+   *                  file:
+   *                    type: string
+   *                    format: binary
+   *                    description: attachment data
+   *              encoding:
+   *                path:
+   *                  contentType: application/x-www-form-urlencoded
+   *            "*\/*":
+   *              schema:
+   *                properties:
+   *                  page_id:
+   *                    nullable: true
+   *                    type: string
+   *                  path:
+   *                    nullable: true
+   *                    type: string
+   *                  file:
+   *                    type: string
+   *                    format: binary
+   *                    description: attachment data
+   *              encoding:
+   *                path:
+   *                  contentType: application/x-www-form-urlencoded
+   *        responses:
+   *          200:
+   *            description: Succeeded to add attachment.
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    page:
+   *                      $ref: '#/components/schemas/Page'
+   *                    attachment:
+   *                      $ref: '#/components/schemas/Attachment'
+   *                    url:
+   *                      $ref: '#/components/schemas/Attachment/properties/url'
+   *                    pageCreated:
+   *                      type: boolean
+   *                      description: whether the page was created
+   *                      example: false
+   *          403:
+   *            $ref: '#/components/responses/403'
+   *          500:
+   *            $ref: '#/components/responses/500'
+   */
+  /**
+   * @api {post} /attachment Add attachment to the page
+   * @apiName AddAttachment
+   * @apiGroup Attachment
+   *
+   * @apiParam {String} page_id
+   * @apiParam {String} path
+   * @apiParam {File} file
+   */
+  router.post('/', uploads.single('file'), autoReap, accessTokenParser, loginRequiredStrictly, excludeReadOnlyUser,
+    validator.retrieveAddAttachment, apiV3FormValidator, addActivity,
+    async(req, res) => {
+
+      const pageId = req.body.page_id;
+
+      // check params
+      const file = req.file || null;
+      if (file == null) {
+        return res.apiv3Err('File error.');
+      }
+
+      try {
+        const page = await Page.findById(pageId);
+
+        // check the user is accessible
+        const isAccessible = await Page.isAccessiblePageByViewer(page.id, req.user);
+        if (!isAccessible) {
+          return res.apiv3Err(`Forbidden to access to the page '${page.id}'`);
+        }
+
+        const attachment = await attachmentService.createAttachment(file, req.user, pageId, AttachmentType.WIKI_PAGE);
+
+        const result = {
+          page: serializePageSecurely(page),
+          revision: serializeRevisionSecurely(page.revision),
+          attachment: attachment.toObject({ virtuals: true }),
+        };
+
+        activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ATTACHMENT_ADD });
+
+        res.apiv3(result);
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err(err.message);
+      }
+    });
+
   /**
    * @swagger
    *

+ 0 - 162
apps/app/src/server/routes/attachment/api.js

@@ -176,168 +176,6 @@ export const routesFactory = (crowi) => {
   //   return responseForAttachment(req, res, attachment, true);
   // };
 
-  /**
-   * @swagger
-   *
-   *    /attachments.limit:
-   *      get:
-   *        tags: [Attachments]
-   *        operationId: getAttachmentsLimit
-   *        summary: /attachments.limit
-   *        description: Get available capacity of uploaded file with GridFS
-   *        parameters:
-   *          - in: query
-   *            name: fileSize
-   *            schema:
-   *              type: number
-   *              description: file size
-   *              example: 23175
-   *            required: true
-   *        responses:
-   *          200:
-   *            description: Succeeded to get available capacity of uploaded file with GridFS.
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  properties:
-   *                    isUploadable:
-   *                      type: boolean
-   *                      description: uploadable
-   *                      example: true
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *          403:
-   *            $ref: '#/components/responses/403'
-   *          500:
-   *            $ref: '#/components/responses/500'
-   */
-  /**
-   * @api {get} /attachments.limit get available capacity of uploaded file with GridFS
-   * @apiName AddAttachments
-   * @apiGroup Attachment
-   */
-  api.limit = async function(req, res) {
-    const { fileUploadService } = crowi;
-    const fileSize = Number(req.query.fileSize);
-    return res.json(ApiResponse.success(await fileUploadService.checkLimit(fileSize)));
-  };
-
-  /**
-   * @swagger
-   *
-   *    /attachments.add:
-   *      post:
-   *        tags: [Attachments, CrowiCompatibles]
-   *        operationId: addAttachment
-   *        summary: /attachments.add
-   *        description: Add attachment to the page
-   *        requestBody:
-   *          content:
-   *            "multipart/form-data":
-   *              schema:
-   *                properties:
-   *                  page_id:
-   *                    nullable: true
-   *                    type: string
-   *                  path:
-   *                    nullable: true
-   *                    type: string
-   *                  file:
-   *                    type: string
-   *                    format: binary
-   *                    description: attachment data
-   *              encoding:
-   *                path:
-   *                  contentType: application/x-www-form-urlencoded
-   *            "*\/*":
-   *              schema:
-   *                properties:
-   *                  page_id:
-   *                    nullable: true
-   *                    type: string
-   *                  path:
-   *                    nullable: true
-   *                    type: string
-   *                  file:
-   *                    type: string
-   *                    format: binary
-   *                    description: attachment data
-   *              encoding:
-   *                path:
-   *                  contentType: application/x-www-form-urlencoded
-   *        responses:
-   *          200:
-   *            description: Succeeded to add attachment.
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *                    page:
-   *                      $ref: '#/components/schemas/Page'
-   *                    attachment:
-   *                      $ref: '#/components/schemas/Attachment'
-   *                    url:
-   *                      $ref: '#/components/schemas/Attachment/properties/url'
-   *                    pageCreated:
-   *                      type: boolean
-   *                      description: whether the page was created
-   *                      example: false
-   *          403:
-   *            $ref: '#/components/responses/403'
-   *          500:
-   *            $ref: '#/components/responses/500'
-   */
-  /**
-   * @api {post} /attachments.add Add attachment to the page
-   * @apiName AddAttachments
-   * @apiGroup Attachment
-   *
-   * @apiParam {String} page_id
-   * @apiParam {File} file
-   */
-  api.add = async function(req, res) {
-    const pageId = req.body.page_id || null;
-    const pagePath = req.body.path || null;
-
-    // check params
-    if (pageId == null && pagePath == null) {
-      return res.json(ApiResponse.error('Either page_id or path is required.'));
-    }
-    if (req.file == null) {
-      return res.json(ApiResponse.error('File error.'));
-    }
-
-    const file = req.file;
-
-    try {
-      const page = await Page.findById(pageId);
-
-      // check the user is accessible
-      const isAccessible = await Page.isAccessiblePageByViewer(page.id, req.user);
-      if (!isAccessible) {
-        return res.json(ApiResponse.error(`Forbidden to access to the page '${page.id}'`));
-      }
-
-      const attachment = await attachmentService.createAttachment(file, req.user, pageId, AttachmentType.WIKI_PAGE);
-
-      const result = {
-        page: serializePageSecurely(page),
-        revision: serializeRevisionSecurely(page.revision),
-        attachment: attachment.toObject({ virtuals: true }),
-      };
-
-      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ATTACHMENT_ADD });
-
-      res.json(ApiResponse.success(result));
-    }
-    catch (err) {
-      logger.error(err);
-      return res.json(ApiResponse.error(err.message));
-    }
-  };
-
   /**
    * @swagger
    *

+ 0 - 2
apps/app/src/server/routes/index.js

@@ -139,11 +139,9 @@ module.exports = function(crowi, app) {
   apiV1Router.post('/comments.update'    , comment.api.validators.add(), accessTokenParser , loginRequiredStrictly , excludeReadOnlyUser, addActivity, comment.api.update);
   apiV1Router.post('/comments.remove'    , accessTokenParser , loginRequiredStrictly , excludeReadOnlyUser, addActivity, comment.api.remove);
 
-  apiV1Router.post('/attachments.add'                  , uploads.single('file'), autoReap, accessTokenParser, loginRequiredStrictly , excludeReadOnlyUser, addActivity , attachmentApi.add);
   apiV1Router.post('/attachments.uploadProfileImage'   , uploads.single('file'), autoReap, accessTokenParser, loginRequiredStrictly , excludeReadOnlyUser, attachmentApi.uploadProfileImage);
   apiV1Router.post('/attachments.remove'               , accessTokenParser , loginRequiredStrictly , excludeReadOnlyUser, addActivity ,attachmentApi.remove);
   apiV1Router.post('/attachments.removeProfileImage'   , accessTokenParser , loginRequiredStrictly , excludeReadOnlyUser, attachmentApi.removeProfileImage);
-  apiV1Router.get('/attachments.limit'   , accessTokenParser , loginRequiredStrictly, attachmentApi.limit);
 
   // API v1
   app.use('/_api', unavailableWhenMaintenanceModeForApi, apiV1Router);