浏览代码

Removed Search-Index and LokiJS, replaced with Mongo + Updated VueJs

NGPixel 8 年之前
父节点
当前提交
6ea243e8d4

+ 1 - 2
.travis.yml

@@ -1,8 +1,7 @@
 language: node_js
 node_js:
 - '6'
-- '5'
-- '4.4'
+- '4'
 addons:
   apt:
     sources:

+ 31 - 49
agent.js

@@ -25,18 +25,22 @@ if(!process.argv[2] || process.argv[2].length !== 40) {
 global.WSInternalKey = process.argv[2];
 
 // ----------------------------------------
-// Load modules
+// Load global modules
 // ----------------------------------------
 
 winston.info('[AGENT] Background Agent is initializing...');
 
 var appconfig = require('./models/config')('./config.yml');
-global.lcdata = require('./models/localdata').init(appconfig, 'agent');
-var upl = require('./models/uploads').init(appconfig);
-
+global.db = require('./models/mongo').init(appconfig);
+global.upl = require('./models/agent/uploads').init(appconfig);
 global.git = require('./models/git').init(appconfig);
 global.entries = require('./models/entries').init(appconfig);
 global.mark = require('./models/markdown');
+global.ws = require('socket.io-client')('http://localhost:' + appconfig.wsPort, { reconnectionAttempts: 10 });
+
+// ----------------------------------------
+// Load modules
+// ----------------------------------------
 
 var _ = require('lodash');
 var moment = require('moment');
@@ -45,10 +49,6 @@ var fs = Promise.promisifyAll(require("fs-extra"));
 var path = require('path');
 var cron = require('cron').CronJob;
 
-global.ws = require('socket.io-client')('http://localhost:' + appconfig.wsPort, { reconnectionAttempts: 10 });
-
-const mimeImgTypes = ['image/png', 'image/jpg']
-
 // ----------------------------------------
 // Start Cron
 // ----------------------------------------
@@ -72,16 +72,17 @@ var job = new cron({
 		// Prepare async job collector
 
 		let jobs = [];
-		let repoPath = path.resolve(ROOTPATH, appconfig.datadir.repo);
-		let dataPath = path.resolve(ROOTPATH, appconfig.datadir.db);
+		let repoPath = path.resolve(ROOTPATH, appconfig.paths.repo);
+		let dataPath = path.resolve(ROOTPATH, appconfig.paths.data);
 		let uploadsPath = path.join(repoPath, 'uploads');
+		let uploadsTempPath = path.join(dataPath, 'temp-upload');
 
 		// ----------------------------------------
-		// Compile Jobs
+		// REGULAR JOBS
 		// ----------------------------------------
 
 		//*****************************************
-		//-> Resync with Git remote
+		//-> Sync with Git remote
 		//*****************************************
 
 		jobs.push(git.onReady.then(() => {
@@ -143,51 +144,30 @@ var job = new cron({
 		}));
 
 		//*****************************************
-		//-> Refresh uploads data
+		//-> Clear failed temporary upload files
 		//*****************************************
 
-		jobs.push(fs.readdirAsync(uploadsPath).then((ls) => {
+		jobs.push(
+			fs.readdirAsync(uploadsTempPath).then((ls) => {
 
-			return Promise.map(ls, (f) => {
-				return fs.statAsync(path.join(uploadsPath, f)).then((s) => { return { filename: f, stat: s }; });
-			}).filter((s) => { return s.stat.isDirectory(); }).then((arrDirs) => {
+				let fifteenAgo = moment().subtract(15, 'minutes');
 
-				let folderNames = _.map(arrDirs, 'filename');
-				folderNames.unshift('');
+				return Promise.map(ls, (f) => {
+					return fs.statAsync(path.join(uploadsTempPath, f)).then((s) => { return { filename: f, stat: s }; });
+				}).filter((s) => { return s.stat.isFile(); }).then((arrFiles) => {
+					return Promise.map(arrFiles, (f) => {
 
-				ws.emit('uploadsSetFolders', {
-					auth: WSInternalKey,
-					content: folderNames
-				});
+						if(moment(f.stat.ctime).isBefore(fifteenAgo, 'minute')) {
+							return fs.unlinkAsync(path.join(uploadsTempPath, f.filename));
+						} else {
+							return true;
+						}
 
-				let allFiles = [];
-
-				// Travel each directory
-
-				return Promise.map(folderNames, (fldName) => {
-					let fldPath = path.join(uploadsPath, fldName);
-					return fs.readdirAsync(fldPath).then((fList) => {
-						return Promise.map(fList, (f) => {
-							return upl.processFile(fldName, f).then((mData) => {
-								if(mData) {
-									allFiles.push(mData);
-								}
-							});
-						}, {concurrency: 3});
-					});
-				}, {concurrency: 1}).finally(() => {
-
-					ws.emit('uploadsSetFiles', {
-						auth: WSInternalKey,
-						content: allFiles
 					});
-
 				});
 
-				return true;
-			});
-
-		}));
+			})
+		);
 
 		// ----------------------------------------
 		// Run
@@ -198,9 +178,11 @@ var job = new cron({
 
 			if(!jobUplWatchStarted) {
 				jobUplWatchStarted = true;
-				upl.watch();
+				upl.initialScan();
 			}
 
+			return true;
+
 		}).catch((err) => {
 			winston.error('[AGENT] One or more jobs have failed: ', err);
 		}).finally(() => {

文件差异内容过多而无法显示
+ 0 - 0
assets/js/app.js


文件差异内容过多而无法显示
+ 0 - 0
assets/js/libs.js


+ 2 - 2
client/js/components/alerts.js

@@ -104,9 +104,9 @@ class Alerts {
 
 		if(nAlertIdx >= 0 && nAlert) {
 			nAlert.class += ' exit';
-			self.mdl.children.$set(nAlertIdx, nAlert);
+			Vue.set(self.mdl.children, nAlertIdx, nAlert);
 			_.delay(() => {
-				self.mdl.children.$remove(nAlert);
+				self.mdl.children.splice(nAlertIdx, 1);
 			}, 500);
 		}
 

+ 4 - 9
client/js/components/search.js

@@ -6,11 +6,6 @@ jQuery( document ).ready(function( $ ) {
 
 		$('#search-input').focus();
 
-		Vue.transition('slide', {
-			enterClass: 'slideInDown',
-			leaveClass: 'fadeOutUp'
-		});
-
 		$('.searchresults').css('display', 'block');
 
 		var vueHeader = new Vue({
@@ -47,8 +42,8 @@ jQuery( document ).ready(function( $ ) {
 				},
 				searchmoveidx: (val, oldVal) => {
 					if(val > 0) {
-						vueHeader.searchmovekey = (vueHeader.searchmovearr[val - 1].document) ?
-																				'res.' + vueHeader.searchmovearr[val - 1].document.entryPath :
+						vueHeader.searchmovekey = (vueHeader.searchmovearr[val - 1]) ?
+																				'res.' + vueHeader.searchmovearr[val - 1]._id :
 																				'sug.' + vueHeader.searchmovearr[val - 1];
 					} else {
 						vueHeader.searchmovekey = '';
@@ -66,8 +61,8 @@ jQuery( document ).ready(function( $ ) {
 					if(vueHeader.searchmoveidx < 1) { return; }
 					let i = vueHeader.searchmoveidx - 1;
 
-					if(vueHeader.searchmovearr[i].document) {
-						window.location.assign('/' + vueHeader.searchmovearr[i].document.entryPath);
+					if(vueHeader.searchmovearr[i]) {
+						window.location.assign('/' + vueHeader.searchmovearr[i]._id);
 					} else {
 						vueHeader.searchq = vueHeader.searchmovearr[i];
 					}

+ 8 - 2
config.sample.yml

@@ -34,9 +34,15 @@ wsPort: 8080
 # Data Directories
 # ---------------------------------------------------------------------
 
-datadir:
+paths:
   repo: ./repo
-  db: ./data
+  data: ./data
+
+# ---------------------------------------------------------------------
+# Database Connection String
+# ---------------------------------------------------------------------
+
+db: mongodb://localhost:27017/wiki
 
 # ---------------------------------------------------------------------
 # Git Connection Info

+ 5 - 3
controllers/auth.js

@@ -2,14 +2,16 @@ var express = require('express');
 var router = express.Router();
 var passport = require('passport');
 var ExpressBrute = require('express-brute');
-var ExpressBruteLokiStore = require('express-brute-loki');
+var ExpressBruteMongoStore = require('express-brute-mongo');
 var moment = require('moment');
 
 /**
  * Setup Express-Brute
  */
-var EBstore = new ExpressBruteLokiStore({
-    path: './data/brute.db'
+var EBstore = new ExpressBruteMongoStore((ready) => {
+	db.onReady.then(() => {
+		ready(db.connection.collection('bruteforce-store'));
+	});
 });
 var bruteforce = new ExpressBrute(EBstore, {
 	freeRetries: 5,

+ 47 - 37
controllers/uploads.js

@@ -40,58 +40,68 @@ router.get('/t/*', (req, res, next) => {
 router.post('/img', lcdata.uploadImgHandler, (req, res, next) => {
 
 	let destFolder = _.chain(req.body.folder).trim().toLower().value();
-	let destFolderPath = lcdata.validateUploadsFolder(destFolder);
 
-	Promise.map(req.files, (f) => {
+	ws.emit('uploadsValidateFolder', {
+		auth: WSInternalKey,
+		content: destFolder
+	}, (destFolderPath) => {
+		
+		if(!destFolderPath) {
+			return res.json({ ok: false, msg: 'Invalid Folder' });
+		}
 
-		let destFilename = '';
-		let destFilePath = '';
+		Promise.map(req.files, (f) => {
 
-		return lcdata.validateUploadsFilename(f.originalname, destFolder).then((fname) => {
-			
-			destFilename = fname;
-			destFilePath = path.resolve(destFolderPath, destFilename);
+			let destFilename = '';
+			let destFilePath = '';
 
-			return readChunk(f.path, 0, 262);
+			return lcdata.validateUploadsFilename(f.originalname, destFolder).then((fname) => {
+				
+				destFilename = fname;
+				destFilePath = path.resolve(destFolderPath, destFilename);
 
-		}).then((buf) => {
+				return readChunk(f.path, 0, 262);
 
-			//-> Check MIME type by magic number
+			}).then((buf) => {
 
-			let mimeInfo = fileType(buf);
-			if(!_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], mimeInfo.mime)) {
-				return Promise.reject(new Error('Invalid file type.'));
-			}
-			return true;
+				//-> Check MIME type by magic number
 
-		}).then(() => {
+				let mimeInfo = fileType(buf);
+				if(!_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], mimeInfo.mime)) {
+					return Promise.reject(new Error('Invalid file type.'));
+				}
+				return true;
 
-			//-> Move file to final destination
+			}).then(() => {
 
-			return fs.moveAsync(f.path, destFilePath, { clobber: false });
+				//-> Move file to final destination
 
-		}).then(() => {
-			return {
-				ok: true,
-				filename: destFilename,
-				filesize: f.size
-			};
-		}).reflect();
+				return fs.moveAsync(f.path, destFilePath, { clobber: false });
 
-	}, {concurrency: 3}).then((results) => {
-		let uplResults = _.map(results, (r) => {
-			if(r.isFulfilled()) {
-				return r.value();
-			} else {
+			}).then(() => {
 				return {
-					ok: false,
-					msg: r.reason().message
+					ok: true,
+					filename: destFilename,
+					filesize: f.size
+				};
+			}).reflect();
+
+		}, {concurrency: 3}).then((results) => {
+			let uplResults = _.map(results, (r) => {
+				if(r.isFulfilled()) {
+					return r.value();
+				} else {
+					return {
+						ok: false,
+						msg: r.reason().message
+					}
 				}
-			}
+			});
+			res.json({ ok: true, results: uplResults });
+		}).catch((err) => {
+			res.json({ ok: false, msg: err.message });
 		});
-		res.json({ ok: true, results: uplResults });
-	}).catch((err) => {
-		res.json({ ok: false, msg: err.message });
+
 	});
 
 });

+ 1 - 1
gulpfile.js

@@ -78,7 +78,7 @@ var paths = {
 /**
  * TASK - Starts server in development mode
  */
-gulp.task('server', ['scripts', 'css', 'fonts'], function() {
+gulp.task('server', ['scripts', 'css'/*, 'fonts'*/], function() {
 	nodemon({
 		script: './server',
 		ignore: ['assets/', 'client/', 'data/', 'repo/', 'tests/'],

+ 125 - 18
models/uploads.js → models/agent/uploads.js

@@ -32,8 +32,8 @@ module.exports = {
 
 		let self = this;
 
-		self._uploadsPath = path.resolve(ROOTPATH, appconfig.datadir.repo, 'uploads');
-		self._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.datadir.db, 'thumbs');
+		self._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads');
+		self._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs');
 
 		return self;
 
@@ -51,22 +51,129 @@ module.exports = {
 			awaitWriteFinish: true
 		});
 
+		//-> Add new upload file
+
 		self._watcher.on('add', (p) => {
 
-			let pInfo = lcdata.parseUploadsRelPath(p);
+			let pInfo = self.parseUploadsRelPath(p);
 			return self.processFile(pInfo.folder, pInfo.filename).then((mData) => {
 				ws.emit('uploadsAddFiles', {
 					auth: WSInternalKey,
 					content: mData
 				});
 			}).then(() => {
-				return git.commitUploads();
+				return git.commitUploads('Uploaded ' + p);
+			});
+
+		});
+
+		//-> Remove upload file
+
+		self._watcher.on('unlink', (p) => {
+
+			let pInfo = self.parseUploadsRelPath(p);
+			return self.deleteFile(pInfo.folder, pInfo.filename).then((uID) => {
+				ws.emit('uploadsRemoveFiles', {
+					auth: WSInternalKey,
+					content: uID
+				});
+			}).then(() => {
+				return git.commitUploads('Deleted ' + p);
 			});
 
 		});
 
 	},
 
+	/**
+	 * Initial Uploads scan
+	 *
+	 * @return     {Promise<Void>}  Promise of the scan operation
+	 */
+	initialScan() {
+
+		let self = this;
+
+		return fs.readdirAsync(self._uploadsPath).then((ls) => {
+
+			// Get all folders
+
+			return Promise.map(ls, (f) => {
+				return fs.statAsync(path.join(self._uploadsPath, f)).then((s) => { return { filename: f, stat: s }; });
+			}).filter((s) => { return s.stat.isDirectory(); }).then((arrDirs) => {
+
+				let folderNames = _.map(arrDirs, 'filename');
+				folderNames.unshift('');
+
+				// Add folders to DB
+
+				return db.UplFolder.remove({}).then(() => {
+					return db.UplFolder.insertMany(_.map(folderNames, (f) => {
+						return { name: f };
+					}));
+				}).then(() => {
+
+					// Travel each directory and scan files
+
+					let allFiles = [];
+
+					return Promise.map(folderNames, (fldName) => {
+
+						let fldPath = path.join(self._uploadsPath, fldName);
+						return fs.readdirAsync(fldPath).then((fList) => {
+							return Promise.map(fList, (f) => {
+								return upl.processFile(fldName, f).then((mData) => {
+									if(mData) {
+										allFiles.push(mData);
+									}
+									return true;
+								});
+							}, {concurrency: 3});
+						});
+					}, {concurrency: 1}).finally(() => {
+
+						// Add files to DB
+
+						return db.UplFile.remove({}).then(() => {
+							if(_.isArray(allFiles) && allFiles.length > 0) {
+								return db.UplFile.insertMany(allFiles);
+							} else {
+								return true;
+							}
+						});
+
+					});
+
+				});
+				
+			});
+
+		}).then(() => {
+
+			// Watch for new changes
+
+			return upl.watch();
+
+		})
+
+	},
+
+	/**
+	 * Parse relative Uploads path
+	 *
+	 * @param      {String}  f       Relative Uploads path
+	 * @return     {Object}  Parsed path (folder and filename)
+	 */
+	parseUploadsRelPath(f) {
+
+		let fObj = path.parse(f);
+		return {
+			folder: fObj.dir,
+			filename: fObj.base
+		};
+
+	},
+
 	processFile(fldName, f) {
 
 		let self = this;
@@ -88,20 +195,21 @@ module.exports = {
 
 			if(s.size < 3145728) { // ignore files larger than 3MB
 				if(_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], mimeInfo.mime)) {
-					return self.getImageMetadata(fPath).then((mData) => {
+					return self.getImageMetadata(fPath).then((mImgData) => {
 
 						let cacheThumbnailPath = path.parse(path.join(self._uploadsThumbsPath, fUid + '.png'));
 						let cacheThumbnailPathStr = path.format(cacheThumbnailPath);
 
-						mData = _.pick(mData, ['format', 'width', 'height', 'density', 'hasAlpha', 'orientation']);
-						mData.uid = fUid;
-						mData.category = 'image';
-						mData.mime = mimeInfo.mime;
-						mData.folder = fldName;
-						mData.filename = f;
-						mData.basename = fPathObj.name;
-						mData.filesize = s.size;
-						mData.uploadedOn = moment().utc();
+						let mData = {
+							_id: fUid,
+							category: 'image',
+							mime: mimeInfo.mime,
+							extra: _.pick(mImgData, ['format', 'width', 'height', 'density', 'hasAlpha', 'orientation']),
+							folder: null,
+							filename: f,
+							basename: fPathObj.name,
+							filesize: s.size
+						}
 
 						// Generate thumbnail
 
@@ -124,14 +232,13 @@ module.exports = {
 			// Other Files
 			
 			return {
-				uid: fUid,
-				category: 'file',
+				_id: fUid,
+				category: 'binary',
 				mime: mimeInfo.mime,
 				folder: fldName,
 				filename: f,
 				basename: fPathObj.name,
-				filesize: s.size,
-				uploadedOn: moment().utc()
+				filesize: s.size
 			};
 
 		});

+ 2 - 2
models/auth.js

@@ -45,7 +45,7 @@ module.exports = function(passport, appconfig) {
 
     db.onReady.then(() => {
 
-        if(db.User.count() < 1) {
+        /*if(db.User.count() < 1) {
             winston.info('No administrator account found. Creating a new one...');
             if(db.User.insert({
                 email: appconfig.admin,
@@ -57,7 +57,7 @@ module.exports = function(passport, appconfig) {
             } else {
                 winston.error('An error occured while creating administrator account: ');
             }
-        }
+        }*/
 
         return true;
 

+ 0 - 52
models/db.js

@@ -1,52 +0,0 @@
-"use strict";
-
-var loki = require('lokijs'),
-	 fs   = require("fs"),
-	 path = require("path"),
-	 Promise = require('bluebird'),
-	 _ = require('lodash');
-
-var cols = ['User', 'Entry'];
-
-/**
- * Loki.js module
- *
- * @param      {Object}  appconfig  Application config
- * @return     {Object}  LokiJS instance
- */
-module.exports = function(appconfig) {
-
-	let dbReadyResolve;
-	let dbReady = new Promise((resolve, reject) => {
-		dbReadyResolve = resolve;
-	});
-
-	// Initialize Loki.js
-
-	let dbModel = {
-		Store: new loki(path.join(appconfig.datadir.db, 'app.db'), {
-			env: 'NODEJS',
-			autosave: true,
-			autosaveInterval: 5000
-		}),
-		onReady: dbReady
-	};
-
-	// Load Models
-
-	dbModel.Store.loadDatabase({}, () => {
-
-		_.forEach(cols, (col) => {
-			dbModel[col] = dbModel.Store.getCollection(col);
-			if(!dbModel[col]) {
-				dbModel[col] = dbModel.Store.addCollection(col);
-			}
-		});
-
-		dbReadyResolve();
-
-	});
-
-	return dbModel;
-
-};

+ 54 - 0
models/db/entry.js

@@ -0,0 +1,54 @@
+"use strict";
+
+const modb = require('mongoose'),
+			Promise = require('bluebird'),
+			_ = require('lodash');
+
+/**
+ * Entry schema
+ *
+ * @type       {<Mongoose.Schema>}
+ */
+var entrySchema = modb.Schema({
+
+	_id: String,
+
+  title: {
+    type: String,
+    required: true,
+    minlength: 2
+  },
+  subtitle: {
+    type: String,
+    default: ''
+  },
+  parent: {
+    type: String,
+    default: ''
+  },
+  content: {
+    type: String,
+    default: ''
+  }
+
+},
+{
+	timestamps: {}
+});
+
+entrySchema.index({
+  _id: 'text',
+  title: 'text',
+  subtitle: 'text',
+  content: 'text'
+}, {
+  weights: {
+    _id: 3,
+    title: 10,
+    subtitle: 5,
+    content: 1
+  },
+  name: 'EntriesTextIndex'
+});
+
+module.exports = modb.model('Entry', entrySchema);

+ 51 - 0
models/db/upl-file.js

@@ -0,0 +1,51 @@
+"use strict";
+
+const modb = require('mongoose'),
+			Promise = require('bluebird'),
+			_ = require('lodash');
+
+/**
+ * Upload File schema
+ *
+ * @type       {<Mongoose.Schema>}
+ */
+var uplFileSchema = modb.Schema({
+
+	_id: String,
+
+  category: {
+    type: String,
+    required: true,
+    default: 'binary'
+  },
+  mime: {
+    type: String,
+    required: true,
+    default: 'application/octet-stream'
+  },
+  extra: {
+    type: Object
+  },
+  folder: {
+    type: String,
+    ref: 'UplFolder'
+  },
+  filename: {
+    type: String,
+    required: true
+  },
+  basename: {
+    type: String,
+    required: true
+  },
+  filesize: {
+    type: Number,
+    required: true
+  }
+
+},
+{
+	timestamps: {}
+});
+
+module.exports = modb.model('UplFile', uplFileSchema);

+ 23 - 0
models/db/upl-folder.js

@@ -0,0 +1,23 @@
+"use strict";
+
+const modb = require('mongoose'),
+			Promise = require('bluebird'),
+			_ = require('lodash');
+
+/**
+ * Upload Folder schema
+ *
+ * @type       {<Mongoose.Schema>}
+ */
+var uplFolderSchema = modb.Schema({
+
+  name: {
+    type: String
+  }
+
+},
+{
+	timestamps: {}
+});
+
+module.exports = modb.model('UplFolder', uplFolderSchema);

+ 24 - 0
models/db/user.js

@@ -0,0 +1,24 @@
+"use strict";
+
+const modb = require('mongoose'),
+			Promise = require('bluebird'),
+			_ = require('lodash');
+
+/**
+ * Region schema
+ *
+ * @type       {<Mongoose.Schema>}
+ */
+var userSchema = modb.Schema({
+
+	email: {
+		type: String,
+		required: true
+	}
+
+},
+{
+	timestamps: {}
+});
+
+module.exports = modb.model('User', userSchema);

+ 8 - 6
models/entries.js

@@ -25,8 +25,8 @@ module.exports = {
 
 		let self = this;
 
-		self._repoPath = path.resolve(ROOTPATH, appconfig.datadir.repo);
-		self._cachePath = path.resolve(ROOTPATH, appconfig.datadir.db, 'cache');
+		self._repoPath = path.resolve(ROOTPATH, appconfig.paths.repo);
+		self._cachePath = path.resolve(ROOTPATH, appconfig.paths.data, 'cache');
 
 		return self;
 
@@ -321,11 +321,13 @@ module.exports = {
 				text: mark.removeMarkdown(pageData.markdown)
 			};
 		}).then((content) => {
-			ws.emit('searchAdd', {
-				auth: WSInternalKey,
-				content
+			return db.Entry.create({
+				_id: content.entryPath,
+				title: content.meta.title || content.entryPath,
+				subtitle: content.meta.subtitle || '',
+				parent: content.parent.title || '',
+				content: content.text || ''
 			});
-			return true;
 		});
 
 	},

+ 6 - 4
models/git.js

@@ -43,10 +43,10 @@ module.exports = {
 
 		//-> Build repository path
 		
-		if(_.isEmpty(appconfig.datadir.repo)) {
+		if(_.isEmpty(appconfig.paths.repo)) {
 			self._repo.path = path.join(ROOTPATH, 'repo');
 		} else {
-			self._repo.path = appconfig.datadir.repo;
+			self._repo.path = appconfig.paths.repo;
 		}
 
 		//-> Initialize repository
@@ -240,14 +240,16 @@ module.exports = {
 	/**
 	 * Commits uploads changes.
 	 *
+	 * @param      {String}   msg     The commit message
 	 * @return     {Promise}  Resolve on commit success
 	 */
-	commitUploads() {
+	commitUploads(msg) {
 
 		let self = this;
+		msg = msg || "Uploads repository sync";
 
 		return self._git.add('uploads').then(() => {
-			return self._git.commit("Uploads repository sync").catch((err) => {
+			return self._git.commit(msg).catch((err) => {
 			  if(_.includes(err.stdout, 'nothing to commit')) { return true; }
 			});
 		});

+ 0 - 335
models/localdata.js

@@ -1,335 +0,0 @@
-"use strict";
-
-var path = require('path'),
-	loki = require('lokijs'),
-	Promise = require('bluebird'),
-	fs = Promise.promisifyAll(require('fs-extra')),
-	multer  = require('multer'),
-	_ = require('lodash');
-
-var regFolderName = new RegExp("^[a-z0-9][a-z0-9\-]*[a-z0-9]$");
-
-/**
- * Local Data Storage
- *
- * @param      {Object}  appconfig  The application configuration
- */
-module.exports = {
-
-	_uploadsPath: './repo/uploads',
-	_uploadsThumbsPath: './data/thumbs',
-	_uploadsFolders: [],
-	_uploadsDb: null,
-
-	uploadImgHandler: null,
-
-	/**
-	 * Initialize Local Data Storage model
-	 *
-	 * @param      {Object}  appconfig  The application config
-	 * @return     {Object}  Local Data Storage model instance
-	 */
-	init(appconfig, mode = 'server') {
-
-		let self = this;
-
-		self._uploadsPath = path.resolve(ROOTPATH, appconfig.datadir.repo, 'uploads');
-		self._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.datadir.db, 'thumbs');
-
-		// Finish initialization tasks
-
-		switch(mode) {
-			case 'agent':
-				//todo
-			break;
-			case 'server':
-				self.createBaseDirectories(appconfig);
-				self.initMulter(appconfig);
-			break;
-			case 'ws':
-				self.initDb(appconfig);
-			break;
-		}
-
-		return self;
-
-	},
-
-	/**
-	 * Initialize Uploads DB
-	 *
-	 * @param      {Object}   appconfig  The application config
-	 * @return     {boolean}  Void
-	 */
-	initDb(appconfig) {
-
-		let self = this;
-
-		let dbReadyResolve;
-		let dbReady = new Promise((resolve, reject) => {
-			dbReadyResolve = resolve;
-		});
-
-		// Initialize Loki.js
-
-		let dbModel = {
-			Store: new loki(path.join(appconfig.datadir.db, 'uploads.db'), {
-				env: 'NODEJS',
-				autosave: true,
-				autosaveInterval: 15000
-			}),
-			onReady: dbReady
-		};
-
-		// Load Models
-
-		dbModel.Store.loadDatabase({}, () => {
-
-			dbModel.Files = dbModel.Store.getCollection('Files');
-			if(!dbModel.Files) {
-				dbModel.Files = dbModel.Store.addCollection('Files', {
-					indices: ['category', 'folder']
-				});
-			}
-
-			dbReadyResolve();
-
-		});
-
-		self._uploadsDb = dbModel;
-
-		return true;
-
-	},
-
-	/**
-	 * Init Multer upload handlers
-	 *
-	 * @param      {Object}   appconfig  The application config
-	 * @return     {boolean}  Void
-	 */
-	initMulter(appconfig) {
-
-		this.uploadImgHandler = multer({
-			storage: multer.diskStorage({
-				destination: (req, f, cb) => {
-					cb(null, path.resolve(ROOTPATH, appconfig.datadir.db, 'temp-upload'))
-				}
-			}),
-			fileFilter: (req, f, cb) => {
-
-				//-> Check filesize (3 MB max)
-
-				if(f.size > 3145728) {
-					return cb(null, false);
-				}
-
-				//-> Check MIME type (quick check only)
-
-				if(!_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], f.mimetype)) {
-					return cb(null, false);
-				}
-
-				cb(null, true);
-			}
-		}).array('imgfile', 20);
-
-		return true;
-
-	},
-
-	/**
-	 * Gets the thumbnails folder path.
-	 *
-	 * @return     {String}  The thumbs path.
-	 */
-	getThumbsPath() {
-		return this._uploadsThumbsPath;
-	},
-
-	/**
-	 * Creates a base directories (Synchronous).
-	 *
-	 * @param      {Object}  appconfig  The application config
-	 * @return     {Void}  Void
-	 */
-	createBaseDirectories(appconfig) {
-
-		winston.info('[SERVER] Checking data directories...');
-
-		try {
-			fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.datadir.db));
-			fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.datadir.db, './cache'));
-			fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.datadir.db, './thumbs'));
-			fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.datadir.db, './temp-upload'));
-
-			fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.datadir.repo));
-			fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.datadir.repo, './uploads'));
-		} catch (err) {
-			winston.error(err);
-		}
-
-		winston.info('[SERVER] Data and Repository directories are OK.');
-
-		return;
-
-	},
-
-	/**
-	 * Sets the uploads folders.
-	 *
-	 * @param      {Array<String>}  arrFolders  The arr folders
-	 * @return     {Void}  Void
-	 */
-	setUploadsFolders(arrFolders) {
-
-		this._uploadsFolders = arrFolders;
-		return;
-
-	},
-
-	/**
-	 * Gets the uploads folders.
-	 *
-	 * @return     {Array<String>}  The uploads folders.
-	 */
-	getUploadsFolders() {
-		return this._uploadsFolders;
-	},
-
-	/**
-	 * Creates an uploads folder.
-	 *
-	 * @param      {String}  folderName  The folder name
-	 * @return     {Promise}  Promise of the operation
-	 */
-	createUploadsFolder(folderName) {
-
-		let self = this;
-
-		folderName = _.kebabCase(_.trim(folderName));
-
-		if(_.isEmpty(folderName) || !regFolderName.test(folderName)) {
-			return Promise.resolve(self.getUploadsFolders());
-		}
-
-		return fs.ensureDirAsync(path.join(self._uploadsPath, folderName)).then(() => {
-			if(!_.includes(self._uploadsFolders, folderName)) {
-				self._uploadsFolders.push(folderName);
-				self._uploadsFolders = _.sortBy(self._uploadsFolders);
-			}
-			return self.getUploadsFolders();
-		});
-
-	},
-
-	/**
-	 * Check if folder is valid and exists
-	 *
-	 * @param      {String}  folderName  The folder name
-	 * @return     {Boolean}   True if valid
-	 */
-	validateUploadsFolder(folderName) {
-
-		folderName = (_.includes(this._uploadsFolders, folderName)) ? folderName : '';
-		return path.resolve(this._uploadsPath, folderName);
-
-	},
-
-	/**
-	 * Check if filename is valid and unique
-	 *
-	 * @param      {String}           f       The filename
-	 * @param      {String}           fld     The containing folder
-	 * @return     {Promise<String>}  Promise of the accepted filename
-	 */
-	validateUploadsFilename(f, fld) {
-
-		let fObj = path.parse(f);
-		let fname = _.chain(fObj.name).trim().toLower().kebabCase().value().replace(/[^a-z0-9\-]+/g, '');
-		let fext = _.toLower(fObj.ext);
-
-		if(!_.includes(['.jpg', '.jpeg', '.png', '.gif', '.webp'], fext)) {
-			fext = '.png';
-		}
-
-		f = fname + fext;
-		let fpath = path.resolve(this._uploadsPath, fld, f);
-
-		return fs.statAsync(fpath).then((s) => {
-			throw new Error('File ' + f + ' already exists.');
-		}).catch((err) => {
-			if(err.code === 'ENOENT') {
-				return f;
-			}
-			throw err;
-		});
-
-	},
-
-	/**
-	 * Parse relative Uploads path
-	 *
-	 * @param      {String}  f       Relative Uploads path
-	 * @return     {Object}  Parsed path (folder and filename)
-	 */
-	parseUploadsRelPath(f) {
-
-		let fObj = path.parse(f);
-		return {
-			folder: fObj.dir,
-			filename: fObj.base
-		};
-
-	},
-
-	/**
-	 * Sets the uploads files.
-	 *
-	 * @param      {Array<Object>}  arrFiles  The uploads files
-	 * @return     {Void}  Void
-	 */
-	setUploadsFiles(arrFiles) {
-
-		let self = this;
-
-		if(_.isArray(arrFiles) && arrFiles.length > 0) {
-			self._uploadsDb.Files.clear();
-			self._uploadsDb.Files.insert(arrFiles);
-			self._uploadsDb.Files.ensureIndex('category', true);
-			self._uploadsDb.Files.ensureIndex('folder', true);
-		}
-
-		return;
-
-	},
-
-	/**
-	 * Adds one or more uploads files.
-	 *
-	 * @param      {Array<Object>}  arrFiles  The uploads files
-	 * @return     {Void}  Void
-	 */
-	addUploadsFiles(arrFiles) {
-		if(_.isArray(arrFiles) || _.isPlainObject(arrFiles)) {
-			this._uploadsDb.Files.insert(arrFiles);
-		}
-		return;
-	},
-
-	/**
-	 * Gets the uploads files.
-	 *
-	 * @param      {String}  cat     Category type
-	 * @param      {String}  fld     Folder
-	 * @return     {Array<Object>}  The files matching the query
-	 */
-	getUploadsFiles(cat, fld) {
-
-		return this._uploadsDb.Files.chain().find({
-			'$and': [{ 'category' : cat	},{ 'folder' : fld }]
-		}).simplesort('filename').data();
-
-	}
-
-};

+ 64 - 0
models/mongo.js

@@ -0,0 +1,64 @@
+"use strict";
+
+const modb = require('mongoose'),
+			fs   = require("fs"),
+			path = require("path"),
+			_ = require('lodash');
+
+/**
+ * MongoDB module
+ *
+ * @param      {Object}  appconfig  Application config
+ * @return     {Object}  MongoDB wrapper instance
+ */
+module.exports = {
+
+	/**
+	 * Initialize DB
+	 *
+	 * @param      {Object}  appconfig  The application config
+	 * @return     {Object}  DB instance
+	 */
+	init(appconfig) {
+
+		let self = this;
+
+		let dbModelsPath = path.resolve(ROOTPATH, 'models', 'db');
+
+		modb.Promise = require('bluebird');
+
+		// Event handlers
+
+		modb.connection.on('error', (err) => {
+			winston.error('[' + PROCNAME + '] Failed to connect to MongoDB instance.');
+		});
+		modb.connection.once('open', function() {
+			winston.log('[' + PROCNAME + '] Connected to MongoDB instance.');
+		});
+
+		// Store connection handle
+
+		self.connection = modb.connection;
+		self.ObjectId = modb.Types.ObjectId;
+
+		// Load DB Models
+
+		fs
+		.readdirSync(dbModelsPath)
+		.filter(function(file) {
+			return (file.indexOf(".") !== 0);
+		})
+		.forEach(function(file) {
+			let modelName = _.upperFirst(_.camelCase(_.split(file,'.')[0]));
+			self[modelName] = require(path.join(dbModelsPath, file));
+		});
+
+		// Connect
+
+		self.onReady = modb.connect(appconfig.db);
+
+		return self;
+
+	}
+
+};

+ 0 - 198
models/search.js

@@ -1,198 +0,0 @@
-"use strict";
-
-var Promise = require('bluebird'),
-	_ = require('lodash'),
-	path = require('path'),
-	searchIndex = require('search-index'),
-	stopWord = require('stopword');
-
-/**
- * Search Model
- */
-module.exports = {
-
-	_si: null,
-
-	/**
-	 * Initialize Search model
-	 *
-	 * @param      {Object}  appconfig  The application config
-	 * @return     {Object}  Search model instance
-	 */
-	init(appconfig) {
-
-		let self = this;
-		let dbPath = path.resolve(ROOTPATH, appconfig.datadir.db, 'search');
-
-		searchIndex({
-			deletable: true,
-			fieldedSearch: true,
-			indexPath: dbPath,
-			logLevel: 'error',
-			stopwords: stopWord.getStopwords(appconfig.lang).sort()
-		}, (err, si) => {
-			if(err) {
-				winston.error('Failed to initialize search-index.', err);
-			} else {
-				self._si = Promise.promisifyAll(si);
-			}
-		});
-
-		return self;
-
-	},
-
-	find(terms) {
-
-		let self = this;
-		terms = _.chain(terms)
-							.deburr()
-							.toLower()
-							.trim()
-							.replace(/[^a-z0-9 ]/g, '')
-							.value();
-
-		let arrTerms = _.chain(terms)
-										.split(' ')
-										.filter((f) => { return !_.isEmpty(f); })
-										.value();
-
-
-		return self._si.searchAsync({
-			query: {
-				AND: [{ '*': arrTerms }]
-			},
-			pageSize: 10
-		}).get('hits').then((hits) => {
-
-			if(hits.length < 5) {
-				return self._si.matchAsync({
-					beginsWith: terms,
-					threshold: 3,
-					limit: 5,
-					type: 'simple'
-				}).then((matches) => {
-
-					return {
-						match: hits,
-						suggest: matches
-					};
-
-				});
-			} else {
-				return {
-					match: hits,
-					suggest: []
-				};
-			}
-
-		}).catch((err) => {
-
-			if(err.type === 'NotFoundError') {
-				return {
-					match: [],
-					suggest: []
-				};
-			} else {
-				winston.error(err);
-			}
-
-		});
-
-	},
-
-	/**
-	 * Add a document to the index
-	 *
-	 * @param      {Object}   content  Document content
-	 * @return     {Promise}  Promise of the add operation
-	 */
-	add(content) {
-
-		let self = this;
-
-		return self.delete(content.entryPath).then(() => {
-
-			return self._si.addAsync({
-				entryPath: content.entryPath,
-				title: content.meta.title,
-				subtitle: content.meta.subtitle || '',
-				parent: content.parent.title || '',
-				content: content.text || ''
-			}, {
-				fieldOptions: [{
-					fieldName: 'entryPath',
-					searchable: true,
-					weight: 2
-				},
-				{
-					fieldName: 'title',
-					nGramLength: [1, 2],
-					searchable: true,
-					weight: 3
-				},
-				{
-					fieldName: 'subtitle',
-					searchable: true,
-					weight: 1,
-					store: false
-				},
-				{
-					fieldName: 'parent',
-					searchable: false,
-				},
-				{
-					fieldName: 'content',
-					searchable: true,
-					weight: 0,
-					store: false
-				}]
-			}).then(() => {
-				winston.info('Entry ' + content.entryPath + ' added/updated to index.');
-				return true;
-			}).catch((err) => {
-				winston.error(err);
-			});
-
-		}).catch((err) => {
-			winston.error(err);
-		});
-
-	},
-
-	/**
-	 * Delete an entry from the index
-	 *
-	 * @param      {String}   The     entry path
-	 * @return     {Promise}  Promise of the operation
-	 */
-	delete(entryPath) {
-
-		let self = this;
-
-		return self._si.searchAsync({
-			query: {
-				AND: [{ 'entryPath': [entryPath] }]
-			}
-		}).then((results) => {
-
-			if(results.totalHits > 0) {
-				let delIds = _.map(results.hits, 'id');
-				return self._si.delAsync(delIds);
-			} else {
-				return true;
-			}
-
-		}).catch((err) => {
-
-			if(err.type === 'NotFoundError') {
-				return true;
-			} else {
-				winston.error(err);
-			}
-
-		});
-
-	}
-
-};

+ 152 - 0
models/server/local.js

@@ -0,0 +1,152 @@
+"use strict";
+
+var path = require('path'),
+	Promise = require('bluebird'),
+	fs = Promise.promisifyAll(require('fs-extra')),
+	multer  = require('multer'),
+	_ = require('lodash');
+
+/**
+ * Local Data Storage
+ *
+ * @param      {Object}  appconfig  The application configuration
+ */
+module.exports = {
+
+	_uploadsPath: './repo/uploads',
+	_uploadsThumbsPath: './data/thumbs',
+
+	uploadImgHandler: null,
+
+	/**
+	 * Initialize Local Data Storage model
+	 *
+	 * @param      {Object}  appconfig  The application config
+	 * @return     {Object}  Local Data Storage model instance
+	 */
+	init(appconfig) {
+
+		this._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads');
+		this._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs');
+
+		this.createBaseDirectories(appconfig);
+		this.initMulter(appconfig);
+
+		return this;
+
+	},
+
+	/**
+	 * Init Multer upload handlers
+	 *
+	 * @param      {Object}   appconfig  The application config
+	 * @return     {boolean}  Void
+	 */
+	initMulter(appconfig) {
+
+		this.uploadImgHandler = multer({
+			storage: multer.diskStorage({
+				destination: (req, f, cb) => {
+					cb(null, path.resolve(ROOTPATH, appconfig.paths.data, 'temp-upload'))
+				}
+			}),
+			fileFilter: (req, f, cb) => {
+
+				//-> Check filesize (3 MB max)
+
+				if(f.size > 3145728) {
+					return cb(null, false);
+				}
+
+				//-> Check MIME type (quick check only)
+
+				if(!_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], f.mimetype)) {
+					return cb(null, false);
+				}
+
+				cb(null, true);
+			}
+		}).array('imgfile', 20);
+
+		return true;
+
+	},
+
+	/**
+	 * Creates a base directories (Synchronous).
+	 *
+	 * @param      {Object}  appconfig  The application config
+	 * @return     {Void}  Void
+	 */
+	createBaseDirectories(appconfig) {
+
+		winston.info('[SERVER] Checking data directories...');
+
+		try {
+			fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data));
+			fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './cache'));
+			fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './thumbs'));
+			fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './temp-upload'));
+
+			fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo));
+			fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo, './uploads'));
+		} catch (err) {
+			winston.error(err);
+		}
+
+		winston.info('[SERVER] Data and Repository directories are OK.');
+
+		return;
+
+	},
+
+	/**
+	 * Gets the uploads path.
+	 *
+	 * @return     {String}  The uploads path.
+	 */
+	getUploadsPath() {
+		return this._uploadsPath;
+	},
+
+	/**
+	 * Gets the thumbnails folder path.
+	 *
+	 * @return     {String}  The thumbs path.
+	 */
+	getThumbsPath() {
+		return this._uploadsThumbsPath;
+	},
+
+	/**
+	 * Check if filename is valid and unique
+	 *
+	 * @param      {String}           f       The filename
+	 * @param      {String}           fld     The containing folder
+	 * @return     {Promise<String>}  Promise of the accepted filename
+	 */
+	validateUploadsFilename(f, fld) {
+
+		let fObj = path.parse(f);
+		let fname = _.chain(fObj.name).trim().toLower().kebabCase().value().replace(/[^a-z0-9\-]+/g, '');
+		let fext = _.toLower(fObj.ext);
+
+		if(!_.includes(['.jpg', '.jpeg', '.png', '.gif', '.webp'], fext)) {
+			fext = '.png';
+		}
+
+		f = fname + fext;
+		let fpath = path.resolve(this._uploadsPath, fld, f);
+
+		return fs.statAsync(fpath).then((s) => {
+			throw new Error('File ' + f + ' already exists.');
+		}).catch((err) => {
+			if(err.code === 'ENOENT') {
+				return f;
+			}
+			throw err;
+		});
+
+	},
+
+};

+ 133 - 0
models/ws/search.js

@@ -0,0 +1,133 @@
+"use strict";
+
+const Promise = require('bluebird'),
+			_ = require('lodash'),
+			path = require('path');
+
+/**
+ * Search Model
+ */
+module.exports = {
+
+	_si: null,
+
+	/**
+	 * Initialize Search model
+	 *
+	 * @param      {Object}  appconfig  The application config
+	 * @return     {Object}  Search model instance
+	 */
+	init(appconfig) {
+
+		let self = this;
+
+		return self;
+
+	},
+
+	find(terms) {
+
+		let self = this;
+		terms = _.chain(terms)
+							.deburr()
+							.toLower()
+							.trim()
+							.replace(/[^a-z0-9 ]/g, '')
+							.split(' ')
+							.filter((f) => { return !_.isEmpty(f); })
+							.join(' ')
+							.value();
+
+		return db.Entry.find(
+			{ $text: { $search: terms } },
+			{ score: { $meta: "textScore" }, title: 1 }
+		)
+		.sort({ score: { $meta: "textScore" } })
+		.limit(10)
+		.exec()
+		.then((hits) => {
+
+			/*if(hits.length < 5) {
+				return self._si.matchAsync({
+					beginsWith: terms,
+					threshold: 3,
+					limit: 5,
+					type: 'simple'
+				}).then((matches) => {
+
+					return {
+						match: hits,
+						suggest: matches
+					};
+
+				});
+			} else {*/
+				return {
+					match: hits,
+					suggest: []
+				};
+			//}
+
+		}).catch((err) => {
+
+			if(err.type === 'NotFoundError') {
+				return {
+					match: [],
+					suggest: []
+				};
+			} else {
+				winston.error(err);
+			}
+
+		});
+
+	},
+
+	/**
+	 * Delete an entry from the index
+	 *
+	 * @param      {String}   The     entry path
+	 * @return     {Promise}  Promise of the operation
+	 */
+	delete(entryPath) {
+
+		let self = this;
+		/*let hasResults = false;
+
+		return new Promise((resolve, reject) => {
+
+			self._si.search({
+				query: {
+					AND: { 'entryPath': [entryPath] }
+				}
+			}).on('data', (results) => {
+
+				hasResults = true;
+
+				if(results.totalHits > 0) {
+					let delIds = _.map(results.hits, 'id');
+					self._si.del(delIds).on('end', () => { return resolve(true); });
+				} else {
+					resolve(true);
+				}
+
+			}).on('error', (err) => {
+
+				if(err.type === 'NotFoundError') {
+					resolve(true);
+				} else {
+					winston.error(err);
+					reject(err);
+				}
+
+			}).on('end', () => {
+				if(!hasResults) {
+					resolve(true);
+				}
+			});
+
+		});*/
+
+	}
+
+};

+ 160 - 0
models/ws/uploads.js

@@ -0,0 +1,160 @@
+"use strict";
+
+var path = require('path'),
+	Promise = require('bluebird'),
+	fs = Promise.promisifyAll(require('fs-extra')),
+	multer  = require('multer'),
+	_ = require('lodash');
+
+var regFolderName = new RegExp("^[a-z0-9][a-z0-9\-]*[a-z0-9]$");
+
+/**
+ * Uploads
+ */
+module.exports = {
+
+	_uploadsPath: './repo/uploads',
+	_uploadsThumbsPath: './data/thumbs',
+
+	/**
+	 * Initialize Local Data Storage model
+	 *
+	 * @param      {Object}  appconfig  The application config
+	 * @return     {Object}  Uploads model instance
+	 */
+	init(appconfig) {
+
+		this._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads');
+		this._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs');
+
+		return this;
+
+	},
+
+	/**
+	 * Gets the thumbnails folder path.
+	 *
+	 * @return     {String}  The thumbs path.
+	 */
+	getThumbsPath() {
+		return this._uploadsThumbsPath;
+	},
+
+	/**
+	 * Sets the uploads folders.
+	 *
+	 * @param      {Array<String>}  arrFolders  The arr folders
+	 * @return     {Void}  Void
+	 */
+	setUploadsFolders(arrFolders) {
+
+		this._uploadsFolders = arrFolders;
+		return;
+
+	},
+
+	/**
+	 * Gets the uploads folders.
+	 *
+	 * @return     {Array<String>}  The uploads folders.
+	 */
+	getUploadsFolders() {
+		return this._uploadsFolders;
+	},
+
+	/**
+	 * Creates an uploads folder.
+	 *
+	 * @param      {String}  folderName  The folder name
+	 * @return     {Promise}  Promise of the operation
+	 */
+	createUploadsFolder(folderName) {
+
+		let self = this;
+
+		folderName = _.kebabCase(_.trim(folderName));
+
+		if(_.isEmpty(folderName) || !regFolderName.test(folderName)) {
+			return Promise.resolve(self.getUploadsFolders());
+		}
+
+		return fs.ensureDirAsync(path.join(self._uploadsPath, folderName)).then(() => {
+			if(!_.includes(self._uploadsFolders, folderName)) {
+				self._uploadsFolders.push(folderName);
+				self._uploadsFolders = _.sortBy(self._uploadsFolders);
+			}
+			return self.getUploadsFolders();
+		});
+
+	},
+
+	/**
+	 * Check if folder is valid and exists
+	 *
+	 * @param      {String}  folderName  The folder name
+	 * @return     {Boolean}   True if valid
+	 */
+	validateUploadsFolder(folderName) {
+
+		if(_.includes(this._uploadsFolders, folderName)) {
+			return path.resolve(this._uploadsPath, folderName);
+		} else {
+			return false;
+		}
+
+	},
+
+	/**
+	 * Sets the uploads files.
+	 *
+	 * @param      {Array<Object>}  arrFiles  The uploads files
+	 * @return     {Void}  Void
+	 */
+	setUploadsFiles(arrFiles) {
+
+		let self = this;
+
+		/*if(_.isArray(arrFiles) && arrFiles.length > 0) {
+			self._uploadsDb.Files.clear();
+			self._uploadsDb.Files.insert(arrFiles);
+			self._uploadsDb.Files.ensureIndex('category', true);
+			self._uploadsDb.Files.ensureIndex('folder', true);
+		}*/
+
+		return;
+
+	},
+
+	/**
+	 * Adds one or more uploads files.
+	 *
+	 * @param      {Array<Object>}  arrFiles  The uploads files
+	 * @return     {Void}  Void
+	 */
+	addUploadsFiles(arrFiles) {
+		if(_.isArray(arrFiles) || _.isPlainObject(arrFiles)) {
+			//this._uploadsDb.Files.insert(arrFiles);
+		}
+		return;
+	},
+
+	/**
+	 * Gets the uploads files.
+	 *
+	 * @param      {String}  cat     Category type
+	 * @param      {String}  fld     Folder
+	 * @return     {Array<Object>}  The files matching the query
+	 */
+	getUploadsFiles(cat, fld) {
+
+		return /*this._uploadsDb.Files.chain().find({
+			'$and': [{ 'category' : cat	},{ 'folder' : fld }]
+		}).simplesort('filename').data()*/;
+
+	},
+
+	deleteUploadsFile(fldName, f) {
+
+	}
+
+};

+ 14 - 17
package.json

@@ -29,26 +29,24 @@
   },
   "homepage": "https://github.com/Requarks/wiki#readme",
   "engines": {
-    "node": ">=4.4.5"
+    "node": ">=4.6"
   },
   "dependencies": {
     "auto-load": "^2.1.0",
     "bcryptjs-then": "^1.0.1",
     "bluebird": "^3.4.6",
     "body-parser": "^1.15.2",
-    "bson": "^0.5.5",
     "cheerio": "^0.22.0",
     "child-process-promise": "^2.1.3",
     "chokidar": "^1.6.0",
     "compression": "^1.6.2",
     "connect-flash": "^0.1.1",
-    "connect-loki": "^1.0.6",
-    "connect-redis": "^3.1.0",
+    "connect-mongo": "^1.3.2",
     "cookie-parser": "^1.4.3",
-    "cron": "^1.1.0",
+    "cron": "^1.1.1",
     "express": "^4.14.0",
     "express-brute": "^1.0.0",
-    "express-brute-loki": "^1.0.0",
+    "express-brute-mongo": "^0.1.0",
     "express-session": "^1.14.1",
     "express-validator": "^2.20.10",
     "farmhash": "^1.2.1",
@@ -61,31 +59,30 @@
     "i18next-express-middleware": "^1.0.2",
     "i18next-node-fs-backend": "^0.1.2",
     "js-yaml": "^3.6.1",
-    "lodash": "^4.16.2",
-    "lokijs": "^1.4.1",
+    "lodash": "^4.16.4",
     "markdown-it": "^8.0.0",
     "markdown-it-abbr": "^1.0.4",
     "markdown-it-anchor": "^2.5.0",
     "markdown-it-attrs": "^0.7.1",
-    "markdown-it-emoji": "^1.2.0",
+    "markdown-it-emoji": "^1.3.0",
     "markdown-it-expand-tabs": "^1.0.11",
-    "markdown-it-external-links": "0.0.5",
+    "markdown-it-external-links": "0.0.6",
     "markdown-it-footnote": "^3.0.1",
     "markdown-it-task-lists": "^1.4.1",
     "moment": "^2.15.1",
-    "moment-timezone": "^0.5.5",
+    "moment-timezone": "^0.5.6",
+    "mongoose": "^4.6.3",
     "multer": "^1.2.0",
     "passport": "^0.3.2",
     "passport-local": "^1.0.0",
     "pug": "^2.0.0-beta6",
     "read-chunk": "^2.0.0",
     "remove-markdown": "^0.1.0",
-    "search-index": "^0.8.15",
     "serve-favicon": "^2.3.0",
     "sharp": "^0.16.0",
     "simplemde": "^1.11.2",
     "snyk": "^1.19.1",
-    "socket.io": "^1.4.8",
+    "socket.io": "^1.5.0",
     "sticky-js": "^1.0.7",
     "validator": "^6.0.0",
     "validator-as-promised": "^1.0.2",
@@ -96,7 +93,7 @@
     "babel-preset-es2015": "^6.16.0",
     "bulma": "^0.1.2",
     "chai": "^3.5.0",
-    "chai-as-promised": "^5.3.0",
+    "chai-as-promised": "^6.0.0",
     "codacy-coverage": "^2.0.0",
     "filesize.js": "^1.0.1",
     "font-awesome": "^4.6.3",
@@ -120,10 +117,10 @@
     "merge-stream": "^1.0.0",
     "mocha": "^3.1.0",
     "mocha-lcov-reporter": "^1.2.0",
-    "nodemon": "^1.10.2",
-    "sticky-js": "^1.1.0",
+    "nodemon": "^1.11.0",
+    "sticky-js": "^1.1.2",
     "twemoji-awesome": "^1.0.4",
-    "vue": "^1.0.28"
+    "vue": "^2.0.1"
   },
   "snyk": true
 }

+ 7 - 4
server.js

@@ -20,8 +20,8 @@ winston.info('[SERVER] Requarks Wiki is initializing...');
 // ----------------------------------------
 
 var appconfig = require('./models/config')('./config.yml');
-global.lcdata = require('./models/localdata').init(appconfig, 'server');
-global.db = require('./models/db')(appconfig);
+global.lcdata = require('./models/server/local').init(appconfig);
+global.db = require('./models/mongo').init(appconfig);
 global.git = require('./models/git').init(appconfig, false);
 global.entries = require('./models/entries').init(appconfig);
 global.mark = require('./models/markdown');
@@ -35,7 +35,7 @@ var express = require('express');
 var path = require('path');
 var favicon = require('serve-favicon');
 var session = require('express-session');
-var lokiStore = require('connect-loki')(session);
+const mongoStore = require('connect-mongo')(session);
 var cookieParser = require('cookie-parser');
 var bodyParser = require('body-parser');
 var flash = require('connect-flash');
@@ -73,7 +73,10 @@ var strategy = require('./models/auth')(passport, appconfig);
 app.use(cookieParser());
 app.use(session({
   name: 'requarkswiki.sid',
-  store: new lokiStore({ path: path.join(appconfig.datadir.db, 'sessions.db') }),
+  store: new mongoStore({
+    mongooseConnection: db.connection,
+    touchAfter: 15
+  }),
   secret: appconfig.sessionSecret,
   resave: false,
   saveUninitialized: false

+ 15 - 14
views/common/header.pug

@@ -20,19 +20,20 @@
 			block rootNavRight
 				i.nav-item#notifload
 
-	.box.searchresults.animated(v-show='searchactive', transition='slide', v-cloak, style={'display':'none'})
-		.menu
-			p.menu-label
-				| Search Results
-			ul.menu-list
-				li(v-if="searchres.length === 0")
-					a: em No results matching your query
-				li(v-for='sres in searchres')
-					a(href='/{{ sres.document.entryPath }}', v-bind:class="{ 'is-active': searchmovekey === 'res.' + sres.document.entryPath }") {{ sres.document.title }}
-			p.menu-label(v-if='searchsuggest.length > 0')
-				| Did you mean...?
-			ul.menu-list(v-if='searchsuggest.length > 0')
-				li(v-for='sug in searchsuggest')
-					a(v-on:click="useSuggestion(sug)", v-bind:class="{ 'is-active': searchmovekey === 'sug.' + sug }") {{ sug }}
+	transition(name="searchresults-anim", enter-active-class="slideInDown", leave-active-class="fadeOutUp")
+		.box.searchresults.animated(v-show='searchactive', v-cloak, style={'display':'none'})
+			.menu
+				p.menu-label
+					| Search Results
+				ul.menu-list
+					li(v-if="searchres.length === 0")
+						a: em No results matching your query
+					li(v-for='sres in searchres')
+						a(v-bind:href="'/' + sres._id", v-bind:class="{ 'is-active': searchmovekey === 'res.' + sres._id }") {{ sres.title }}
+				p.menu-label(v-if='searchsuggest.length > 0')
+					| Did you mean...?
+				ul.menu-list(v-if='searchsuggest.length > 0')
+					li(v-for='sug in searchsuggest')
+						a(v-on:click="useSuggestion(sug)", v-bind:class="{ 'is-active': searchmovekey === 'sug.' + sug }") {{ sug }}
 
 

+ 24 - 22
ws-server.js

@@ -31,11 +31,11 @@ global.internalAuth = require('./lib/internalAuth').init(process.argv[2]);;
 winston.info('[WS] WS Server is initializing...');
 
 var appconfig = require('./models/config')('./config.yml');
-let lcdata = require('./models/localdata').init(appconfig, 'ws');
-
+global.db = require('./models/mongo').init(appconfig);
+global.upl = require('./models/ws/uploads').init(appconfig);
 global.entries = require('./models/entries').init(appconfig);
 global.mark = require('./models/markdown');
-global.search = require('./models/search').init(appconfig);
+global.search = require('./models/ws/search').init(appconfig);
 
 // ----------------------------------------
 // Load local modules
@@ -108,14 +108,14 @@ io.on('connection', (socket) => {
 	});
 
 	socket.on('searchDel', (data, cb) => {
-		cb = cb || _.noop
+		cb = cb || _.noop;
 		if(internalAuth.validateKey(data.auth)) {
 			search.delete(data.entryPath);
 		}
 	});
 
 	socket.on('search', (data, cb) => {
-		cb = cb || _.noop
+		cb = cb || _.noop;
 		search.find(data.terms).then((results) => {
 			cb(results);
 		});
@@ -125,44 +125,46 @@ io.on('connection', (socket) => {
 	// UPLOADS
 	//-----------------------------------------
 
-	socket.on('uploadsSetFolders', (data, cb) => {
-		cb = cb || _.noop
+	socket.on('uploadsSetFolders', (data) => {
 		if(internalAuth.validateKey(data.auth)) {
-			lcdata.setUploadsFolders(data.content);
+			upl.setUploadsFolders(data.content);
 		}
 	});
 
 	socket.on('uploadsGetFolders', (data, cb) => {
-		cb = cb || _.noop
-		cb(lcdata.getUploadsFolders());
+		cb = cb || _.noop;
+		cb(upl.getUploadsFolders());
+	});
+
+	socket.on('uploadsValidateFolder', (data, cb) => {
+		cb = cb || _.noop;
+		if(internalAuth.validateKey(data.auth)) {
+			cb(upl.validateUploadsFolder(data.content));
+		}
 	});
 
 	socket.on('uploadsCreateFolder', (data, cb) => {
-		cb = cb || _.noop
-		lcdata.createUploadsFolder(data.foldername).then((fldList) => {
+		cb = cb || _.noop;
+		upl.createUploadsFolder(data.foldername).then((fldList) => {
 			cb(fldList);
 		});
 	});
 
-	socket.on('uploadsSetFiles', (data, cb) => {
-		cb = cb || _.noop;
+	socket.on('uploadsSetFiles', (data) => {
 		if(internalAuth.validateKey(data.auth)) {
-			lcdata.setUploadsFiles(data.content);
-			cb(true);
+			upl.setUploadsFiles(data.content);
 		}
 	});
 
-	socket.on('uploadsAddFiles', (data, cb) => {
-		cb = cb || _.noop
+	socket.on('uploadsAddFiles', (data) => {
 		if(internalAuth.validateKey(data.auth)) {
-			lcdata.addUploadsFiles(data.content);
-			cb(true);
+			upl.addUploadsFiles(data.content);
 		}
 	});
 
 	socket.on('uploadsGetImages', (data, cb) => {
-		cb = cb || _.noop
-		cb(lcdata.getUploadsFiles('image', data.folder));
+		cb = cb || _.noop;
+		cb(upl.getUploadsFiles('image', data.folder));
 	});
 
 });

部分文件因为文件数量过多而无法显示