Files
JMuseum/routes/drawing.js
2018-02-26 14:09:18 +08:00

490 lines
21 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const fileSystem = require("fs");
const User = require("../models/mongooseSchemas/User");
const Painting = require("../models/mongooseSchemas/Painting");
const router = require("express").Router();
const Jimp = require("jimp");
const uuid = require("uuid/v4");
const dataRender = require("../models/DataRender");
/** 畫作儲存時,通用的伺服器錯誤回應訊息。 */
const GeneralSavingErrorMessage = { isOK: false, field: "SERVER", message: "伺服器內部錯誤,請稍後再嘗試儲存。" };
/**
* 頁面「繪圖創作」的路由處理。
*/
router.get(["/drawing", "/drawing/:users_painting_id"], (req, res) => {
dataRender.DataRender("drawing", req.url, req.session, (err, dataObj) => {
// 若找不到相對應的圖畫,則跳轉到指定的訊息頁面。
if (Painting.IsError_PaintingNotExist(err)) {
req.session.painting_not_exist = true;
res.redirect("/painting_not_exist");
}
else if (err) {
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.status(500);
res.end("Server side error 500 : " + err);
}
else {
res.render("drawing", dataObj);
}
});
});
/** 確認字串資料流中的格式是否為png圖像(image/png)且為base64格式。 */
const regexCheckType = /^data:image\/png;base64,/;
/**
* 檢查使用者是否有登入。若有登入,則進行下一步處理;若無,則回送尚未登入的訊息至客戶端。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
* @param {Function} next 導向函式。
*/
function CheckLogin(req, res, next) {
res.setHeader("Content-Type", "application/json");
// 先檢驗使用者是否登入,若有登入則再進行下一步。
if (req.session.passport && req.session.passport.user) {
next();
}
// 若沒有登入,則回送未登入的訊息。
else {
res.send({isOK: false, field: "SERVER", message: "您尚未登入,請先登入後再執行操作。"});
}
}
/**
* 檢查畫作作品的「畫作名稱」、「敘述」與「訪問權限」的內容是否正確。若正確則進行下一步;若無,則回送錯誤訊息至客戶端。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
* @param {Function} next 導向函式。
*/
function CheckBasicPaintingInfo(req, res, next) {
// 驗證「畫作名稱」
req.checkBody("name")
.notEmpty()
.withMessage("請在「畫作名稱」上為你的作品命名。")
.matches(/^([^<>\&"']{1,64})$/)
.withMessage("請在「畫作名稱」上輸入1~64字的作品名稱其中不可包含「<>&\"'」非法字元。");
// 驗證「敘述」
req.checkBody("description")
.notEmpty()
.withMessage("請為您的作品寫上1~300字間的敘述。")
.matches(/^([^<>\&"']{1,300})$/)
.withMessage("作品敘述中不可包含「<>&\"'」非法字元。");
// 驗證「訪問權限」
req.checkBody("view_authority")
.notEmpty()
.withMessage("在作品「訪問權限」中,您必須選擇其中一種權限,決定其他使用者對此作品的能見度。")
.isInt({min: 0, max: 2})
.withMessage("作品「訪問權限」的權限值必須為0~2之間。");
// 取得以上的驗證結果
req.getValidationResult().then(result => {
let errors = result.mapped();
// 若驗證結果為無任何錯誤,則繼續驗證剩下的「標籤」清單與圖畫影像
if (result.isEmpty()) {
next();
}
// 若有錯誤,則將結果回傳
else {
let firstErr = Object.values(errors)[0];
res.send({isOK: false, field: firstErr.param, message: firstErr.msg});
}
});
}
/**
* 檢查畫作作品的「標籤」清單。若正確則進行下一步;若無,則回送錯誤訊息至客戶端。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
* @param {Function} next 導向函式。
*/
function CheckPaintingsTags(req, res, next) {
let user_id = req.session.passport.user; // 取得使用者的_id
// 尋找目標使用者
User.findOne({"_id": user_id})
.populate({ path: "paintings", select: { "id": 1, "isFinished": 1, "isLocked": 1 } })
.exec((err, userDocs) => {
if (err) return res.send(GeneralSavingErrorMessage);
// 檢查畫作的標籤是否接在使用者的定義之下
if (userDocs.IsInTagsList(req.body.taglist)) {
req.session.userDocs = userDocs;
next();
}
// 若否,則傳回錯誤訊息
else {
res.send({isOK: false, field: "taglist", message: "有一至多個被選取的標籤在作品「標籤」清單中,並未在您所定義的標籤清單中。"});
}
}
);
}
/**
* 檢查畫作資訊是否正確。若處理完成正確無誤,則導向下一個處理程序;若有錯誤則回送錯誤訊息至客戶端。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
* @param {Function} next 導向函式。
*/
function CheckPaintingInfo(req, res, next) {
let body = req.body; // 資料主體
let userDocs = req.session.userDocs; // 取得資料庫之使用者資料
let responseMsg = {isOK: false}; // 定義回送訊息包
let img_id = body.id; // 取得畫作的id
let imgData = body.painting_image; // 取得圖片資料
delete req.session.userDocs;
// 若畫作id存在 且 該畫作不為使用者所有的畫作,則回送錯誤訊息
if (img_id && !userDocs.IsPaintingsOwner(img_id)) {
res.send({isOK: false, field: "id", message: "此畫作id無法在您的作品集中找到。您目前所繪製的作品並不屬於您的。"});
return;
}
// 接下來檢查圖畫作品是否符合指定的格式
if (regexCheckType.test(imgData)) {
let base64Data = imgData.replace(/^data:image\/png;base64,/, "");
let dataBuffer = new Buffer(base64Data, "base64");
// 以Buffer的方式讀取Base64的影像檔案
Jimp.read(dataBuffer, (err, image) => {
if (err) return res.send({isOK: false, field: "painting_image", message: "無法解析傳送至伺服端的圖畫影像,您是否是以非正當的方式儲存圖畫呢?若有問題請回饋給我們。"});
// 檢查圖畫影像的長寬比例是否正確
if (image.bitmap.width === 800 && image.bitmap.height === 450) {
// 將必要的資料存在Session中之後在呼叫next()導向下一個處理。
req.session.userDocs = userDocs;
req.session.image = image;
next();
}
// 若不正確則回送錯誤訊息
else {
res.send({isOK: false, field: "painting_image", message: "傳送至伺服端的圖畫之長寬大小與規定不符。您是否是以非正當的方式儲存圖畫呢?請以正確的方式儲存。"});
}
});
}
// 若不符合指定格式,則回送錯誤訊息
else {
res.send({isOK: false, field: "painting_image", message: "傳送的圖畫影像格式與規定不符。您是否是以非正當的方式儲存圖畫呢?請以正確的方式儲存。"});
}
}
/**
* 儲存畫作影像與資料。若成功則,則將成功訊息回送至客戶端;若失敗,則同樣將錯誤訊息回送。
* 呼叫此處理函式時req.session中必須包含讀取後的使用者資料(userDocs)、圖畫影像資料物件Jimp(image),否則會出錯。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
* @param {Function} next 導向函式。
*/
function SavingPaintingAndResponse(req, res, next) {
let userDocs = req.session.userDocs;
let image = req.session.image;
delete req.session.userDocs;
delete req.session.image;
// 檢查使用者的畫作ID若存在則更新不存在則新增
if (req.body.id)
UpdatePaintingInfo(res, req.body, image, false); // 若畫作已存在,則更新其資訊
else
CreateNewPainting(res, req.body, userDocs, image, false); // 若為新畫作則加入新資料至Painting並連結至User。
}
/**
* 路由「POST: /drawing/save」的一連串處理中為最後儲存的部分。
* 對畫作做「Painting資料更新」與「更新圖畫影像檔案」的動作。
* @param {Express.Response} res Express的Response物件。
* @param {Object} body Express的資料主體(body)。
* @param {User} userDocs 使用者資料(Mongoose)。
* @param {Jimp.Jimp} image 圖畫影像檔案。
* @param {boolean} isFinish 是否要完成圖畫。
*/
function UpdatePaintingInfo(res, body, image, isFinish) {
let lastModified = new Date(); // 取得目前的時間日期
body.lastModified = lastModified; // 更新「最後修改時間」欄位
body.isFinished = isFinish; // 為資料主體添上 isFinished = false 屬性,以符合輸入的要求。
Painting.UpdateInfoById(body, body.id, image, (err, result) => {
// 若有錯誤,則檢查錯誤為何種型態。
if (err) {
if (Painting.IsError_PaintingHasFinished(err)) {
res.send({isOK: false, field: "SERVER", message: "此畫作狀態已為「完成」狀態,無法再二次「完成」此畫作。"});
}
else if (Painting.IsError_PaintingIsLocked(err)) {
res.send({isOK: false, field: "SERVER", message: "此畫作已被鎖定,無法對其做任何更改。"});
}
else if (Painting.IsError_PaintingNotExist(err)) {
res.send({isOK: false, field: "SERVER", message: "指定的畫作並不存在。請檢查目前畫作是否正確或是否屬於您的。"});
}
else {
res.send(GeneralSavingErrorMessage);
}
return;
}
// 更新、儲存成功,將成功訊息回送至客戶端。若為完成畫作,則傳送轉跳頁面網址。
if (isFinish)
res.send({isOK: true, url: "/painting_finished"});
else
res.send({isOK: true, id: body.id, lastModified: result.lastModified.toLocaleString(), message: "已成功儲存了您的畫作!"});
});
}
/**
* 路由「POST: /drawing/save」的一連串處理中為最後儲存的部分。
* 對新的畫作做「Painting資料庫新增資料」、「將新增的Painting資料連接User」與「儲存影像檔案」的動作。
* @param {Express.Response} res Express的Response物件。
* @param {Object} body Express的資料主體(body)。
* @param {User} userDocs 使用者資料(Mongoose)。
* @param {Jimp.Jimp} image 圖畫影像檔案。
* @param {boolean} isFinish 圖畫是否完成。
*/
function CreateNewPainting(res, body, userDocs, image, isFinish) {
// 定義新畫作的基本資訊物件
let newPaintingData = {
name: body.name,
description: body.description,
artist: userDocs.username,
tags: body.taglist,
viewAuthority: body.view_authority,
isFinished: isFinish
};
// 以 newPaintingData 來建立新畫作資料
Painting.createNewPainting(newPaintingData, (err, result) => {
if (err) return res.send(GeneralSavingErrorMessage);
let imageFileName = "./db/paintings/" + result.id + ".png"; // 定義儲存的檔案名稱
userDocs.paintings.push(result._id); // 將畫作id增加至使用者資料的paintings中
// 以定義的檔案名稱 imageFileName 將影像儲存
image.write(imageFileName, (err) => {
if (err) return res.send(GeneralSavingErrorMessage);
// 儲存使用者資料
userDocs.save((err) => {
if (err) return res.send(GeneralSavingErrorMessage);
// 所有資料儲存成功,回送成功訊息。若為完成畫作,則送出跳轉頁面網址。
if (isFinish)
res.send({isOK: true, url: "/painting_finished"});
else
res.send({isOK: true, id: result.id, lastModified: result.lastModified.toLocaleString(), message: "已成功儲存了您的新畫作!"});
});
});
});
}
/**
* 在頁面「繪圖創作」下,客戶端傳送「畫作儲存」的相關資料至伺服端時的處理。
*/
router.post("/drawing/save", CheckLogin, CheckBasicPaintingInfo, CheckPaintingsTags, CheckPaintingInfo, SavingPaintingAndResponse);
/**
* 確認畫作是否已經有建立、儲存過一次。若有則繼續接下來的儲存動作;若無則回送訊息。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
* @param {Function} next 導向函式。
*/
function CheckHaveCreatedPainting(req, res, next) {
if (req.body.id) {
next();
}
else {
res.send({isOK: false, field: "SERVER", message: "此為新建立的畫作,請先手動儲存過一次後,再啟用「自動儲存」功能。"});
}
}
/**
* 處理「自動儲存」的處理,
*/
router.post("/drawing/autosave", CheckLogin, CheckHaveCreatedPainting, CheckBasicPaintingInfo, CheckPaintingsTags, CheckPaintingInfo, SavingPaintingAndResponse);
/**
* 路由「POST: /drawing/save」的一連串處理中為最後儲存的部分。
* 對畫作做「Painting資料更新」與「更新圖畫影像檔案」的動作。此次
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
* @param {Function} next 導向函式。
*/
function FinishPainting(req, res, next) {
let userDocs = req.session.userDocs;
let image = req.session.image;
delete req.session.userDocs;
delete req.session.image;
// 檢查使用者的畫作ID若存在則更新不存在則新增
if (req.body.id)
UpdatePaintingInfo(res, req.body, image, true); // 若畫作已存在,則更新其資訊
else
CreateNewPainting(res, req.body, userDocs, image, true); // 若為新畫作則加入新資料至Painting並連結至User。
}
/**
* 在頁面「繪圖創作」下,客戶端傳送「完成畫作」的相關資料至伺服端時的處理。
*/
router.post("/drawing/finish", CheckLogin, CheckBasicPaintingInfo, CheckPaintingsTags, CheckPaintingInfo, FinishPainting);
/**
* 在頁面「繪圖創作」下,成功「完成畫作」動作之後的跳轉頁面。
*/
router.get("/painting_finished", (req, res) => {
dataRender.DataRender("message_form", req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.status(500);
res.end("Server side error 500 : " + err);
}
else {
res.render("message_form", dataObj);
}
});
});
/**
* 找不到指定的畫作作品時的轉跳頁面。
*/
router.get("/painting_not_exist", (req, res) => {
if (req.session.painting_not_exist) {
delete req.session.painting_not_exist; // 刪除標記
dataRender.DataRender("message_form", req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.status(500);
res.end("Server side error 500 : " + err);
}
else {
res.render("message_form", dataObj);
}
});
}
else {
res.redirect("/");
}
});
/**
* 確認指定的畫作屬於使用者。若要近一步檢查大小請使用CheckPaintingInfo函式檢查。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
* @param {Function} next 導向函式。
*/
function CheckPaintingBelongsToUser(req, res, next) {
let user_id = req.session.passport.user; // 取得使用者的_id
let paintingId = req.body.id; // 取得指定要刪除的圖畫id
// 若沒有指定畫作Id則回送錯誤
if (!paintingId) {
res.send({isOK: false, field: "id", message: "沒有指定所要刪除的圖畫Id。"});
return;
}
User.findOne({"_id": user_id})
.populate({ path: "paintings", select: { "id": 1 } })
.exec((err, userDocs) => {
if (err) return res.send(GeneralSavingErrorMessage);
if (!userDocs) return callback({isOK: false, field: "SERVER", message: "找不到使用者資料。請重新登入再嘗試,或請聯繫我們。"});
// 檢查指定的畫作Id是否為使用者擁有。若是則繼續下一步驟若否則回送錯誤訊息。
if (userDocs.IsPaintingsOwner(paintingId)) {
// 由於userDocs.paintings中的每一項皆是ObjectId物件無法直接透過Array提供的indexOf與splice來刪除
// 因此只能一個個比較後再刪除
let paintingList = userDocs.paintings;
let length = paintingList.length;
for (let i = 0; i < length; i++) {
// 若第i個畫作Id與paintingId相同則刪除其。
if (paintingList[i].equals(paintingId)) {
paintingList.splice(i, 1);
break;
}
}
// 儲存後進到下一步驟「刪除畫作資料」本身
userDocs.save((err) => {
if (err) return res.send(GeneralSavingErrorMessage);
next();
});
}
else {
res.send({isOK: false, field: "id", message: "指定要刪除的畫作作品並不屬於您的,無法刪除。"});
}
}
);
}
/**
* 執行刪除畫作的動作,完成後將轉跳頁面網址送回。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
*/
function DeletePainting_AndResponse(req, res) {
let paintingId = req.body.id; // 取的指定要刪除的圖畫Id
Painting.findOne({"id": paintingId}, (err, paintingDocs) => {
if (err) return res.send(GeneralSavingErrorMessage);
if (!paintingDocs) return res.send({isOK: false, field: "SERVER", message: "找不到指定要刪除的畫作。請聯繫我們來解決此問題。"});
let isJoinedActivity = paintingDocs.activity; // 紀錄是否在參加過活動
// 若畫作已被鎖定,則無法刪除畫作,回送錯誤訊息。
if (paintingDocs.isLocked) {
res.send({isOK: false, field: "id", message: "此畫作已被鎖定,無法進行刪除動作。"});
return;
}
// 刪除與此畫作相關的「留言」、「評分」與「參與活動」資訊
paintingDocs.RemoveAllReferenceInfo((err, isOK) => {
if (err) return res.send(GeneralSavingErrorMessage);
// 刪除圖畫影像
fileSystem.unlink(global.__dirname + paintingDocs.links, (err) => { if (err) console.log(err); });
// 刪除此畫作後,回送轉跳網址
Painting.deleteOne({"_id": paintingDocs._id}, (err) => {
if (err) return res.send(GeneralSavingErrorMessage);
req.session.paintingDeleted = true; // 標記使用者已刪除畫作。有此標記,轉跳頁面才可以顯示
if (isJoinedActivity) // 若使用者刪除的畫作有參加活動的話,在下個轉跳頁面中提醒使用者其畫作仍然會看得到
req.session.paintingDeleted_Activity = true;
res.send({isOK: true, url: "/painting_deleted"});
});
});
});
}
/**
* 使用者傳送「刪除畫作」的要求至伺服器。
*/
router.delete("/drawing/delete", CheckLogin, CheckPaintingBelongsToUser, DeletePainting_AndResponse);
/**
* 使用者成功刪除畫作之後的轉跳頁面。
*/
router.get("/painting_deleted", (req, res) => {
// 使用者有登入且有被標記paintingDeleted回應跳轉訊息頁面
if (req.session.passport && req.session.passport.user && req.session.paintingDeleted) {
delete req.session.paintingDeleted; // 刪除標記
dataRender.DataRender("message_form", req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.status(500);
res.end("Server side error 500 : " + err);
}
else {
res.render("message_form", dataObj);
}
});
}
// 否則跳轉到首頁
else {
res.redirect("/");
}
});
module.exports = router;