mirror of https://github.com/requarks/wiki
parent
a508b2a7f4
commit
414dc386d6
@ -0,0 +1,24 @@
|
||||
# Change Log
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- Change log
|
||||
|
||||
### Fixed
|
||||
- Fixed issue with social accounts with empty name
|
||||
|
||||
### Changed
|
||||
- Updated dependencies + snyk policy
|
||||
- Conversion to Standard JS compliant code
|
||||
|
||||
## [v1.0-beta.2] - 2017-01-30
|
||||
### Added
|
||||
- Save own profile under My Account
|
||||
|
||||
### Changed
|
||||
- Updated dependencies + snyk policy
|
||||
|
||||
[Unreleased]: https://github.com/Requarks/wiki/compare/v1.0-beta.2...HEAD
|
||||
[v1.0-beta.2]: https://github.com/Requarks/wiki/releases/tag/v1.0-beta.2
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,59 +1,57 @@
|
||||
"use strict";
|
||||
|
||||
jQuery( document ).ready(function( $ ) {
|
||||
'use strict'
|
||||
|
||||
jQuery(document).ready(function ($) {
|
||||
// ====================================
|
||||
// Scroll
|
||||
// ====================================
|
||||
|
||||
$('a').smoothScroll({
|
||||
speed: 400,
|
||||
offset: -70
|
||||
});
|
||||
$('a').smoothScroll({
|
||||
speed: 400,
|
||||
offset: -70
|
||||
})
|
||||
|
||||
var sticky = new Sticky('.stickyscroll');
|
||||
var sticky = new Sticky('.stickyscroll')
|
||||
|
||||
// ====================================
|
||||
// Notifications
|
||||
// ====================================
|
||||
|
||||
$(window).bind('beforeunload', () => {
|
||||
$('#notifload').addClass('active');
|
||||
});
|
||||
$(document).ajaxSend(() => {
|
||||
$('#notifload').addClass('active');
|
||||
}).ajaxComplete(() => {
|
||||
$('#notifload').removeClass('active');
|
||||
});
|
||||
$(window).bind('beforeunload', () => {
|
||||
$('#notifload').addClass('active')
|
||||
})
|
||||
$(document).ajaxSend(() => {
|
||||
$('#notifload').addClass('active')
|
||||
}).ajaxComplete(() => {
|
||||
$('#notifload').removeClass('active')
|
||||
})
|
||||
|
||||
var alerts = new Alerts();
|
||||
if(alertsData) {
|
||||
_.forEach(alertsData, (alertRow) => {
|
||||
alerts.push(alertRow);
|
||||
});
|
||||
}
|
||||
var alerts = new Alerts()
|
||||
if (alertsData) {
|
||||
_.forEach(alertsData, (alertRow) => {
|
||||
alerts.push(alertRow)
|
||||
})
|
||||
}
|
||||
|
||||
// ====================================
|
||||
// Establish WebSocket connection
|
||||
// ====================================
|
||||
|
||||
var socket = io(window.location.origin);
|
||||
var socket = io(window.location.origin)
|
||||
|
||||
//=include components/search.js
|
||||
// =include components/search.js
|
||||
|
||||
// ====================================
|
||||
// Pages logic
|
||||
// ====================================
|
||||
|
||||
//=include pages/view.js
|
||||
//=include pages/create.js
|
||||
//=include pages/edit.js
|
||||
//=include pages/source.js
|
||||
//=include pages/admin.js
|
||||
|
||||
});
|
||||
// =include pages/view.js
|
||||
// =include pages/create.js
|
||||
// =include pages/edit.js
|
||||
// =include pages/source.js
|
||||
// =include pages/admin.js
|
||||
})
|
||||
|
||||
//=include helpers/form.js
|
||||
//=include helpers/pages.js
|
||||
// =include helpers/form.js
|
||||
// =include helpers/pages.js
|
||||
|
||||
//=include components/alerts.js
|
||||
// =include components/alerts.js
|
||||
|
@ -1,78 +1,74 @@
|
||||
|
||||
let modelist = ace.require("ace/ext/modelist");
|
||||
let codeEditor = null;
|
||||
let modelist = ace.require('ace/ext/modelist')
|
||||
let codeEditor = null
|
||||
|
||||
// ACE - Mode Loader
|
||||
|
||||
let modelistLoaded = [];
|
||||
let modelistLoaded = []
|
||||
let loadAceMode = (m) => {
|
||||
return $.ajax({
|
||||
url: '/js/ace/mode-' + m + '.js',
|
||||
dataType: "script",
|
||||
cache: true,
|
||||
beforeSend: () => {
|
||||
if(_.includes(modelistLoaded, m)) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
success: () => {
|
||||
modelistLoaded.push(m);
|
||||
}
|
||||
});
|
||||
};
|
||||
return $.ajax({
|
||||
url: '/js/ace/mode-' + m + '.js',
|
||||
dataType: 'script',
|
||||
cache: true,
|
||||
beforeSend: () => {
|
||||
if (_.includes(modelistLoaded, m)) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
success: () => {
|
||||
modelistLoaded.push(m)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Vue Code Block instance
|
||||
|
||||
let vueCodeBlock = new Vue({
|
||||
el: '#modal-editor-codeblock',
|
||||
data: {
|
||||
modes: modelist.modesByName,
|
||||
modeSelected: 'text',
|
||||
initContent: ''
|
||||
},
|
||||
watch: {
|
||||
modeSelected: (val, oldVal) => {
|
||||
loadAceMode(val).done(() => {
|
||||
ace.require("ace/mode/" + val);
|
||||
codeEditor.getSession().setMode("ace/mode/" + val);
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open: (ev) => {
|
||||
el: '#modal-editor-codeblock',
|
||||
data: {
|
||||
modes: modelist.modesByName,
|
||||
modeSelected: 'text',
|
||||
initContent: ''
|
||||
},
|
||||
watch: {
|
||||
modeSelected: (val, oldVal) => {
|
||||
loadAceMode(val).done(() => {
|
||||
ace.require('ace/mode/' + val)
|
||||
codeEditor.getSession().setMode('ace/mode/' + val)
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open: (ev) => {
|
||||
$('#modal-editor-codeblock').addClass('is-active')
|
||||
|
||||
$('#modal-editor-codeblock').addClass('is-active');
|
||||
_.delay(() => {
|
||||
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)
|
||||
|
||||
_.delay(() => {
|
||||
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.setValue(vueCodeBlock.initContent);
|
||||
codeEditor.focus()
|
||||
codeEditor.renderer.updateFull()
|
||||
}, 300)
|
||||
},
|
||||
cancel: (ev) => {
|
||||
mdeModalOpenState = false
|
||||
$('#modal-editor-codeblock').removeClass('is-active')
|
||||
vueCodeBlock.initContent = ''
|
||||
},
|
||||
insertCode: (ev) => {
|
||||
if (mde.codemirror.doc.somethingSelected()) {
|
||||
mde.codemirror.execCommand('singleSelection')
|
||||
}
|
||||
let codeBlockText = '\n```' + vueCodeBlock.modeSelected + '\n' + codeEditor.getValue() + '\n```\n'
|
||||
|
||||
codeEditor.focus();
|
||||
codeEditor.renderer.updateFull();
|
||||
}, 300);
|
||||
|
||||
},
|
||||
cancel: (ev) => {
|
||||
mdeModalOpenState = false;
|
||||
$('#modal-editor-codeblock').removeClass('is-active');
|
||||
vueCodeBlock.initContent = '';
|
||||
},
|
||||
insertCode: (ev) => {
|
||||
|
||||
if(mde.codemirror.doc.somethingSelected()) {
|
||||
mde.codemirror.execCommand('singleSelection');
|
||||
}
|
||||
let codeBlockText = '\n```' + vueCodeBlock.modeSelected + '\n' + codeEditor.getValue() + '\n```\n';
|
||||
|
||||
mde.codemirror.doc.replaceSelection(codeBlockText);
|
||||
vueCodeBlock.cancel();
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
mde.codemirror.doc.replaceSelection(codeBlockText)
|
||||
vueCodeBlock.cancel()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1,365 +1,351 @@
|
||||
|
||||
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');
|
||||
},
|
||||
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) => {
|
||||
selectFile: (fileId) => {
|
||||
vueFile.currentFile = fileId
|
||||
},
|
||||
insertFileLink: (ev) => {
|
||||
if (mde.codemirror.doc.somethingSelected()) {
|
||||
mde.codemirror.execCommand('singleSelection')
|
||||
}
|
||||
|
||||
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 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 + '")'
|
||||
|
||||
let fileText = '[' + selFile.titleGuess + '](/uploads/' + selFile.normalizedPath + ' "' + selFile.titleGuess + '")';
|
||||
|
||||
mde.codemirror.doc.replaceSelection(fileText);
|
||||
vueFile.cancel();
|
||||
|
||||
},
|
||||
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;
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
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();
|
||||
});
|
||||
});
|
||||
},
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
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) => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
$('#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
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
})
|
||||
|
@ -1,412 +1,396 @@
|
||||
|
||||
let vueImage = new Vue({
|
||||
el: '#modal-editor-image',
|
||||
data: {
|
||||
isLoading: false,
|
||||
isLoadingText: '',
|
||||
newFolderName: '',
|
||||
newFolderShow: false,
|
||||
newFolderError: false,
|
||||
fetchFromUrlURL: '',
|
||||
fetchFromUrlShow: false,
|
||||
folders: [],
|
||||
currentFolder: '',
|
||||
currentImage: '',
|
||||
currentAlign: 'left',
|
||||
images: [],
|
||||
uploadSucceeded: false,
|
||||
postUploadChecks: 0,
|
||||
renameImageShow: false,
|
||||
renameImageId: '',
|
||||
renameImageFilename: '',
|
||||
deleteImageShow: false,
|
||||
deleteImageId: '',
|
||||
deleteImageFilename: ''
|
||||
},
|
||||
methods: {
|
||||
|
||||
open: () => {
|
||||
mdeModalOpenState = true;
|
||||
$('#modal-editor-image').addClass('is-active');
|
||||
vueImage.refreshFolders();
|
||||
},
|
||||
cancel: (ev) => {
|
||||
mdeModalOpenState = false;
|
||||
$('#modal-editor-image').removeClass('is-active');
|
||||
},
|
||||
el: '#modal-editor-image',
|
||||
data: {
|
||||
isLoading: false,
|
||||
isLoadingText: '',
|
||||
newFolderName: '',
|
||||
newFolderShow: false,
|
||||
newFolderError: false,
|
||||
fetchFromUrlURL: '',
|
||||
fetchFromUrlShow: false,
|
||||
folders: [],
|
||||
currentFolder: '',
|
||||
currentImage: '',
|
||||
currentAlign: 'left',
|
||||
images: [],
|
||||
uploadSucceeded: false,
|
||||
postUploadChecks: 0,
|
||||
renameImageShow: false,
|
||||
renameImageId: '',
|
||||
renameImageFilename: '',
|
||||
deleteImageShow: false,
|
||||
deleteImageId: '',
|
||||
deleteImageFilename: ''
|
||||
},
|
||||
methods: {
|
||||
|
||||
open: () => {
|
||||
mdeModalOpenState = true
|
||||
$('#modal-editor-image').addClass('is-active')
|
||||
vueImage.refreshFolders()
|
||||
},
|
||||
cancel: (ev) => {
|
||||
mdeModalOpenState = false
|
||||
$('#modal-editor-image').removeClass('is-active')
|
||||
},
|
||||
|
||||
// -------------------------------------------
|
||||
// INSERT IMAGE
|
||||
// -------------------------------------------
|
||||
|
||||
selectImage: (imageId) => {
|
||||
vueImage.currentImage = imageId;
|
||||
},
|
||||
insertImage: (ev) => {
|
||||
|
||||
if(mde.codemirror.doc.somethingSelected()) {
|
||||
mde.codemirror.execCommand('singleSelection');
|
||||
}
|
||||
|
||||
let selImage = _.find(vueImage.images, ['_id', vueImage.currentImage]);
|
||||
selImage.normalizedPath = (selImage.folder === 'f:') ? selImage.filename : selImage.folder.slice(2) + '/' + selImage.filename;
|
||||
selImage.titleGuess = _.startCase(selImage.basename);
|
||||
|
||||
let imageText = '![' + selImage.titleGuess + '](/uploads/' + selImage.normalizedPath + ' "' + selImage.titleGuess + '")';
|
||||
switch(vueImage.currentAlign) {
|
||||
case 'center':
|
||||
imageText += '{.align-center}';
|
||||
break;
|
||||
case 'right':
|
||||
imageText += '{.align-right}';
|
||||
break;
|
||||
case 'logo':
|
||||
imageText += '{.pagelogo}';
|
||||
break;
|
||||
}
|
||||
|
||||
mde.codemirror.doc.replaceSelection(imageText);
|
||||
vueImage.cancel();
|
||||
|
||||
},
|
||||
selectImage: (imageId) => {
|
||||
vueImage.currentImage = imageId
|
||||
},
|
||||
insertImage: (ev) => {
|
||||
if (mde.codemirror.doc.somethingSelected()) {
|
||||
mde.codemirror.execCommand('singleSelection')
|
||||
}
|
||||
|
||||
let selImage = _.find(vueImage.images, ['_id', vueImage.currentImage])
|
||||
selImage.normalizedPath = (selImage.folder === 'f:') ? selImage.filename : selImage.folder.slice(2) + '/' + selImage.filename
|
||||
selImage.titleGuess = _.startCase(selImage.basename)
|
||||
|
||||
let imageText = '![' + selImage.titleGuess + '](/uploads/' + selImage.normalizedPath + ' "' + selImage.titleGuess + '")'
|
||||
switch (vueImage.currentAlign) {
|
||||
case 'center':
|
||||
imageText += '{.align-center}'
|
||||
break
|
||||
case 'right':
|
||||
imageText += '{.align-right}'
|
||||
break
|
||||
case 'logo':
|
||||
imageText += '{.pagelogo}'
|
||||
break
|
||||
}
|
||||
|
||||
mde.codemirror.doc.replaceSelection(imageText)
|
||||
vueImage.cancel()
|
||||
},
|
||||
|
||||
// -------------------------------------------
|
||||
// NEW FOLDER
|
||||
// -------------------------------------------
|
||||
|
||||
newFolder: (ev) => {
|
||||
vueImage.newFolderName = '';
|
||||
vueImage.newFolderError = false;
|
||||
vueImage.newFolderShow = true;
|
||||
_.delay(() => { $('#txt-editor-image-newfoldername').focus(); }, 400);
|
||||
},
|
||||
newFolderDiscard: (ev) => {
|
||||
vueImage.newFolderShow = false;
|
||||
},
|
||||
newFolderCreate: (ev) => {
|
||||
|
||||
let regFolderName = new RegExp("^[a-z0-9][a-z0-9\-]*[a-z0-9]$");
|
||||
vueImage.newFolderName = _.kebabCase(_.trim(vueImage.newFolderName));
|
||||
|
||||
if(_.isEmpty(vueImage.newFolderName) || !regFolderName.test(vueImage.newFolderName)) {
|
||||
vueImage.newFolderError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
vueImage.newFolderDiscard();
|
||||
vueImage.isLoadingText = 'Creating new folder...';
|
||||
vueImage.isLoading = true;
|
||||
|
||||
Vue.nextTick(() => {
|
||||
socket.emit('uploadsCreateFolder', { foldername: vueImage.newFolderName }, (data) => {
|
||||
vueImage.folders = data;
|
||||
vueImage.currentFolder = vueImage.newFolderName;
|
||||
vueImage.images = [];
|
||||
vueImage.isLoading = false;
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
newFolder: (ev) => {
|
||||
vueImage.newFolderName = ''
|
||||
vueImage.newFolderError = false
|
||||
vueImage.newFolderShow = true
|
||||
_.delay(() => { $('#txt-editor-image-newfoldername').focus() }, 400)
|
||||
},
|
||||
newFolderDiscard: (ev) => {
|
||||
vueImage.newFolderShow = false
|
||||
},
|
||||
newFolderCreate: (ev) => {
|
||||
let regFolderName = new RegExp('^[a-z0-9][a-z0-9\-]*[a-z0-9]$')
|
||||
vueImage.newFolderName = _.kebabCase(_.trim(vueImage.newFolderName))
|
||||
|
||||
if (_.isEmpty(vueImage.newFolderName) || !regFolderName.test(vueImage.newFolderName)) {
|
||||
vueImage.newFolderError = true
|
||||
return
|
||||
}
|
||||
|
||||
vueImage.newFolderDiscard()
|
||||
vueImage.isLoadingText = 'Creating new folder...'
|
||||
vueImage.isLoading = true
|
||||
|
||||
Vue.nextTick(() => {
|
||||
socket.emit('uploadsCreateFolder', { foldername: vueImage.newFolderName }, (data) => {
|
||||
vueImage.folders = data
|
||||
vueImage.currentFolder = vueImage.newFolderName
|
||||
vueImage.images = []
|
||||
vueImage.isLoading = false
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// -------------------------------------------
|
||||
// FETCH FROM URL
|
||||
// -------------------------------------------
|
||||
|
||||
fetchFromUrl: (ev) => {
|
||||
vueImage.fetchFromUrlURL = '';
|
||||
vueImage.fetchFromUrlShow = true;
|
||||
_.delay(() => { $('#txt-editor-image-fetchurl').focus(); }, 400);
|
||||
},
|
||||
fetchFromUrlDiscard: (ev) => {
|
||||
vueImage.fetchFromUrlShow = false;
|
||||
},
|
||||
fetchFromUrlGo: (ev) => {
|
||||
|
||||
vueImage.fetchFromUrlDiscard();
|
||||
vueImage.isLoadingText = 'Fetching image...';
|
||||
vueImage.isLoading = true;
|
||||
|
||||
Vue.nextTick(() => {
|
||||
socket.emit('uploadsFetchFileFromURL', { folder: vueImage.currentFolder, fetchUrl: vueImage.fetchFromUrlURL }, (data) => {
|
||||
if(data.ok) {
|
||||
vueImage.waitChangeComplete(vueImage.images.length, true);
|
||||
} else {
|
||||
vueImage.isLoading = false;
|
||||
alerts.pushError('Upload error', data.msg);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
fetchFromUrl: (ev) => {
|
||||
vueImage.fetchFromUrlURL = ''
|
||||
vueImage.fetchFromUrlShow = true
|
||||
_.delay(() => { $('#txt-editor-image-fetchurl').focus() }, 400)
|
||||
},
|
||||
fetchFromUrlDiscard: (ev) => {
|
||||
vueImage.fetchFromUrlShow = false
|
||||
},
|
||||
fetchFromUrlGo: (ev) => {
|
||||
vueImage.fetchFromUrlDiscard()
|
||||
vueImage.isLoadingText = 'Fetching image...'
|
||||
vueImage.isLoading = true
|
||||
|
||||
Vue.nextTick(() => {
|
||||
socket.emit('uploadsFetchFileFromURL', { folder: vueImage.currentFolder, fetchUrl: vueImage.fetchFromUrlURL }, (data) => {
|
||||
if (data.ok) {
|
||||
vueImage.waitChangeComplete(vueImage.images.length, true)
|
||||
} else {
|
||||
vueImage.isLoading = false
|
||||
alerts.pushError('Upload error', data.msg)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// -------------------------------------------
|
||||
// RENAME IMAGE
|
||||
// -------------------------------------------
|
||||
|
||||
renameImage: () => {
|
||||
|
||||
let c = _.find(vueImage.images, ['_id', vueImage.renameImageId ]);
|
||||
vueImage.renameImageFilename = c.basename || '';
|
||||
vueImage.renameImageShow = true;
|
||||
_.delay(() => {
|
||||
$('#txt-editor-image-rename').focus();
|
||||
_.defer(() => { $('#txt-editor-image-rename').select(); });
|
||||
}, 400);
|
||||
},
|
||||
renameImageDiscard: () => {
|
||||
vueImage.renameImageShow = false;
|
||||
},
|
||||
renameImageGo: () => {
|
||||
|
||||
vueImage.renameImageDiscard();
|
||||
vueImage.isLoadingText = 'Renaming image...';
|
||||
vueImage.isLoading = true;
|
||||
|
||||
Vue.nextTick(() => {
|
||||
socket.emit('uploadsRenameFile', { uid: vueImage.renameImageId, folder: vueImage.currentFolder, filename: vueImage.renameImageFilename }, (data) => {
|
||||
if(data.ok) {
|
||||
vueImage.waitChangeComplete(vueImage.images.length, false);
|
||||
} else {
|
||||
vueImage.isLoading = false;
|
||||
alerts.pushError('Rename error', data.msg);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
renameImage: () => {
|
||||
let c = _.find(vueImage.images, ['_id', vueImage.renameImageId ])
|
||||
vueImage.renameImageFilename = c.basename || ''
|
||||
vueImage.renameImageShow = true
|
||||
_.delay(() => {
|
||||
$('#txt-editor-image-rename').focus()
|
||||
_.defer(() => { $('#txt-editor-image-rename').select() })
|
||||
}, 400)
|
||||
},
|
||||
renameImageDiscard: () => {
|
||||
vueImage.renameImageShow = false
|
||||
},
|
||||
renameImageGo: () => {
|
||||
vueImage.renameImageDiscard()
|
||||
vueImage.isLoadingText = 'Renaming image...'
|
||||
vueImage.isLoading = true
|
||||
|
||||
Vue.nextTick(() => {
|
||||
socket.emit('uploadsRenameFile', { uid: vueImage.renameImageId, folder: vueImage.currentFolder, filename: vueImage.renameImageFilename }, (data) => {
|
||||
if (data.ok) {
|
||||
vueImage.waitChangeComplete(vueImage.images.length, false)
|
||||
} else {
|
||||
vueImage.isLoading = false
|
||||
alerts.pushError('Rename error', data.msg)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// -------------------------------------------
|
||||
// MOVE IMAGE
|
||||
// -------------------------------------------
|
||||
|
||||
moveImage: (uid, fld) => {
|
||||
vueImage.isLoadingText = 'Moving image...';
|
||||
vueImage.isLoading = true;
|
||||
Vue.nextTick(() => {
|
||||
socket.emit('uploadsMoveFile', { uid, folder: fld }, (data) => {
|
||||
if(data.ok) {
|
||||
vueImage.loadImages();
|
||||
} else {
|
||||
vueImage.isLoading = false;
|
||||
alerts.pushError('Rename error', data.msg);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
moveImage: (uid, fld) => {
|
||||
vueImage.isLoadingText = 'Moving image...'
|
||||
vueImage.isLoading = true
|
||||
Vue.nextTick(() => {
|
||||
socket.emit('uploadsMoveFile', { uid, folder: fld }, (data) => {
|
||||
if (data.ok) {
|
||||
vueImage.loadImages()
|
||||
} else {
|
||||
vueImage.isLoading = false
|
||||
alerts.pushError('Rename error', data.msg)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// -------------------------------------------
|
||||
// DELETE IMAGE
|
||||
// -------------------------------------------
|
||||
|
||||
deleteImageWarn: (show) => {
|
||||
if(show) {
|
||||
let c = _.find(vueImage.images, ['_id', vueImage.deleteImageId ]);
|
||||
vueImage.deleteImageFilename = c.filename || 'this image';
|
||||
}
|
||||
vueImage.deleteImageShow = show;
|
||||
},
|
||||
deleteImageGo: () => {
|
||||
vueImage.deleteImageWarn(false);
|
||||
vueImage.isLoadingText = 'Deleting image...';
|
||||
vueImage.isLoading = true;
|
||||
Vue.nextTick(() => {
|
||||
socket.emit('uploadsDeleteFile', { uid: vueImage.deleteImageId }, (data) => {
|
||||
vueImage.loadImages();
|
||||
});
|
||||
});
|
||||
},
|
||||
deleteImageWarn: (show) => {
|
||||
if (show) {
|
||||
let c = _.find(vueImage.images, ['_id', vueImage.deleteImageId ])
|
||||
vueImage.deleteImageFilename = c.filename || 'this image'
|
||||
}
|
||||
vueImage.deleteImageShow = show
|
||||
},
|
||||
deleteImageGo: () => {
|
||||
vueImage.deleteImageWarn(false)
|
||||
vueImage.isLoadingText = 'Deleting image...'
|
||||
vueImage.isLoading = true
|
||||
Vue.nextTick(() => {
|
||||
socket.emit('uploadsDeleteFile', { uid: vueImage.deleteImageId }, (data) => {
|
||||
vueImage.loadImages()
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// -------------------------------------------
|
||||
// LOAD FROM REMOTE
|
||||
// -------------------------------------------
|
||||
|
||||
selectFolder: (fldName) => {
|
||||
vueImage.currentFolder = fldName;
|
||||
vueImage.loadImages();
|
||||
},
|
||||
|
||||
refreshFolders: () => {
|
||||
vueImage.isLoadingText = 'Fetching folders list...';
|
||||
vueImage.isLoading = true;
|
||||
vueImage.currentFolder = '';
|
||||
vueImage.currentImage = '';
|
||||
Vue.nextTick(() => {
|
||||
socket.emit('uploadsGetFolders', { }, (data) => {
|
||||
vueImage.folders = data;
|
||||
vueImage.loadImages();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
loadImages: (silent) => {
|
||||
if(!silent) {
|
||||
vueImage.isLoadingText = 'Fetching images...';
|
||||
vueImage.isLoading = true;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
Vue.nextTick(() => {
|
||||
socket.emit('uploadsGetImages', { folder: vueImage.currentFolder }, (data) => {
|
||||
vueImage.images = data;
|
||||
if(!silent) {
|
||||
vueImage.isLoading = false;
|
||||
}
|
||||
vueImage.attachContextMenus();
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
waitChangeComplete: (oldAmount, expectChange) => {
|
||||
|
||||
expectChange = (_.isBoolean(expectChange)) ? expectChange : true;
|
||||
|
||||
vueImage.postUploadChecks++;
|
||||
vueImage.isLoadingText = 'Processing...';
|
||||
|
||||
Vue.nextTick(() => {
|
||||
vueImage.loadImages(true).then(() => {
|
||||
if((vueImage.images.length !== oldAmount) === expectChange) {
|
||||
vueImage.postUploadChecks = 0;
|
||||
vueImage.isLoading = false;
|
||||
} else if(vueImage.postUploadChecks > 5) {
|
||||
vueImage.postUploadChecks = 0;
|
||||
vueImage.isLoading = false;
|
||||
alerts.pushError('Unable to fetch updated listing', 'Try again later');
|
||||
} else {
|
||||
_.delay(() => {
|
||||
vueImage.waitChangeComplete(oldAmount, expectChange);
|
||||
}, 1500);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
selectFolder: (fldName) => {
|
||||
vueImage.currentFolder = fldName
|
||||
vueImage.loadImages()
|
||||
},
|
||||
|
||||
refreshFolders: () => {
|
||||
vueImage.isLoadingText = 'Fetching folders list...'
|
||||
vueImage.isLoading = true
|
||||
vueImage.currentFolder = ''
|
||||
vueImage.currentImage = ''
|
||||
Vue.nextTick(() => {
|
||||
socket.emit('uploadsGetFolders', { }, (data) => {
|
||||
vueImage.folders = data
|
||||
vueImage.loadImages()
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
loadImages: (silent) => {
|
||||
if (!silent) {
|
||||
vueImage.isLoadingText = 'Fetching images...'
|
||||
vueImage.isLoading = true
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
Vue.nextTick(() => {
|
||||
socket.emit('uploadsGetImages', { folder: vueImage.currentFolder }, (data) => {
|
||||
vueImage.images = data
|
||||
if (!silent) {
|
||||
vueImage.isLoading = false
|
||||
}
|
||||
vueImage.attachContextMenus()
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
waitChangeComplete: (oldAmount, expectChange) => {
|
||||
expectChange = (_.isBoolean(expectChange)) ? expectChange : true
|
||||
|
||||
vueImage.postUploadChecks++
|
||||
vueImage.isLoadingText = 'Processing...'
|
||||
|
||||
Vue.nextTick(() => {
|
||||
vueImage.loadImages(true).then(() => {
|
||||
if ((vueImage.images.length !== oldAmount) === expectChange) {
|
||||
vueImage.postUploadChecks = 0
|
||||
vueImage.isLoading = false
|
||||
} else if (vueImage.postUploadChecks > 5) {
|
||||
vueImage.postUploadChecks = 0
|
||||
vueImage.isLoading = false
|
||||
alerts.pushError('Unable to fetch updated listing', 'Try again later')
|
||||
} else {
|
||||
_.delay(() => {
|
||||
vueImage.waitChangeComplete(oldAmount, expectChange)
|
||||
}, 1500)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// -------------------------------------------
|
||||
// IMAGE CONTEXT MENU
|
||||
// -------------------------------------------
|
||||
|
||||
attachContextMenus: () => {
|
||||
|
||||
let moveFolders = _.map(vueImage.folders, (f) => {
|
||||
return {
|
||||
name: (f !== '') ? f : '/ (root)',
|
||||
icon: 'fa-folder',
|
||||
callback: (key, opt) => {
|
||||
let moveImageId = _.toString($(opt.$trigger).data('uid'));
|
||||
let moveImageDestFolder = _.nth(vueImage.folders, key);
|
||||
vueImage.moveImage(moveImageId, moveImageDestFolder);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
$.contextMenu('destroy', '.editor-modal-image-choices > figure');
|
||||
$.contextMenu({
|
||||
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();
|
||||
let trigDim = { w: $(opt.$trigger).width() / 2, 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) => {
|
||||
vueImage.renameImageId = _.toString(opt.$trigger[0].dataset.uid);
|
||||
vueImage.renameImage();
|
||||
}
|
||||
},
|
||||
move: {
|
||||
name: "Move to...",
|
||||
icon: "fa-folder-open-o",
|
||||
items: moveFolders
|
||||
},
|
||||
delete: {
|
||||
name: "Delete",
|
||||
icon: "fa-trash",
|
||||
callback: (key, opt) => {
|
||||
vueImage.deleteImageId = _.toString(opt.$trigger[0].dataset.uid);
|
||||
vueImage.deleteImageWarn(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
$('#btn-editor-image-upload input').on('change', (ev) => {
|
||||
attachContextMenus: () => {
|
||||
let moveFolders = _.map(vueImage.folders, (f) => {
|
||||
return {
|
||||
name: (f !== '') ? f : '/ (root)',
|
||||
icon: 'fa-folder',
|
||||
callback: (key, opt) => {
|
||||
let moveImageId = _.toString($(opt.$trigger).data('uid'))
|
||||
let moveImageDestFolder = _.nth(vueImage.folders, key)
|
||||
vueImage.moveImage(moveImageId, moveImageDestFolder)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
$.contextMenu('destroy', '.editor-modal-image-choices > figure')
|
||||
$.contextMenu({
|
||||
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()
|
||||
let trigDim = { w: $(opt.$trigger).width() / 2, 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) => {
|
||||
vueImage.renameImageId = _.toString(opt.$trigger[0].dataset.uid)
|
||||
vueImage.renameImage()
|
||||
}
|
||||
},
|
||||
move: {
|
||||
name: 'Move to...',
|
||||
icon: 'fa-folder-open-o',
|
||||
items: moveFolders
|
||||
},
|
||||
delete: {
|
||||
name: 'Delete',
|
||||
icon: 'fa-trash',
|
||||
callback: (key, opt) => {
|
||||
vueImage.deleteImageId = _.toString(opt.$trigger[0].dataset.uid)
|
||||
vueImage.deleteImageWarn(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
let curImageAmount = vueImage.images.length;
|
||||
|
||||
$(ev.currentTarget).simpleUpload("/uploads/img", {
|
||||
|
||||
name: 'imgfile',
|
||||
data: {
|
||||
folder: vueImage.currentFolder
|
||||
},
|
||||
limit: 20,
|
||||
expect: 'json',
|
||||
allowedExts: ["jpg", "jpeg", "gif", "png", "webp"],
|
||||
allowedTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
|
||||
maxFileSize: 3145728, // max 3 MB
|
||||
|
||||
init: (totalUploads) => {
|
||||
vueImage.uploadSucceeded = false;
|
||||
vueImage.isLoadingText = 'Preparing to upload...';
|
||||
vueImage.isLoading = true;
|
||||
},
|
||||
|
||||
progress: (progress) => {
|
||||
vueImage.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.'
|
||||
});
|
||||
vueImage.uploadSucceeded = true;
|
||||
}
|
||||
} else {
|
||||
vueImage.uploadSucceeded = true;
|
||||
}
|
||||
|
||||
} else {
|
||||
alerts.pushError('Upload error', data.msg);
|
||||
}
|
||||
},
|
||||
|
||||
error: (error) => {
|
||||
alerts.pushError(error.message, this.upload.file.name);
|
||||
},
|
||||
|
||||
finish: () => {
|
||||
if(vueImage.uploadSucceeded) {
|
||||
vueImage.waitChangeComplete(curImageAmount, true);
|
||||
} else {
|
||||
vueImage.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
$('#btn-editor-image-upload input').on('change', (ev) => {
|
||||
let curImageAmount = vueImage.images.length
|
||||
|
||||
$(ev.currentTarget).simpleUpload('/uploads/img', {
|
||||
|
||||
name: 'imgfile',
|
||||
data: {
|
||||
folder: vueImage.currentFolder
|
||||
},
|
||||
limit: 20,
|
||||
expect: 'json',
|
||||
allowedExts: ['jpg', 'jpeg', 'gif', 'png', 'webp'],
|
||||
allowedTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
|
||||
maxFileSize: 3145728, // max 3 MB
|
||||
|
||||
init: (totalUploads) => {
|
||||
vueImage.uploadSucceeded = false
|
||||
vueImage.isLoadingText = 'Preparing to upload...'
|
||||
vueImage.isLoading = true
|
||||
},
|
||||
|
||||
progress: (progress) => {
|
||||
vueImage.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.'
|
||||
})
|
||||
vueImage.uploadSucceeded = true
|
||||
}
|
||||
} else {
|
||||
vueImage.uploadSucceeded = true
|
||||
}
|
||||
} else {
|
||||
alerts.pushError('Upload error', data.msg)
|
||||
}
|
||||
},
|
||||
|
||||
error: (error) => {
|
||||
alerts.pushError(error.message, this.upload.file.name)
|
||||
},
|
||||
|
||||
finish: () => {
|
||||
if (vueImage.uploadSucceeded) {
|
||||
vueImage.waitChangeComplete(curImageAmount, true)
|
||||
} else {
|
||||
vueImage.isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
})
|
||||
|
@ -1,49 +1,47 @@
|
||||
|
||||
const videoRules = {
|
||||
'youtube': new RegExp(/(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*/, 'i'),
|
||||
'vimeo': new RegExp(/vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^\/]*)\/videos\/|album\/(?:\d+)\/video\/|)(\d+)(?:$|\/|\?)/, 'i'),
|
||||
'dailymotion': new RegExp(/(?:dailymotion\.com(?:\/embed)?(?:\/video|\/hub)|dai\.ly)\/([0-9a-z]+)(?:[\-_0-9a-zA-Z]+(?:#video=)?([a-z0-9]+)?)?/, 'i')
|
||||
};
|
||||
'youtube': new RegExp(/(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*/, 'i'),
|
||||
'vimeo': new RegExp(/vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^\/]*)\/videos\/|album\/(?:\d+)\/video\/|)(\d+)(?:$|\/|\?)/, 'i'),
|
||||
'dailymotion': new RegExp(/(?:dailymotion\.com(?:\/embed)?(?:\/video|\/hub)|dai\.ly)\/([0-9a-z]+)(?:[\-_0-9a-zA-Z]+(?:#video=)?([a-z0-9]+)?)?/, 'i')
|
||||
}
|
||||
|
||||
// Vue Video instance
|
||||
|
||||
let vueVideo = new Vue({
|
||||
el: '#modal-editor-video',
|
||||
data: {
|
||||
link: ''
|
||||
},
|
||||
methods: {
|
||||
open: (ev) => {
|
||||
$('#modal-editor-video').addClass('is-active');
|
||||
$('#modal-editor-video input').focus();
|
||||
},
|
||||
cancel: (ev) => {
|
||||
mdeModalOpenState = false;
|
||||
$('#modal-editor-video').removeClass('is-active');
|
||||
vueVideo.link = '';
|
||||
},
|
||||
insertVideo: (ev) => {
|
||||
|
||||
if(mde.codemirror.doc.somethingSelected()) {
|
||||
mde.codemirror.execCommand('singleSelection');
|
||||
}
|
||||
el: '#modal-editor-video',
|
||||
data: {
|
||||
link: ''
|
||||
},
|
||||
methods: {
|
||||
open: (ev) => {
|
||||
$('#modal-editor-video').addClass('is-active')
|
||||
$('#modal-editor-video input').focus()
|
||||
},
|
||||
cancel: (ev) => {
|
||||
mdeModalOpenState = false
|
||||
$('#modal-editor-video').removeClass('is-active')
|
||||
vueVideo.link = ''
|
||||
},
|
||||
insertVideo: (ev) => {
|
||||
if (mde.codemirror.doc.somethingSelected()) {
|
||||
mde.codemirror.execCommand('singleSelection')
|
||||
}
|
||||
|
||||
// Guess video type
|
||||
|
||||
let videoType = _.findKey(videoRules, (vr) => {
|
||||
return vr.test(vueVideo.link);
|
||||
});
|
||||
if(_.isNil(videoType)) {
|
||||
videoType = 'video';
|
||||
}
|
||||
let videoType = _.findKey(videoRules, (vr) => {
|
||||
return vr.test(vueVideo.link)
|
||||
})
|
||||
if (_.isNil(videoType)) {
|
||||
videoType = 'video'
|
||||
}
|
||||
|
||||
// Insert video tag
|
||||
|
||||
let videoText = '[video](' + vueVideo.link + '){.' + videoType + '}\n';
|
||||
let videoText = '[video](' + vueVideo.link + '){.' + videoType + '}\n'
|
||||
|
||||
mde.codemirror.doc.replaceSelection(videoText);
|
||||
vueVideo.cancel();
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
mde.codemirror.doc.replaceSelection(videoText)
|
||||
vueVideo.cancel()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1,84 +1,81 @@
|
||||
"use strict";
|
||||
'use strict'
|
||||
|
||||
if($('#search-input').length) {
|
||||
if ($('#search-input').length) {
|
||||
$('#search-input').focus()
|
||||
|
||||
$('#search-input').focus();
|
||||
$('.searchresults').css('display', 'block')
|
||||
|
||||
$('.searchresults').css('display', 'block');
|
||||
|
||||
var vueHeader = new Vue({
|
||||
el: '#header-container',
|
||||
data: {
|
||||
searchq: '',
|
||||
searchres: [],
|
||||
searchsuggest: [],
|
||||
searchload: 0,
|
||||
searchactive: false,
|
||||
searchmoveidx: 0,
|
||||
searchmovekey: '',
|
||||
searchmovearr: []
|
||||
},
|
||||
watch: {
|
||||
searchq: (val, oldVal) => {
|
||||
vueHeader.searchmoveidx = 0;
|
||||
if(val.length >= 3) {
|
||||
vueHeader.searchactive = true;
|
||||
vueHeader.searchload++;
|
||||
socket.emit('search', { terms: val }, (data) => {
|
||||
vueHeader.searchres = data.match;
|
||||
vueHeader.searchsuggest = data.suggest;
|
||||
vueHeader.searchmovearr = _.concat([], vueHeader.searchres, vueHeader.searchsuggest);
|
||||
if(vueHeader.searchload > 0) { vueHeader.searchload--; }
|
||||
});
|
||||
} else {
|
||||
vueHeader.searchactive = false;
|
||||
vueHeader.searchres = [];
|
||||
vueHeader.searchsuggest = [];
|
||||
vueHeader.searchmovearr = [];
|
||||
vueHeader.searchload = 0;
|
||||
}
|
||||
},
|
||||
searchmoveidx: (val, oldVal) => {
|
||||
if(val > 0) {
|
||||
vueHeader.searchmovekey = (vueHeader.searchmovearr[val - 1]) ?
|
||||
var vueHeader = new Vue({
|
||||
el: '#header-container',
|
||||
data: {
|
||||
searchq: '',
|
||||
searchres: [],
|
||||
searchsuggest: [],
|
||||
searchload: 0,
|
||||
searchactive: false,
|
||||
searchmoveidx: 0,
|
||||
searchmovekey: '',
|
||||
searchmovearr: []
|
||||
},
|
||||
watch: {
|
||||
searchq: (val, oldVal) => {
|
||||
vueHeader.searchmoveidx = 0
|
||||
if (val.length >= 3) {
|
||||
vueHeader.searchactive = true
|
||||
vueHeader.searchload++
|
||||
socket.emit('search', { terms: val }, (data) => {
|
||||
vueHeader.searchres = data.match
|
||||
vueHeader.searchsuggest = data.suggest
|
||||
vueHeader.searchmovearr = _.concat([], vueHeader.searchres, vueHeader.searchsuggest)
|
||||
if (vueHeader.searchload > 0) { vueHeader.searchload-- }
|
||||
})
|
||||
} else {
|
||||
vueHeader.searchactive = false
|
||||
vueHeader.searchres = []
|
||||
vueHeader.searchsuggest = []
|
||||
vueHeader.searchmovearr = []
|
||||
vueHeader.searchload = 0
|
||||
}
|
||||
},
|
||||
searchmoveidx: (val, oldVal) => {
|
||||
if (val > 0) {
|
||||
vueHeader.searchmovekey = (vueHeader.searchmovearr[val - 1]) ?
|
||||
'res.' + vueHeader.searchmovearr[val - 1]._id :
|
||||
'sug.' + vueHeader.searchmovearr[val - 1];
|
||||
} else {
|
||||
vueHeader.searchmovekey = '';
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
useSuggestion: (sug) => {
|
||||
vueHeader.searchq = sug;
|
||||
},
|
||||
closeSearch: () => {
|
||||
vueHeader.searchq = '';
|
||||
},
|
||||
moveSelectSearch: () => {
|
||||
if(vueHeader.searchmoveidx < 1) { return; }
|
||||
let i = vueHeader.searchmoveidx - 1;
|
||||
|
||||
if(vueHeader.searchmovearr[i]) {
|
||||
window.location.assign('/' + vueHeader.searchmovearr[i]._id);
|
||||
} else {
|
||||
vueHeader.searchq = vueHeader.searchmovearr[i];
|
||||
}
|
||||
|
||||
},
|
||||
moveDownSearch: () => {
|
||||
if(vueHeader.searchmoveidx < vueHeader.searchmovearr.length) {
|
||||
vueHeader.searchmoveidx++;
|
||||
}
|
||||
},
|
||||
moveUpSearch: () => {
|
||||
if(vueHeader.searchmoveidx > 0) {
|
||||
vueHeader.searchmoveidx--;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
'sug.' + vueHeader.searchmovearr[val - 1]
|
||||
} else {
|
||||
vueHeader.searchmovekey = ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
useSuggestion: (sug) => {
|
||||
vueHeader.searchq = sug
|
||||
},
|
||||
closeSearch: () => {
|
||||
vueHeader.searchq = ''
|
||||
},
|
||||
moveSelectSearch: () => {
|
||||
if (vueHeader.searchmoveidx < 1) { return }
|
||||
let i = vueHeader.searchmoveidx - 1
|
||||
|
||||
$('main').on('click', vueHeader.closeSearch);
|
||||
if (vueHeader.searchmovearr[i]) {
|
||||
window.location.assign('/' + vueHeader.searchmovearr[i]._id)
|
||||
} else {
|
||||
vueHeader.searchq = vueHeader.searchmovearr[i]
|
||||
}
|
||||
},
|
||||
moveDownSearch: () => {
|
||||
if (vueHeader.searchmoveidx < vueHeader.searchmovearr.length) {
|
||||
vueHeader.searchmoveidx++
|
||||
}
|
||||
},
|
||||
moveUpSearch: () => {
|
||||
if (vueHeader.searchmoveidx > 0) {
|
||||
vueHeader.searchmoveidx--
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
$('main').on('click', vueHeader.closeSearch)
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
|
||||
function setInputSelection(input, startPos, endPos) {
|
||||
input.focus();
|
||||
if (typeof input.selectionStart != "undefined") {
|
||||
input.selectionStart = startPos;
|
||||
input.selectionEnd = endPos;
|
||||
} else if (document.selection && document.selection.createRange) {
|
||||
function setInputSelection (input, startPos, endPos) {
|
||||
input.focus()
|
||||
if (typeof input.selectionStart !== 'undefined') {
|
||||
input.selectionStart = startPos
|
||||
input.selectionEnd = endPos
|
||||
} else if (document.selection && document.selection.createRange) {
|
||||
// IE branch
|
||||
input.select();
|
||||
var range = document.selection.createRange();
|
||||
range.collapse(true);
|
||||
range.moveEnd("character", endPos);
|
||||
range.moveStart("character", startPos);
|
||||
range.select();
|
||||
}
|
||||
}
|
||||
input.select()
|
||||
var range = document.selection.createRange()
|
||||
range.collapse(true)
|
||||
range.moveEnd('character', endPos)
|
||||
range.moveStart('character', startPos)
|
||||
range.select()
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,9 @@
|
||||
|
||||
function makeSafePath(rawPath) {
|
||||
|
||||
let rawParts = _.split(_.trim(rawPath), '/');
|
||||
rawParts = _.map(rawParts, (r) => {
|
||||
return _.kebabCase(_.deburr(_.trim(r)));
|
||||
});
|
||||
|
||||
return _.join(_.filter(rawParts, (r) => { return !_.isEmpty(r); }), '/');
|
||||
|
||||
}
|
||||
function makeSafePath (rawPath) {
|
||||
let rawParts = _.split(_.trim(rawPath), '/')
|
||||
rawParts = _.map(rawParts, (r) => {
|
||||
return _.kebabCase(_.deburr(_.trim(r)))
|
||||
})
|
||||
|
||||
return _.join(_.filter(rawParts, (r) => { return !_.isEmpty(r) }), '/')
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
"use strict";
|
||||
'use strict'
|
||||
|
||||
jQuery( document ).ready(function( $ ) {
|
||||
|
||||
$('#login-user').focus();
|
||||
|
||||
});
|
||||
jQuery(document).ready(function ($) {
|
||||
$('#login-user').focus()
|
||||
})
|
||||
|
@ -1,29 +1,27 @@
|
||||
|
||||
//-> Create New Document
|
||||
// -> Create New Document
|
||||
|
||||
let suggestedCreatePath = currentBasePath + '/new-page';
|
||||
let suggestedCreatePath = currentBasePath + '/new-page'
|
||||
|
||||
$('.btn-create-prompt').on('click', (ev) => {
|
||||
$('#txt-create-prompt').val(suggestedCreatePath);
|
||||
$('#modal-create-prompt').toggleClass('is-active');
|
||||
setInputSelection($('#txt-create-prompt').get(0), currentBasePath.length + 1, suggestedCreatePath.length);
|
||||
$('#txt-create-prompt').removeClass('is-danger').next().addClass('is-hidden');
|
||||
});
|
||||
$('#txt-create-prompt').val(suggestedCreatePath)
|
||||
$('#modal-create-prompt').toggleClass('is-active')
|
||||
setInputSelection($('#txt-create-prompt').get(0), currentBasePath.length + 1, suggestedCreatePath.length)
|
||||
$('#txt-create-prompt').removeClass('is-danger').next().addClass('is-hidden')
|
||||
})
|
||||
|
||||
$('#txt-create-prompt').on('keypress', (ev) => {
|
||||
if(ev.which === 13) {
|
||||
$('.btn-create-go').trigger('click');
|
||||
}
|
||||
});
|
||||
if (ev.which === 13) {
|
||||
$('.btn-create-go').trigger('click')
|
||||
}
|
||||
})
|
||||
|
||||
$('.btn-create-go').on('click', (ev) => {
|
||||
|
||||
let newDocPath = makeSafePath($('#txt-create-prompt').val());
|
||||
if(_.isEmpty(newDocPath)) {
|
||||
$('#txt-create-prompt').addClass('is-danger').next().removeClass('is-hidden');
|
||||
} else {
|
||||
$('#txt-create-prompt').parent().addClass('is-loading');
|
||||
window.location.assign('/create/' + newDocPath);
|
||||
}
|
||||
|
||||
});
|
||||
let newDocPath = makeSafePath($('#txt-create-prompt').val())
|
||||
if (_.isEmpty(newDocPath)) {
|
||||
$('#txt-create-prompt').addClass('is-danger').next().removeClass('is-hidden')
|
||||
} else {
|
||||
$('#txt-create-prompt').parent().addClass('is-loading')
|
||||
window.location.assign('/create/' + newDocPath)
|
||||
}
|
||||
})
|
||||
|
@ -1,49 +1,46 @@
|
||||
|
||||
//-> Move Existing Document
|
||||
// -> Move Existing Document
|
||||
|
||||
if(currentBasePath !== '') {
|
||||
$('.btn-move-prompt').removeClass('is-hidden');
|
||||
if (currentBasePath !== '') {
|
||||
$('.btn-move-prompt').removeClass('is-hidden')
|
||||
}
|
||||
|
||||
let moveInitialDocument = _.lastIndexOf(currentBasePath, '/') + 1;
|
||||
let moveInitialDocument = _.lastIndexOf(currentBasePath, '/') + 1
|
||||
|
||||
$('.btn-move-prompt').on('click', (ev) => {
|
||||
$('#txt-move-prompt').val(currentBasePath);
|
||||
$('#modal-move-prompt').toggleClass('is-active');
|
||||
setInputSelection($('#txt-move-prompt').get(0), moveInitialDocument, currentBasePath.length);
|
||||
$('#txt-move-prompt').removeClass('is-danger').next().addClass('is-hidden');
|
||||
});
|
||||
$('#txt-move-prompt').val(currentBasePath)
|
||||
$('#modal-move-prompt').toggleClass('is-active')
|
||||
setInputSelection($('#txt-move-prompt').get(0), moveInitialDocument, currentBasePath.length)
|
||||
$('#txt-move-prompt').removeClass('is-danger').next().addClass('is-hidden')
|
||||
})
|
||||
|
||||
$('#txt-move-prompt').on('keypress', (ev) => {
|
||||
if(ev.which === 13) {
|
||||
$('.btn-move-go').trigger('click');
|
||||
}
|
||||
});
|
||||
if (ev.which === 13) {
|
||||
$('.btn-move-go').trigger('click')
|
||||
}
|
||||
})
|
||||
|
||||
$('.btn-move-go').on('click', (ev) => {
|
||||
|
||||
let newDocPath = makeSafePath($('#txt-move-prompt').val());
|
||||
if(_.isEmpty(newDocPath) || newDocPath === currentBasePath || newDocPath === 'home') {
|
||||
$('#txt-move-prompt').addClass('is-danger').next().removeClass('is-hidden');
|
||||
} else {
|
||||
$('#txt-move-prompt').parent().addClass('is-loading');
|
||||
|
||||
$.ajax(window.location.href, {
|
||||
data: {
|
||||
move: newDocPath
|
||||
},
|
||||
dataType: 'json',
|
||||
method: 'PUT'
|
||||
}).then((rData, rStatus, rXHR) => {
|
||||
if(rData.ok) {
|
||||
window.location.assign('/' + newDocPath);
|
||||
} else {
|
||||
alerts.pushError('Something went wrong', rData.error);
|
||||
}
|
||||
}, (rXHR, rStatus, err) => {
|
||||
alerts.pushError('Something went wrong', 'Save operation failed.');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
let newDocPath = makeSafePath($('#txt-move-prompt').val())
|
||||
if (_.isEmpty(newDocPath) || newDocPath === currentBasePath || newDocPath === 'home') {
|
||||
$('#txt-move-prompt').addClass('is-danger').next().removeClass('is-hidden')
|
||||
} else {
|
||||
$('#txt-move-prompt').parent().addClass('is-loading')
|
||||
|
||||
$.ajax(window.location.href, {
|
||||
data: {
|
||||
move: newDocPath
|
||||
},
|
||||
dataType: 'json',
|
||||
method: 'PUT'
|
||||
}).then((rData, rStatus, rXHR) => {
|
||||
if (rData.ok) {
|
||||
window.location.assign('/' + newDocPath)
|
||||
} else {
|
||||
alerts.pushError('Something went wrong', rData.error)
|
||||
}
|
||||
}, (rXHR, rStatus, err) => {
|
||||
alerts.pushError('Something went wrong', 'Save operation failed.')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -1,102 +1,96 @@
|
||||
|
||||
if($('#page-type-admin-profile').length) {
|
||||
if ($('#page-type-admin-profile').length) {
|
||||
let vueProfile = new Vue({
|
||||
el: '#page-type-admin-profile',
|
||||
data: {
|
||||
password: '********',
|
||||
passwordVerify: '********',
|
||||
name: ''
|
||||
},
|
||||
methods: {
|
||||
saveUser: (ev) => {
|
||||
if (vueProfile.password !== vueProfile.passwordVerify) {
|
||||
alerts.pushError('Error', "Passwords don't match!")
|
||||
return
|
||||
}
|
||||
$.post(window.location.href, {
|
||||
password: vueProfile.password,
|
||||
name: vueProfile.name
|
||||
}).done((resp) => {
|
||||
alerts.pushSuccess('Saved successfully', 'Changes have been applied.')
|
||||
}).fail((jqXHR, txtStatus, resp) => {
|
||||
alerts.pushError('Error', resp)
|
||||
})
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
this.name = usrDataName
|
||||
}
|
||||
})
|
||||
} else if ($('#page-type-admin-users').length) {
|
||||
|
||||
let vueProfile = new Vue({
|
||||
el: '#page-type-admin-profile',
|
||||
data: {
|
||||
password: '********',
|
||||
passwordVerify: '********',
|
||||
name: ''
|
||||
},
|
||||
methods: {
|
||||
saveUser: (ev) => {
|
||||
if(vueProfile.password !== vueProfile.passwordVerify) {
|
||||
alerts.pushError('Error', "Passwords don't match!");
|
||||
return;
|
||||
}
|
||||
$.post(window.location.href, {
|
||||
password: vueProfile.password,
|
||||
name: vueProfile.name
|
||||
}).done((resp) => {
|
||||
alerts.pushSuccess('Saved successfully', 'Changes have been applied.');
|
||||
}).fail((jqXHR, txtStatus, resp) => {
|
||||
alerts.pushError('Error', resp);
|
||||
})
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.name = usrDataName;
|
||||
}
|
||||
});
|
||||
// =include ../modals/admin-users-create.js
|
||||
|
||||
} else if($('#page-type-admin-users').length) {
|
||||
} else if ($('#page-type-admin-users-edit').length) {
|
||||
let vueEditUser = new Vue({
|
||||
el: '#page-type-admin-users-edit',
|
||||
data: {
|
||||
id: '',
|
||||
email: '',
|
||||
password: '********',
|
||||
name: '',
|
||||
rights: [],
|
||||
roleoverride: 'none'
|
||||
},
|
||||
methods: {
|
||||
addRightsRow: (ev) => {
|
||||
vueEditUser.rights.push({
|
||||
role: 'write',
|
||||
path: '/',
|
||||
exact: false,
|
||||
deny: false
|
||||
})
|
||||
},
|
||||
removeRightsRow: (idx) => {
|
||||
_.pullAt(vueEditUser.rights, idx)
|
||||
vueEditUser.$forceUpdate()
|
||||
},
|
||||
saveUser: (ev) => {
|
||||
let formattedRights = _.cloneDeep(vueEditUser.rights)
|
||||
switch (vueEditUser.roleoverride) {
|
||||
case 'admin':
|
||||
formattedRights.push({
|
||||
role: 'admin',
|
||||
path: '/',
|
||||
exact: false,
|
||||
deny: false
|
||||
})
|
||||
break
|
||||
}
|
||||
$.post(window.location.href, {
|
||||
password: vueEditUser.password,
|
||||
name: vueEditUser.name,
|
||||
rights: JSON.stringify(formattedRights)
|
||||
}).done((resp) => {
|
||||
alerts.pushSuccess('Saved successfully', 'Changes have been applied.')
|
||||
}).fail((jqXHR, txtStatus, resp) => {
|
||||
alerts.pushError('Error', resp)
|
||||
})
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
this.id = usrData._id
|
||||
this.email = usrData.email
|
||||
this.name = usrData.name
|
||||
|
||||
//=include ../modals/admin-users-create.js
|
||||
if (_.find(usrData.rights, { role: 'admin' })) {
|
||||
this.rights = _.reject(usrData.rights, ['role', 'admin'])
|
||||
this.roleoverride = 'admin'
|
||||
} else {
|
||||
this.rights = usrData.rights
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
} else if($('#page-type-admin-users-edit').length) {
|
||||
|
||||
let vueEditUser = new Vue({
|
||||
el: '#page-type-admin-users-edit',
|
||||
data: {
|
||||
id: '',
|
||||
email: '',
|
||||
password: '********',
|
||||
name: '',
|
||||
rights: [],
|
||||
roleoverride: 'none'
|
||||
},
|
||||
methods: {
|
||||
addRightsRow: (ev) => {
|
||||
vueEditUser.rights.push({
|
||||
role: 'write',
|
||||
path: '/',
|
||||
exact: false,
|
||||
deny: false
|
||||
});
|
||||
},
|
||||
removeRightsRow: (idx) => {
|
||||
_.pullAt(vueEditUser.rights, idx)
|
||||
vueEditUser.$forceUpdate()
|
||||
},
|
||||
saveUser: (ev) => {
|
||||
let formattedRights = _.cloneDeep(vueEditUser.rights)
|
||||
switch(vueEditUser.roleoverride) {
|
||||
case 'admin':
|
||||
formattedRights.push({
|
||||
role: 'admin',
|
||||
path: '/',
|
||||
exact: false,
|
||||
deny: false
|
||||
})
|
||||
break;
|
||||
}
|
||||
$.post(window.location.href, {
|
||||
password: vueEditUser.password,
|
||||
name: vueEditUser.name,
|
||||
rights: JSON.stringify(formattedRights)
|
||||
}).done((resp) => {
|
||||
alerts.pushSuccess('Saved successfully', 'Changes have been applied.');
|
||||
}).fail((jqXHR, txtStatus, resp) => {
|
||||
alerts.pushError('Error', resp);
|
||||
})
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
|
||||
this.id = usrData._id;
|
||||
this.email = usrData.email;
|
||||
this.name = usrData.name;
|
||||
|
||||
if(_.find(usrData.rights, { role: 'admin' })) {
|
||||
this.rights = _.reject(usrData.rights, ['role', 'admin']);
|
||||
this.roleoverride = 'admin';
|
||||
} else {
|
||||
this.rights = usrData.rights;
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
//=include ../modals/admin-users-delete.js
|
||||
|
||||
}
|
||||
// =include ../modals/admin-users-delete.js
|
||||
}
|
||||
|
@ -1,14 +1,12 @@
|
||||
|
||||
if($('#page-type-create').length) {
|
||||
if ($('#page-type-create').length) {
|
||||
let pageEntryPath = $('#page-type-create').data('entrypath')
|
||||
|
||||
let pageEntryPath = $('#page-type-create').data('entrypath');
|
||||
// -> Discard
|
||||
|
||||
//-> Discard
|
||||
$('.btn-create-discard').on('click', (ev) => {
|
||||
$('#modal-create-discard').toggleClass('is-active')
|
||||
})
|
||||
|
||||
$('.btn-create-discard').on('click', (ev) => {
|
||||
$('#modal-create-discard').toggleClass('is-active');
|
||||
});
|
||||
|
||||
//=include ../components/editor.js
|
||||
|
||||
}
|
||||
// =include ../components/editor.js
|
||||
}
|
||||
|
@ -1,14 +1,12 @@
|
||||
|
||||
if($('#page-type-edit').length) {
|
||||
if ($('#page-type-edit').length) {
|
||||
let pageEntryPath = $('#page-type-edit').data('entrypath')
|
||||
|
||||
let pageEntryPath = $('#page-type-edit').data('entrypath');
|
||||
// -> Discard
|
||||
|
||||
//-> Discard
|
||||
$('.btn-edit-discard').on('click', (ev) => {
|
||||
$('#modal-edit-discard').toggleClass('is-active')
|
||||
})
|
||||
|
||||
$('.btn-edit-discard').on('click', (ev) => {
|
||||
$('#modal-edit-discard').toggleClass('is-active');
|
||||
});
|
||||
|
||||
//=include ../components/editor.js
|
||||
|
||||
}
|
||||
// =include ../components/editor.js
|
||||
}
|
||||
|
@ -1,18 +1,16 @@
|
||||
|
||||
if($('#page-type-source').length) {
|
||||
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()
|
||||
|
||||
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();
|
||||
let currentBasePath = ($('#page-type-source').data('entrypath') !== 'home') ? $('#page-type-source').data('entrypath') : ''
|
||||
|
||||
let currentBasePath = ($('#page-type-source').data('entrypath') !== 'home') ? $('#page-type-source').data('entrypath') : '';
|
||||
|
||||
//=include ../modals/create.js
|
||||
//=include ../modals/move.js
|
||||
|
||||
}
|
||||
// =include ../modals/create.js
|
||||
// =include ../modals/move.js
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
|
||||
if($('#page-type-view').length) {
|
||||
if ($('#page-type-view').length) {
|
||||
let currentBasePath = ($('#page-type-view').data('entrypath') !== 'home') ? $('#page-type-view').data('entrypath') : ''
|
||||
|
||||
let currentBasePath = ($('#page-type-view').data('entrypath') !== 'home') ? $('#page-type-view').data('entrypath') : '';
|
||||
|
||||
//=include ../modals/create.js
|
||||
//=include ../modals/move.js
|
||||
|
||||
}
|
||||
// =include ../modals/create.js
|
||||
// =include ../modals/move.js
|
||||
}
|
||||
|
@ -1,162 +1,146 @@
|
||||
"use strict";
|
||||
'use strict'
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
const Promise = require('bluebird');
|
||||
const validator = require('validator');
|
||||
const _ = require('lodash');
|
||||
var express = require('express')
|
||||
var router = express.Router()
|
||||
const Promise = require('bluebird')
|
||||
const validator = require('validator')
|
||||
const _ = require('lodash')
|
||||
|
||||
/**
|
||||
* Admin
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
res.redirect('/admin/profile');
|
||||
});
|
||||
res.redirect('/admin/profile')
|
||||
})
|
||||
|
||||
router.get('/profile', (req, res) => {
|
||||
if (res.locals.isGuest) {
|
||||
return res.render('error-forbidden')
|
||||
}
|
||||
|
||||
if(res.locals.isGuest) {
|
||||
return res.render('error-forbidden');
|
||||
}
|
||||
|
||||
res.render('pages/admin/profile', { adminTab: 'profile' });
|
||||
|
||||
});
|
||||
res.render('pages/admin/profile', { adminTab: 'profile' })
|
||||
})
|
||||
|
||||
router.post('/profile', (req, res) => {
|
||||
|
||||
if(res.locals.isGuest) {
|
||||
return res.render('error-forbidden');
|
||||
}
|
||||
|
||||
return db.User.findById(req.user.id).then((usr) => {
|
||||
usr.name = _.trim(req.body.name);
|
||||
if(usr.provider === 'local' && req.body.password !== '********') {
|
||||
let nPwd = _.trim(req.body.password);
|
||||
if(nPwd.length < 6) {
|
||||
return Promise.reject(new Error('New Password too short!'))
|
||||
} else {
|
||||
return db.User.hashPassword(nPwd).then((pwd) => {
|
||||
usr.password = pwd;
|
||||
return usr.save();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return usr.save();
|
||||
}
|
||||
}).then(() => {
|
||||
return res.json({ msg: 'OK' });
|
||||
}).catch((err) => {
|
||||
res.status(400).json({ msg: err.message });
|
||||
})
|
||||
|
||||
});
|
||||
if (res.locals.isGuest) {
|
||||
return res.render('error-forbidden')
|
||||
}
|
||||
|
||||
return db.User.findById(req.user.id).then((usr) => {
|
||||
usr.name = _.trim(req.body.name)
|
||||
if (usr.provider === 'local' && req.body.password !== '********') {
|
||||
let nPwd = _.trim(req.body.password)
|
||||
if (nPwd.length < 6) {
|
||||
return Promise.reject(new Error('New Password too short!'))
|
||||
} else {
|
||||
return db.User.hashPassword(nPwd).then((pwd) => {
|
||||
usr.password = pwd
|
||||
return usr.save()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return usr.save()
|
||||
}
|
||||
}).then(() => {
|
||||
return res.json({ msg: 'OK' })
|
||||
}).catch((err) => {
|
||||
res.status(400).json({ msg: err.message })
|
||||
})
|
||||
})
|
||||
|
||||
router.get('/stats', (req, res) => {
|
||||
|
||||
if(res.locals.isGuest) {
|
||||
return res.render('error-forbidden');
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
db.Entry.count(),
|
||||
db.UplFile.count(),
|
||||
db.User.count()
|
||||
]).spread((totalEntries, totalUploads, totalUsers) => {
|
||||
return res.render('pages/admin/stats', {
|
||||
totalEntries, totalUploads, totalUsers,
|
||||
adminTab: 'stats'
|
||||
}) || true;
|
||||
}).catch((err) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
});
|
||||
if (res.locals.isGuest) {
|
||||
return res.render('error-forbidden')
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
db.Entry.count(),
|
||||
db.UplFile.count(),
|
||||
db.User.count()
|
||||
]).spread((totalEntries, totalUploads, totalUsers) => {
|
||||
return res.render('pages/admin/stats', {
|
||||
totalEntries, totalUploads, totalUsers, adminTab: 'stats'
|
||||
}) || true
|
||||
}).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
})
|
||||
|
||||
router.get('/users', (req, res) => {
|
||||
|
||||
if(!res.locals.rights.manage) {
|
||||
return res.render('error-forbidden');
|
||||
}
|
||||
|
||||
db.User.find({})
|
||||
.select('-password -rights')
|
||||
.sort('name email')
|
||||
.exec().then((usrs) => {
|
||||
res.render('pages/admin/users', { adminTab: 'users', usrs });
|
||||
});
|
||||
|
||||
});
|
||||
if (!res.locals.rights.manage) {
|
||||
return res.render('error-forbidden')
|
||||
}
|
||||
|
||||
db.User.find({})
|
||||
.select('-password -rights')
|
||||
.sort('name email')
|
||||
.exec().then((usrs) => {
|
||||
res.render('pages/admin/users', { adminTab: 'users', usrs })
|
||||
})
|
||||
})
|
||||
|
||||
router.get('/users/:id', (req, res) => {
|
||||
|
||||
if(!res.locals.rights.manage) {
|
||||
return res.render('error-forbidden');
|
||||
}
|
||||
|
||||
if(!validator.isMongoId(req.params.id)) {
|
||||
return res.render('error-forbidden');
|
||||
}
|
||||
|
||||
db.User.findById(req.params.id)
|
||||
.select('-password -providerId')
|
||||
.exec().then((usr) => {
|
||||
|
||||
let usrOpts = {
|
||||
canChangeEmail: (usr.email !== 'guest' && usr.provider === 'local' && usr.email !== req.app.locals.appconfig.admin),
|
||||
canChangeName: (usr.email !== 'guest'),
|
||||
canChangePassword: (usr.email !== 'guest' && usr.provider === 'local'),
|
||||
canChangeRole: (usr.email !== 'guest' && !(usr.provider === 'local' && usr.email === req.app.locals.appconfig.admin)),
|
||||
canBeDeleted: (usr.email !== 'guest' && !(usr.provider === 'local' && usr.email === req.app.locals.appconfig.admin))
|
||||
};
|
||||
|
||||
res.render('pages/admin/users-edit', { adminTab: 'users', usr, usrOpts });
|
||||
});
|
||||
|
||||
});
|
||||
if (!res.locals.rights.manage) {
|
||||
return res.render('error-forbidden')
|
||||
}
|
||||
|
||||
if (!validator.isMongoId(req.params.id)) {
|
||||
return res.render('error-forbidden')
|
||||
}
|
||||
|
||||
db.User.findById(req.params.id)
|
||||
.select('-password -providerId')
|
||||
.exec().then((usr) => {
|
||||
let usrOpts = {
|
||||
canChangeEmail: (usr.email !== 'guest' && usr.provider === 'local' && usr.email !== req.app.locals.appconfig.admin),
|
||||
canChangeName: (usr.email !== 'guest'),
|
||||
canChangePassword: (usr.email !== 'guest' && usr.provider === 'local'),
|
||||
canChangeRole: (usr.email !== 'guest' && !(usr.provider === 'local' && usr.email === req.app.locals.appconfig.admin)),
|
||||
canBeDeleted: (usr.email !== 'guest' && !(usr.provider === 'local' && usr.email === req.app.locals.appconfig.admin))
|
||||
}
|
||||
|
||||
res.render('pages/admin/users-edit', { adminTab: 'users', usr, usrOpts })
|
||||
})
|
||||
})
|
||||
|
||||
router.post('/users/:id', (req, res) => {
|
||||
|
||||
if(!res.locals.rights.manage) {
|
||||
return res.status(401).json({ msg: 'Unauthorized' });
|
||||
}
|
||||
|
||||
if(!validator.isMongoId(req.params.id)) {
|
||||
return res.status(400).json({ msg: 'Invalid User ID' });
|
||||
}
|
||||
|
||||
return db.User.findById(req.params.id).then((usr) => {
|
||||
usr.name = _.trim(req.body.name);
|
||||
usr.rights = JSON.parse(req.body.rights);
|
||||
if(usr.provider === 'local' && req.body.password !== '********') {
|
||||
let nPwd = _.trim(req.body.password);
|
||||
if(nPwd.length < 6) {
|
||||
return Promise.reject(new Error('New Password too short!'))
|
||||
} else {
|
||||
return db.User.hashPassword(nPwd).then((pwd) => {
|
||||
usr.password = pwd;
|
||||
return usr.save();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return usr.save();
|
||||
}
|
||||
}).then(() => {
|
||||
return res.json({ msg: 'OK' });
|
||||
}).catch((err) => {
|
||||
res.status(400).json({ msg: err.message });
|
||||
})
|
||||
|
||||
});
|
||||
if (!res.locals.rights.manage) {
|
||||
return res.status(401).json({ msg: 'Unauthorized' })
|
||||
}
|
||||
|
||||
if (!validator.isMongoId(req.params.id)) {
|
||||
return res.status(400).json({ msg: 'Invalid User ID' })
|
||||
}
|
||||
|
||||
return db.User.findById(req.params.id).then((usr) => {
|
||||
usr.name = _.trim(req.body.name)
|
||||
usr.rights = JSON.parse(req.body.rights)
|
||||
if (usr.provider === 'local' && req.body.password !== '********') {
|
||||
let nPwd = _.trim(req.body.password)
|
||||
if (nPwd.length < 6) {
|
||||
return Promise.reject(new Error('New Password too short!'))
|
||||
} else {
|
||||
return db.User.hashPassword(nPwd).then((pwd) => {
|
||||
usr.password = pwd
|
||||
return usr.save()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return usr.save()
|
||||
}
|
||||
}).then(() => {
|
||||
return res.json({ msg: 'OK' })
|
||||
}).catch((err) => {
|
||||
res.status(400).json({ msg: err.message })
|
||||
})
|
||||
})
|
||||
|
||||
router.get('/settings', (req, res) => {
|
||||
if (!res.locals.rights.manage) {
|
||||
return res.render('error-forbidden')
|
||||
}
|
||||
|
||||
if(!res.locals.rights.manage) {
|
||||
return res.render('error-forbidden');
|
||||
}
|
||||
|
||||
res.render('pages/admin/settings', { adminTab: 'settings' });
|
||||
|
||||
});
|
||||
res.render('pages/admin/settings', { adminTab: 'settings' })
|
||||
})
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router
|
||||
|
@ -1,80 +1,80 @@
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var passport = require('passport');
|
||||
var ExpressBrute = require('express-brute');
|
||||
var ExpressBruteMongooseStore = require('express-brute-mongoose');
|
||||
var moment = require('moment');
|
||||
'use strict'
|
||||
|
||||
const express = require('express')
|
||||
const router = express.Router()
|
||||
const passport = require('passport')
|
||||
const ExpressBrute = require('express-brute')
|
||||
const ExpressBruteMongooseStore = require('express-brute-mongoose')
|
||||
const moment = require('moment')
|
||||
|
||||
/**
|
||||
* Setup Express-Brute
|
||||
*/
|
||||
var EBstore = new ExpressBruteMongooseStore(db.Bruteforce);
|
||||
var bruteforce = new ExpressBrute(EBstore, {
|
||||
freeRetries: 5,
|
||||
minWait: 60 * 1000,
|
||||
maxWait: 5 * 60 * 1000,
|
||||
refreshTimeoutOnRequest: false,
|
||||
failCallback(req, res, next, nextValidRequestDate) {
|
||||
req.flash('alert', {
|
||||
class: 'error',
|
||||
title: 'Too many attempts!',
|
||||
message: "You've made too many failed attempts in a short period of time, please try again " + moment(nextValidRequestDate).fromNow() + '.',
|
||||
iconClass: 'fa-times'
|
||||
});
|
||||
res.redirect('/login');
|
||||
}
|
||||
});
|
||||
const EBstore = new ExpressBruteMongooseStore(db.Bruteforce)
|
||||
const bruteforce = new ExpressBrute(EBstore, {
|
||||
freeRetries: 5,
|
||||
minWait: 60 * 1000,
|
||||
maxWait: 5 * 60 * 1000,
|
||||
refreshTimeoutOnRequest: false,
|
||||
failCallback (req, res, next, nextValidRequestDate) {
|
||||
req.flash('alert', {
|
||||
class: 'error',
|
||||
title: 'Too many attempts!',
|
||||
message: "You've made too many failed attempts in a short period of time, please try again " + moment(nextValidRequestDate).fromNow() + '.',
|
||||
iconClass: 'fa-times'
|
||||
})
|
||||
res.redirect('/login')
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Login form
|
||||
*/
|
||||
router.get('/login', function(req, res, next) {
|
||||
res.render('auth/login', {
|
||||
usr: res.locals.usr
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/login', bruteforce.prevent, function(req, res, next) {
|
||||
passport.authenticate('local', function(err, user, info) {
|
||||
router.get('/login', function (req, res, next) {
|
||||
res.render('auth/login', {
|
||||
usr: res.locals.usr
|
||||
})
|
||||
})
|
||||
|
||||
if (err) { return next(err); }
|
||||
router.post('/login', bruteforce.prevent, function (req, res, next) {
|
||||
passport.authenticate('local', function (err, user, info) {
|
||||
if (err) { return next(err) }
|
||||
|
||||
if (!user) {
|
||||
req.flash('alert', {
|
||||
title: 'Invalid login',
|
||||
message: "The email or password is invalid."
|
||||
});
|
||||
return res.redirect('/login');
|
||||
}
|
||||
if (!user) {
|
||||
req.flash('alert', {
|
||||
title: 'Invalid login',
|
||||
message: 'The email or password is invalid.'
|
||||
})
|
||||
return res.redirect('/login')
|
||||
}
|
||||
|
||||
req.logIn(user, function(err) {
|
||||
if (err) { return next(err); }
|
||||
req.logIn(user, function (err) {
|
||||
if (err) { return next(err) }
|
||||
req.brute.reset(function () {
|
||||
return res.redirect('/');
|
||||
});
|
||||
});
|
||||
|
||||
})(req, res, next);
|
||||
});
|
||||
return res.redirect('/')
|
||||
})
|
||||
})
|
||||
})(req, res, next)
|
||||
})
|
||||
|
||||
/**
|
||||
* Social Login
|
||||
*/
|
||||
|
||||
router.get('/login/ms', passport.authenticate('windowslive', { scope: ['wl.signin', 'wl.basic', 'wl.emails'] }));
|
||||
router.get('/login/google', passport.authenticate('google', { scope: ['profile', 'email'] }));
|
||||
router.get('/login/facebook', passport.authenticate('facebook', { scope: ['public_profile', 'email'] }));
|
||||
router.get('/login/ms', passport.authenticate('windowslive', { scope: ['wl.signin', 'wl.basic', 'wl.emails'] }))
|
||||
router.get('/login/google', passport.authenticate('google', { scope: ['profile', 'email'] }))
|
||||
router.get('/login/facebook', passport.authenticate('facebook', { scope: ['public_profile', 'email'] }))
|
||||
|
||||
router.get('/login/ms/callback', passport.authenticate('windowslive', { failureRedirect: '/login', successRedirect: '/' }));
|
||||
router.get('/login/google/callback', passport.authenticate('google', { failureRedirect: '/login', successRedirect: '/' }));
|
||||
router.get('/login/facebook/callback', passport.authenticate('facebook', { failureRedirect: '/login', successRedirect: '/' }));
|
||||
router.get('/login/ms/callback', passport.authenticate('windowslive', { failureRedirect: '/login', successRedirect: '/' }))
|
||||
router.get('/login/google/callback', passport.authenticate('google', { failureRedirect: '/login', successRedirect: '/' }))
|
||||
router.get('/login/facebook/callback', passport.authenticate('facebook', { failureRedirect: '/login', successRedirect: '/' }))
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*/
|
||||
router.get('/logout', function(req, res) {
|
||||
req.logout();
|
||||
res.redirect('/');
|
||||
});
|
||||
router.get('/logout', function (req, res) {
|
||||
req.logout()
|
||||
res.redirect('/')
|
||||
})
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router
|
||||
|
@ -1,184 +1,160 @@
|
||||
"use strict";
|
||||
'use strict'
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
const express = require('express')
|
||||
const router = express.Router()
|
||||
|
||||
var readChunk = require('read-chunk'),
|
||||
fileType = require('file-type'),
|
||||
Promise = require('bluebird'),
|
||||
fs = Promise.promisifyAll(require('fs-extra')),
|
||||
path = require('path'),
|
||||
_ = require('lodash');
|
||||
const readChunk = require('read-chunk')
|
||||
const fileType = require('file-type')
|
||||
const Promise = require('bluebird')
|
||||
const fs = Promise.promisifyAll(require('fs-extra'))
|
||||
const path = require('path')
|
||||
const _ = require('lodash')
|
||||
|
||||
var validPathRe = new RegExp("^([a-z0-9\\/-]+\\.[a-z0-9]+)$");
|
||||
var validPathThumbsRe = new RegExp("^([0-9]+\\.png)$");
|
||||
const validPathRe = new RegExp('^([a-z0-9\\/-]+\\.[a-z0-9]+)$')
|
||||
const validPathThumbsRe = new RegExp('^([0-9]+\\.png)$')
|
||||
|
||||
// ==========================================
|
||||
// SERVE UPLOADS FILES
|
||||
// ==========================================
|
||||
|
||||
router.get('/t/*', (req, res, next) => {
|
||||
|
||||
let fileName = req.params[0];
|
||||
if(!validPathThumbsRe.test(fileName)) {
|
||||
return res.sendStatus(404).end();
|
||||
}
|
||||
|
||||
//todo: Authentication-based access
|
||||
|
||||
res.sendFile(fileName, {
|
||||
root: lcdata.getThumbsPath(),
|
||||
dotfiles: 'deny'
|
||||
}, (err) => {
|
||||
if (err) {
|
||||
res.status(err.status).end();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
let fileName = req.params[0]
|
||||
if (!validPathThumbsRe.test(fileName)) {
|
||||
return res.sendStatus(404).end()
|
||||
}
|
||||
|
||||
// todo: Authentication-based access
|
||||
|
||||
res.sendFile(fileName, {
|
||||
root: lcdata.getThumbsPath(),
|
||||
dotfiles: 'deny'
|
||||
}, (err) => {
|
||||
if (err) {
|
||||
res.status(err.status).end()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
router.post('/img', lcdata.uploadImgHandler, (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, true).then((fname) => {
|
||||
|
||||
destFilename = fname;
|
||||
destFilePath = path.resolve(destFolderPath, destFilename);
|
||||
|
||||
return readChunk(f.path, 0, 262);
|
||||
|
||||
}).then((buf) => {
|
||||
|
||||
//-> Check MIME type by magic number
|
||||
|
||||
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;
|
||||
|
||||
}).then(() => {
|
||||
|
||||
//-> 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;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
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, true).then((fname) => {
|
||||
destFilename = fname
|
||||
destFilePath = path.resolve(destFolderPath, destFilename)
|
||||
|
||||
return readChunk(f.path, 0, 262)
|
||||
}).then((buf) => {
|
||||
// -> Check MIME type by magic number
|
||||
|
||||
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
|
||||
}).then(() => {
|
||||
// -> 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.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;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
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];
|
||||
if(!validPathRe.test(fileName)) {
|
||||
return res.sendStatus(404).end();
|
||||
}
|
||||
|
||||
//todo: Authentication-based access
|
||||
|
||||
res.sendFile(fileName, {
|
||||
root: git.getRepoPath() + '/uploads/',
|
||||
dotfiles: 'deny'
|
||||
}, (err) => {
|
||||
if (err) {
|
||||
res.status(err.status).end();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
let fileName = req.params[0]
|
||||
if (!validPathRe.test(fileName)) {
|
||||
return res.sendStatus(404).end()
|
||||
}
|
||||
|
||||
// todo: Authentication-based access
|
||||
|
||||
res.sendFile(fileName, {
|
||||
root: git.getRepoPath() + '/uploads/',
|
||||
dotfiles: 'deny'
|
||||
}, (err) => {
|
||||
if (err) {
|
||||
res.status(err.status).end()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
@ -1,95 +1,95 @@
|
||||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const _ = require('lodash')
|
||||
|
||||
module.exports = (socket) => {
|
||||
|
||||
if(!socket.request.user.logged_in) {
|
||||
return;
|
||||
if (!socket.request.user.logged_in) {
|
||||
return
|
||||
}
|
||||
|
||||
//-----------------------------------------
|
||||
// -----------------------------------------
|
||||
// SEARCH
|
||||
//-----------------------------------------
|
||||
// -----------------------------------------
|
||||
|
||||
socket.on('search', (data, cb) => {
|
||||
cb = cb || _.noop;
|
||||
cb = cb || _.noop
|
||||
entries.search(data.terms).then((results) => {
|
||||
return cb(results) || true;
|
||||
});
|
||||
});
|
||||
return cb(results) || true
|
||||
})
|
||||
})
|
||||
|
||||
//-----------------------------------------
|
||||
// -----------------------------------------
|
||||
// UPLOADS
|
||||
//-----------------------------------------
|
||||
// -----------------------------------------
|
||||
|
||||
socket.on('uploadsGetFolders', (data, cb) => {
|
||||
cb = cb || _.noop;
|
||||
cb = cb || _.noop
|
||||
upl.getUploadsFolders().then((f) => {
|
||||
return cb(f) || true;
|
||||
});
|
||||
});
|
||||
return cb(f) || true
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('uploadsCreateFolder', (data, cb) => {
|
||||
cb = cb || _.noop;
|
||||
cb = cb || _.noop
|
||||
upl.createUploadsFolder(data.foldername).then((f) => {
|
||||
return cb(f) || true;
|
||||
});
|
||||
});
|
||||
return cb(f) || true
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('uploadsGetImages', (data, cb) => {
|
||||
cb = cb || _.noop;
|
||||
cb = cb || _.noop
|
||||
upl.getUploadsFiles('image', data.folder).then((f) => {
|
||||
return cb(f) || true;
|
||||
});
|
||||
});
|
||||
return cb(f) || true
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('uploadsGetFiles', (data, cb) => {
|
||||
cb = cb || _.noop;
|
||||
cb = cb || _.noop
|
||||
upl.getUploadsFiles('binary', data.folder).then((f) => {
|
||||
return cb(f) || true;
|
||||
});
|
||||
});
|
||||
return cb(f) || true
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('uploadsDeleteFile', (data, cb) => {
|
||||
cb = cb || _.noop;
|
||||
cb = cb || _.noop
|
||||
upl.deleteUploadsFile(data.uid).then((f) => {
|
||||
return cb(f) || true;
|
||||
});
|
||||
});
|
||||
return cb(f) || true
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('uploadsFetchFileFromURL', (data, cb) => {
|
||||
cb = cb || _.noop;
|
||||
cb = cb || _.noop
|
||||
upl.downloadFromUrl(data.folder, data.fetchUrl).then((f) => {
|
||||
return cb({ ok: true }) || true;
|
||||
return cb({ ok: true }) || true
|
||||
}).catch((err) => {
|
||||
return cb({
|
||||
ok: false,
|
||||
msg: err.message
|
||||
}) || true;
|
||||
});
|
||||
});
|
||||
return cb({
|
||||
ok: false,
|
||||
msg: err.message
|
||||
}) || true
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('uploadsRenameFile', (data, cb) => {
|
||||
cb = cb || _.noop;
|
||||
cb = cb || _.noop
|
||||
upl.moveUploadsFile(data.uid, data.folder, data.filename).then((f) => {
|
||||
return cb({ ok: true }) || true;
|
||||
return cb({ ok: true }) || true
|
||||
}).catch((err) => {
|
||||
return cb({
|
||||
ok: false,
|
||||
msg: err.message
|
||||
}) || true;
|
||||
});
|
||||
});
|
||||
return cb({
|
||||
ok: false,
|
||||
msg: err.message
|
||||
}) || true
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('uploadsMoveFile', (data, cb) => {
|
||||
cb = cb || _.noop;
|
||||
cb = cb || _.noop
|
||||
upl.moveUploadsFile(data.uid, data.folder).then((f) => {
|
||||
return cb({ ok: true }) || true;
|
||||
return cb({ ok: true }) || true
|
||||
}).catch((err) => {
|
||||
return cb({
|
||||
ok: false,
|
||||
msg: err.message
|
||||
}) || true;
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
return cb({
|
||||
ok: false,
|
||||
msg: err.message
|
||||
}) || true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -1,215 +1,210 @@
|
||||
var gulp = require("gulp");
|
||||
var watch = require('gulp-watch');
|
||||
var merge = require('merge-stream');
|
||||
var babel = require("gulp-babel");
|
||||
var uglify = require('gulp-uglify');
|
||||
var concat = require('gulp-concat');
|
||||
var nodemon = require('gulp-nodemon');
|
||||
var plumber = require('gulp-plumber');
|
||||
var zip = require('gulp-zip');
|
||||
var tar = require('gulp-tar');
|
||||
var gzip = require('gulp-gzip');
|
||||
var sass = require('gulp-sass');
|
||||
var cleanCSS = require('gulp-clean-css');
|
||||
var include = require("gulp-include");
|
||||
var run = require('run-sequence');
|
||||
var _ = require('lodash');
|
||||
'use strict'
|
||||
|
||||
const gulp = require('gulp')
|
||||
const watch = require('gulp-watch')
|
||||
const merge = require('merge-stream')
|
||||
const babel = require('gulp-babel')
|
||||
const uglify = require('gulp-uglify')
|
||||
const concat = require('gulp-concat')
|
||||
const nodemon = require('gulp-nodemon')
|
||||
const plumber = require('gulp-plumber')
|
||||
const zip = require('gulp-zip')
|
||||
const tar = require('gulp-tar')
|
||||
const gzip = require('gulp-gzip')
|
||||
const sass = require('gulp-sass')
|
||||
const cleanCSS = require('gulp-clean-css')
|
||||
const include = require('gulp-include')
|
||||
const run = require('run-sequence')
|
||||
|
||||
/**
|
||||
* Paths
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
var paths = {
|
||||
scripts: {
|
||||
combine: [
|
||||
'./node_modules/socket.io-client/dist/socket.io.min.js',
|
||||
'./node_modules/jquery/dist/jquery.min.js',
|
||||
'./node_modules/vue/dist/vue.min.js',
|
||||
'./node_modules/jquery-smooth-scroll/jquery.smooth-scroll.min.js',
|
||||
'./node_modules/jquery-simple-upload/simpleUpload.min.js',
|
||||
'./node_modules/jquery-contextmenu/dist/jquery.contextMenu.min.js',
|
||||
'./node_modules/sticky-js/dist/sticky.min.js',
|
||||
'./node_modules/simplemde/dist/simplemde.min.js',
|
||||
'./node_modules/ace-builds/src-min-noconflict/ace.js',
|
||||
'./node_modules/ace-builds/src-min-noconflict/ext-modelist.js',
|
||||
'./node_modules/ace-builds/src-min-noconflict/mode-markdown.js',
|
||||
'./node_modules/ace-builds/src-min-noconflict/theme-tomorrow_night.js',
|
||||
'./node_modules/filesize.js/dist/filesize.min.js',
|
||||
'./node_modules/lodash/lodash.min.js'
|
||||
],
|
||||
ace: [
|
||||
'./node_modules/ace-builds/src-min-noconflict/mode-*.js',
|
||||
'!./node_modules/ace-builds/src-min-noconflict/mode-markdown.js'
|
||||
],
|
||||
compile: [
|
||||
'./client/js/*.js'
|
||||
],
|
||||
watch: [
|
||||
'./client/js/**/*.js'
|
||||
]
|
||||
},
|
||||
css: {
|
||||
combine: [
|
||||
'./node_modules/highlight.js/styles/tomorrow.css',
|
||||
'./node_modules/simplemde/dist/simplemde.min.css'
|
||||
],
|
||||
compile: [
|
||||
'./client/scss/*.scss'
|
||||
],
|
||||
includes: [
|
||||
'./node_modules/requarks-core' //! MUST BE LAST
|
||||
],
|
||||
watch: [
|
||||
'./client/scss/**/*.scss',
|
||||
'../core/core-client/scss/**/*.scss'
|
||||
]
|
||||
},
|
||||
fonts: [
|
||||
'../node_modules/requarks-core/core-client/fonts/**/*' //! MUST BE LAST
|
||||
],
|
||||
deploy: [
|
||||
'./**/*',
|
||||
'!node_modules', '!node_modules/**',
|
||||
'!coverage', '!coverage/**',
|
||||
'!client/js', '!client/js/**',
|
||||
'!client/scss', '!client/scss/**',
|
||||
'!dist', '!dist/**',
|
||||
'!tests', '!tests/**',
|
||||
'!data', '!data/**',
|
||||
'!repo', '!repo/**',
|
||||
'!.babelrc', '!.gitattributes', '!.gitignore', '!.snyk', '!.travis.yml',
|
||||
'!gulpfile.js', '!inch.json', '!config.yml', '!wiki.sublime-project'
|
||||
]
|
||||
};
|
||||
const paths = {
|
||||
scripts: {
|
||||
combine: [
|
||||
'./node_modules/socket.io-client/dist/socket.io.min.js',
|
||||
'./node_modules/jquery/dist/jquery.min.js',
|
||||
'./node_modules/vue/dist/vue.min.js',
|
||||
'./node_modules/jquery-smooth-scroll/jquery.smooth-scroll.min.js',
|
||||
'./node_modules/jquery-simple-upload/simpleUpload.min.js',
|
||||
'./node_modules/jquery-contextmenu/dist/jquery.contextMenu.min.js',
|
||||
'./node_modules/sticky-js/dist/sticky.min.js',
|
||||
'./node_modules/simplemde/dist/simplemde.min.js',
|
||||
'./node_modules/ace-builds/src-min-noconflict/ace.js',
|
||||
'./node_modules/ace-builds/src-min-noconflict/ext-modelist.js',
|
||||
'./node_modules/ace-builds/src-min-noconflict/mode-markdown.js',
|
||||
'./node_modules/ace-builds/src-min-noconflict/theme-tomorrow_night.js',
|
||||
'./node_modules/filesize.js/dist/filesize.min.js',
|
||||
'./node_modules/lodash/lodash.min.js'
|
||||
],
|
||||
ace: [
|
||||
'./node_modules/ace-builds/src-min-noconflict/mode-*.js',
|
||||
'!./node_modules/ace-builds/src-min-noconflict/mode-markdown.js'
|
||||
],
|
||||
compile: [
|
||||
'./client/js/*.js'
|
||||
],
|
||||
watch: [
|
||||
'./client/js/**/*.js'
|
||||
]
|
||||
},
|
||||
css: {
|
||||
combine: [
|
||||
'./node_modules/highlight.js/styles/tomorrow.css',
|
||||
'./node_modules/simplemde/dist/simplemde.min.css'
|
||||
],
|
||||
compile: [
|
||||
'./client/scss/*.scss'
|
||||
],
|
||||
includes: [
|
||||
'./node_modules/requarks-core' //! MUST BE LAST
|
||||
],
|
||||
watch: [
|
||||
'./client/scss/**/*.scss',
|
||||
'../core/core-client/scss/**/*.scss'
|
||||
]
|
||||
},
|
||||
fonts: [
|
||||
'../node_modules/requarks-core/core-client/fonts/**/*' //! MUST BE LAST
|
||||
],
|
||||
deploy: [
|
||||
'./**/*',
|
||||
'!node_modules', '!node_modules/**',
|
||||
'!coverage', '!coverage/**',
|
||||
'!client/js', '!client/js/**',
|
||||
'!client/scss', '!client/scss/**',
|
||||
'!dist', '!dist/**',
|
||||
'!tests', '!tests/**',
|
||||
'!data', '!data/**',
|
||||
'!repo', '!repo/**',
|
||||
'!.babelrc', '!.gitattributes', '!.gitignore', '!.snyk', '!.travis.yml',
|
||||
'!gulpfile.js', '!inch.json', '!config.yml', '!wiki.sublime-project'
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* TASK - Starts server in development mode
|
||||
*/
|
||||
gulp.task('server', ['scripts', 'css', 'fonts'], function() {
|
||||
nodemon({
|
||||
script: './server',
|
||||
ignore: ['assets/', 'client/', 'data/', 'repo/', 'tests/'],
|
||||
ext: 'js json',
|
||||
env: { 'NODE_ENV': 'development' }
|
||||
});
|
||||
});
|
||||
gulp.task('server', ['scripts', 'css', 'fonts'], function () {
|
||||
nodemon({
|
||||
script: './server',
|
||||
ignore: ['assets/', 'client/', 'data/', 'repo/', 'tests/'],
|
||||
ext: 'js json',
|
||||
env: { 'NODE_ENV': 'development' }
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* TASK - Process all scripts processes
|
||||
*/
|
||||
gulp.task("scripts", ['scripts-libs', 'scripts-app']);
|
||||
gulp.task('scripts', ['scripts-libs', 'scripts-app'])
|
||||
|
||||
/**
|
||||
* TASK - Combine js libraries
|
||||
*/
|
||||
gulp.task("scripts-libs", function () {
|
||||
gulp.task('scripts-libs', function () {
|
||||
return merge(
|
||||
|
||||
return merge(
|
||||
gulp.src(paths.scripts.combine)
|
||||
.pipe(concat('libs.js', {newLine: ';\n'}))
|
||||
.pipe(uglify({ mangle: false }))
|
||||
.pipe(gulp.dest('./assets/js')),
|
||||
|
||||
gulp.src(paths.scripts.combine)
|
||||
.pipe(concat('libs.js', {newLine: ';\n'}))
|
||||
.pipe(uglify({ mangle: false }))
|
||||
.pipe(gulp.dest("./assets/js")),
|
||||
gulp.src(paths.scripts.ace)
|
||||
.pipe(gulp.dest('./assets/js/ace'))
|
||||
|
||||
gulp.src(paths.scripts.ace)
|
||||
.pipe(gulp.dest("./assets/js/ace"))
|
||||
|
||||
);
|
||||
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* TASK - Combine, make compatible and compress js app scripts
|
||||
*/
|
||||
gulp.task("scripts-app", function () {
|
||||
|
||||
return gulp.src(paths.scripts.compile)
|
||||
.pipe(plumber())
|
||||
.pipe(include({ extensions: "js" }))
|
||||
.pipe(babel())
|
||||
.pipe(uglify())
|
||||
.pipe(plumber.stop())
|
||||
.pipe(gulp.dest("./assets/js"));
|
||||
|
||||
});
|
||||
gulp.task('scripts-app', function () {
|
||||
return gulp.src(paths.scripts.compile)
|
||||
.pipe(plumber())
|
||||
.pipe(include({ extensions: 'js' }))
|
||||
.pipe(babel())
|
||||
.pipe(uglify())
|
||||
.pipe(plumber.stop())
|
||||
.pipe(gulp.dest('./assets/js'))
|
||||
})
|
||||
|
||||
/**
|
||||
* TASK - Process all css processes
|
||||
*/
|
||||
gulp.task("css", ['css-libs', 'css-app']);
|
||||
gulp.task('css', ['css-libs', 'css-app'])
|
||||
|
||||
/**
|
||||
* TASK - Combine css libraries
|
||||
*/
|
||||
gulp.task("css-libs", function () {
|
||||
return gulp.src(paths.css.combine)
|
||||
.pipe(plumber())
|
||||
.pipe(concat('libs.css'))
|
||||
.pipe(cleanCSS({ keepSpecialComments: 0 }))
|
||||
.pipe(plumber.stop())
|
||||
.pipe(gulp.dest("./assets/css"));
|
||||
});
|
||||
gulp.task('css-libs', function () {
|
||||
return gulp.src(paths.css.combine)
|
||||
.pipe(plumber())
|
||||
.pipe(concat('libs.css'))
|
||||
.pipe(cleanCSS({ keepSpecialComments: 0 }))
|
||||
.pipe(plumber.stop())
|
||||
.pipe(gulp.dest('./assets/css'))
|
||||
})
|
||||
|
||||
/**
|
||||
* TASK - Combine app css
|
||||
*/
|
||||
gulp.task("css-app", function () {
|
||||
return gulp.src(paths.css.compile)
|
||||
.pipe(plumber())
|
||||
.pipe(sass.sync({ includePaths: paths.css.includes }))
|
||||
.pipe(cleanCSS({ keepSpecialComments: 0 }))
|
||||
.pipe(plumber.stop())
|
||||
.pipe(gulp.dest("./assets/css"));
|
||||
});
|
||||
gulp.task('css-app', function () {
|
||||
return gulp.src(paths.css.compile)
|
||||
.pipe(plumber())
|
||||
.pipe(sass.sync({ includePaths: paths.css.includes }))
|
||||
.pipe(cleanCSS({ keepSpecialComments: 0 }))
|
||||
.pipe(plumber.stop())
|
||||
.pipe(gulp.dest('./assets/css'))
|
||||
})
|
||||
|
||||
/**
|
||||
* TASK - Copy web fonts
|
||||
*/
|
||||
gulp.task("fonts", function () {
|
||||
return gulp.src(paths.fonts)
|
||||
.pipe(gulp.dest("./assets/fonts"));
|
||||
});
|
||||
gulp.task('fonts', function () {
|
||||
return gulp.src(paths.fonts)
|
||||
.pipe(gulp.dest('./assets/fonts'))
|
||||
})
|
||||
|
||||
/**
|
||||
* TASK - Start dev watchers
|
||||
*/
|
||||
gulp.task('watch', function() {
|
||||
return merge(
|
||||
watch(paths.scripts.watch, {base: './'}, function() { return gulp.start('scripts-app'); }),
|
||||
watch(paths.css.watch, {base: './'}, function() { return gulp.start('css-app'); })
|
||||
);
|
||||
});
|
||||
gulp.task('watch', function () {
|
||||
return merge(
|
||||
watch(paths.scripts.watch, {base: './'}, function () { return gulp.start('scripts-app') }),
|
||||
watch(paths.css.watch, {base: './'}, function () { return gulp.start('css-app') })
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* TASK - Starts development server with watchers
|
||||
*/
|
||||
gulp.task('default', ['watch', 'server']);
|
||||
|
||||
gulp.task('dev', function() {
|
||||
|
||||
paths.css.includes.pop();
|
||||
paths.css.includes.push('../core');
|
||||
gulp.task('default', ['watch', 'server'])
|
||||
|
||||
paths.fonts.pop();
|
||||
paths.fonts.push('../core/core-client/fonts/**/*');
|
||||
gulp.task('dev', function () {
|
||||
paths.css.includes.pop()
|
||||
paths.css.includes.push('../core')
|
||||
|
||||
return run('default');
|
||||
paths.fonts.pop()
|
||||
paths.fonts.push('../core/core-client/fonts/**/*')
|
||||
|
||||
});
|
||||
return run('default')
|
||||
})
|
||||
|
||||
/**
|
||||
* TASK - Creates deployment packages
|
||||
*/
|
||||
gulp.task('deploy', ['scripts', 'css', 'fonts'], function() {
|
||||
var zipStream = gulp.src(paths.deploy)
|
||||
.pipe(zip('wiki-js.zip'))
|
||||
.pipe(gulp.dest('dist'));
|
||||
|
||||
var targzStream = gulp.src(paths.deploy)
|
||||
.pipe(tar('wiki-js.tar'))
|
||||
.pipe(gzip())
|
||||
.pipe(gulp.dest('dist'));
|
||||
|
||||
return merge(zipStream, targzStream);
|
||||
});
|
||||
gulp.task('deploy', ['scripts', 'css', 'fonts'], function () {
|
||||
var zipStream = gulp.src(paths.deploy)
|
||||
.pipe(zip('wiki-js.zip'))
|
||||
.pipe(gulp.dest('dist'))
|
||||
|
||||
var targzStream = gulp.src(paths.deploy)
|
||||
.pipe(tar('wiki-js.tar'))
|
||||
.pipe(gzip())
|
||||
.pipe(gulp.dest('dist'))
|
||||
|
||||
return merge(zipStream, targzStream)
|
||||
})
|
||||
|
@ -1,500 +1,452 @@
|
||||
"use strict";
|
||||
'use strict'
|
||||
|
||||
var Promise = require('bluebird'),
|
||||
path = require('path'),
|
||||
fs = Promise.promisifyAll(require("fs-extra")),
|
||||
_ = require('lodash'),
|
||||
farmhash = require('farmhash'),
|
||||
moment = require('moment');
|
||||
const Promise = require('bluebird')
|
||||
const path = require('path')
|
||||
const fs = Promise.promisifyAll(require('fs-extra'))
|
||||
const _ = require('lodash')
|
||||
const farmhash = require('farmhash')
|
||||
|
||||
/**
|
||||
* Entries Model
|
||||
*/
|
||||
module.exports = {
|
||||
|
||||
_repoPath: 'repo',
|
||||
_cachePath: 'data/cache',
|
||||
|
||||
/**
|
||||
* Initialize Entries model
|
||||
*
|
||||
* @return {Object} Entries model instance
|
||||
*/
|
||||
init() {
|
||||
|
||||
let self = this;
|
||||
|
||||
self._repoPath = path.resolve(ROOTPATH, appconfig.paths.repo);
|
||||
self._cachePath = path.resolve(ROOTPATH, appconfig.paths.data, 'cache');
|
||||
|
||||
return self;
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a document already exists
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @return {Promise<Boolean>} True if exists, false otherwise
|
||||
*/
|
||||
exists(entryPath) {
|
||||
|
||||
let self = this;
|
||||
|
||||
return self.fetchOriginal(entryPath, {
|
||||
parseMarkdown: false,
|
||||
parseMeta: false,
|
||||
parseTree: false,
|
||||
includeMarkdown: false,
|
||||
includeParentInfo: false,
|
||||
cache: false
|
||||
}).then(() => {
|
||||
return true;
|
||||
}).catch((err) => {
|
||||
return false;
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch a document from cache, otherwise the original
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @return {Promise<Object>} Page Data
|
||||
*/
|
||||
fetch(entryPath) {
|
||||
|
||||
let self = this;
|
||||
|
||||
let cpath = self.getCachePath(entryPath);
|
||||
|
||||
return fs.statAsync(cpath).then((st) => {
|
||||
return st.isFile();
|
||||
}).catch((err) => {
|
||||
return false;
|
||||
}).then((isCache) => {
|
||||
|
||||
if(isCache) {
|
||||
|
||||
// Load from cache
|
||||
|
||||
return fs.readFileAsync(cpath).then((contents) => {
|
||||
return JSON.parse(contents);
|
||||
}).catch((err) => {
|
||||
winston.error('Corrupted cache file. Deleting it...');
|
||||
fs.unlinkSync(cpath);
|
||||
return false;
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
// Load original
|
||||
|
||||
return self.fetchOriginal(entryPath);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches the original document entry
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @param {Object} options The options
|
||||
* @return {Promise<Object>} Page data
|
||||
*/
|
||||
fetchOriginal(entryPath, options) {
|
||||
|
||||
let self = this;
|
||||
|
||||
let fpath = self.getFullPath(entryPath);
|
||||
let cpath = self.getCachePath(entryPath);
|
||||
|
||||
options = _.defaults(options, {
|
||||
parseMarkdown: true,
|
||||
parseMeta: true,
|
||||
parseTree: true,
|
||||
includeMarkdown: false,
|
||||
includeParentInfo: true,
|
||||
cache: true
|
||||
});
|
||||
|
||||
return fs.statAsync(fpath).then((st) => {
|
||||
if(st.isFile()) {
|
||||
return fs.readFileAsync(fpath, 'utf8').then((contents) => {
|
||||
|
||||
// Parse contents
|
||||
|
||||
let pageData = {
|
||||
markdown: (options.includeMarkdown) ? contents : '',
|
||||
html: (options.parseMarkdown) ? mark.parseContent(contents) : '',
|
||||
meta: (options.parseMeta) ? mark.parseMeta(contents) : {},
|
||||
tree: (options.parseTree) ? mark.parseTree(contents) : []
|
||||
};
|
||||
|
||||
if(!pageData.meta.title) {
|
||||
pageData.meta.title = _.startCase(entryPath);
|
||||
}
|
||||
|
||||
pageData.meta.path = entryPath;
|
||||
|
||||
// Get parent
|
||||
|
||||
let parentPromise = (options.includeParentInfo) ? self.getParentInfo(entryPath).then((parentData) => {
|
||||
return (pageData.parent = parentData);
|
||||
}).catch((err) => {
|
||||
return (pageData.parent = false);
|
||||
}) : Promise.resolve(true);
|
||||
|
||||
return parentPromise.then(() => {
|
||||
|
||||
// Cache to disk
|
||||
|
||||
if(options.cache) {
|
||||
let cacheData = JSON.stringify(_.pick(pageData, ['html', 'meta', 'tree', 'parent']), false, false, false);
|
||||
return fs.writeFileAsync(cpath, cacheData).catch((err) => {
|
||||
winston.error('Unable to write to cache! Performance may be affected.');
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
}).return(pageData);
|
||||
|
||||
});
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}).catch((err) => {
|
||||
return Promise.reject(new Promise.OperationalError('Entry ' + entryPath + ' does not exist!'));
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Parse raw url path and make it safe
|
||||
*
|
||||
* @param {String} urlPath The url path
|
||||
* @return {String} Safe entry path
|
||||
*/
|
||||
parsePath(urlPath) {
|
||||
|
||||
let wlist = new RegExp('[^a-z0-9/\-]','g');
|
||||
|
||||
urlPath = _.toLower(urlPath).replace(wlist, '');
|
||||
|
||||
if(urlPath === '/') {
|
||||
urlPath = 'home';
|
||||
}
|
||||
|
||||
let urlParts = _.filter(_.split(urlPath, '/'), (p) => { return !_.isEmpty(p); });
|
||||
|
||||
return _.join(urlParts, '/');
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the parent information.
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @return {Promise<Object|False>} The parent information.
|
||||
*/
|
||||
getParentInfo(entryPath) {
|
||||
|
||||
let self = this;
|
||||
|
||||
if(_.includes(entryPath, '/')) {
|
||||
|
||||
let parentParts = _.initial(_.split(entryPath, '/'));
|
||||
let parentPath = _.join(parentParts,'/');
|
||||
let parentFile = _.last(parentParts);
|
||||
let fpath = self.getFullPath(parentPath);
|
||||
|
||||
return fs.statAsync(fpath).then((st) => {
|
||||
if(st.isFile()) {
|
||||
return fs.readFileAsync(fpath, 'utf8').then((contents) => {
|
||||
|
||||
let pageMeta = mark.parseMeta(contents);
|
||||
|
||||
return {
|
||||
path: parentPath,
|
||||
title: (pageMeta.title) ? pageMeta.title : _.startCase(parentFile),
|
||||
subtitle: (pageMeta.subtitle) ? pageMeta.subtitle : false
|
||||
};
|
||||
|
||||
});
|
||||
} else {
|
||||
return Promise.reject(new Error('Parent entry is not a valid file.'));
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
return Promise.reject(new Error('Parent entry is root.'));
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the full original path of a document.
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @return {String} The full path.
|
||||
*/
|
||||
getFullPath(entryPath) {
|
||||
return path.join(this._repoPath, entryPath + '.md');
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the full cache path of a document.
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @return {String} The full cache path.
|
||||
*/
|
||||
getCachePath(entryPath) {
|
||||
return path.join(this._cachePath, farmhash.fingerprint32(entryPath) + '.json');
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the entry path from full path.
|
||||
*
|
||||
* @param {String} fullPath The full path
|
||||
* @return {String} The entry path
|
||||
*/
|
||||
getEntryPathFromFullPath(fullPath) {
|
||||
let absRepoPath = path.resolve(ROOTPATH, this._repoPath);
|
||||
return _.chain(fullPath).replace(absRepoPath, '').replace('.md', '').replace(new RegExp('\\\\', 'g'),'/').value();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an existing document
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @param {String} contents The markdown-formatted contents
|
||||
* @return {Promise<Boolean>} True on success, false on failure
|
||||
*/
|
||||
update(entryPath, contents) {
|
||||
|
||||
let self = this;
|
||||
let fpath = self.getFullPath(entryPath);
|
||||
|
||||
return fs.statAsync(fpath).then((st) => {
|
||||
if(st.isFile()) {
|
||||
return self.makePersistent(entryPath, contents).then(() => {
|
||||
return self.updateCache(entryPath);
|
||||
});
|
||||
} else {
|
||||
return Promise.reject(new Error('Entry does not exist!'));
|
||||
}
|
||||
}).catch((err) => {
|
||||
winston.error(err);
|
||||
return Promise.reject(new Error('Failed to save document.'));
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Update local cache and search index
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @return {Promise} Promise of the operation
|
||||
*/
|
||||
updateCache(entryPath) {
|
||||
|
||||
let self = this;
|
||||
|
||||
return self.fetchOriginal(entryPath, {
|
||||
parseMarkdown: true,
|
||||
parseMeta: true,
|
||||
parseTree: true,
|
||||
includeMarkdown: true,
|
||||
includeParentInfo: true,
|
||||
cache: true
|
||||
}).then((pageData) => {
|
||||
return {
|
||||
entryPath,
|
||||
meta: pageData.meta,
|
||||
parent: pageData.parent || {},
|
||||
text: mark.removeMarkdown(pageData.markdown)
|
||||
};
|
||||
}).then((content) => {
|
||||
return db.Entry.findOneAndUpdate({
|
||||
_id: content.entryPath
|
||||
}, {
|
||||
_id: content.entryPath,
|
||||
title: content.meta.title || content.entryPath,
|
||||
subtitle: content.meta.subtitle || '',
|
||||
parent: content.parent.title || '',
|
||||
content: content.text || ''
|
||||
}, {
|
||||
new: true,
|
||||
upsert: true
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new document
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @param {String} contents The markdown-formatted contents
|
||||
* @return {Promise<Boolean>} True on success, false on failure
|
||||
*/
|
||||
create(entryPath, contents) {
|
||||
|
||||
let self = this;
|
||||
|
||||
return self.exists(entryPath).then((docExists) => {
|
||||
if(!docExists) {
|
||||
return self.makePersistent(entryPath, contents).then(() => {
|
||||
return self.updateCache(entryPath);
|
||||
});
|
||||
} else {
|
||||
return Promise.reject(new Error('Entry already exists!'));
|
||||
}
|
||||
}).catch((err) => {
|
||||
winston.error(err);
|
||||
return Promise.reject(new Error('Something went wrong.'));
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Makes a document persistent to disk and git repository
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @param {String} contents The markdown-formatted contents
|
||||
* @return {Promise<Boolean>} True on success, false on failure
|
||||
*/
|
||||
makePersistent(entryPath, contents) {
|
||||
|
||||
let self = this;
|
||||
let fpath = self.getFullPath(entryPath);
|
||||
|
||||
return fs.outputFileAsync(fpath, contents).then(() => {
|
||||
return git.commitDocument(entryPath);
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Move a document
|
||||
*
|
||||
* @param {String} entryPath The current entry path
|
||||
* @param {String} newEntryPath The new entry path
|
||||
* @return {Promise} Promise of the operation
|
||||
*/
|
||||
move(entryPath, newEntryPath) {
|
||||
|
||||
let self = this;
|
||||
|
||||
if(_.isEmpty(entryPath) || entryPath === 'home') {
|
||||
return Promise.reject(new Error('Invalid path!'));
|
||||
}
|
||||
|
||||
return git.moveDocument(entryPath, newEntryPath).then(() => {
|
||||
return git.commitDocument(newEntryPath).then(() => {
|
||||
|
||||
// Delete old cache version
|
||||
|
||||
let oldEntryCachePath = self.getCachePath(entryPath);
|
||||
fs.unlinkAsync(oldEntryCachePath).catch((err) => { return true; });
|
||||
|
||||
// Delete old index entry
|
||||
|
||||
ws.emit('searchDel', {
|
||||
auth: WSInternalKey,
|
||||
entryPath
|
||||
});
|
||||
|
||||
// Create cache for new entry
|
||||
|
||||
return self.updateCache(newEntryPath);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate a starter page content based on the entry path
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @return {Promise<String>} Starter content
|
||||
*/
|
||||
getStarter(entryPath) {
|
||||
|
||||
let self = this;
|
||||
let formattedTitle = _.startCase(_.last(_.split(entryPath, '/')));
|
||||
|
||||
return fs.readFileAsync(path.join(ROOTPATH, 'client/content/create.md'), 'utf8').then((contents) => {
|
||||
return _.replace(contents, new RegExp('{TITLE}', 'g'), formattedTitle);
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Searches entries based on terms.
|
||||
*
|
||||
* @param {String} terms The terms to search for
|
||||
* @return {Promise<Object>} Promise of the search results
|
||||
*/
|
||||
search(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) {
|
||||
let regMatch = new RegExp('^' + _.split(terms, ' ')[0]);
|
||||
return db.Entry.find({
|
||||
_id: { $regex: regMatch }
|
||||
}, '_id')
|
||||
.sort('_id')
|
||||
.limit(5)
|
||||
.exec()
|
||||
.then((matches) => {
|
||||
return {
|
||||
match: hits,
|
||||
suggest: (matches) ? _.map(matches, '_id') : []
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return {
|
||||
match: _.filter(hits, (h) => { return h._doc.score >= 1; }),
|
||||
suggest: []
|
||||
};
|
||||
}
|
||||
|
||||
}).catch((err) => {
|
||||
|
||||
winston.error(err);
|
||||
return {
|
||||
match: [],
|
||||
suggest: []
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
_repoPath: 'repo',
|
||||
_cachePath: 'data/cache',
|
||||
|
||||
/**
|
||||
* Initialize Entries model
|
||||
*
|
||||
* @return {Object} Entries model instance
|
||||
*/
|
||||
init () {
|
||||
let self = this
|
||||
|
||||
self._repoPath = path.resolve(ROOTPATH, appconfig.paths.repo)
|
||||
self._cachePath = path.resolve(ROOTPATH, appconfig.paths.data, 'cache')
|
||||
|
||||
return self
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a document already exists
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @return {Promise<Boolean>} True if exists, false otherwise
|
||||
*/
|
||||
exists (entryPath) {
|
||||
let self = this
|
||||
|
||||
return self.fetchOriginal(entryPath, {
|
||||
parseMarkdown: false,
|
||||
parseMeta: false,
|
||||
parseTree: false,
|
||||
includeMarkdown: false,
|
||||
includeParentInfo: false,
|
||||
cache: false
|
||||
}).then(() => {
|
||||
return true
|
||||
}).catch((err) => { // eslint-disable-line handle-callback-err
|
||||
return false
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch a document from cache, otherwise the original
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @return {Promise<Object>} Page Data
|
||||
*/
|
||||
fetch (entryPath) {
|
||||
let self = this
|
||||
|
||||
let cpath = self.getCachePath(entryPath)
|
||||
|
||||
return fs.statAsync(cpath).then((st) => {
|
||||
return st.isFile()
|
||||
}).catch((err) => { // eslint-disable-line handle-callback-err
|
||||
return false
|
||||
}).then((isCache) => {
|
||||
if (isCache) {
|
||||
// Load from cache
|
||||
|
||||
return fs.readFileAsync(cpath).then((contents) => {
|
||||
return JSON.parse(contents)
|
||||
}).catch((err) => { // eslint-disable-line handle-callback-err
|
||||
winston.error('Corrupted cache file. Deleting it...')
|
||||
fs.unlinkSync(cpath)
|
||||
return false
|
||||
})
|
||||
} else {
|
||||
// Load original
|
||||
|
||||
return self.fetchOriginal(entryPath)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches the original document entry
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @param {Object} options The options
|
||||
* @return {Promise<Object>} Page data
|
||||
*/
|
||||
fetchOriginal (entryPath, options) {
|
||||
let self = this
|
||||
|
||||
let fpath = self.getFullPath(entryPath)
|
||||
let cpath = self.getCachePath(entryPath)
|
||||
|
||||
options = _.defaults(options, {
|
||||
parseMarkdown: true,
|
||||
parseMeta: true,
|
||||
parseTree: true,
|
||||
includeMarkdown: false,
|
||||
includeParentInfo: true,
|
||||
cache: true
|
||||
})
|
||||
|
||||
return fs.statAsync(fpath).then((st) => {
|
||||
if (st.isFile()) {
|
||||
return fs.readFileAsync(fpath, 'utf8').then((contents) => {
|
||||
// Parse contents
|
||||
|
||||
let pageData = {
|
||||
markdown: (options.includeMarkdown) ? contents : '',
|
||||
html: (options.parseMarkdown) ? mark.parseContent(contents) : '',
|
||||
meta: (options.parseMeta) ? mark.parseMeta(contents) : {},
|
||||
tree: (options.parseTree) ? mark.parseTree(contents) : []
|
||||
}
|
||||
|
||||
if (!pageData.meta.title) {
|
||||
pageData.meta.title = _.startCase(entryPath)
|
||||
}
|
||||
|
||||
pageData.meta.path = entryPath
|
||||
|
||||
// Get parent
|
||||
|
||||
let parentPromise = (options.includeParentInfo) ? self.getParentInfo(entryPath).then((parentData) => {
|
||||
return (pageData.parent = parentData)
|
||||
}).catch((err) => { // eslint-disable-line handle-callback-err
|
||||
return (pageData.parent = false)
|
||||
}) : Promise.resolve(true)
|
||||
|
||||
return parentPromise.then(() => {
|
||||
// Cache to disk
|
||||
|
||||
if (options.cache) {
|
||||
let cacheData = JSON.stringify(_.pick(pageData, ['html', 'meta', 'tree', 'parent']), false, false, false)
|
||||
return fs.writeFileAsync(cpath, cacheData).catch((err) => {
|
||||
winston.error('Unable to write to cache! Performance may be affected.')
|
||||
winston.error(err)
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}).return(pageData)
|
||||
})
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}).catch((err) => { // eslint-disable-line handle-callback-err
|
||||
return Promise.reject(new Promise.OperationalError('Entry ' + entryPath + ' does not exist!'))
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Parse raw url path and make it safe
|
||||
*
|
||||
* @param {String} urlPath The url path
|
||||
* @return {String} Safe entry path
|
||||
*/
|
||||
parsePath (urlPath) {
|
||||
let wlist = new RegExp('[^a-z0-9/-]', 'g')
|
||||
|
||||
urlPath = _.toLower(urlPath).replace(wlist, '')
|
||||
|
||||
if (urlPath === '/') {
|
||||
urlPath = 'home'
|
||||
}
|
||||
|
||||
let urlParts = _.filter(_.split(urlPath, '/'), (p) => { return !_.isEmpty(p) })
|
||||
|
||||
return _.join(urlParts, '/')
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the parent information.
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @return {Promise<Object|False>} The parent information.
|
||||
*/
|
||||
getParentInfo (entryPath) {
|
||||
let self = this
|
||||
|
||||
if (_.includes(entryPath, '/')) {
|
||||
let parentParts = _.initial(_.split(entryPath, '/'))
|
||||
let parentPath = _.join(parentParts, '/')
|
||||
let parentFile = _.last(parentParts)
|
||||
let fpath = self.getFullPath(parentPath)
|
||||
|
||||
return fs.statAsync(fpath).then((st) => {
|
||||
if (st.isFile()) {
|
||||
return fs.readFileAsync(fpath, 'utf8').then((contents) => {
|
||||
let pageMeta = mark.parseMeta(contents)
|
||||
|
||||
return {
|
||||
path: parentPath,
|
||||
title: (pageMeta.title) ? pageMeta.title : _.startCase(parentFile),
|
||||
subtitle: (pageMeta.subtitle) ? pageMeta.subtitle : false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return Promise.reject(new Error('Parent entry is not a valid file.'))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return Promise.reject(new Error('Parent entry is root.'))
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the full original path of a document.
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @return {String} The full path.
|
||||
*/
|
||||
getFullPath (entryPath) {
|
||||
return path.join(this._repoPath, entryPath + '.md')
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the full cache path of a document.
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @return {String} The full cache path.
|
||||
*/
|
||||
getCachePath (entryPath) {
|
||||
return path.join(this._cachePath, farmhash.fingerprint32(entryPath) + '.json')
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the entry path from full path.
|
||||
*
|
||||
* @param {String} fullPath The full path
|
||||
* @return {String} The entry path
|
||||
*/
|
||||
getEntryPathFromFullPath (fullPath) {
|
||||
let absRepoPath = path.resolve(ROOTPATH, this._repoPath)
|
||||
return _.chain(fullPath).replace(absRepoPath, '').replace('.md', '').replace(new RegExp('\\\\', 'g'), '/').value()
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an existing document
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @param {String} contents The markdown-formatted contents
|
||||
* @return {Promise<Boolean>} True on success, false on failure
|
||||
*/
|
||||
update (entryPath, contents) {
|
||||
let self = this
|
||||
let fpath = self.getFullPath(entryPath)
|
||||
|
||||
return fs.statAsync(fpath).then((st) => {
|
||||
if (st.isFile()) {
|
||||
return self.makePersistent(entryPath, contents).then(() => {
|
||||
return self.updateCache(entryPath)
|
||||
})
|
||||
} else {
|
||||
return Promise.reject(new Error('Entry does not exist!'))
|
||||
}
|
||||
}).catch((err) => {
|
||||
winston.error(err)
|
||||
return Promise.reject(new Error('Failed to save document.'))
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Update local cache and search index
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @return {Promise} Promise of the operation
|
||||
*/
|
||||
updateCache (entryPath) {
|
||||
let self = this
|
||||
|
||||
return self.fetchOriginal(entryPath, {
|
||||
parseMarkdown: true,
|
||||
parseMeta: true,
|
||||
parseTree: true,
|
||||
includeMarkdown: true,
|
||||
includeParentInfo: true,
|
||||
cache: true
|
||||
}).then((pageData) => {
|
||||
return {
|
||||
entryPath,
|
||||
meta: pageData.meta,
|
||||
parent: pageData.parent || {},
|
||||
text: mark.removeMarkdown(pageData.markdown)
|
||||
}
|
||||
}).then((content) => {
|
||||
return db.Entry.findOneAndUpdate({
|
||||
_id: content.entryPath
|
||||
}, {
|
||||
_id: content.entryPath,
|
||||
title: content.meta.title || content.entryPath,
|
||||
subtitle: content.meta.subtitle || '',
|
||||
parent: content.parent.title || '',
|
||||
content: content.text || ''
|
||||
}, {
|
||||
new: true,
|
||||
upsert: true
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new document
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @param {String} contents The markdown-formatted contents
|
||||
* @return {Promise<Boolean>} True on success, false on failure
|
||||
*/
|
||||
create (entryPath, contents) {
|
||||
let self = this
|
||||
|
||||
return self.exists(entryPath).then((docExists) => {
|
||||
if (!docExists) {
|
||||
return self.makePersistent(entryPath, contents).then(() => {
|
||||
return self.updateCache(entryPath)
|
||||
})
|
||||
} else {
|
||||
return Promise.reject(new Error('Entry already exists!'))
|
||||
}
|
||||
}).catch((err) => {
|
||||
winston.error(err)
|
||||
return Promise.reject(new Error('Something went wrong.'))
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Makes a document persistent to disk and git repository
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @param {String} contents The markdown-formatted contents
|
||||
* @return {Promise<Boolean>} True on success, false on failure
|
||||
*/
|
||||
makePersistent (entryPath, contents) {
|
||||
let self = this
|
||||
let fpath = self.getFullPath(entryPath)
|
||||
|
||||
return fs.outputFileAsync(fpath, contents).then(() => {
|
||||
return git.commitDocument(entryPath)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Move a document
|
||||
*
|
||||
* @param {String} entryPath The current entry path
|
||||
* @param {String} newEntryPath The new entry path
|
||||
* @return {Promise} Promise of the operation
|
||||
*/
|
||||
move (entryPath, newEntryPath) {
|
||||
let self = this
|
||||
|
||||
if (_.isEmpty(entryPath) || entryPath === 'home') {
|
||||
return Promise.reject(new Error('Invalid path!'))
|
||||
}
|
||||
|
||||
return git.moveDocument(entryPath, newEntryPath).then(() => {
|
||||
return git.commitDocument(newEntryPath).then(() => {
|
||||
// Delete old cache version
|
||||
|
||||
let oldEntryCachePath = self.getCachePath(entryPath)
|
||||
fs.unlinkAsync(oldEntryCachePath).catch((err) => { return true }) // eslint-disable-line handle-callback-err
|
||||
|
||||
// Delete old index entry
|
||||
|
||||
ws.emit('searchDel', {
|
||||
auth: WSInternalKey,
|
||||
entryPath
|
||||
})
|
||||
|
||||
// Create cache for new entry
|
||||
|
||||
return self.updateCache(newEntryPath)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate a starter page content based on the entry path
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @return {Promise<String>} Starter content
|
||||
*/
|
||||
getStarter (entryPath) {
|
||||
let formattedTitle = _.startCase(_.last(_.split(entryPath, '/')))
|
||||
|
||||
return fs.readFileAsync(path.join(ROOTPATH, 'client/content/create.md'), 'utf8').then((contents) => {
|
||||
return _.replace(contents, new RegExp('{TITLE}', 'g'), formattedTitle)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Searches entries based on terms.
|
||||
*
|
||||
* @param {String} terms The terms to search for
|
||||
* @return {Promise<Object>} Promise of the search results
|
||||
*/
|
||||
search (terms) {
|
||||
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) {
|
||||
let regMatch = new RegExp('^' + _.split(terms, ' ')[0])
|
||||
return db.Entry.find({
|
||||
_id: { $regex: regMatch }
|
||||
}, '_id')
|
||||
.sort('_id')
|
||||
.limit(5)
|
||||
.exec()
|
||||
.then((matches) => {
|
||||
return {
|
||||
match: hits,
|
||||
suggest: (matches) ? _.map(matches, '_id') : []
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return {
|
||||
match: _.filter(hits, (h) => { return h._doc.score >= 1 }),
|
||||
suggest: []
|
||||
}
|
||||
}
|
||||
}).catch((err) => {
|
||||
winston.error(err)
|
||||
return {
|
||||
match: [],
|
||||
suggest: []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,258 +1,231 @@
|
||||
"use strict";
|
||||
'use strict'
|
||||
|
||||
var Git = require("git-wrapper2-promise"),
|
||||
Promise = require('bluebird'),
|
||||
path = require('path'),
|
||||
os = require('os'),
|
||||
fs = Promise.promisifyAll(require("fs")),
|
||||
moment = require('moment'),
|
||||
_ = require('lodash'),
|
||||
URL = require('url');
|
||||
const Git = require('git-wrapper2-promise')
|
||||
const Promise = require('bluebird')
|
||||
const path = require('path')
|
||||
const fs = Promise.promisifyAll(require('fs'))
|
||||
const _ = require('lodash')
|
||||
const URL = require('url')
|
||||
|
||||
/**
|
||||
* Git Model
|
||||
*/
|
||||
module.exports = {
|
||||
|
||||
_git: null,
|
||||
_url: '',
|
||||
_repo: {
|
||||
path: '',
|
||||
branch: 'master',
|
||||
exists: false
|
||||
},
|
||||
_signature: {
|
||||
name: 'Wiki',
|
||||
email: 'user@example.com'
|
||||
},
|
||||
_opts: {
|
||||
clone: {},
|
||||
push: {}
|
||||
},
|
||||
onReady: null,
|
||||
|
||||
/**
|
||||
* Initialize Git model
|
||||
*
|
||||
* @return {Object} Git model instance
|
||||
*/
|
||||
init() {
|
||||
|
||||
let self = this;
|
||||
|
||||
//-> Build repository path
|
||||
|
||||
if(_.isEmpty(appconfig.paths.repo)) {
|
||||
self._repo.path = path.join(ROOTPATH, 'repo');
|
||||
} else {
|
||||
self._repo.path = appconfig.paths.repo;
|
||||
}
|
||||
|
||||
//-> Initialize repository
|
||||
|
||||
self.onReady = self._initRepo(appconfig);
|
||||
|
||||
// Define signature
|
||||
|
||||
self._signature.name = appconfig.git.signature.name || 'Wiki';
|
||||
self._signature.email = appconfig.git.signature.email || 'user@example.com';
|
||||
|
||||
return self;
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize Git repository
|
||||
*
|
||||
* @param {Object} appconfig The application config
|
||||
* @return {Object} Promise
|
||||
*/
|
||||
_initRepo(appconfig) {
|
||||
|
||||
let self = this;
|
||||
|
||||
winston.info('[' + PROCNAME + '][GIT] Checking Git repository...');
|
||||
|
||||
//-> Check if path is accessible
|
||||
|
||||
return fs.mkdirAsync(self._repo.path).catch((err) => {
|
||||
if(err.code !== 'EEXIST') {
|
||||
winston.error('[' + PROCNAME + '][GIT] Invalid Git repository path or missing permissions.');
|
||||
}
|
||||
}).then(() => {
|
||||
|
||||
self._git = new Git({ 'git-dir': self._repo.path });
|
||||
|
||||
//-> Check if path already contains a git working folder
|
||||
|
||||
return self._git.isRepo().then((isRepo) => {
|
||||
self._repo.exists = isRepo;
|
||||
return (!isRepo) ? self._git.exec('init') : true;
|
||||
}).catch((err) => {
|
||||
self._repo.exists = false;
|
||||
});
|
||||
|
||||
}).then(() => {
|
||||
|
||||
// Initialize remote
|
||||
|
||||
let urlObj = URL.parse(appconfig.git.url);
|
||||
urlObj.auth = appconfig.git.auth.username + ((appconfig.git.auth.type !== 'ssh') ? ':' + appconfig.git.auth.password : '');
|
||||
self._url = URL.format(urlObj);
|
||||
|
||||
return self._git.exec('remote', 'show').then((cProc) => {
|
||||
let out = cProc.stdout.toString();
|
||||
if(_.includes(out, 'origin')) {
|
||||
return true;
|
||||
} else {
|
||||
return Promise.join(
|
||||
self._git.exec('config', ['--local', 'user.name', self._signature.name]),
|
||||
self._git.exec('config', ['--local', 'user.email', self._signature.email])
|
||||
).then(() => {
|
||||
return self._git.exec('remote', ['add', 'origin', self._url]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}).catch((err) => {
|
||||
winston.error('[' + PROCNAME + '][GIT] Git remote error!');
|
||||
throw err;
|
||||
}).then(() => {
|
||||
winston.info('[' + PROCNAME + '][GIT] Git repository is OK.');
|
||||
return true;
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the repo path.
|
||||
*
|
||||
* @return {String} The repo path.
|
||||
*/
|
||||
getRepoPath() {
|
||||
|
||||
return this._repo.path || path.join(ROOTPATH, 'repo');
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Sync with the remote repository
|
||||
*
|
||||
* @return {Promise} Resolve on sync success
|
||||
*/
|
||||
resync() {
|
||||
|
||||
let self = this;
|
||||
|
||||
// Fetch
|
||||
|
||||
winston.info('[' + PROCNAME + '][GIT] Performing pull from remote repository...');
|
||||
return self._git.pull('origin', self._repo.branch).then((cProc) => {
|
||||
winston.info('[' + PROCNAME + '][GIT] Pull completed.');
|
||||
})
|
||||
.catch((err) => {
|
||||
winston.error('[' + PROCNAME + '][GIT] Unable to fetch from git origin!');
|
||||
throw err;
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
// Check for changes
|
||||
|
||||
return self._git.exec('log', 'origin/' + self._repo.branch + '..HEAD').then((cProc) => {
|
||||
let out = cProc.stdout.toString();
|
||||
|
||||
if(_.includes(out, 'commit')) {
|
||||
|
||||
winston.info('[' + PROCNAME + '][GIT] Performing push to remote repository...');
|
||||
return self._git.push('origin', self._repo.branch).then(() => {
|
||||
return winston.info('[' + PROCNAME + '][GIT] Push completed.');
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
winston.info('[' + PROCNAME + '][GIT] Push skipped. Repository is already in sync.');
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
});
|
||||
|
||||
})
|
||||
.catch((err) => {
|
||||
winston.error('[' + PROCNAME + '][GIT] Unable to push changes to remote!');
|
||||
throw err;
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Commits a document.
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @return {Promise} Resolve on commit success
|
||||
*/
|
||||
commitDocument(entryPath) {
|
||||
|
||||
let self = this;
|
||||
let gitFilePath = entryPath + '.md';
|
||||
let commitMsg = '';
|
||||
|
||||
return self._git.exec('ls-files', gitFilePath).then((cProc) => {
|
||||
let out = cProc.stdout.toString();
|
||||
return _.includes(out, gitFilePath);
|
||||
}).then((isTracked) => {
|
||||
commitMsg = (isTracked) ? 'Updated ' + gitFilePath : 'Added ' + gitFilePath;
|
||||
return self._git.add(gitFilePath);
|
||||
}).then(() => {
|
||||
return self._git.commit(commitMsg).catch((err) => {
|
||||
if(_.includes(err.stdout, 'nothing to commit')) { return true; }
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Move a document.
|
||||
*
|
||||
* @param {String} entryPath The current entry path
|
||||
* @param {String} newEntryPath The new entry path
|
||||
* @return {Promise<Boolean>} Resolve on success
|
||||
*/
|
||||
moveDocument(entryPath, newEntryPath) {
|
||||
|
||||
let self = this;
|
||||
let gitFilePath = entryPath + '.md';
|
||||
let gitNewFilePath = newEntryPath + '.md';
|
||||
|
||||
return self._git.exec('mv', [gitFilePath, gitNewFilePath]).then((cProc) => {
|
||||
let out = cProc.stdout.toString();
|
||||
if(_.includes(out, 'fatal')) {
|
||||
let errorMsg = _.capitalize(_.head(_.split(_.replace(out, 'fatal: ', ''), ',')));
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Commits uploads changes.
|
||||
*
|
||||
* @param {String} msg The commit message
|
||||
* @return {Promise} Resolve on commit success
|
||||
*/
|
||||
commitUploads(msg) {
|
||||
|
||||
let self = this;
|
||||
msg = msg || "Uploads repository sync";
|
||||
|
||||
return self._git.add('uploads').then(() => {
|
||||
return self._git.commit(msg).catch((err) => {
|
||||
if(_.includes(err.stdout, 'nothing to commit')) { return true; }
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
_git: null,
|
||||
_url: '',
|
||||
_repo: {
|
||||
path: '',
|
||||
branch: 'master',
|
||||
exists: false
|
||||
},
|
||||
_signature: {
|
||||
name: 'Wiki',
|
||||
email: 'user@example.com'
|
||||
},
|
||||
_opts: {
|
||||
clone: {},
|
||||
push: {}
|
||||
},
|
||||
onReady: null,
|
||||
|
||||
/**
|
||||
* Initialize Git model
|
||||
*
|
||||
* @return {Object} Git model instance
|
||||
*/
|
||||
init () {
|
||||
let self = this
|
||||
|
||||
// -> Build repository path
|
||||
|
||||
if (_.isEmpty(appconfig.paths.repo)) {
|
||||
self._repo.path = path.join(ROOTPATH, 'repo')
|
||||
} else {
|
||||
self._repo.path = appconfig.paths.repo
|
||||
}
|
||||
|
||||
// -> Initialize repository
|
||||
|
||||
self.onReady = self._initRepo(appconfig)
|
||||
|
||||
// Define signature
|
||||
|
||||
self._signature.name = appconfig.git.signature.name || 'Wiki'
|
||||
self._signature.email = appconfig.git.signature.email || 'user@example.com'
|
||||
|
||||
return self
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize Git repository
|
||||
*
|
||||
* @param {Object} appconfig The application config
|
||||
* @return {Object} Promise
|
||||
*/
|
||||
_initRepo (appconfig) {
|
||||
let self = this
|
||||
|
||||
winston.info('[' + PROCNAME + '][GIT] Checking Git repository...')
|
||||
|
||||
// -> Check if path is accessible
|
||||
|
||||
return fs.mkdirAsync(self._repo.path).catch((err) => {
|
||||
if (err.code !== 'EEXIST') {
|
||||
winston.error('[' + PROCNAME + '][GIT] Invalid Git repository path or missing permissions.')
|
||||
}
|
||||
}).then(() => {
|
||||
self._git = new Git({ 'git-dir': self._repo.path })
|
||||
|
||||
// -> Check if path already contains a git working folder
|
||||
|
||||
return self._git.isRepo().then((isRepo) => {
|
||||
self._repo.exists = isRepo
|
||||
return (!isRepo) ? self._git.exec('init') : true
|
||||
}).catch((err) => { // eslint-disable-line handle-callback-err
|
||||
self._repo.exists = false
|
||||
})
|
||||
}).then(() => {
|
||||
// Initialize remote
|
||||
|
||||
let urlObj = URL.parse(appconfig.git.url)
|
||||
urlObj.auth = appconfig.git.auth.username + ((appconfig.git.auth.type !== 'ssh') ? ':' + appconfig.git.auth.password : '')
|
||||
self._url = URL.format(urlObj)
|
||||
|
||||
return self._git.exec('remote', 'show').then((cProc) => {
|
||||
let out = cProc.stdout.toString()
|
||||
if (_.includes(out, 'origin')) {
|
||||
return true
|
||||
} else {
|
||||
return Promise.join(
|
||||
self._git.exec('config', ['--local', 'user.name', self._signature.name]),
|
||||
self._git.exec('config', ['--local', 'user.email', self._signature.email])
|
||||
).then(() => {
|
||||
return self._git.exec('remote', ['add', 'origin', self._url])
|
||||
})
|
||||
}
|
||||
})
|
||||
}).catch((err) => {
|
||||
winston.error('[' + PROCNAME + '][GIT] Git remote error!')
|
||||
throw err
|
||||
}).then(() => {
|
||||
winston.info('[' + PROCNAME + '][GIT] Git repository is OK.')
|
||||
return true
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the repo path.
|
||||
*
|
||||
* @return {String} The repo path.
|
||||
*/
|
||||
getRepoPath () {
|
||||
return this._repo.path || path.join(ROOTPATH, 'repo')
|
||||
},
|
||||
|
||||
/**
|
||||
* Sync with the remote repository
|
||||
*
|
||||
* @return {Promise} Resolve on sync success
|
||||
*/
|
||||
resync () {
|
||||
let self = this
|
||||
|
||||
// Fetch
|
||||
|
||||
winston.info('[' + PROCNAME + '][GIT] Performing pull from remote repository...')
|
||||
return self._git.pull('origin', self._repo.branch).then((cProc) => {
|
||||
winston.info('[' + PROCNAME + '][GIT] Pull completed.')
|
||||
})
|
||||
.catch((err) => {
|
||||
winston.error('[' + PROCNAME + '][GIT] Unable to fetch from git origin!')
|
||||
throw err
|
||||
})
|
||||
.then(() => {
|
||||
// Check for changes
|
||||
|
||||
return self._git.exec('log', 'origin/' + self._repo.branch + '..HEAD').then((cProc) => {
|
||||
let out = cProc.stdout.toString()
|
||||
|
||||
if (_.includes(out, 'commit')) {
|
||||
winston.info('[' + PROCNAME + '][GIT] Performing push to remote repository...')
|
||||
return self._git.push('origin', self._repo.branch).then(() => {
|
||||
return winston.info('[' + PROCNAME + '][GIT] Push completed.')
|
||||
})
|
||||
} else {
|
||||
winston.info('[' + PROCNAME + '][GIT] Push skipped. Repository is already in sync.')
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
winston.error('[' + PROCNAME + '][GIT] Unable to push changes to remote!')
|
||||
throw err
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Commits a document.
|
||||
*
|
||||
* @param {String} entryPath The entry path
|
||||
* @return {Promise} Resolve on commit success
|
||||
*/
|
||||
commitDocument (entryPath) {
|
||||
let self = this
|
||||
let gitFilePath = entryPath + '.md'
|
||||
let commitMsg = ''
|
||||
|
||||
return self._git.exec('ls-files', gitFilePath).then((cProc) => {
|
||||
let out = cProc.stdout.toString()
|
||||
return _.includes(out, gitFilePath)
|
||||
}).then((isTracked) => {
|
||||
commitMsg = (isTracked) ? 'Updated ' + gitFilePath : 'Added ' + gitFilePath
|
||||
return self._git.add(gitFilePath)
|
||||
}).then(() => {
|
||||
return self._git.commit(commitMsg).catch((err) => {
|
||||
if (_.includes(err.stdout, 'nothing to commit')) { return true }
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Move a document.
|
||||
*
|
||||
* @param {String} entryPath The current entry path
|
||||
* @param {String} newEntryPath The new entry path
|
||||
* @return {Promise<Boolean>} Resolve on success
|
||||
*/
|
||||
moveDocument (entryPath, newEntryPath) {
|
||||
let self = this
|
||||
let gitFilePath = entryPath + '.md'
|
||||
let gitNewFilePath = newEntryPath + '.md'
|
||||
|
||||
return self._git.exec('mv', [gitFilePath, gitNewFilePath]).then((cProc) => {
|
||||
let out = cProc.stdout.toString()
|
||||
if (_.includes(out, 'fatal')) {
|
||||
let errorMsg = _.capitalize(_.head(_.split(_.replace(out, 'fatal: ', ''), ',')))
|
||||
throw new Error(errorMsg)
|
||||
}
|
||||
return true
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Commits uploads changes.
|
||||
*
|
||||
* @param {String} msg The commit message
|
||||
* @return {Promise} Resolve on commit success
|
||||
*/
|
||||
commitUploads (msg) {
|
||||
let self = this
|
||||
msg = msg || 'Uploads repository sync'
|
||||
|
||||
return self._git.add('uploads').then(() => {
|
||||
return self._git.commit(msg).catch((err) => {
|
||||
if (_.includes(err.stdout, 'nothing to commit')) { return true }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,32 +1,26 @@
|
||||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const crypto = require('crypto');
|
||||
const crypto = require('crypto')
|
||||
|
||||
/**
|
||||
* Internal Authentication
|
||||
*/
|
||||
module.exports = {
|
||||
|
||||
_curKey: false,
|
||||
_curKey: false,
|
||||
|
||||
init(inKey) {
|
||||
init (inKey) {
|
||||
this._curKey = inKey
|
||||
|
||||
this._curKey = inKey;
|
||||
return this
|
||||
},
|
||||
|
||||
return this;
|
||||
generateKey () {
|
||||
return crypto.randomBytes(20).toString('hex')
|
||||
},
|
||||
|
||||
},
|
||||
validateKey (inKey) {
|
||||
return inKey === this._curKey
|
||||
}
|
||||
|
||||
generateKey() {
|
||||
|
||||
return crypto.randomBytes(20).toString('hex');
|
||||
|
||||
},
|
||||
|
||||
validateKey(inKey) {
|
||||
|
||||
return inKey === this._curKey;
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
@ -1,187 +1,176 @@
|
||||
"use strict";
|
||||
'use strict'
|
||||
|
||||
var path = require('path'),
|
||||
Promise = require('bluebird'),
|
||||
fs = Promise.promisifyAll(require('fs-extra')),
|
||||
multer = require('multer'),
|
||||
os = require('os'),
|
||||
_ = require('lodash');
|
||||
const path = require('path')
|
||||
const Promise = require('bluebird')
|
||||
const fs = Promise.promisifyAll(require('fs-extra'))
|
||||
const multer = require('multer')
|
||||
const os = require('os')
|
||||
const _ = require('lodash')
|
||||
|
||||
/**
|
||||
* Local Data Storage
|
||||
*/
|
||||
module.exports = {
|
||||
|
||||
_uploadsPath: './repo/uploads',
|
||||
_uploadsThumbsPath: './data/thumbs',
|
||||
|
||||
uploadImgHandler: null,
|
||||
|
||||
/**
|
||||
* Initialize Local Data Storage model
|
||||
*
|
||||
* @return {Object} Local Data Storage model instance
|
||||
*/
|
||||
init() {
|
||||
|
||||
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) {
|
||||
|
||||
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) => {
|
||||
cb(null, path.resolve(ROOTPATH, appconfig.paths.data, 'temp-upload'));
|
||||
}
|
||||
}),
|
||||
fileFilter: (req, f, cb) => {
|
||||
|
||||
//-> Check filesize
|
||||
|
||||
if(f.size > maxFileSizes.img) {
|
||||
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);
|
||||
|
||||
//-> 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;
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 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'));
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
* @param {boolean} isImage Indicates if image
|
||||
* @return {Promise<String>} Promise of the accepted filename
|
||||
*/
|
||||
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(isImage && !_.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;
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
};
|
||||
_uploadsPath: './repo/uploads',
|
||||
_uploadsThumbsPath: './data/thumbs',
|
||||
|
||||
uploadImgHandler: null,
|
||||
|
||||
/**
|
||||
* Initialize Local Data Storage model
|
||||
*
|
||||
* @return {Object} Local Data Storage model instance
|
||||
*/
|
||||
init () {
|
||||
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) {
|
||||
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) => {
|
||||
cb(null, path.resolve(ROOTPATH, appconfig.paths.data, 'temp-upload'))
|
||||
}
|
||||
}),
|
||||
fileFilter: (req, f, cb) => {
|
||||
// -> Check filesize
|
||||
|
||||
if (f.size > maxFileSizes.img) {
|
||||
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)
|
||||
|
||||
// -> 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
|
||||
},
|
||||
|
||||
/**
|
||||
* 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'))
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
* @param {boolean} isImage Indicates if image
|
||||
* @return {Promise<String>} Promise of the accepted filename
|
||||
*/
|
||||
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 (isImage && !_.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
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,292 +1,255 @@
|
||||
"use strict";
|
||||
|
||||
var path = require('path'),
|
||||
Promise = require('bluebird'),
|
||||
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'),
|
||||
sharp = require('sharp'),
|
||||
_ = require('lodash');
|
||||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
const Promise = require('bluebird')
|
||||
const fs = Promise.promisifyAll(require('fs-extra'))
|
||||
const readChunk = require('read-chunk')
|
||||
const fileType = require('file-type')
|
||||
const mime = require('mime-types')
|
||||
const farmhash = require('farmhash')
|
||||
const chokidar = require('chokidar')
|
||||
const sharp = require('sharp')
|
||||
const _ = require('lodash')
|
||||
|
||||
/**
|
||||
* Uploads - Agent
|
||||
*/
|
||||
module.exports = {
|
||||
|
||||
_uploadsPath: './repo/uploads',
|
||||
_uploadsThumbsPath: './data/thumbs',
|
||||
|
||||
_watcher: null,
|
||||
|
||||
/**
|
||||
* Initialize Uploads model
|
||||
*
|
||||
* @return {Object} Uploads model instance
|
||||
*/
|
||||
init() {
|
||||
|
||||
let self = this;
|
||||
|
||||
self._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads');
|
||||
self._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs');
|
||||
|
||||
// Disable Sharp cache, as it cause file locks issues when deleting uploads.
|
||||
sharp.cache(false);
|
||||
|
||||
return self;
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Watch the uploads folder for changes
|
||||
*
|
||||
* @return {Void} Void
|
||||
*/
|
||||
watch() {
|
||||
|
||||
let self = this;
|
||||
|
||||
self._watcher = chokidar.watch(self._uploadsPath, {
|
||||
persistent: true,
|
||||
ignoreInitial: true,
|
||||
cwd: self._uploadsPath,
|
||||
depth: 1,
|
||||
awaitWriteFinish: true
|
||||
});
|
||||
|
||||
//-> Add new upload file
|
||||
|
||||
self._watcher.on('add', (p) => {
|
||||
|
||||
let pInfo = self.parseUploadsRelPath(p);
|
||||
return self.processFile(pInfo.folder, pInfo.filename).then((mData) => {
|
||||
return db.UplFile.findByIdAndUpdate(mData._id, mData, { upsert: true });
|
||||
}).then(() => {
|
||||
return git.commitUploads('Uploaded ' + p);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
//-> Remove upload file
|
||||
|
||||
self._watcher.on('unlink', (p) => {
|
||||
|
||||
let pInfo = self.parseUploadsRelPath(p);
|
||||
return git.commitUploads('Deleted/Renamed ' + 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 {
|
||||
_id: 'f:' + f,
|
||||
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
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Get metadata from file and generate thumbnails if necessary
|
||||
*
|
||||
* @param {String} fldName The folder name
|
||||
* @param {String} f The filename
|
||||
* @return {Promise<Object>} Promise of the file metadata
|
||||
*/
|
||||
processFile(fldName, f) {
|
||||
|
||||
let self = this;
|
||||
|
||||
let fldPath = path.join(self._uploadsPath, fldName);
|
||||
let fPath = path.join(fldPath, f);
|
||||
let fPathObj = path.parse(fPath);
|
||||
let fUid = farmhash.fingerprint32(fldName + '/' + f);
|
||||
|
||||
return fs.statAsync(fPath).then((s) => {
|
||||
|
||||
if(!s.isFile()) { return false; }
|
||||
|
||||
// Get MIME info
|
||||
|
||||
let mimeInfo = fileType(readChunk.sync(fPath, 0, 262));
|
||||
if(_.isNil(mimeInfo)) {
|
||||
mimeInfo = {
|
||||
mime: mime.lookup(fPathObj.ext) || 'application/octet-stream'
|
||||
};
|
||||
}
|
||||
|
||||
// Images
|
||||
|
||||
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((mImgData) => {
|
||||
|
||||
let cacheThumbnailPath = path.parse(path.join(self._uploadsThumbsPath, fUid + '.png'));
|
||||
let cacheThumbnailPathStr = path.format(cacheThumbnailPath);
|
||||
|
||||
let mData = {
|
||||
_id: fUid,
|
||||
category: 'image',
|
||||
mime: mimeInfo.mime,
|
||||
extra: _.pick(mImgData, ['format', 'width', 'height', 'density', 'hasAlpha', 'orientation']),
|
||||
folder: 'f:' + fldName,
|
||||
filename: f,
|
||||
basename: fPathObj.name,
|
||||
filesize: s.size
|
||||
};
|
||||
|
||||
// Generate thumbnail
|
||||
|
||||
return fs.statAsync(cacheThumbnailPathStr).then((st) => {
|
||||
return st.isFile();
|
||||
}).catch((err) => {
|
||||
return false;
|
||||
}).then((thumbExists) => {
|
||||
|
||||
return (thumbExists) ? mData : fs.ensureDirAsync(cacheThumbnailPath.dir).then(() => {
|
||||
return self.generateThumbnail(fPath, cacheThumbnailPathStr);
|
||||
}).return(mData);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Other Files
|
||||
|
||||
return {
|
||||
_id: fUid,
|
||||
category: 'binary',
|
||||
mime: mimeInfo.mime,
|
||||
folder: 'f:' + fldName,
|
||||
filename: f,
|
||||
basename: fPathObj.name,
|
||||
filesize: s.size
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate thumbnail of image
|
||||
*
|
||||
* @param {String} sourcePath The source path
|
||||
* @param {String} destPath The destination path
|
||||
* @return {Promise<Object>} Promise returning the resized image info
|
||||
*/
|
||||
generateThumbnail(sourcePath, destPath) {
|
||||
|
||||
return sharp(sourcePath)
|
||||
.withoutEnlargement()
|
||||
.resize(150,150)
|
||||
.background('white')
|
||||
.embed()
|
||||
.flatten()
|
||||
.toFormat('png')
|
||||
.toFile(destPath);
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the image metadata.
|
||||
*
|
||||
* @param {String} sourcePath The source path
|
||||
* @return {Object} The image metadata.
|
||||
*/
|
||||
getImageMetadata(sourcePath) {
|
||||
|
||||
return sharp(sourcePath).metadata();
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
_uploadsPath: './repo/uploads',
|
||||
_uploadsThumbsPath: './data/thumbs',
|
||||
|
||||
_watcher: null,
|
||||
|
||||
/**
|
||||
* Initialize Uploads model
|
||||
*
|
||||
* @return {Object} Uploads model instance
|
||||
*/
|
||||
init () {
|
||||
let self = this
|
||||
|
||||
self._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads')
|
||||
self._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs')
|
||||
|
||||
// Disable Sharp cache, as it cause file locks issues when deleting uploads.
|
||||
sharp.cache(false)
|
||||
|
||||
return self
|
||||
},
|
||||
|
||||
/**
|
||||
* Watch the uploads folder for changes
|
||||
*
|
||||
* @return {Void} Void
|
||||
*/
|
||||
watch () {
|
||||
let self = this
|
||||
|
||||
self._watcher = chokidar.watch(self._uploadsPath, {
|
||||
persistent: true,
|
||||
ignoreInitial: true,
|
||||
cwd: self._uploadsPath,
|
||||
depth: 1,
|
||||
awaitWriteFinish: true
|
||||
})
|
||||
|
||||
// -> Add new upload file
|
||||
|
||||
self._watcher.on('add', (p) => {
|
||||
let pInfo = self.parseUploadsRelPath(p)
|
||||
return self.processFile(pInfo.folder, pInfo.filename).then((mData) => {
|
||||
return db.UplFile.findByIdAndUpdate(mData._id, mData, { upsert: true })
|
||||
}).then(() => {
|
||||
return git.commitUploads('Uploaded ' + p)
|
||||
})
|
||||
})
|
||||
|
||||
// -> Remove upload file
|
||||
|
||||
self._watcher.on('unlink', (p) => {
|
||||
return git.commitUploads('Deleted/Renamed ' + 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 {
|
||||
_id: 'f:' + f,
|
||||
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
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get metadata from file and generate thumbnails if necessary
|
||||
*
|
||||
* @param {String} fldName The folder name
|
||||
* @param {String} f The filename
|
||||
* @return {Promise<Object>} Promise of the file metadata
|
||||
*/
|
||||
processFile (fldName, f) {
|
||||
let self = this
|
||||
|
||||
let fldPath = path.join(self._uploadsPath, fldName)
|
||||
let fPath = path.join(fldPath, f)
|
||||
let fPathObj = path.parse(fPath)
|
||||
let fUid = farmhash.fingerprint32(fldName + '/' + f)
|
||||
|
||||
return fs.statAsync(fPath).then((s) => {
|
||||
if (!s.isFile()) { return false }
|
||||
|
||||
// Get MIME info
|
||||
|
||||
let mimeInfo = fileType(readChunk.sync(fPath, 0, 262))
|
||||
if (_.isNil(mimeInfo)) {
|
||||
mimeInfo = {
|
||||
mime: mime.lookup(fPathObj.ext) || 'application/octet-stream'
|
||||
}
|
||||
}
|
||||
|
||||
// Images
|
||||
|
||||
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((mImgData) => {
|
||||
let cacheThumbnailPath = path.parse(path.join(self._uploadsThumbsPath, fUid + '.png'))
|
||||
let cacheThumbnailPathStr = path.format(cacheThumbnailPath)
|
||||
|
||||
let mData = {
|
||||
_id: fUid,
|
||||
category: 'image',
|
||||
mime: mimeInfo.mime,
|
||||
extra: _.pick(mImgData, ['format', 'width', 'height', 'density', 'hasAlpha', 'orientation']),
|
||||
folder: 'f:' + fldName,
|
||||
filename: f,
|
||||
basename: fPathObj.name,
|
||||
filesize: s.size
|
||||
}
|
||||
|
||||
// Generate thumbnail
|
||||
|
||||
return fs.statAsync(cacheThumbnailPathStr).then((st) => {
|
||||
return st.isFile()
|
||||
}).catch((err) => { // eslint-disable-line handle-callback-err
|
||||
return false
|
||||
}).then((thumbExists) => {
|
||||
return (thumbExists) ? mData : fs.ensureDirAsync(cacheThumbnailPath.dir).then(() => {
|
||||
return self.generateThumbnail(fPath, cacheThumbnailPathStr)
|
||||
}).return(mData)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Other Files
|
||||
|
||||
return {
|
||||
_id: fUid,
|
||||
category: 'binary',
|
||||
mime: mimeInfo.mime,
|
||||
folder: 'f:' + fldName,
|
||||
filename: f,
|
||||
basename: fPathObj.name,
|
||||
filesize: s.size
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate thumbnail of image
|
||||
*
|
||||
* @param {String} sourcePath The source path
|
||||
* @param {String} destPath The destination path
|
||||
* @return {Promise<Object>} Promise returning the resized image info
|
||||
*/
|
||||
generateThumbnail (sourcePath, destPath) {
|
||||
return sharp(sourcePath)
|
||||
.withoutEnlargement()
|
||||
.resize(150, 150)
|
||||
.background('white')
|
||||
.embed()
|
||||
.flatten()
|
||||
.toFormat('png')
|
||||
.toFile(destPath)
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the image metadata.
|
||||
*
|
||||
* @param {String} sourcePath The source path
|
||||
* @return {Object} The image metadata.
|
||||
*/
|
||||
getImageMetadata (sourcePath) {
|
||||
return sharp(sourcePath).metadata()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,308 +1,280 @@
|
||||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const path = require('path'),
|
||||
Promise = require('bluebird'),
|
||||
fs = Promise.promisifyAll(require('fs-extra')),
|
||||
multer = require('multer'),
|
||||
request = require('request'),
|
||||
url = require('url'),
|
||||
farmhash = require('farmhash'),
|
||||
_ = require('lodash');
|
||||
const path = require('path')
|
||||
const Promise = require('bluebird')
|
||||
const fs = Promise.promisifyAll(require('fs-extra'))
|
||||
const request = require('request')
|
||||
const url = require('url')
|
||||
const farmhash = require('farmhash')
|
||||
const _ = require('lodash')
|
||||
|
||||
var regFolderName = new RegExp("^[a-z0-9][a-z0-9\-]*[a-z0-9]$");
|
||||
const maxDownloadFileSize = 3145728; // 3 MB
|
||||
var regFolderName = new RegExp('^[a-z0-9][a-z0-9-]*[a-z0-9]$')
|
||||
const maxDownloadFileSize = 3145728 // 3 MB
|
||||
|
||||
/**
|
||||
* Uploads
|
||||
*/
|
||||
module.exports = {
|
||||
|
||||
_uploadsPath: './repo/uploads',
|
||||
_uploadsThumbsPath: './data/thumbs',
|
||||
|
||||
/**
|
||||
* Initialize Local Data Storage model
|
||||
*
|
||||
* @return {Object} Uploads model instance
|
||||
*/
|
||||
init() {
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the uploads folders.
|
||||
*
|
||||
* @return {Array<String>} The uploads folders.
|
||||
*/
|
||||
getUploadsFolders() {
|
||||
return db.UplFolder.find({}, 'name').sort('name').exec().then((results) => {
|
||||
return (results) ? _.map(results, 'name') : [{ name: '' }];
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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(() => {
|
||||
return db.UplFolder.findOneAndUpdate({
|
||||
_id: 'f:' + folderName
|
||||
}, {
|
||||
name: folderName
|
||||
}, {
|
||||
upsert: true
|
||||
});
|
||||
}).then(() => {
|
||||
return self.getUploadsFolders();
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if folder is valid and exists
|
||||
*
|
||||
* @param {String} folderName The folder name
|
||||
* @return {Boolean} True if valid
|
||||
*/
|
||||
validateUploadsFolder(folderName) {
|
||||
|
||||
return db.UplFolder.findOne({ name: folderName }).then((f) => {
|
||||
return (f) ? path.resolve(this._uploadsPath, folderName) : false;
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 db.UplFile.find({
|
||||
category: cat,
|
||||
folder: 'f:' + fld
|
||||
}).sort('filename').exec();
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes an uploads file.
|
||||
*
|
||||
* @param {string} uid The file unique ID
|
||||
* @return {Promise} Promise of the operation
|
||||
*/
|
||||
deleteUploadsFile(uid) {
|
||||
|
||||
let self = this;
|
||||
|
||||
return db.UplFile.findOneAndRemove({ _id: uid }).then((f) => {
|
||||
if(f) {
|
||||
return self.deleteUploadsFileTry(f, 0);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
deleteUploadsFileTry(f, attempt) {
|
||||
|
||||
let self = this;
|
||||
|
||||
let fFolder = (f.folder && f.folder !== 'f:') ? f.folder.slice(2) : './';
|
||||
|
||||
return Promise.join(
|
||||
fs.removeAsync(path.join(self._uploadsThumbsPath, f._id + '.png')),
|
||||
fs.removeAsync(path.resolve(self._uploadsPath, fFolder, f.filename))
|
||||
).catch((err) => {
|
||||
if(err.code === 'EBUSY' && attempt < 5) {
|
||||
return Promise.delay(100).then(() => {
|
||||
return self.deleteUploadsFileTry(f, attempt + 1);
|
||||
});
|
||||
} else {
|
||||
winston.warn('Unable to delete uploads file ' + f.filename + '. File is locked by another process and multiple attempts failed.');
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Downloads a file from url.
|
||||
*
|
||||
* @param {String} fFolder The folder
|
||||
* @param {String} fUrl The full URL
|
||||
* @return {Promise} Promise of the operation
|
||||
*/
|
||||
downloadFromUrl(fFolder, fUrl) {
|
||||
|
||||
let self = this;
|
||||
|
||||
let fUrlObj = url.parse(fUrl);
|
||||
let fUrlFilename = _.last(_.split(fUrlObj.pathname, '/'));
|
||||
let destFolder = _.chain(fFolder).trim().toLower().value();
|
||||
|
||||
return upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
|
||||
|
||||
if(!destFolderPath) {
|
||||
return Promise.reject(new Error('Invalid Folder'));
|
||||
}
|
||||
|
||||
return lcdata.validateUploadsFilename(fUrlFilename, destFolder).then((destFilename) => {
|
||||
|
||||
let destFilePath = path.resolve(destFolderPath, destFilename);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let rq = request({
|
||||
url: fUrl,
|
||||
method: 'GET',
|
||||
followRedirect: true,
|
||||
maxRedirects: 5,
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
let destFileStream = fs.createWriteStream(destFilePath);
|
||||
let curFileSize = 0;
|
||||
|
||||
rq.on('data', (data) => {
|
||||
curFileSize += data.length;
|
||||
if(curFileSize > maxDownloadFileSize) {
|
||||
rq.abort();
|
||||
destFileStream.destroy();
|
||||
fs.remove(destFilePath);
|
||||
reject(new Error('Remote file is too large!'));
|
||||
}
|
||||
}).on('error', (err) => {
|
||||
destFileStream.destroy();
|
||||
fs.remove(destFilePath);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
destFileStream.on('finish', () => {
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
rq.pipe(destFileStream);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Move/Rename a file
|
||||
*
|
||||
* @param {String} uid The file ID
|
||||
* @param {String} fld The destination folder
|
||||
* @param {String} nFilename The new filename (optional)
|
||||
* @return {Promise} Promise of the operation
|
||||
*/
|
||||
moveUploadsFile(uid, fld, nFilename) {
|
||||
|
||||
let self = this;
|
||||
|
||||
return db.UplFolder.findById('f:' + fld).then((folder) => {
|
||||
if(folder) {
|
||||
return db.UplFile.findById(uid).then((originFile) => {
|
||||
|
||||
//-> Check if rename is valid
|
||||
|
||||
let nameCheck = null;
|
||||
if(nFilename) {
|
||||
let originFileObj = path.parse(originFile.filename);
|
||||
nameCheck = lcdata.validateUploadsFilename(nFilename + originFileObj.ext, folder.name);
|
||||
} else {
|
||||
nameCheck = Promise.resolve(originFile.filename);
|
||||
}
|
||||
|
||||
return nameCheck.then((destFilename) => {
|
||||
|
||||
let originFolder = (originFile.folder && originFile.folder !== 'f:') ? originFile.folder.slice(2) : './';
|
||||
let sourceFilePath = path.resolve(self._uploadsPath, originFolder, originFile.filename);
|
||||
let destFilePath = path.resolve(self._uploadsPath, folder.name, destFilename);
|
||||
let preMoveOps = [];
|
||||
|
||||
//-> Check for invalid operations
|
||||
|
||||
if(sourceFilePath === destFilePath) {
|
||||
return Promise.reject(new Error('Invalid Operation!'));
|
||||
}
|
||||
|
||||
//-> Delete DB entry
|
||||
|
||||
preMoveOps.push(db.UplFile.findByIdAndRemove(uid));
|
||||
|
||||
//-> Move thumbnail ahead to avoid re-generation
|
||||
|
||||
if(originFile.category === 'image') {
|
||||
let fUid = farmhash.fingerprint32(folder.name + '/' + destFilename);
|
||||
let sourceThumbPath = path.resolve(self._uploadsThumbsPath, originFile._id + '.png');
|
||||
let destThumbPath = path.resolve(self._uploadsThumbsPath, fUid + '.png');
|
||||
preMoveOps.push(fs.moveAsync(sourceThumbPath, destThumbPath));
|
||||
} else {
|
||||
preMoveOps.push(Promise.resolve(true));
|
||||
}
|
||||
|
||||
//-> Proceed to move actual file
|
||||
|
||||
return Promise.all(preMoveOps).then(() => {
|
||||
return fs.moveAsync(sourceFilePath, destFilePath, {
|
||||
clobber: false
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
});
|
||||
} else {
|
||||
return Promise.reject(new Error('Invalid Destination Folder'));
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
_uploadsPath: './repo/uploads',
|
||||
_uploadsThumbsPath: './data/thumbs',
|
||||
|
||||
/**
|
||||
* Initialize Local Data Storage model
|
||||
*
|
||||
* @return {Object} Uploads model instance
|
||||
*/
|
||||
init () {
|
||||
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
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the uploads folders.
|
||||
*
|
||||
* @return {Array<String>} The uploads folders.
|
||||
*/
|
||||
getUploadsFolders () {
|
||||
return db.UplFolder.find({}, 'name').sort('name').exec().then((results) => {
|
||||
return (results) ? _.map(results, 'name') : [{ name: '' }]
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 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(() => {
|
||||
return db.UplFolder.findOneAndUpdate({
|
||||
_id: 'f:' + folderName
|
||||
}, {
|
||||
name: folderName
|
||||
}, {
|
||||
upsert: true
|
||||
})
|
||||
}).then(() => {
|
||||
return self.getUploadsFolders()
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if folder is valid and exists
|
||||
*
|
||||
* @param {String} folderName The folder name
|
||||
* @return {Boolean} True if valid
|
||||
*/
|
||||
validateUploadsFolder (folderName) {
|
||||
return db.UplFolder.findOne({ name: folderName }).then((f) => {
|
||||
return (f) ? path.resolve(this._uploadsPath, folderName) : false
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 db.UplFile.find({
|
||||
category: cat,
|
||||
folder: 'f:' + fld
|
||||
}).sort('filename').exec()
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes an uploads file.
|
||||
*
|
||||
* @param {string} uid The file unique ID
|
||||
* @return {Promise} Promise of the operation
|
||||
*/
|
||||
deleteUploadsFile (uid) {
|
||||
let self = this
|
||||
|
||||
return db.UplFile.findOneAndRemove({ _id: uid }).then((f) => {
|
||||
if (f) {
|
||||
return self.deleteUploadsFileTry(f, 0)
|
||||
}
|
||||
return true
|
||||
})
|
||||
},
|
||||
|
||||
deleteUploadsFileTry (f, attempt) {
|
||||
let self = this
|
||||
|
||||
let fFolder = (f.folder && f.folder !== 'f:') ? f.folder.slice(2) : './'
|
||||
|
||||
return Promise.join(
|
||||
fs.removeAsync(path.join(self._uploadsThumbsPath, f._id + '.png')),
|
||||
fs.removeAsync(path.resolve(self._uploadsPath, fFolder, f.filename))
|
||||
).catch((err) => {
|
||||
if (err.code === 'EBUSY' && attempt < 5) {
|
||||
return Promise.delay(100).then(() => {
|
||||
return self.deleteUploadsFileTry(f, attempt + 1)
|
||||
})
|
||||
} else {
|
||||
winston.warn('Unable to delete uploads file ' + f.filename + '. File is locked by another process and multiple attempts failed.')
|
||||
return true
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Downloads a file from url.
|
||||
*
|
||||
* @param {String} fFolder The folder
|
||||
* @param {String} fUrl The full URL
|
||||
* @return {Promise} Promise of the operation
|
||||
*/
|
||||
downloadFromUrl (fFolder, fUrl) {
|
||||
let fUrlObj = url.parse(fUrl)
|
||||
let fUrlFilename = _.last(_.split(fUrlObj.pathname, '/'))
|
||||
let destFolder = _.chain(fFolder).trim().toLower().value()
|
||||
|
||||
return upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
|
||||
if (!destFolderPath) {
|
||||
return Promise.reject(new Error('Invalid Folder'))
|
||||
}
|
||||
|
||||
return lcdata.validateUploadsFilename(fUrlFilename, destFolder).then((destFilename) => {
|
||||
let destFilePath = path.resolve(destFolderPath, destFilename)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let rq = request({
|
||||
url: fUrl,
|
||||
method: 'GET',
|
||||
followRedirect: true,
|
||||
maxRedirects: 5,
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
let destFileStream = fs.createWriteStream(destFilePath)
|
||||
let curFileSize = 0
|
||||
|
||||
rq.on('data', (data) => {
|
||||
curFileSize += data.length
|
||||
if (curFileSize > maxDownloadFileSize) {
|
||||
rq.abort()
|
||||
destFileStream.destroy()
|
||||
fs.remove(destFilePath)
|
||||
reject(new Error('Remote file is too large!'))
|
||||
}
|
||||
}).on('error', (err) => {
|
||||
destFileStream.destroy()
|
||||
fs.remove(destFilePath)
|
||||
reject(err)
|
||||
})
|
||||
|
||||
destFileStream.on('finish', () => {
|
||||
resolve(true)
|
||||
})
|
||||
|
||||
rq.pipe(destFileStream)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Move/Rename a file
|
||||
*
|
||||
* @param {String} uid The file ID
|
||||
* @param {String} fld The destination folder
|
||||
* @param {String} nFilename The new filename (optional)
|
||||
* @return {Promise} Promise of the operation
|
||||
*/
|
||||
moveUploadsFile (uid, fld, nFilename) {
|
||||
let self = this
|
||||
|
||||
return db.UplFolder.findById('f:' + fld).then((folder) => {
|
||||
if (folder) {
|
||||
return db.UplFile.findById(uid).then((originFile) => {
|
||||
// -> Check if rename is valid
|
||||
|
||||
let nameCheck = null
|
||||
if (nFilename) {
|
||||
let originFileObj = path.parse(originFile.filename)
|
||||
nameCheck = lcdata.validateUploadsFilename(nFilename + originFileObj.ext, folder.name)
|
||||
} else {
|
||||
nameCheck = Promise.resolve(originFile.filename)
|
||||
}
|
||||
|
||||
return nameCheck.then((destFilename) => {
|
||||
let originFolder = (originFile.folder && originFile.folder !== 'f:') ? originFile.folder.slice(2) : './'
|
||||
let sourceFilePath = path.resolve(self._uploadsPath, originFolder, originFile.filename)
|
||||
let destFilePath = path.resolve(self._uploadsPath, folder.name, destFilename)
|
||||
let preMoveOps = []
|
||||
|
||||
// -> Check for invalid operations
|
||||
|
||||
if (sourceFilePath === destFilePath) {
|
||||
return Promise.reject(new Error('Invalid Operation!'))
|
||||
}
|
||||
|
||||
// -> Delete DB entry
|
||||
|
||||
preMoveOps.push(db.UplFile.findByIdAndRemove(uid))
|
||||
|
||||
// -> Move thumbnail ahead to avoid re-generation
|
||||
|
||||
if (originFile.category === 'image') {
|
||||
let fUid = farmhash.fingerprint32(folder.name + '/' + destFilename)
|
||||
let sourceThumbPath = path.resolve(self._uploadsThumbsPath, originFile._id + '.png')
|
||||
let destThumbPath = path.resolve(self._uploadsThumbsPath, fUid + '.png')
|
||||
preMoveOps.push(fs.moveAsync(sourceThumbPath, destThumbPath))
|
||||
} else {
|
||||
preMoveOps.push(Promise.resolve(true))
|
||||
}
|
||||
|
||||
// -> Proceed to move actual file
|
||||
|
||||
return Promise.all(preMoveOps).then(() => {
|
||||
return fs.moveAsync(sourceFilePath, destFilePath, {
|
||||
clobber: false
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
} else {
|
||||
return Promise.reject(new Error('Invalid Destination Folder'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
'use strict'
|
||||
|
||||
// TODO
|
@ -1,11 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
let path = require('path'),
|
||||
fs = require('fs');
|
||||
|
||||
// ========================================
|
||||
// Load global modules
|
||||
// ========================================
|
||||
|
||||
global._ = require('lodash');
|
||||
global.winston = require('winston');
|
Loading…
Reference in new issue