Selaa lähdekoodia

Files Management + Editor Modal + Code Editor fixes

NGPixel 8 vuotta sitten
vanhempi
sitoutus
9caaeee682

+ 8 - 3
README.md

@@ -12,17 +12,18 @@
 [![Known Vulnerabilities](https://snyk.io/test/github/requarks/wiki/badge.svg)](https://snyk.io/test/github/requarks/wiki)
 
 ##### A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown
-*Under development*
+*Under active development*
 
 ### Documentation
 
-- [Installation Guide](https://wiki.requarks.io/install)
+- [Official Website](https://wiki.requarks.io/)
+- [Installation Guide](https://wiki.requarks.io/get-started.html)
 
 ##### Milestones
 - [ ] Account Management
 - [x] Assets Management
 	- [x] Images
-	- [ ] Files/Documents
+	- [x] Files/Documents
 - [x] Authentication
 	- [x] Strategies
 		- [x] Local
@@ -46,6 +47,10 @@
 - [x] Markdown Editor
 	- [x] Basic Formatting
 	- [ ] Links
+	- [x] Image Selection modal
+	- [x] File Selection modal
+	- [x] Inline Code
+	- [x] Code Editor modal
 	- [ ] Table Editor
 - [x] Move Entry
 - [x] Navigation

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
assets/css/app.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
assets/js/app.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
assets/js/libs.js


+ 15 - 11
client/js/components/editor-codeblock.js

@@ -1,12 +1,6 @@
 
-let codeEditor = ace.edit("codeblock-editor");
-codeEditor.setTheme("ace/theme/tomorrow_night");
-codeEditor.getSession().setMode("ace/mode/markdown");
-codeEditor.setOption('fontSize', '14px');
-codeEditor.setOption('hScrollBarAlwaysVisible', false);
-codeEditor.setOption('wrap', true);
-
 let modelist = ace.require("ace/ext/modelist");
+let codeEditor = null;
 
 // ACE - Mode Loader
 
@@ -33,7 +27,8 @@ let vueCodeBlock = new Vue({
 	el: '#modal-editor-codeblock',
 	data: {
 		modes: modelist.modesByName,
-		modeSelected: 'text'
+		modeSelected: 'text',
+		initContent: ''
 	},
 	watch: {
 		modeSelected: (val, oldVal) => {
@@ -45,19 +40,28 @@ let vueCodeBlock = new Vue({
 	},
 	methods: {
 		open: (ev) => {
+
 			$('#modal-editor-codeblock').addClass('is-active');
 
 			_.delay(() => {
-				codeEditor.resize();
+				codeEditor = ace.edit("codeblock-editor");
+				codeEditor.setTheme("ace/theme/tomorrow_night");
+				codeEditor.getSession().setMode("ace/mode/" + vueCodeBlock.modeSelected);
+				codeEditor.setOption('fontSize', '14px');
+				codeEditor.setOption('hScrollBarAlwaysVisible', false);
+				codeEditor.setOption('wrap', true);
+
+				codeEditor.setValue(vueCodeBlock.initContent);
+
 				codeEditor.focus();
-				codeEditor.setAutoScrollEditorIntoView(true);
 				codeEditor.renderer.updateFull();
-			}, 1000);
+			}, 300);
 			
 		},
 		cancel: (ev) => {
 			mdeModalOpenState = false;
 			$('#modal-editor-codeblock').removeClass('is-active');
+			vueCodeBlock.initContent = '';
 		},
 		insertCode: (ev) => {
 

+ 365 - 0
client/js/components/editor-file.js

@@ -0,0 +1,365 @@
+
+let vueFile = new Vue({
+	el: '#modal-editor-file',
+	data: {
+		isLoading: false,
+		isLoadingText: '',
+		newFolderName: '',
+		newFolderShow: false,
+		newFolderError: false,
+		folders: [],
+		currentFolder: '',
+		currentFile: '',
+		files: [],
+		uploadSucceeded: false,
+		postUploadChecks: 0,
+		renameFileShow: false,
+		renameFileId: '',
+		renameFileFilename: '',
+		deleteFileShow: false,
+		deleteFileId: '',
+		deleteFileFilename: ''
+	},
+	methods: {
+
+		open: () => {
+			mdeModalOpenState = true;
+			$('#modal-editor-file').addClass('is-active');
+			vueFile.refreshFolders();
+		},
+		cancel: (ev) => {
+			mdeModalOpenState = false;
+			$('#modal-editor-file').removeClass('is-active');
+		},
+
+		// -------------------------------------------
+		// INSERT LINK TO FILE
+		// -------------------------------------------
+
+		selectFile: (fileId) => {
+			vueFile.currentFile = fileId;
+		},
+		insertFileLink: (ev) => {
+
+			if(mde.codemirror.doc.somethingSelected()) {
+				mde.codemirror.execCommand('singleSelection');
+			}
+
+			let selFile = _.find(vueFile.files, ['_id', vueFile.currentFile]);
+			selFile.normalizedPath = (selFile.folder === 'f:') ? selFile.filename : selFile.folder.slice(2) + '/' + selFile.filename;
+			selFile.titleGuess = _.startCase(selFile.basename);
+
+			let fileText = '[' + selFile.titleGuess + '](/uploads/' + selFile.normalizedPath + ' "' + selFile.titleGuess + '")';
+
+			mde.codemirror.doc.replaceSelection(fileText);
+			vueFile.cancel();
+
+		},
+
+		// -------------------------------------------
+		// NEW FOLDER
+		// -------------------------------------------
+
+		newFolder: (ev) => {
+			vueFile.newFolderName = '';
+			vueFile.newFolderError = false;
+			vueFile.newFolderShow = true;
+			_.delay(() => { $('#txt-editor-file-newfoldername').focus(); }, 400);
+		},
+		newFolderDiscard: (ev) => {
+			vueFile.newFolderShow = false;
+		},
+		newFolderCreate: (ev) => {
+
+			let regFolderName = new RegExp("^[a-z0-9][a-z0-9\-]*[a-z0-9]$");
+			vueFile.newFolderName = _.kebabCase(_.trim(vueFile.newFolderName));
+
+			if(_.isEmpty(vueFile.newFolderName) || !regFolderName.test(vueFile.newFolderName)) {
+				vueFile.newFolderError = true;
+				return;
+			}
+
+			vueFile.newFolderDiscard();
+			vueFile.isLoadingText = 'Creating new folder...';
+			vueFile.isLoading = true;
+
+			Vue.nextTick(() => {
+				socket.emit('uploadsCreateFolder', { foldername: vueFile.newFolderName }, (data) => {
+					vueFile.folders = data;
+					vueFile.currentFolder = vueFile.newFolderName;
+					vueFile.files = [];
+					vueFile.isLoading = false;
+				});
+			});
+
+		},
+
+		// -------------------------------------------
+		// RENAME FILE
+		// -------------------------------------------
+
+		renameFile: () => {
+
+			let c = _.find(vueFile.files, ['_id', vueFile.renameFileId ]);
+			vueFile.renameFileFilename = c.basename || '';
+			vueFile.renameFileShow = true;
+			_.delay(() => {
+				$('#txt-editor-renamefile').focus();
+				_.defer(() => { $('#txt-editor-file-rename').select(); });
+			}, 400);
+		},
+		renameFileDiscard: () => {
+			vueFile.renameFileShow = false;
+		},
+		renameFileGo: () => {
+			
+			vueFile.renameFileDiscard();
+			vueFile.isLoadingText = 'Renaming file...';
+			vueFile.isLoading = true;
+
+			Vue.nextTick(() => {
+				socket.emit('uploadsRenameFile', { uid: vueFile.renameFileId, folder: vueFile.currentFolder, filename: vueFile.renameFileFilename }, (data) => {
+					if(data.ok) {
+						vueFile.waitChangeComplete(vueFile.files.length, false);
+					} else {
+						vueFile.isLoading = false;
+						alerts.pushError('Rename error', data.msg);
+					}
+				});
+			});
+
+		},
+
+		// -------------------------------------------
+		// MOVE FILE
+		// -------------------------------------------
+
+		moveFile: (uid, fld) => {
+			vueFile.isLoadingText = 'Moving file...';
+			vueFile.isLoading = true;
+			Vue.nextTick(() => {
+				socket.emit('uploadsMoveFile', { uid, folder: fld }, (data) => {
+					if(data.ok) {
+						vueFile.loadFiles();
+					} else {
+						vueFile.isLoading = false;
+						alerts.pushError('Rename error', data.msg);
+					}
+				});
+			});
+		},
+
+		// -------------------------------------------
+		// DELETE FILE
+		// -------------------------------------------
+
+		deleteFileWarn: (show) => {
+			if(show) {
+				let c = _.find(vueFile.files, ['_id', vueFile.deleteFileId ]);
+				vueFile.deleteFileFilename = c.filename || 'this file';
+			}
+			vueFile.deleteFileShow = show;
+		},
+		deleteFileGo: () => {
+			vueFile.deleteFileWarn(false);
+			vueFile.isLoadingText = 'Deleting file...';
+			vueFile.isLoading = true;
+			Vue.nextTick(() => {
+				socket.emit('uploadsDeleteFile', { uid: vueFile.deleteFileId }, (data) => {
+					vueFile.loadFiles();
+				});
+			});
+		},
+
+		// -------------------------------------------
+		// LOAD FROM REMOTE
+		// -------------------------------------------
+
+		selectFolder: (fldName) => {
+			vueFile.currentFolder = fldName;
+			vueFile.loadFiles();
+		},
+
+		refreshFolders: () => {
+			vueFile.isLoadingText = 'Fetching folders list...';
+			vueFile.isLoading = true;
+			vueFile.currentFolder = '';
+			vueFile.currentImage = '';
+			Vue.nextTick(() => {
+				socket.emit('uploadsGetFolders', { }, (data) => {
+					vueFile.folders = data;
+					vueFile.loadFiles();
+				});
+			});
+		},
+
+		loadFiles: (silent) => {
+			if(!silent) {
+				vueFile.isLoadingText = 'Fetching files...';
+				vueFile.isLoading = true;
+			}
+			return new Promise((resolve, reject) => {
+				Vue.nextTick(() => {
+					socket.emit('uploadsGetFiles', { folder: vueFile.currentFolder }, (data) => {
+						vueFile.files = data;
+						if(!silent) {
+							vueFile.isLoading = false;
+						}
+						vueFile.attachContextMenus();
+						resolve(true);
+					});
+				});
+			});
+		},
+
+		waitChangeComplete: (oldAmount, expectChange) => {
+
+			expectChange = (_.isBoolean(expectChange)) ? expectChange : true;
+
+			vueFile.postUploadChecks++;
+			vueFile.isLoadingText = 'Processing...';
+
+			Vue.nextTick(() => {
+				vueFile.loadFiles(true).then(() => {
+					if((vueFile.files.length !== oldAmount) === expectChange) {
+						vueFile.postUploadChecks = 0;
+						vueFile.isLoading = false;
+					} else if(vueFile.postUploadChecks > 5) {
+						vueFile.postUploadChecks = 0;
+						vueFile.isLoading = false;
+						alerts.pushError('Unable to fetch updated listing', 'Try again later');
+					} else {
+						_.delay(() => {
+							vueFile.waitChangeComplete(oldAmount, expectChange);
+						}, 1500);
+					}
+				});
+			});
+
+		},
+
+		// -------------------------------------------
+		// IMAGE CONTEXT MENU
+		// -------------------------------------------
+		
+		attachContextMenus: () => {
+
+			let moveFolders = _.map(vueFile.folders, (f) => {
+				return {
+					name: (f !== '') ? f : '/ (root)',
+					icon: 'fa-folder',
+					callback: (key, opt) => {
+						let moveFileId = _.toString($(opt.$trigger).data('uid'));
+						let moveFileDestFolder = _.nth(vueFile.folders, key);
+						vueFile.moveFile(moveFileId, moveFileDestFolder);
+					}
+				};
+			});
+
+			$.contextMenu('destroy', '.editor-modal-file-choices > figure');
+			$.contextMenu({
+				selector: '.editor-modal-file-choices > figure',
+				appendTo: '.editor-modal-file-choices',
+				position: (opt, x, y) => {
+					$(opt.$trigger).addClass('is-contextopen');
+					let trigPos = $(opt.$trigger).position();
+					let trigDim = { w: $(opt.$trigger).width() / 5, h: $(opt.$trigger).height() / 2 };
+					opt.$menu.css({ top: trigPos.top + trigDim.h, left: trigPos.left + trigDim.w });
+				},
+				events: {
+					hide: (opt) => {
+						$(opt.$trigger).removeClass('is-contextopen');   
+					}
+				},
+				items: {
+					rename: {
+						name: "Rename",
+						icon: "fa-edit",
+						callback: (key, opt) => {
+							vueFile.renameFileId = _.toString(opt.$trigger[0].dataset.uid);
+							vueFile.renameFile();
+						}
+					},
+					move: {
+						name: "Move to...",
+						icon: "fa-folder-open-o",
+						items: moveFolders
+					},
+					delete: {
+						name: "Delete",
+						icon: "fa-trash",
+						callback: (key, opt) => {
+							vueFile.deleteFileId = _.toString(opt.$trigger[0].dataset.uid);
+							vueFile.deleteFileWarn(true);
+						}
+					}
+				}
+			});
+		}
+
+	}
+});
+
+$('#btn-editor-file-upload input').on('change', (ev) => {
+
+	let curFileAmount = vueFile.files.length;
+
+	$(ev.currentTarget).simpleUpload("/uploads/file", {
+
+		name: 'binfile',
+		data: {
+			folder: vueFile.currentFolder
+		},
+		limit: 20,
+		expect: 'json',
+		maxFileSize: 0,
+
+		init: (totalUploads) => {
+			vueFile.uploadSucceeded = false;
+			vueFile.isLoadingText = 'Preparing to upload...';
+			vueFile.isLoading = true;
+		},
+
+		progress: (progress) => {
+			vueFile.isLoadingText = 'Uploading...' + Math.round(progress) + '%';
+		},
+
+		success: (data) => {
+			if(data.ok) {
+
+				let failedUpls = _.filter(data.results, ['ok', false]);
+				if(failedUpls.length) {
+					_.forEach(failedUpls, (u) => {
+						alerts.pushError('Upload error', u.msg);
+					});
+					if(failedUpls.length < data.results.length) {
+						alerts.push({
+							title: 'Some uploads succeeded',
+							message: 'Files that are not mentionned in the errors above were uploaded successfully.'
+						});
+						vueFile.uploadSucceeded = true;
+					} 
+				} else {
+					vueFile.uploadSucceeded = true;
+				}
+
+			} else {
+				alerts.pushError('Upload error', data.msg);
+			}
+		},
+
+		error: (error) => {
+			alerts.pushError(error.message, this.upload.file.name);
+		},
+
+		finish: () => {
+			if(vueFile.uploadSucceeded) {
+				vueFile.waitChangeComplete(curFileAmount, true);
+			} else {
+				vueFile.isLoading = false;
+			}
+		}
+
+	});
+
+});

+ 8 - 8
client/js/components/editor-image.js

@@ -78,7 +78,7 @@ let vueImage = new Vue({
 			vueImage.newFolderName = '';
 			vueImage.newFolderError = false;
 			vueImage.newFolderShow = true;
-			_.delay(() => { $('#txt-editor-newfoldername').focus(); }, 400);
+			_.delay(() => { $('#txt-editor-image-newfoldername').focus(); }, 400);
 		},
 		newFolderDiscard: (ev) => {
 			vueImage.newFolderShow = false;
@@ -115,7 +115,7 @@ let vueImage = new Vue({
 		fetchFromUrl: (ev) => {
 			vueImage.fetchFromUrlURL = '';
 			vueImage.fetchFromUrlShow = true;
-			_.delay(() => { $('#txt-editor-fetchimgurl').focus(); }, 400);
+			_.delay(() => { $('#txt-editor-image-fetchurl').focus(); }, 400);
 		},
 		fetchFromUrlDiscard: (ev) => {
 			vueImage.fetchFromUrlShow = false;
@@ -149,8 +149,8 @@ let vueImage = new Vue({
 			vueImage.renameImageFilename = c.basename || '';
 			vueImage.renameImageShow = true;
 			_.delay(() => {
-				$('#txt-editor-renameimage').focus();
-				_.defer(() => { $('#txt-editor-renameimage').select(); });
+				$('#txt-editor-image-rename').focus();
+				_.defer(() => { $('#txt-editor-image-rename').select(); });
 			}, 400);
 		},
 		renameImageDiscard: () => {
@@ -301,10 +301,10 @@ let vueImage = new Vue({
 				};
 			});
 
-			$.contextMenu('destroy', '.editor-modal-imagechoices > figure');
+			$.contextMenu('destroy', '.editor-modal-image-choices > figure');
 			$.contextMenu({
-				selector: '.editor-modal-imagechoices > figure',
-				appendTo: '.editor-modal-imagechoices',
+				selector: '.editor-modal-image-choices > figure',
+				appendTo: '.editor-modal-image-choices',
 				position: (opt, x, y) => {
 					$(opt.$trigger).addClass('is-contextopen');
 					let trigPos = $(opt.$trigger).position();
@@ -345,7 +345,7 @@ let vueImage = new Vue({
 	}
 });
 
-$('#btn-editor-uploadimage input').on('change', (ev) => {
+$('#btn-editor-image-upload input').on('change', (ev) => {
 
 	let curImageAmount = vueImage.images.length;
 

+ 20 - 6
client/js/components/editor.js

@@ -13,6 +13,7 @@ if($('#mk-editor').length === 1) {
 	});
 
 	//=include editor-image.js
+	//=include editor-file.js
 	//=include editor-codeblock.js
 
 	var mde = new SimpleMDE({
@@ -103,7 +104,9 @@ if($('#mk-editor').length === 1) {
 			{
 				name: "file",
 				action: (editor) => {
-					//todo
+					if(!mdeModalOpenState) {
+						vueFile.open();
+					}
 				},
 				className: "fa fa-file-text-o",
 				title: "Insert File",
@@ -133,9 +136,7 @@ if($('#mk-editor').length === 1) {
 						mdeModalOpenState = true;
 
 						if(mde.codemirror.doc.somethingSelected()) {
-							codeEditor.setValue(mde.codemirror.doc.getSelection());
-						} else {
-							codeEditor.setValue('');
+							vueCodeBlock.initContent = mde.codemirror.doc.getSelection();
 						}
 
 						vueCodeBlock.open();
@@ -170,7 +171,21 @@ if($('#mk-editor').length === 1) {
 	//-> Save
 
 	$('.btn-edit-save, .btn-create-save').on('click', (ev) => {
+		saveCurrentDocument(ev);
+	});
+
+	$(window).bind('keydown', (ev) => {
+		if (ev.ctrlKey || ev.metaKey) {
+			switch (String.fromCharCode(ev.which).toLowerCase()) {
+			case 's':
+				ev.preventDefault();
+				saveCurrentDocument(ev);
+				break;
+			}
+		}
+	});
 
+	let saveCurrentDocument = (ev) => {
 		$.ajax(window.location.href, {
 			data: {
 				markdown: mde.value()
@@ -186,7 +201,6 @@ if($('#mk-editor').length === 1) {
 		}, (rXHR, rStatus, err) => {
 			alerts.pushError('Something went wrong', 'Save operation failed.');
 		});
-
-	});
+	}
 
 }

+ 3 - 0
client/js/pages/source.js

@@ -4,6 +4,9 @@ if($('#page-type-source').length) {
 	var scEditor = ace.edit("source-display");
   scEditor.setTheme("ace/theme/tomorrow_night");
   scEditor.getSession().setMode("ace/mode/markdown");
+  scEditor.setOption('fontSize', '14px');
+  scEditor.setOption('hScrollBarAlwaysVisible', false);
+	scEditor.setOption('wrap', true);
   scEditor.setReadOnly(true);
   scEditor.renderer.updateFull();
 

+ 87 - 34
client/scss/components/_editor.scss

@@ -67,7 +67,7 @@
 
 }
 
-#btn-editor-uploadimage {
+#btn-editor-image-upload, #btn-editor-file-upload {
 	position: relative;
 	overflow: hidden;
 
@@ -94,7 +94,7 @@
 
 }
 
-.editor-modal-imagechoices {
+.editor-modal-image-choices {
 	display: flex;
 	flex-wrap: wrap;
 	align-items: flex-start;
@@ -188,6 +188,85 @@
 
 }
 
+.editor-modal-file-choices {
+	overflow: auto;
+	overflow-x: hidden;
+
+	> em {
+		display: flex;
+		align-items: center;
+		padding: 25px;
+		color: mc('grey', '500');
+
+		> i {
+			font-size: 32px;
+			margin-right: 10px;
+			color: mc('grey', '300');
+		}
+
+	}
+
+	> figure {
+		display: flex;
+		background-color: #FAFAFA;
+		border-radius: 3px;
+		padding: 5px;
+		height: 34px;
+		margin: 0 0 5px 0;
+		cursor: pointer;
+		justify-content: flex-start;
+		align-items: center;
+		transition: background-color 0.4s ease;
+
+		> i {
+			width: 16px;
+		}
+
+		> span {
+			font-size: 14px;
+			flex: 0 1 auto;
+			padding: 0 15px;
+			color: mc('grey', '600');
+
+			&:first-of-type {
+				flex: 1 0 auto;
+				color: mc('grey', '800');
+			}
+
+			&:last-of-type {
+				width: 100px;
+			}
+
+		}
+
+		&:hover {
+			background-color: #DDD;
+		}
+
+		&.is-active {
+			background-color: mc('green', '500');
+			color: #FFF;
+
+			> span, strong {
+				color: #FFF;
+			}
+
+		}
+
+		&.is-contextopen {
+			background-color: mc('blue', '500');
+			color: #FFF;
+
+			> span, strong {
+				color: #FFF;
+			}
+
+		}
+
+	}
+
+}
+
 .editor-modal-imagealign {
 
 	.control > span {
@@ -215,6 +294,8 @@
 	overflow-x: hidden;
 }
 
+// CODE MIRROR
+
 .CodeMirror {
 	border-left: none;
 	border-right: none;
@@ -245,16 +326,18 @@
 	font-size: 14px;
 }
 
+// ACE EDITOR
+
 .ace-container {
 	position: relative;
 }
 
-.ace_scroller {
+/*.ace_scroller {
 	width: 100%;
 }
 .ace_content {
 	height: 100%;
-}
+}*/
 
 #page-type-source .ace-container {
 	min-height: 95vh;
@@ -267,13 +350,6 @@
 	position: relative;
 	width: 100%;
 	height: 100%;
-
-	#codeblock-editor {
-		width: 100%;
-		height: 100%;
-		min-height: 500px;
-	}
-
 }
 
 #source-display, #codeblock-editor {
@@ -282,27 +358,4 @@
 	left: 0;
 	bottom: 0;
 	right: 0;
-}
-
-.modallayer {
-	position: fixed;
-	top: 100px;
-	width: 100%;
-	background-color: rgba(255,255,255,0.95);
-	border-bottom: 1px solid mc('grey', '500');
-	z-index: 6;
-	padding: 20px;
-	border-bottom: 1px solid #CCC;
-	box-shadow: 0 2px 3px rgba(17,17,17,.1);
-	display: none;
-
-	> h3, .column > h3 {
-		color: mc('grey', '700');
-		font-size: 24px;
-		font-weight: 300;
-	}
-
-}
-
-.modallayer-content {
 }

+ 9 - 0
config.sample.yml

@@ -31,6 +31,15 @@ paths:
   repo: ./repo
   data: ./data
 
+# ---------------------------------------------------------------------
+# Upload Limits
+# ---------------------------------------------------------------------
+# In megabytes (MB)
+
+uploads:
+  maxImageFileSize: 3
+  maxOtherFileSize: 100
+
 # ---------------------------------------------------------------------
 # Site Language
 # ---------------------------------------------------------------------

+ 56 - 1
controllers/uploads.js

@@ -53,7 +53,7 @@ router.post('/img', lcdata.uploadImgHandler, (req, res, next) => {
 			let destFilename = '';
 			let destFilePath = '';
 
-			return lcdata.validateUploadsFilename(f.originalname, destFolder).then((fname) => {
+			return lcdata.validateUploadsFilename(f.originalname, destFolder, true).then((fname) => {
 				
 				destFilename = fname;
 				destFilePath = path.resolve(destFolderPath, destFilename);
@@ -106,6 +106,61 @@ router.post('/img', lcdata.uploadImgHandler, (req, res, next) => {
 
 });
 
+router.post('/file', lcdata.uploadFileHandler, (req, res, next) => {
+
+	let destFolder = _.chain(req.body.folder).trim().toLower().value();
+
+	upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
+		
+		if(!destFolderPath) {
+			res.json({ ok: false, msg: 'Invalid Folder' });
+			return true;
+		}
+
+		Promise.map(req.files, (f) => {
+
+			let destFilename = '';
+			let destFilePath = '';
+
+			return lcdata.validateUploadsFilename(f.originalname, destFolder, false).then((fname) => {
+				
+				destFilename = fname;
+				destFilePath = path.resolve(destFolderPath, destFilename);
+
+				//-> Move file to final destination
+
+				return fs.moveAsync(f.path, destFilePath, { clobber: false });
+
+			}).then(() => {
+				return {
+					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 });
+			return true;
+		}).catch((err) => {
+			res.json({ ok: false, msg: err.message });
+			return true;
+		});
+
+	});
+
+});
+
 router.get('/*', (req, res, next) => {
 
 	let fileName = req.params[0];

+ 7 - 0
controllers/ws.js

@@ -42,6 +42,13 @@ module.exports = (socket) => {
     });
   });
 
+  socket.on('uploadsGetFiles', (data, cb) => {
+    cb = cb || _.noop;
+    upl.getUploadsFiles('binary', data.folder).then((f) => {
+      return cb(f) || true;
+    });
+  });
+
   socket.on('uploadsDeleteFile', (data, cb) => {
     cb = cb || _.noop;
     upl.deleteUploadsFile(data.uid).then((f) => {

+ 0 - 0
core/core-client/scss/components/form.scss


+ 0 - 4
core/core-client/scss/components/sidebar.scss

@@ -1,4 +0,0 @@
-
-.sidebar {
-	background-color: #FFF;
-}

+ 41 - 4
libs/local.js

@@ -4,6 +4,7 @@ var path = require('path'),
 	Promise = require('bluebird'),
 	fs = Promise.promisifyAll(require('fs-extra')),
 	multer  = require('multer'),
+	os = require('os'),
 	_ = require('lodash');
 
 /**
@@ -44,6 +45,13 @@ module.exports = {
 	 */
 	initMulter(appconfig) {
 
+		let maxFileSizes = {
+			img: appconfig.uploads.maxImageFileSize * 1024 * 1024,
+			file: appconfig.uploads.maxOtherFileSize * 1024 * 1024
+		};
+
+		//-> IMAGES
+
 		this.uploadImgHandler = multer({
 			storage: multer.diskStorage({
 				destination: (req, f, cb) => {
@@ -52,9 +60,9 @@ module.exports = {
 			}),
 			fileFilter: (req, f, cb) => {
 
-				//-> Check filesize (3 MB max)
+				//-> Check filesize
 
-				if(f.size > 3145728) {
+				if(f.size > maxFileSizes.img) {
 					return cb(null, false);
 				}
 
@@ -68,6 +76,26 @@ module.exports = {
 			}
 		}).array('imgfile', 20);
 
+		//-> FILES
+
+		this.uploadFileHandler = multer({
+			storage: multer.diskStorage({
+				destination: (req, f, cb) => {
+					cb(null, path.resolve(ROOTPATH, appconfig.paths.data, 'temp-upload'));
+				}
+			}),
+			fileFilter: (req, f, cb) => {
+
+				//-> Check filesize
+
+				if(f.size > maxFileSizes.file) {
+					return cb(null, false);
+				}
+
+				cb(null, true);
+			}
+		}).array('binfile', 20);
+
 		return true;
 
 	},
@@ -88,8 +116,17 @@ module.exports = {
 			fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './thumbs'));
 			fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './temp-upload'));
 
+			if(os.type() !== 'Windows_NT') {
+				fs.chmodSync(path.resolve(ROOTPATH, appconfig.paths.data, './temp-upload'), '644');
+			}
+
 			fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo));
 			fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo, './uploads'));
+
+			if(os.type() !== 'Windows_NT') {
+				fs.chmodSync(path.resolve(ROOTPATH, appconfig.paths.repo, './upload'), '644');
+			}
+
 		} catch (err) {
 			winston.error(err);
 		}
@@ -125,13 +162,13 @@ module.exports = {
 	 * @param      {String}           fld     The containing folder
 	 * @return     {Promise<String>}  Promise of the accepted filename
 	 */
-	validateUploadsFilename(f, fld) {
+	validateUploadsFilename(f, fld, isImage) {
 
 		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)) {
+		if(isImage && !_.includes(['.jpg', '.jpeg', '.png', '.gif', '.webp'], fext)) {
 			fext = '.png';
 		}
 

+ 7 - 1
libs/uploads-agent.js

@@ -5,6 +5,7 @@ var path = require('path'),
 	fs = Promise.promisifyAll(require('fs-extra')),
 	readChunk = require('read-chunk'),
 	fileType = require('file-type'),
+	mime = require('mime-types'),
 	farmhash = require('farmhash'),
 	moment = require('moment'),
 	chokidar = require('chokidar'),
@@ -199,6 +200,11 @@ module.exports = {
 			// Get MIME info
 
 			let mimeInfo = fileType(readChunk.sync(fPath, 0, 262));
+			if(_.isNil(mimeInfo)) {
+				mimeInfo = {
+					mime: mime.lookup(fPathObj.ext) || 'application/octet-stream'
+				};
+			}
 
 			// Images
 
@@ -244,7 +250,7 @@ module.exports = {
 				_id: fUid,
 				category: 'binary',
 				mime: mimeInfo.mime,
-				folder: fldName,
+				folder: 'f:' + fldName,
 				filename: f,
 				basename: fPathObj.name,
 				filesize: s.size

+ 11 - 12
package.json

@@ -43,7 +43,7 @@
     "connect-flash": "^0.1.1",
     "connect-mongo": "^1.3.2",
     "cookie-parser": "^1.4.3",
-    "cron": "^1.1.1",
+    "cron": "^1.2.1",
     "express": "^4.14.0",
     "express-brute": "^1.0.0",
     "express-brute-mongoose": "0.0.7",
@@ -53,13 +53,13 @@
     "filesize.js": "^1.0.2",
     "fs-extra": "^1.0.0",
     "git-wrapper2-promise": "^0.2.9",
-    "highlight.js": "^9.8.0",
+    "highlight.js": "^9.9.0",
     "i18next": "^4.1.1",
     "i18next-express-middleware": "^1.0.2",
     "i18next-node-fs-backend": "^0.1.3",
     "js-yaml": "^3.7.0",
     "lodash": "^4.17.2",
-    "markdown-it": "^8.2.1",
+    "markdown-it": "^8.2.2",
     "markdown-it-abbr": "^1.0.4",
     "markdown-it-anchor": "^2.6.0",
     "markdown-it-attrs": "^0.8.0",
@@ -68,10 +68,11 @@
     "markdown-it-external-links": "0.0.6",
     "markdown-it-footnote": "^3.0.1",
     "markdown-it-task-lists": "^1.4.1",
+    "mime-types": "^2.1.13",
     "moment": "^2.17.1",
     "moment-timezone": "^0.5.10",
-    "mongoose": "^4.7.2",
-    "multer": "^1.2.0",
+    "mongoose": "^4.7.3",
+    "multer": "^1.2.1",
     "passport": "^0.3.2",
     "passport-facebook": "^2.1.1",
     "passport-google-oauth20": "^1.0.0",
@@ -87,8 +88,7 @@
     "serve-favicon": "^2.3.2",
     "sharp": "^0.16.1",
     "simplemde": "^1.11.2",
-    "snyk": "^1.19.1",
-    "socket.io": "^1.6.0",
+    "socket.io": "^1.7.2",
     "sticky-js": "^1.0.7",
     "validator": "^6.2.0",
     "validator-as-promised": "^1.0.2",
@@ -100,17 +100,16 @@
     "chai": "^3.5.0",
     "chai-as-promised": "^6.0.0",
     "codacy-coverage": "^2.0.0",
-    "filesize.js": "^1.0.1",
     "font-awesome": "^4.6.3",
     "gulp": "^3.9.1",
     "gulp-babel": "^6.1.2",
-    "gulp-clean-css": "^2.2.1",
+    "gulp-clean-css": "^2.3.2",
     "gulp-concat": "^2.6.1",
     "gulp-gzip": "^1.4.0",
     "gulp-include": "^2.3.1",
     "gulp-nodemon": "^2.2.1",
     "gulp-plumber": "^1.1.0",
-    "gulp-sass": "^2.3.2",
+    "gulp-sass": "^3.0.0",
     "gulp-tar": "^1.9.0",
     "gulp-uglify": "^2.0.0",
     "gulp-watch": "^4.3.11",
@@ -125,10 +124,10 @@
     "mocha-lcov-reporter": "^1.2.0",
     "nodemon": "^1.11.0",
     "run-sequence": "^1.2.2",
-    "snyk": "^1.21.2",
+    "snyk": "^1.22.1",
     "sticky-js": "^1.1.6",
     "twemoji-awesome": "^1.0.4",
-    "vue": "^2.1.4"
+    "vue": "^2.1.6"
   },
   "snyk": true
 }

+ 80 - 0
views/modals/editor-file.pug

@@ -0,0 +1,80 @@
+
+.modal#modal-editor-file
+	.modal-background
+	.modal-container
+		.modal-content.is-expanded
+
+			header.is-green
+				span Insert File
+				p.modal-notify(v-bind:class="{ 'is-active': isLoading }")
+					span {{ isLoadingText }}
+					i
+			.modal-toolbar.is-green
+					a.button(v-on:click="newFolder")
+						i.fa.fa-folder
+						span New Folder
+					a.button#btn-editor-file-upload
+						i.fa.fa-upload
+						span Upload File
+						label
+							input(type="file", multiple)
+			section.is-gapless
+				.columns.is-stretched
+					.column.is-one-quarter.modal-sidebar.is-green(style={'max-width':'350px'})
+						.model-sidebar-header Folders
+						ul.model-sidebar-list
+							li(v-for="fld in folders")
+								a(v-on:click="selectFolder(fld)", v-bind:class="{ 'is-active': currentFolder === fld }")
+									i.icon-folder2
+									span / {{ fld }}
+					.column.editor-modal-file-choices
+						figure(v-for="fl in files", v-bind:class="{ 'is-active': currentFile === fl._id }", v-on:click="selectFile(fl._id)", v-bind:data-uid="fl._id")
+							i(class='icon-file')
+							span: strong {{ fl.filename }}
+							span {{ fl.mime }}
+							span {{ fl.filesize | filesize }}
+						em(v-show="files.length < 1")
+							i.icon-marquee-minus
+							| This folder is empty.
+			footer
+				a.button.is-grey.is-outlined(v-on:click="cancel") Discard
+				a.button.is-green(v-on:click="insertFileLink") Insert Link to File
+
+	.modal.is-superimposed(v-bind:class="{ 'is-active': newFolderShow }")
+		.modal-background
+		.modal-container
+			.modal-content
+				header.is-light-blue New Folder
+				section
+					label.label Enter the new folder name:
+					p.control.is-fullwidth
+						input.input#txt-editor-file-newfoldername(type='text', placeholder='folder name', v-model='newFolderName', v-on:keyup.enter="newFolderCreate", v-on:keyup.esc="newFolderDiscard")
+						span.help.is-danger(v-show="newFolderError") This folder name is invalid!
+				footer
+					a.button.is-grey.is-outlined(v-on:click="newFolderDiscard") Discard
+					a.button.is-light-blue(v-on:click="newFolderCreate") Create
+
+	.modal.is-superimposed(v-bind:class="{ 'is-active': renameFileShow }")
+		.modal-background
+		.modal-container
+			.modal-content
+				header.is-indigo Rename File
+				section
+					label.label Enter the new filename (without the extension) of the file:
+					p.control.is-fullwidth
+						input.input#txt-editor-file-rename(type='text', placeholder='filename', v-model='renameFileFilename')
+						span.help.is-danger.is-hidden This filename is invalid!
+				footer
+					a.button.is-grey.is-outlined(v-on:click="renameFileDiscard") Discard
+					a.button.is-light-blue(v-on:click="renameFileGo") Rename
+
+	.modal.is-superimposed(v-bind:class="{ 'is-active': deleteFileShow }")
+		.modal-background
+		.modal-container
+			.modal-content
+				header.is-red Delete file?
+				section
+					span Are you sure you want to delete #[strong {{deleteFileFilename}}]?
+				footer
+					a.button.is-grey.is-outlined(v-on:click="deleteFileWarn(false)") Discard
+					a.button.is-red(v-on:click="deleteFileGo") Delete

+ 5 - 5
views/modals/editor-image.pug

@@ -13,7 +13,7 @@
 					a.button(v-on:click="newFolder")
 						i.fa.fa-folder
 						span New Folder
-					a.button#btn-editor-uploadimage
+					a.button#btn-editor-image-upload
 						i.fa.fa-upload
 						span Upload Image
 						label
@@ -38,7 +38,7 @@
 									option(value='center') Centered
 									option(value='right') Right
 									option(value='logo') Page Logo
-					.column.editor-modal-imagechoices
+					.column.editor-modal-image-choices
 						figure(v-for="img in images", v-bind:class="{ 'is-active': currentImage === img._id }", v-on:click="selectImage(img._id)", v-bind:data-uid="img._id")
 							img(v-bind:src="'/uploads/t/' + img._id + '.png'")
 							span: strong {{ img.basename }}
@@ -58,7 +58,7 @@
 				section
 					label.label Enter the new folder name:
 					p.control.is-fullwidth
-						input.input#txt-editor-newfoldername(type='text', placeholder='folder name', v-model='newFolderName', v-on:keyup.enter="newFolderCreate", v-on:keyup.esc="newFolderDiscard")
+						input.input#txt-editor-image-newfoldername(type='text', placeholder='folder name', v-model='newFolderName', v-on:keyup.enter="newFolderCreate", v-on:keyup.esc="newFolderDiscard")
 						span.help.is-danger(v-show="newFolderError") This folder name is invalid!
 				footer
 					a.button.is-grey.is-outlined(v-on:click="newFolderDiscard") Discard
@@ -72,7 +72,7 @@
 				section
 					label.label Enter full URL path to the image:
 					p.control.is-fullwidth
-						input.input#txt-editor-fetchimgurl(type='text', placeholder='http://www.example.com/some-image.png', v-model='fetchFromUrlURL')
+						input.input#txt-editor-image-fetchurl(type='text', placeholder='http://www.example.com/some-image.png', v-model='fetchFromUrlURL')
 						span.help.is-danger.is-hidden This URL path is invalid!
 				footer
 					a.button.is-grey.is-outlined(v-on:click="fetchFromUrlDiscard") Discard
@@ -86,7 +86,7 @@
 				section
 					label.label Enter the new filename (without the extension) of the image:
 					p.control.is-fullwidth
-						input.input#txt-editor-renameimage(type='text', placeholder='filename', v-model='renameImageFilename')
+						input.input#txt-editor-image-rename(type='text', placeholder='filename', v-model='renameImageFilename')
 						span.help.is-danger.is-hidden This filename is invalid!
 				footer
 					a.button.is-grey.is-outlined(v-on:click="renameImageDiscard") Discard

+ 1 - 1
views/modals/editor-link.pug

@@ -1,5 +1,5 @@
 
-.modallayer#modal-editor-link
+//.modallayer#modal-editor-link
 	.modallayer-content
 		.tabs.is-boxed
 			ul

+ 1 - 0
views/pages/edit.pug

@@ -22,4 +22,5 @@ block content
 	include ../modals/edit-discard.pug
 	include ../modals/editor-link.pug
 	include ../modals/editor-image.pug
+	include ../modals/editor-file.pug
 	include ../modals/editor-codeblock.pug

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä