Commit 9caaeee6 authored by NGPixel's avatar NGPixel

Files Management + Editor Modal + Code Editor fixes

parent eb2e724f
...@@ -12,17 +12,18 @@ ...@@ -12,17 +12,18 @@
[![Known Vulnerabilities](https://snyk.io/test/github/requarks/wiki/badge.svg)](https://snyk.io/test/github/requarks/wiki) [![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 ##### A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown
*Under development* *Under active development*
### Documentation ### Documentation
- [Installation Guide](https://wiki.requarks.io/install) - [Official Website](https://wiki.requarks.io/)
- [Installation Guide](https://wiki.requarks.io/get-started.html)
##### Milestones ##### Milestones
- [ ] Account Management - [ ] Account Management
- [x] Assets Management - [x] Assets Management
- [x] Images - [x] Images
- [ ] Files/Documents - [x] Files/Documents
- [x] Authentication - [x] Authentication
- [x] Strategies - [x] Strategies
- [x] Local - [x] Local
...@@ -46,6 +47,10 @@ ...@@ -46,6 +47,10 @@
- [x] Markdown Editor - [x] Markdown Editor
- [x] Basic Formatting - [x] Basic Formatting
- [ ] Links - [ ] Links
- [x] Image Selection modal
- [x] File Selection modal
- [x] Inline Code
- [x] Code Editor modal
- [ ] Table Editor - [ ] Table Editor
- [x] Move Entry - [x] Move Entry
- [x] Navigation - [x] Navigation
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
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 modelist = ace.require("ace/ext/modelist");
let codeEditor = null;
// ACE - Mode Loader // ACE - Mode Loader
...@@ -33,7 +27,8 @@ let vueCodeBlock = new Vue({ ...@@ -33,7 +27,8 @@ let vueCodeBlock = new Vue({
el: '#modal-editor-codeblock', el: '#modal-editor-codeblock',
data: { data: {
modes: modelist.modesByName, modes: modelist.modesByName,
modeSelected: 'text' modeSelected: 'text',
initContent: ''
}, },
watch: { watch: {
modeSelected: (val, oldVal) => { modeSelected: (val, oldVal) => {
...@@ -45,19 +40,28 @@ let vueCodeBlock = new Vue({ ...@@ -45,19 +40,28 @@ let vueCodeBlock = new Vue({
}, },
methods: { methods: {
open: (ev) => { open: (ev) => {
$('#modal-editor-codeblock').addClass('is-active'); $('#modal-editor-codeblock').addClass('is-active');
_.delay(() => { _.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.focus();
codeEditor.setAutoScrollEditorIntoView(true);
codeEditor.renderer.updateFull(); codeEditor.renderer.updateFull();
}, 1000); }, 300);
}, },
cancel: (ev) => { cancel: (ev) => {
mdeModalOpenState = false; mdeModalOpenState = false;
$('#modal-editor-codeblock').removeClass('is-active'); $('#modal-editor-codeblock').removeClass('is-active');
vueCodeBlock.initContent = '';
}, },
insertCode: (ev) => { insertCode: (ev) => {
......
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;
}
}
});
});
\ No newline at end of file
...@@ -78,7 +78,7 @@ let vueImage = new Vue({ ...@@ -78,7 +78,7 @@ let vueImage = new Vue({
vueImage.newFolderName = ''; vueImage.newFolderName = '';
vueImage.newFolderError = false; vueImage.newFolderError = false;
vueImage.newFolderShow = true; vueImage.newFolderShow = true;
_.delay(() => { $('#txt-editor-newfoldername').focus(); }, 400); _.delay(() => { $('#txt-editor-image-newfoldername').focus(); }, 400);
}, },
newFolderDiscard: (ev) => { newFolderDiscard: (ev) => {
vueImage.newFolderShow = false; vueImage.newFolderShow = false;
...@@ -115,7 +115,7 @@ let vueImage = new Vue({ ...@@ -115,7 +115,7 @@ let vueImage = new Vue({
fetchFromUrl: (ev) => { fetchFromUrl: (ev) => {
vueImage.fetchFromUrlURL = ''; vueImage.fetchFromUrlURL = '';
vueImage.fetchFromUrlShow = true; vueImage.fetchFromUrlShow = true;
_.delay(() => { $('#txt-editor-fetchimgurl').focus(); }, 400); _.delay(() => { $('#txt-editor-image-fetchurl').focus(); }, 400);
}, },
fetchFromUrlDiscard: (ev) => { fetchFromUrlDiscard: (ev) => {
vueImage.fetchFromUrlShow = false; vueImage.fetchFromUrlShow = false;
...@@ -149,8 +149,8 @@ let vueImage = new Vue({ ...@@ -149,8 +149,8 @@ let vueImage = new Vue({
vueImage.renameImageFilename = c.basename || ''; vueImage.renameImageFilename = c.basename || '';
vueImage.renameImageShow = true; vueImage.renameImageShow = true;
_.delay(() => { _.delay(() => {
$('#txt-editor-renameimage').focus(); $('#txt-editor-image-rename').focus();
_.defer(() => { $('#txt-editor-renameimage').select(); }); _.defer(() => { $('#txt-editor-image-rename').select(); });
}, 400); }, 400);
}, },
renameImageDiscard: () => { renameImageDiscard: () => {
...@@ -301,10 +301,10 @@ let vueImage = new Vue({ ...@@ -301,10 +301,10 @@ let vueImage = new Vue({
}; };
}); });
$.contextMenu('destroy', '.editor-modal-imagechoices > figure'); $.contextMenu('destroy', '.editor-modal-image-choices > figure');
$.contextMenu({ $.contextMenu({
selector: '.editor-modal-imagechoices > figure', selector: '.editor-modal-image-choices > figure',
appendTo: '.editor-modal-imagechoices', appendTo: '.editor-modal-image-choices',
position: (opt, x, y) => { position: (opt, x, y) => {
$(opt.$trigger).addClass('is-contextopen'); $(opt.$trigger).addClass('is-contextopen');
let trigPos = $(opt.$trigger).position(); let trigPos = $(opt.$trigger).position();
...@@ -345,7 +345,7 @@ let vueImage = new Vue({ ...@@ -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; let curImageAmount = vueImage.images.length;
......
...@@ -13,6 +13,7 @@ if($('#mk-editor').length === 1) { ...@@ -13,6 +13,7 @@ if($('#mk-editor').length === 1) {
}); });
//=include editor-image.js //=include editor-image.js
//=include editor-file.js
//=include editor-codeblock.js //=include editor-codeblock.js
var mde = new SimpleMDE({ var mde = new SimpleMDE({
...@@ -103,7 +104,9 @@ if($('#mk-editor').length === 1) { ...@@ -103,7 +104,9 @@ if($('#mk-editor').length === 1) {
{ {
name: "file", name: "file",
action: (editor) => { action: (editor) => {
//todo if(!mdeModalOpenState) {
vueFile.open();
}
}, },
className: "fa fa-file-text-o", className: "fa fa-file-text-o",
title: "Insert File", title: "Insert File",
...@@ -133,9 +136,7 @@ if($('#mk-editor').length === 1) { ...@@ -133,9 +136,7 @@ if($('#mk-editor').length === 1) {
mdeModalOpenState = true; mdeModalOpenState = true;
if(mde.codemirror.doc.somethingSelected()) { if(mde.codemirror.doc.somethingSelected()) {
codeEditor.setValue(mde.codemirror.doc.getSelection()); vueCodeBlock.initContent = mde.codemirror.doc.getSelection();
} else {
codeEditor.setValue('');
} }
vueCodeBlock.open(); vueCodeBlock.open();
...@@ -170,7 +171,21 @@ if($('#mk-editor').length === 1) { ...@@ -170,7 +171,21 @@ if($('#mk-editor').length === 1) {
//-> Save //-> Save
$('.btn-edit-save, .btn-create-save').on('click', (ev) => { $('.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, { $.ajax(window.location.href, {
data: { data: {
markdown: mde.value() markdown: mde.value()
...@@ -186,7 +201,6 @@ if($('#mk-editor').length === 1) { ...@@ -186,7 +201,6 @@ if($('#mk-editor').length === 1) {
}, (rXHR, rStatus, err) => { }, (rXHR, rStatus, err) => {
alerts.pushError('Something went wrong', 'Save operation failed.'); alerts.pushError('Something went wrong', 'Save operation failed.');
}); });
}
});
} }
\ No newline at end of file
...@@ -4,6 +4,9 @@ if($('#page-type-source').length) { ...@@ -4,6 +4,9 @@ if($('#page-type-source').length) {
var scEditor = ace.edit("source-display"); var scEditor = ace.edit("source-display");
scEditor.setTheme("ace/theme/tomorrow_night"); scEditor.setTheme("ace/theme/tomorrow_night");
scEditor.getSession().setMode("ace/mode/markdown"); scEditor.getSession().setMode("ace/mode/markdown");
scEditor.setOption('fontSize', '14px');
scEditor.setOption('hScrollBarAlwaysVisible', false);
scEditor.setOption('wrap', true);
scEditor.setReadOnly(true); scEditor.setReadOnly(true);
scEditor.renderer.updateFull(); scEditor.renderer.updateFull();
......
...@@ -67,7 +67,7 @@ ...@@ -67,7 +67,7 @@
} }
#btn-editor-uploadimage { #btn-editor-image-upload, #btn-editor-file-upload {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
...@@ -94,7 +94,7 @@ ...@@ -94,7 +94,7 @@
} }
.editor-modal-imagechoices { .editor-modal-image-choices {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: flex-start; align-items: flex-start;
...@@ -188,6 +188,85 @@ ...@@ -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 { .editor-modal-imagealign {
.control > span { .control > span {
...@@ -215,6 +294,8 @@ ...@@ -215,6 +294,8 @@
overflow-x: hidden; overflow-x: hidden;
} }
// CODE MIRROR
.CodeMirror { .CodeMirror {
border-left: none; border-left: none;
border-right: none; border-right: none;
...@@ -245,16 +326,18 @@ ...@@ -245,16 +326,18 @@
font-size: 14px; font-size: 14px;
} }
// ACE EDITOR
.ace-container { .ace-container {
position: relative; position: relative;
} }
.ace_scroller { /*.ace_scroller {
width: 100%; width: 100%;
} }
.ace_content { .ace_content {
height: 100%; height: 100%;
} }*/
#page-type-source .ace-container { #page-type-source .ace-container {
min-height: 95vh; min-height: 95vh;
...@@ -267,13 +350,6 @@ ...@@ -267,13 +350,6 @@
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
#codeblock-editor {
width: 100%;
height: 100%;
min-height: 500px;
}
} }
#source-display, #codeblock-editor { #source-display, #codeblock-editor {
...@@ -283,26 +359,3 @@ ...@@ -283,26 +359,3 @@
bottom: 0; bottom: 0;
right: 0; right: 0;
} }
\ No newline at end of file
.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 {
}
\ No newline at end of file
...@@ -32,6 +32,15 @@ paths: ...@@ -32,6 +32,15 @@ paths:
data: ./data data: ./data
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# Upload Limits
# ---------------------------------------------------------------------
# In megabytes (MB)
uploads:
maxImageFileSize: 3
maxOtherFileSize: 100
# ---------------------------------------------------------------------
# Site Language # Site Language
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# Possible values: en, fr # Possible values: en, fr
......
...@@ -53,7 +53,7 @@ router.post('/img', lcdata.uploadImgHandler, (req, res, next) => { ...@@ -53,7 +53,7 @@ router.post('/img', lcdata.uploadImgHandler, (req, res, next) => {
let destFilename = ''; let destFilename = '';
let destFilePath = ''; let destFilePath = '';
return lcdata.validateUploadsFilename(f.originalname, destFolder).then((fname) => { return lcdata.validateUploadsFilename(f.originalname, destFolder, true).then((fname) => {
destFilename = fname; destFilename = fname;
destFilePath = path.resolve(destFolderPath, destFilename); destFilePath = path.resolve(destFolderPath, destFilename);
...@@ -106,6 +106,61 @@ router.post('/img', lcdata.uploadImgHandler, (req, res, next) => { ...@@ -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) => { router.get('/*', (req, res, next) => {
let fileName = req.params[0]; let fileName = req.params[0];
......
...@@ -42,6 +42,13 @@ module.exports = (socket) => { ...@@ -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) => { socket.on('uploadsDeleteFile', (data, cb) => {
cb = cb || _.noop; cb = cb || _.noop;
upl.deleteUploadsFile(data.uid).then((f) => { upl.deleteUploadsFile(data.uid).then((f) => {
......
...@@ -4,6 +4,7 @@ var path = require('path'), ...@@ -4,6 +4,7 @@ var path = require('path'),
Promise = require('bluebird'), Promise = require('bluebird'),
fs = Promise.promisifyAll(require('fs-extra')), fs = Promise.promisifyAll(require('fs-extra')),
multer = require('multer'), multer = require('multer'),
os = require('os'),
_ = require('lodash'); _ = require('lodash');
/** /**
...@@ -44,6 +45,13 @@ module.exports = { ...@@ -44,6 +45,13 @@ module.exports = {
*/ */
initMulter(appconfig) { initMulter(appconfig) {
let maxFileSizes = {
img: appconfig.uploads.maxImageFileSize * 1024 * 1024,
file: appconfig.uploads.maxOtherFileSize * 1024 * 1024
};
//-> IMAGES
this.uploadImgHandler = multer({ this.uploadImgHandler = multer({
storage: multer.diskStorage({ storage: multer.diskStorage({
destination: (req, f, cb) => { destination: (req, f, cb) => {
...@@ -52,9 +60,9 @@ module.exports = { ...@@ -52,9 +60,9 @@ module.exports = {
}), }),
fileFilter: (req, f, cb) => { fileFilter: (req, f, cb) => {
//-> Check filesize (3 MB max) //-> Check filesize
if(f.size > 3145728) { if(f.size > maxFileSizes.img) {
return cb(null, false); return cb(null, false);
} }
...@@ -68,6 +76,26 @@ module.exports = { ...@@ -68,6 +76,26 @@ module.exports = {
} }
}).array('imgfile', 20); }).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; return true;
}, },
...@@ -88,8 +116,17 @@ module.exports = { ...@@ -88,8 +116,17 @@ module.exports = {
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './thumbs')); 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.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));
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo, './uploads')); 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) { } catch (err) {
winston.error(err); winston.error(err);
} }
...@@ -125,13 +162,13 @@ module.exports = { ...@@ -125,13 +162,13 @@ module.exports = {
* @param {String} fld The containing folder * @param {String} fld The containing folder
* @return {Promise<String>} Promise of the accepted filename * @return {Promise<String>} Promise of the accepted filename
*/ */
validateUploadsFilename(f, fld) { validateUploadsFilename(f, fld, isImage) {
let fObj = path.parse(f); let fObj = path.parse(f);
let fname = _.chain(fObj.name).trim().toLower().kebabCase().value().replace(/[^a-z0-9\-]+/g, ''); let fname = _.chain(fObj.name).trim().toLower().kebabCase().value().replace(/[^a-z0-9\-]+/g, '');
let fext = _.toLower(fObj.ext); let fext = _.toLower(fObj.ext);
if(!_.includes(['.jpg', '.jpeg', '.png', '.gif', '.webp'], fext)) { if(isImage && !_.includes(['.jpg', '.jpeg', '.png', '.gif', '.webp'], fext)) {
fext = '.png'; fext = '.png';
} }
......
...@@ -5,6 +5,7 @@ var path = require('path'), ...@@ -5,6 +5,7 @@ var path = require('path'),
fs = Promise.promisifyAll(require('fs-extra')), fs = Promise.promisifyAll(require('fs-extra')),
readChunk = require('read-chunk'), readChunk = require('read-chunk'),
fileType = require('file-type'), fileType = require('file-type'),
mime = require('mime-types'),
farmhash = require('farmhash'), farmhash = require('farmhash'),
moment = require('moment'), moment = require('moment'),
chokidar = require('chokidar'), chokidar = require('chokidar'),
...@@ -199,6 +200,11 @@ module.exports = { ...@@ -199,6 +200,11 @@ module.exports = {
// Get MIME info // Get MIME info
let mimeInfo = fileType(readChunk.sync(fPath, 0, 262)); let mimeInfo = fileType(readChunk.sync(fPath, 0, 262));
if(_.isNil(mimeInfo)) {
mimeInfo = {
mime: mime.lookup(fPathObj.ext) || 'application/octet-stream'
};
}
// Images // Images
...@@ -244,7 +250,7 @@ module.exports = { ...@@ -244,7 +250,7 @@ module.exports = {
_id: fUid, _id: fUid,
category: 'binary', category: 'binary',
mime: mimeInfo.mime, mime: mimeInfo.mime,
folder: fldName, folder: 'f:' + fldName,
filename: f, filename: f,
basename: fPathObj.name, basename: fPathObj.name,
filesize: s.size filesize: s.size
......
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
"connect-flash": "^0.1.1", "connect-flash": "^0.1.1",
"connect-mongo": "^1.3.2", "connect-mongo": "^1.3.2",
"cookie-parser": "^1.4.3", "cookie-parser": "^1.4.3",
"cron": "^1.1.1", "cron": "^1.2.1",
"express": "^4.14.0", "express": "^4.14.0",
"express-brute": "^1.0.0", "express-brute": "^1.0.0",
"express-brute-mongoose": "0.0.7", "express-brute-mongoose": "0.0.7",
...@@ -53,13 +53,13 @@ ...@@ -53,13 +53,13 @@
"filesize.js": "^1.0.2", "filesize.js": "^1.0.2",
"fs-extra": "^1.0.0", "fs-extra": "^1.0.0",
"git-wrapper2-promise": "^0.2.9", "git-wrapper2-promise": "^0.2.9",
"highlight.js": "^9.8.0", "highlight.js": "^9.9.0",
"i18next": "^4.1.1", "i18next": "^4.1.1",
"i18next-express-middleware": "^1.0.2", "i18next-express-middleware": "^1.0.2",
"i18next-node-fs-backend": "^0.1.3", "i18next-node-fs-backend": "^0.1.3",
"js-yaml": "^3.7.0", "js-yaml": "^3.7.0",
"lodash": "^4.17.2", "lodash": "^4.17.2",
"markdown-it": "^8.2.1", "markdown-it": "^8.2.2",
"markdown-it-abbr": "^1.0.4", "markdown-it-abbr": "^1.0.4",
"markdown-it-anchor": "^2.6.0", "markdown-it-anchor": "^2.6.0",
"markdown-it-attrs": "^0.8.0", "markdown-it-attrs": "^0.8.0",
...@@ -68,10 +68,11 @@ ...@@ -68,10 +68,11 @@
"markdown-it-external-links": "0.0.6", "markdown-it-external-links": "0.0.6",
"markdown-it-footnote": "^3.0.1", "markdown-it-footnote": "^3.0.1",
"markdown-it-task-lists": "^1.4.1", "markdown-it-task-lists": "^1.4.1",
"mime-types": "^2.1.13",
"moment": "^2.17.1", "moment": "^2.17.1",
"moment-timezone": "^0.5.10", "moment-timezone": "^0.5.10",
"mongoose": "^4.7.2", "mongoose": "^4.7.3",
"multer": "^1.2.0", "multer": "^1.2.1",
"passport": "^0.3.2", "passport": "^0.3.2",
"passport-facebook": "^2.1.1", "passport-facebook": "^2.1.1",
"passport-google-oauth20": "^1.0.0", "passport-google-oauth20": "^1.0.0",
...@@ -87,8 +88,7 @@ ...@@ -87,8 +88,7 @@
"serve-favicon": "^2.3.2", "serve-favicon": "^2.3.2",
"sharp": "^0.16.1", "sharp": "^0.16.1",
"simplemde": "^1.11.2", "simplemde": "^1.11.2",
"snyk": "^1.19.1", "socket.io": "^1.7.2",
"socket.io": "^1.6.0",
"sticky-js": "^1.0.7", "sticky-js": "^1.0.7",
"validator": "^6.2.0", "validator": "^6.2.0",
"validator-as-promised": "^1.0.2", "validator-as-promised": "^1.0.2",
...@@ -100,17 +100,16 @@ ...@@ -100,17 +100,16 @@
"chai": "^3.5.0", "chai": "^3.5.0",
"chai-as-promised": "^6.0.0", "chai-as-promised": "^6.0.0",
"codacy-coverage": "^2.0.0", "codacy-coverage": "^2.0.0",
"filesize.js": "^1.0.1",
"font-awesome": "^4.6.3", "font-awesome": "^4.6.3",
"gulp": "^3.9.1", "gulp": "^3.9.1",
"gulp-babel": "^6.1.2", "gulp-babel": "^6.1.2",
"gulp-clean-css": "^2.2.1", "gulp-clean-css": "^2.3.2",
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",
"gulp-gzip": "^1.4.0", "gulp-gzip": "^1.4.0",
"gulp-include": "^2.3.1", "gulp-include": "^2.3.1",
"gulp-nodemon": "^2.2.1", "gulp-nodemon": "^2.2.1",
"gulp-plumber": "^1.1.0", "gulp-plumber": "^1.1.0",
"gulp-sass": "^2.3.2", "gulp-sass": "^3.0.0",
"gulp-tar": "^1.9.0", "gulp-tar": "^1.9.0",
"gulp-uglify": "^2.0.0", "gulp-uglify": "^2.0.0",
"gulp-watch": "^4.3.11", "gulp-watch": "^4.3.11",
...@@ -125,10 +124,10 @@ ...@@ -125,10 +124,10 @@
"mocha-lcov-reporter": "^1.2.0", "mocha-lcov-reporter": "^1.2.0",
"nodemon": "^1.11.0", "nodemon": "^1.11.0",
"run-sequence": "^1.2.2", "run-sequence": "^1.2.2",
"snyk": "^1.21.2", "snyk": "^1.22.1",
"sticky-js": "^1.1.6", "sticky-js": "^1.1.6",
"twemoji-awesome": "^1.0.4", "twemoji-awesome": "^1.0.4",
"vue": "^2.1.4" "vue": "^2.1.6"
}, },
"snyk": true "snyk": true
} }
.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
\ No newline at end of file
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
a.button(v-on:click="newFolder") a.button(v-on:click="newFolder")
i.fa.fa-folder i.fa.fa-folder
span New Folder span New Folder
a.button#btn-editor-uploadimage a.button#btn-editor-image-upload
i.fa.fa-upload i.fa.fa-upload
span Upload Image span Upload Image
label label
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
option(value='center') Centered option(value='center') Centered
option(value='right') Right option(value='right') Right
option(value='logo') Page Logo 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") 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'") img(v-bind:src="'/uploads/t/' + img._id + '.png'")
span: strong {{ img.basename }} span: strong {{ img.basename }}
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
section section
label.label Enter the new folder name: label.label Enter the new folder name:
p.control.is-fullwidth 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! span.help.is-danger(v-show="newFolderError") This folder name is invalid!
footer footer
a.button.is-grey.is-outlined(v-on:click="newFolderDiscard") Discard a.button.is-grey.is-outlined(v-on:click="newFolderDiscard") Discard
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
section section
label.label Enter full URL path to the image: label.label Enter full URL path to the image:
p.control.is-fullwidth 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! span.help.is-danger.is-hidden This URL path is invalid!
footer footer
a.button.is-grey.is-outlined(v-on:click="fetchFromUrlDiscard") Discard a.button.is-grey.is-outlined(v-on:click="fetchFromUrlDiscard") Discard
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
section section
label.label Enter the new filename (without the extension) of the image: label.label Enter the new filename (without the extension) of the image:
p.control.is-fullwidth 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! span.help.is-danger.is-hidden This filename is invalid!
footer footer
a.button.is-grey.is-outlined(v-on:click="renameImageDiscard") Discard a.button.is-grey.is-outlined(v-on:click="renameImageDiscard") Discard
......
.modallayer#modal-editor-link //.modallayer#modal-editor-link
.modallayer-content .modallayer-content
.tabs.is-boxed .tabs.is-boxed
ul ul
......
...@@ -22,4 +22,5 @@ block content ...@@ -22,4 +22,5 @@ block content
include ../modals/edit-discard.pug include ../modals/edit-discard.pug
include ../modals/editor-link.pug include ../modals/editor-link.pug
include ../modals/editor-image.pug include ../modals/editor-image.pug
include ../modals/editor-file.pug
include ../modals/editor-codeblock.pug include ../modals/editor-codeblock.pug
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment