spaces.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. "use strict";
  2. var config = require('config');
  3. const os = require('os');
  4. const db = require('../../models/db');
  5. const Sequelize = require('sequelize');
  6. const Op = Sequelize.Op;
  7. const uuidv4 = require('uuid/v4');
  8. var redis = require('../../helpers/redis');
  9. var mailer = require('../../helpers/mailer');
  10. var uploader = require('../../helpers/uploader');
  11. var space_render = require('../../helpers/space-render');
  12. var phantom = require('../../helpers/phantom');
  13. var payloadConverter = require('../../helpers/artifact_converter');
  14. var slug = require('slug');
  15. var fs = require('fs');
  16. var async = require('async');
  17. var _ = require("underscore");
  18. var request = require('request');
  19. var url = require("url");
  20. var path = require("path");
  21. var crypto = require('crypto');
  22. var glob = require('glob');
  23. var gm = require('gm');
  24. const exec = require('child_process');
  25. var express = require('express');
  26. var router = express.Router();
  27. // JSON MAPPINGS
  28. var userMapping = {
  29. _id: 1,
  30. nickname: 1,
  31. email: 1,
  32. avatar_thumb_uri: 1
  33. };
  34. var spaceMapping = {
  35. _id: 1,
  36. name: 1,
  37. thumbnail_url: 1
  38. };
  39. router.get('/', function(req, res, next) {
  40. if (!req.user) {
  41. res.status(403).json({
  42. error: "auth required"
  43. });
  44. } else {
  45. if (req.query.writablefolders) {
  46. db.Membership.find({where: {
  47. user_id: req.user._id
  48. }}, (memberships) => {
  49. var validMemberships = memberships.filter((m) => {
  50. if (!m.space_id || (m.space_id == "undefined"))
  51. return false;
  52. return true;
  53. });
  54. var editorMemberships = validMemberships.filter((m) => {
  55. return (m.role == "editor") || (m.role == "admin")
  56. });
  57. var spaceIds = editorMemberships.map(function(m) {
  58. return m.space_id;
  59. });
  60. // TODO port
  61. var q = {
  62. "space_type": "folder",
  63. "$or": [{
  64. "creator": req.user._id
  65. }, {
  66. "_id": {
  67. "$in": spaceIds
  68. },
  69. "creator": {
  70. "$ne": req.user._id
  71. }
  72. }]
  73. };
  74. db.Space
  75. .findAll({where: q})
  76. .then(function(spaces) {
  77. var updatedSpaces = spaces.map(function(s) {
  78. var spaceObj = s; //.toObject();
  79. return spaceObj;
  80. });
  81. async.map(spaces, (space, cb) => {
  82. Space.getRecursiveSubspacesForSpace(space, (err, spaces) => {
  83. var allSpaces = spaces;
  84. cb(err, allSpaces);
  85. })
  86. }, (err, spaces) => {
  87. var allSpaces = _.flatten(spaces);
  88. var onlyFolders = _.filter(allSpaces, (s) => {
  89. return s.space_type == "folder";
  90. })
  91. var uniqueFolders = _.unique(onlyFolders, (s) => {
  92. return s._id;
  93. })
  94. res.status(200).json(uniqueFolders);
  95. });
  96. });
  97. });
  98. } else if (req.query.search) {
  99. db.Membership.findAll({where:{
  100. user_id: req.user._id
  101. }}).then(memberships => {
  102. var validMemberships = memberships.filter(function(m) {
  103. if (!m.space_id || (m.space_id == "undefined"))
  104. return false;
  105. else
  106. return true;
  107. });
  108. var spaceIds = validMemberships.map(function(m) {
  109. return m.space_id;
  110. });
  111. // TODO FIXME port
  112. var q = { where: {
  113. [Op.or]: [{"creator_id": req.user._id},
  114. {"_id": {[Op.in]: spaceIds}},
  115. {"parent_space_id": {[Op.in]: spaceIds}}],
  116. name: {[Op.like]: "%"+req.query.search+"%"}
  117. }, include: ['creator']};
  118. db.Space
  119. .findAll(q)
  120. .then(function(spaces) {
  121. res.status(200).json(spaces);
  122. });
  123. });
  124. } else if (req.query.parent_space_id && req.query.parent_space_id != req.user.home_folder_id) {
  125. db.Space
  126. .findOne({where: {
  127. _id: req.query.parent_space_id
  128. }})
  129. //.populate('creator', userMapping)
  130. .then(function(space) {
  131. if (space) {
  132. db.getUserRoleInSpace(space, req.user, function(role) {
  133. if (role == "none") {
  134. if (space.access_mode == "public") {
  135. role = "viewer";
  136. }
  137. }
  138. if (role != "none") {
  139. db.Space
  140. .findAll({where:{
  141. parent_space_id: req.query.parent_space_id
  142. }, include:['creator']})
  143. .then(function(spaces) {
  144. res.status(200).json(spaces);
  145. });
  146. } else {
  147. res.status(403).json({"error": "no authorized"});
  148. }
  149. });
  150. } else {
  151. res.status(404).json({"error": "space not found"});
  152. }
  153. });
  154. } else {
  155. db.Membership.findAll({ where: {
  156. user_id: req.user._id
  157. }}).then(memberships => {
  158. if (!memberships) memberships = [];
  159. var validMemberships = memberships.filter(function(m) {
  160. if (!m.space_id || (m.space_id == "undefined"))
  161. return false;
  162. });
  163. var spaceIds = validMemberships.map(function(m) {
  164. return m.space_id;
  165. });
  166. var q = {
  167. [Op.or]: [{
  168. "creator_id": req.user._id,
  169. "parent_space_id": req.user.home_folder_id
  170. }, {
  171. "_id": {
  172. [Op.in]: spaceIds
  173. },
  174. "creator_id": {
  175. [Op.ne]: req.user._id
  176. }
  177. }]
  178. };
  179. db.Space
  180. .findAll({where: q, include: ['creator']})
  181. .then(function(spaces) {
  182. var updatedSpaces = spaces.map(function(s) {
  183. var spaceObj = db.spaceToObject(s);
  184. return spaceObj;
  185. });
  186. res.status(200).json(spaces);
  187. });
  188. });
  189. }
  190. }
  191. });
  192. // create a space
  193. router.post('/', function(req, res, next) {
  194. if (req.user) {
  195. var attrs = req.body;
  196. var createSpace = () => {
  197. attrs._id = uuidv4();
  198. attrs.creator_id = req.user._id;
  199. attrs.edit_hash = crypto.randomBytes(64).toString('hex').substring(0, 7);
  200. attrs.edit_slug = slug(attrs.name);
  201. db.Space.create(attrs).then(createdSpace => {
  202. //if (err) res.sendStatus(400);
  203. var membership = {
  204. _id: uuidv4(),
  205. user_id: req.user._id,
  206. space_id: attrs._id,
  207. role: "admin"
  208. };
  209. db.Membership.create(membership).then(() => {
  210. res.status(201).json(createdSpace);
  211. });
  212. });
  213. }
  214. if (attrs.parent_space_id) {
  215. db.Space.findOne({ where: {
  216. "_id": attrs.parent_space_id
  217. }}).then(parentSpace => {
  218. if (parentSpace) {
  219. db.getUserRoleInSpace(parentSpace, req.user, (role) => {
  220. if ((role == "editor") || (role == "admin")) {
  221. createSpace();
  222. } else {
  223. res.status(403).json({
  224. "error": "not editor in parent Space. role: "+role
  225. });
  226. }
  227. });
  228. } else {
  229. res.status(404).json({
  230. "error": "parent Space not found"
  231. });
  232. }
  233. });
  234. } else {
  235. createSpace();
  236. }
  237. } else {
  238. res.sendStatus(403);
  239. }
  240. });
  241. router.get('/:id', function(req, res, next) {
  242. res.status(200).json(req.space);
  243. });
  244. router.get('/:id/path', (req, res) => {
  245. // build up a breadcrumb trail (path)
  246. var path = [];
  247. var buildPath = (space) => {
  248. if (space.parent_space_id) {
  249. db.Space.findOne({ where: {
  250. "_id": space.parent_space_id
  251. }}).then(parentSpace => {
  252. if (space._id == parentSpace._id) {
  253. console.error("error: circular parent reference for space " + space._id);
  254. res.send("error: circular reference");
  255. } else {
  256. path.push(parentSpace);
  257. buildPath(parentSpace);
  258. }
  259. });
  260. } else {
  261. // reached the top
  262. res.json(path.reverse());
  263. }
  264. }
  265. buildPath(req.space);
  266. });
  267. router.put('/:id', function(req, res) {
  268. var space = req.space;
  269. var newAttr = req.body;
  270. if (req['spaceRole'] != "editor" && req['spaceRole'] != "admin") {
  271. res.sendStatus(403);
  272. return;
  273. }
  274. newAttr.updated_at = new Date();
  275. newAttr.edit_slug = slug(newAttr['name']);
  276. delete newAttr['_id'];
  277. delete newAttr['editor_name'];
  278. delete newAttr['creator'];
  279. db.Space.update(newAttr, {where: {
  280. "_id": space._id
  281. }}).then(space => {
  282. res.distributeUpdate("Space", space);
  283. });
  284. });
  285. router.post('/:id/background', function(req, res, next) {
  286. var space = req.space;
  287. var newDate = new Date();
  288. var fileName = (req.query.filename || "upload.jpg").replace(/[^a-zA-Z0-9\.]/g, '');
  289. var localFilePath = "/tmp/" + fileName;
  290. var writeStream = fs.createWriteStream(localFilePath);
  291. var stream = req.pipe(writeStream);
  292. req.on('end', function() {
  293. uploader.uploadFile("s" + req.space._id + "/bg_" + newDate.getTime() + "_" + fileName, "image/jpeg", localFilePath, function(err, backgroundUrl) {
  294. if (err) res.status(400).json(err);
  295. else {
  296. if (space.background_uri) {
  297. var oldPath = url.parse(req.space.background_uri).pathname;
  298. uploader.removeFile(oldPath, function(err) {
  299. console.error("removed old bg error:", err);
  300. });
  301. }
  302. db.Space.update({
  303. background_uri: backgroundUrl
  304. }, {
  305. where: { "_id": space._id }
  306. }, function(rows) {
  307. fs.unlink(localFilePath, function(err) {
  308. if (err) {
  309. console.error(err);
  310. res.status(400).json(err);
  311. } else {
  312. res.status(200).json(space);
  313. }
  314. });
  315. });
  316. }
  317. });
  318. });
  319. });
  320. var handleDuplicateSpaceRequest = function(req, res, parentSpace) {
  321. Space.duplicateSpace(req.space, req.user, 0, (err, newSpace) => {
  322. if (err) {
  323. console.error(err);
  324. res.status(400).json(err);
  325. } else {
  326. res.status(201).json(newSpace);
  327. }
  328. }, parentSpace);
  329. }
  330. router.post('/:id/duplicate', (req, res, next) => {
  331. if (req.query.parent_space_id) {
  332. Space.findOne({
  333. _id: req.query.parent_space_id
  334. }).populate('creator', userMapping).exec((err, parentSpace) => {
  335. if (!parentSpace) {
  336. res.status(404).json({
  337. "error": "parent space not found for duplicate"
  338. });
  339. } else {
  340. db.getUserRoleInSpace(parentSpace, req.user, (role) => {
  341. if (role == "admin" ||  role == "editor") {
  342. handleDuplicateSpaceRequest(req, res, parentSpace);
  343. } else {
  344. res.status(403).json({
  345. "error": "not authed for parent_space_id"
  346. });
  347. }
  348. });
  349. }
  350. });
  351. } else {
  352. handleDuplicateSpaceRequest(req, res);
  353. }
  354. });
  355. router.delete('/:id', function(req, res, next) {
  356. if (req.user) {
  357. const space = req.space;
  358. if (req.spaceRole == "admin") {
  359. const attrs = req.body;
  360. space.destroy().then(function() {
  361. res.distributeDelete("Space", space);
  362. });
  363. } else {
  364. res.status(403).json({
  365. "error": "requires admin role"
  366. });
  367. }
  368. } else {
  369. res.sendStatus(403);
  370. }
  371. });
  372. router.post('/:id/artifacts-pdf', function(req, res, next) {
  373. if (req.spaceRole == "editor" || req.spaceRole == "admin") {
  374. var withZones = (req.query.zones) ? req.query.zones == "true" : false;
  375. var fileName = (req.query.filename || "upload.bin").replace(/[^a-zA-Z0-9\.]/g, '');
  376. var localFilePath = os.tmpdir() + "/" + fileName;
  377. var writeStream = fs.createWriteStream(localFilePath);
  378. var stream = req.pipe(writeStream);
  379. req.on('end', function() {
  380. var rawName = fileName.slice(0, fileName.length - 4);
  381. var outputFolder = os.tmpdir() + "/" + rawName;
  382. fs.mkdir(outputFolder, function(err) {
  383. var images = outputFolder + "/" + rawName + "-page-%03d.jpeg";
  384. // FIXME not portable
  385. exec.execFile("gs", ["-sDEVICE=jpeg", "-dDownScaleFactor=4", "-dDOINTERPOLATE", "-dNOPAUSE", "-dJPEGQ=80", "-dBATCH", "-sOutputFile=" + images, "-r250", "-f", localFilePath], {}, function(error, stdout, stderr) {
  386. if (error === null) {
  387. glob(outputFolder + "/*.jpeg", function(er, files) {
  388. var count = files.length;
  389. var delta = 10;
  390. var limitPerRow = Math.ceil(Math.sqrt(count));
  391. var startX = parseInt(req.query.x, delta);
  392. var startY = parseInt(req.query.y, delta);
  393. async.mapLimit(files, 20, function(localfilePath, cb) {
  394. var fileName = path.basename(localfilePath);
  395. var baseName = path.basename(localfilePath, ".jpeg");
  396. var number = parseInt(baseName.slice(baseName.length - 3, baseName.length), 10);
  397. gm(localFilePath).size((err, size) => {
  398. var w = 350;
  399. var h = w;
  400. var x = startX + (((number - 1) % limitPerRow) * w);
  401. var y = startY + ((parseInt(((number - 1) / limitPerRow), 10) + 1) * w);
  402. var userId;
  403. if (req.user) userId = req.user._id;
  404. var a = db.Artifact.create({
  405. _id: uuidv4(),
  406. mime: "image/jpg",
  407. space_id: req.space._id,
  408. user_id: userId,
  409. editor_name: req.guest_name,
  410. w: w,
  411. h: h,
  412. x: x,
  413. y: y,
  414. z: (number + (count + 100))
  415. }).then(a => {
  416. payloadConverter.convert(a, fileName, localfilePath, (error, artifact) => {
  417. if (error) res.status(400).json(error);
  418. else {
  419. if (withZones) {
  420. var zone = {
  421. _id: uuidv4(),
  422. mime: "x-spacedeck/zone",
  423. description: "Zone " + (number),
  424. space_id: req.space._id,
  425. user_id: userId,
  426. editor_name: req.guest_name,
  427. w: artifact.w + 20,
  428. h: artifact.h + 40,
  429. x: x - 10,
  430. y: y - 30,
  431. z: number,
  432. order: number,
  433. valign: "middle",
  434. align: "center"
  435. };
  436. db.Artifact.create(zone).then((z) => {
  437. redis.sendMessage("create", "Artifact", z.toJSON(), req.channelId);
  438. cb(null, [artifact, zone]);
  439. });
  440. } else {
  441. cb(null, [artifact]);
  442. }
  443. }
  444. });
  445. });
  446. });
  447. }, function(err, artifacts) {
  448. // FIXME not portable
  449. exec.execFile("rm", ["-r", outputFolder], function(err) {
  450. res.status(201).json(_.flatten(artifacts));
  451. async.eachLimit(artifacts, 10, (artifact_or_artifacts, cb) => {
  452. if (artifact_or_artifacts instanceof Array) {
  453. _.each(artifact_or_artifacts, (a) => {
  454. redis.sendMessage("create", "Artifact", JSON.stringify(a), req.channelId);
  455. });
  456. } else  {
  457. redis.sendMessage("create", "Artifact", JSON.stringify(artifact_or_artifacts), req.channelId);
  458. }
  459. cb(null);
  460. });
  461. });
  462. });
  463. });
  464. } else {
  465. console.error("error:", error);
  466. // FIXME not portable
  467. exec.execFile("rm", ["-r", outputFolder], function(err) {
  468. fs.unlink(localFilePath);
  469. res.status(400).json({});
  470. });
  471. }
  472. });
  473. });
  474. });
  475. } else {
  476. res.status(401).json({
  477. "error": "no access"
  478. });
  479. }
  480. });
  481. module.exports = router;