spacedeck-open/routes/api/spaces.js
mntmn ebac854da8
Port Backend to SQLite/Sequelize (removes MongoDB), Support Electron (#14)
* The MongoDB/Mongoose data storage is removed in favor of Sequelize. This abstracts over SQLite or RDBMs like PostgreSQL and MSSQL. The default is SQLite, which significantly simplifies deployments in end-user environments.

* As Spacedeck now has no more mandatory server dependencies, we can wrap it in Electron and ship it as a desktop application.

* Removes docker-compose.yml

* First version of import UI
2018-04-12 16:40:58 +00:00

559 lines
16 KiB
JavaScript
Raw 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.

"use strict";
var config = require('config');
const db = require('../../models/db');
const Sequelize = require('sequelize');
const Op = Sequelize.Op;
const uuidv4 = require('uuid/v4');
var redis = require('../../helpers/redis');
var mailer = require('../../helpers/mailer');
var uploader = require('../../helpers/uploader');
var space_render = require('../../helpers/space-render');
var phantom = require('../../helpers/phantom');
var payloadConverter = require('../../helpers/artifact_converter');
var slug = require('slug');
var fs = require('fs');
var async = require('async');
var _ = require("underscore");
var request = require('request');
var url = require("url");
var path = require("path");
var crypto = require('crypto');
var glob = require('glob');
var gm = require('gm');
const exec = require('child_process');
var express = require('express');
var router = express.Router();
// JSON MAPPINGS
var userMapping = {
_id: 1,
nickname: 1,
email: 1,
avatar_thumb_uri: 1
};
var spaceMapping = {
_id: 1,
name: 1,
thumbnail_url: 1
};
router.get('/', function(req, res, next) {
if (!req.user) {
res.status(403).json({
error: "auth required"
});
} else {
if (req.query.writablefolders) {
db.Membership.find({where: {
user_id: req.user._id
}}, (memberships) => {
var validMemberships = memberships.filter((m) => {
if (!m.space_id || (m.space_id == "undefined"))
return false;
return true;
});
var editorMemberships = validMemberships.filter((m) => {
return (m.role == "editor") || (m.role == "admin")
});
var spaceIds = editorMemberships.map(function(m) {
return m.space_id;
});
// TODO port
var q = {
"space_type": "folder",
"$or": [{
"creator": req.user._id
}, {
"_id": {
"$in": spaceIds
},
"creator": {
"$ne": req.user._id
}
}]
};
db.Space
.findAll({where: q})
.then(function(spaces) {
var updatedSpaces = spaces.map(function(s) {
var spaceObj = s; //.toObject();
return spaceObj;
});
async.map(spaces, (space, cb) => {
Space.getRecursiveSubspacesForSpace(space, (err, spaces) => {
var allSpaces = spaces;
cb(err, allSpaces);
})
}, (err, spaces) => {
var allSpaces = _.flatten(spaces);
var onlyFolders = _.filter(allSpaces, (s) => {
return s.space_type == "folder";
})
var uniqueFolders = _.unique(onlyFolders, (s) => {
return s._id;
})
res.status(200).json(uniqueFolders);
});
});
});
} else if (req.query.search) {
db.Membership.findAll({where:{
user_id: req.user._id
}}).then(memberships => {
var validMemberships = memberships.filter(function(m) {
if (!m.space_id || (m.space_id == "undefined"))
return false;
else
return true;
});
var spaceIds = validMemberships.map(function(m) {
return m.space_id;
});
// TODO FIXME port
var q = { where: {
[Op.or]: [{"creator_id": req.user._id},
{"_id": {[Op.in]: spaceIds}},
{"parent_space_id": {[Op.in]: spaceIds}}],
name: {[Op.like]: "%"+req.query.search+"%"}
}, include: ['creator']};
db.Space
.findAll(q)
.then(function(spaces) {
res.status(200).json(spaces);
});
});
} else if (req.query.parent_space_id && req.query.parent_space_id != req.user.home_folder_id) {
db.Space
.findOne({where: {
_id: req.query.parent_space_id
}})
//.populate('creator', userMapping)
.then(function(space) {
if (space) {
db.getUserRoleInSpace(space, req.user, function(role) {
if (role == "none") {
if (space.access_mode == "public") {
role = "viewer";
}
}
if (role != "none") {
db.Space
.findAll({where:{
parent_space_id: req.query.parent_space_id
}, include:['creator']})
.then(function(spaces) {
res.status(200).json(spaces);
});
} else {
res.status(403).json({"error": "no authorized"});
}
});
} else {
res.status(404).json({"error": "space not found"});
}
});
} else {
db.Membership.findAll({ where: {
user_id: req.user._id
}}).then(memberships => {
if (!memberships) memberships = [];
var validMemberships = memberships.filter(function(m) {
if (!m.space_id || (m.space_id == "undefined"))
return false;
});
var spaceIds = validMemberships.map(function(m) {
return m.space_id;
});
var q = {
[Op.or]: [{
"creator_id": req.user._id,
"parent_space_id": req.user.home_folder_id
}, {
"_id": {
[Op.in]: spaceIds
},
"creator_id": {
[Op.ne]: req.user._id
}
}]
};
db.Space
.findAll({where: q, include: ['creator']})
.then(function(spaces) {
var updatedSpaces = spaces.map(function(s) {
var spaceObj = db.spaceToObject(s);
return spaceObj;
});
res.status(200).json(spaces);
});
});
}
}
});
// create a space
router.post('/', function(req, res, next) {
if (req.user) {
var attrs = req.body;
var createSpace = () => {
attrs._id = uuidv4();
attrs.creator_id = req.user._id;
attrs.edit_hash = crypto.randomBytes(64).toString('hex').substring(0, 7);
attrs.edit_slug = slug(attrs.name);
db.Space.create(attrs).then(createdSpace => {
//if (err) res.sendStatus(400);
var membership = {
_id: uuidv4(),
user_id: req.user._id,
space_id: attrs._id,
role: "admin"
};
db.Membership.create(membership).then(() => {
res.status(201).json(createdSpace);
});
});
}
if (attrs.parent_space_id) {
db.Space.findOne({ where: {
"_id": attrs.parent_space_id
}}).then(parentSpace => {
if (parentSpace) {
db.getUserRoleInSpace(parentSpace, req.user, (role) => {
if ((role == "editor") || (role == "admin")) {
createSpace();
} else {
res.status(403).json({
"error": "not editor in parent Space. role: "+role
});
}
});
} else {
res.status(404).json({
"error": "parent Space not found"
});
}
});
} else {
createSpace();
}
} else {
res.sendStatus(403);
}
});
router.get('/:id', function(req, res, next) {
res.status(200).json(req.space);
});
router.get('/:id/path', (req, res) => {
// build up a breadcrumb trail (path)
var path = [];
var buildPath = (space) => {
if (space.parent_space_id) {
db.Space.findOne({ where: {
"_id": space.parent_space_id
}}).then(parentSpace => {
if (space._id == parentSpace._id) {
console.error("error: circular parent reference for space " + space._id);
res.send("error: circular reference");
} else {
path.push(parentSpace);
buildPath(parentSpace);
}
});
} else {
// reached the top
res.json(path.reverse());
}
}
buildPath(req.space);
});
router.put('/:id', function(req, res) {
var space = req.space;
var newAttr = req.body;
if (req['spaceRole'] != "editor" && req['spaceRole'] != "admin") {
res.sendStatus(403);
return;
}
newAttr.updated_at = new Date();
newAttr.edit_slug = slug(newAttr['name']);
delete newAttr['_id'];
delete newAttr['editor_name'];
delete newAttr['creator'];
db.Space.update(newAttr, {where: {
"_id": space._id
}}).then(space => {
res.distributeUpdate("Space", space);
});
});
router.post('/:id/background', function(req, res, next) {
var space = req.space;
var newDate = new Date();
var fileName = (req.query.filename || "upload.jpg").replace(/[^a-zA-Z0-9\.]/g, '');
var localFilePath = "/tmp/" + fileName;
var writeStream = fs.createWriteStream(localFilePath);
var stream = req.pipe(writeStream);
req.on('end', function() {
uploader.uploadFile("s" + req.space._id + "/bg_" + newDate.getTime() + "_" + fileName, "image/jpeg", localFilePath, function(err, backgroundUrl) {
if (err) res.status(400).json(err);
else {
if (space.background_uri) {
var oldPath = url.parse(req.space.background_uri).pathname;
uploader.removeFile(oldPath, function(err) {
console.error("removed old bg error:", err);
});
}
db.Space.update({
background_uri: backgroundUrl
}, {
where: { "_id": space._id }
}, function(rows) {
fs.unlink(localFilePath, function(err) {
if (err) {
console.error(err);
res.status(400).json(err);
} else {
res.status(200).json(space);
}
});
});
}
});
});
});
var handleDuplicateSpaceRequest = function(req, res, parentSpace) {
Space.duplicateSpace(req.space, req.user, 0, (err, newSpace) => {
if (err) {
console.error(err);
res.status(400).json(err);
} else {
res.status(201).json(newSpace);
}
}, parentSpace);
}
router.post('/:id/duplicate', (req, res, next) => {
if (req.query.parent_space_id) {
Space.findOne({
_id: req.query.parent_space_id
}).populate('creator', userMapping).exec((err, parentSpace) => {
if (!parentSpace) {
res.status(404).json({
"error": "parent space not found for duplicate"
});
} else {
db.getUserRoleInSpace(parentSpace, req.user, (role) => {
if (role == "admin" ||  role == "editor") {
handleDuplicateSpaceRequest(req, res, parentSpace);
} else {
res.status(403).json({
"error": "not authed for parent_space_id"
});
}
});
}
});
} else {
handleDuplicateSpaceRequest(req, res);
}
});
router.delete('/:id', function(req, res, next) {
if (req.user) {
const space = req.space;
if (req.spaceRole == "admin") {
const attrs = req.body;
space.destroy().then(function() {
res.distributeDelete("Space", space);
});
} else {
res.status(403).json({
"error": "requires admin role"
});
}
} else {
res.sendStatus(403);
}
});
router.post('/:id/artifacts-pdf', function(req, res, next) {
if (req.spaceRole == "editor" || req.spaceRole == "admin") {
var withZones = (req.query.zones) ? req.query.zones == "true" : false;
var fileName = (req.query.filename || "upload.bin").replace(/[^a-zA-Z0-9\.]/g, '');
var localFilePath = "/tmp/" + fileName;
var writeStream = fs.createWriteStream(localFilePath);
var stream = req.pipe(writeStream);
req.on('end', function() {
var rawName = fileName.slice(0, fileName.length - 4);
var outputFolder = "/tmp/" + rawName;
var rights = 777;
fs.mkdir(outputFolder, function(db) {
var images = outputFolder + "/" + rawName + "-page-%03d.jpeg";
// FIXME not portable
exec.execFile("gs", ["-sDEVICE=jpeg", "-dDownScaleFactor=4", "-dDOINTERPOLATE", "-dNOPAUSE", "-dJPEGQ=80", "-dBATCH", "-sOutputFile=" + images, "-r250", "-f", localFilePath], {}, function(error, stdout, stderr) {
if (error === null) {
glob(outputFolder + "/*.jpeg", function(er, files) {
var count = files.length;
var delta = 10;
var limitPerRow = Math.ceil(Math.sqrt(count));
var startX = parseInt(req.query.x, delta);
var startY = parseInt(req.query.y, delta);
async.mapLimit(files, 20, function(localfilePath, cb) {
var fileName = path.basename(localfilePath);
var baseName = path.basename(localfilePath, ".jpeg");
var number = parseInt(baseName.slice(baseName.length - 3, baseName.length), 10);
gm(localFilePath).size(function(err, size) {
var w = 350;
var h = w;
var x = startX + (((number - 1) % limitPerRow) * w);
var y = startY + ((parseInt(((number - 1) / limitPerRow), 10) + 1) * w);
var userId;
if (req.user)
userId = req.user._id;
var a = new Artifact({
mime: "image/jpg",
space_id: req.space._id,
user_id: userId,
editor_name: req.guest_name,
board: {
w: w,
h: h,
x: x,
y: y,
z: (number + (count + 100))
}
});
payloadConverter.convert(a, fileName, localfilePath, (error, artifact) => {
if (error) res.status(400).json(error);
else {
if (withZones) {
var zone = new Artifact({
mime: "x-spacedeck/zone",
description: "Zone " + (number),
space_id: req.space._id,
user_id: userId,
editor_name: req.guest_name,
board: {
w: artifact.board.w + 20,
h: artifact.board.h + 40,
x: x - 10,
y: y - 30,
z: number
},
style: {
order: number,
valign: "middle",
align: "center"
}
});
zone.save((err) => {
redis.sendMessage("create", "Artifact", zone.toJSON(), req.channelId);
cb(null, [artifact, zone]);
});
} else {
cb(null, [artifact]);
}
}
});
});
}, function(err, artifacts) {
// FIXME not portable
exec.execFile("rm", ["-r", outputFolder], function(err) {
res.status(201).json(_.flatten(artifacts));
async.eachLimit(artifacts, 10, (artifact_or_artifacts, cb) => {
if (artifact_or_artifacts instanceof Array) {
_.each(artifact_or_artifacts, (a) => {
redis.sendMessage("create", "Artifact", a.toJSON(), req.channelId);
});
} else  {
redis.sendMessage("create", "Artifact", artifact_or_artifacts.toJSON(), req.channelId);
}
cb(null);
});
});
});
});
} else {
console.error("error:", error);
// FIXME not portable
exec.execFile("rm", ["-r", outputFolder], function(err) {
fs.unlink(localFilePath);
res.status(400).json({});
});
}
});
});
});
} else {
res.status(401).json({
"error": "no access"
});
}
});
module.exports = router;