|
|
@@ -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
|
|
|
*
|