Mount Manager in Superuser Panel
parent
954609108e
commit
f42f3831f9
|
|
@ -91,6 +91,8 @@ require('./libs/ffmpeg.js')(s,config,lang, async () => {
|
|||
require('./libs/auth/logins.js')(s,config,lang,app)
|
||||
//rally other Shinobi
|
||||
require('./libs/rally.js')(s,config,lang,app,io)
|
||||
//rally other Shinobi
|
||||
require('./libs/mountManager.js')(s,config,lang,app,io)
|
||||
//on-start actions, daemon(s) starter
|
||||
await require('./libs/startup.js')(s,config,lang)
|
||||
//p2p, commander
|
||||
|
|
|
|||
|
|
@ -20,6 +20,15 @@
|
|||
"Geolocation": "Geolocation",
|
||||
"Configure": "Configure",
|
||||
"Scan": "Scan",
|
||||
"See System Logs": "See System Logs",
|
||||
"Mount Manager": "Mount Manager",
|
||||
"mountManagerDescription": "Setup drive mounts here.",
|
||||
"Mount Added": "Mount Added",
|
||||
"mountAddedText": "Mount has been added to /etc/fstab. You may now use it as your default Videos directory.",
|
||||
"mountAddedBadCredentialsText": "Mount has been added to /etc/fstab but it seems credentials are incorrect.",
|
||||
"Failed to Remove Mount": "Failed to Remove Mount",
|
||||
"Failed to Add Mount": "Failed to Add Mount",
|
||||
"Add Mount": "Add Mount",
|
||||
"Rally": "Rally",
|
||||
"rallyApiKeyFieldText": "Permissions required : Get Monitors, Edit Monitors, View Streams, View Videos.",
|
||||
"rallyDescription": "Here you can connect to a separate Shinobi server and add their cameras to this Shinobi server. The connection would be directly from the separate server instead of directly from the cameras.",
|
||||
|
|
@ -533,6 +542,9 @@
|
|||
"Use Global Wasabi Hot Cloud Storage Video Storage": "Use Global Wasabi Hot Cloud Storage Video Storage",
|
||||
"Use Global Backblaze B2 Video Storage": "Use Global Backblaze B2 Video Storage",
|
||||
"Use Global WebDAV Video Storage": "Use Global WebDAV Video Storage",
|
||||
"windowsCantUseFeature": "Windows cannot use this feature",
|
||||
"Mount Point": "Mount Point",
|
||||
"Mounted Drive Storage": "Mounted Drive Storage",
|
||||
"S3-Based Network Storage": "S3-Based Network Storage",
|
||||
"Amazon S3 Upload Error": "Amazon S3 Upload Error",
|
||||
"Wasabi Hot Cloud Storage Upload Error": "Wasabi Hot Cloud Storage Upload Error",
|
||||
|
|
@ -545,7 +557,17 @@
|
|||
"URL": "URL",
|
||||
"Operating Hours": "Operating Hours",
|
||||
"Autosave": "Autosave",
|
||||
"Source": "Source",
|
||||
"Delete Mount": "Delete Mount",
|
||||
"Mount Path": "Mount Path",
|
||||
"Mount Type": "Mount Type",
|
||||
"setVideosDirWarning": "Doing this while Monitors are recording may cause issues. Stop them before making this change.",
|
||||
"Save Directory": "Save Directory",
|
||||
"Path Inside": "Path Inside",
|
||||
"Path Inside Mount": "Path Inside Mount",
|
||||
"New Videos Directory Set": "New Videos Directory Set",
|
||||
"Use Default Videos Directory": "Use Default Videos Directory",
|
||||
"Set New Videos Directory": "Set New Videos Directory?",
|
||||
"CSS": "CSS <small>Style your dashboard.</small>",
|
||||
"Don't Stretch Monitors": "Don't Stretch Monitors",
|
||||
"Force Monitors Per Row": "Force Monitors Per Row",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
const path = require('path')
|
||||
module.exports = (s,config,lang,app,io) => {
|
||||
// for unix-based systems only (has /etc/fstab)
|
||||
if(s.isWin){
|
||||
app.all([
|
||||
'list',
|
||||
'mount',
|
||||
'removeMount',
|
||||
'setVideosDir',
|
||||
].map(item => `${config.webPaths.superApiPrefix}:auth/mountManager/${item}`), function (req,res){
|
||||
s.closeJsonResponse(res, {
|
||||
ok: false,
|
||||
msg: lang.windowsCantUseFeature,
|
||||
error: lang.windowsCantUseFeature
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
const {
|
||||
modifyConfiguration,
|
||||
} = require('./system/utils.js')(config)
|
||||
const {
|
||||
mount,
|
||||
update,
|
||||
remove,
|
||||
list,
|
||||
remountAll,
|
||||
remount,
|
||||
unmount,
|
||||
createMountPoint,
|
||||
checkDiskPathExists,
|
||||
} = require('node-fstab');
|
||||
/**
|
||||
* API : Remount All in fstab
|
||||
*/
|
||||
app.get(config.webPaths.superApiPrefix+':auth/mountManager/list', function (req,res){
|
||||
s.superAuth(req.params, async (resp) => {
|
||||
const response = await list();
|
||||
s.closeJsonResponse(res, response);
|
||||
},res,req);
|
||||
});
|
||||
/**
|
||||
* API : Add Mount to fstab
|
||||
*/
|
||||
app.post(config.webPaths.superApiPrefix+':auth/mountManager/mount', function (req,res){
|
||||
s.superAuth(req.params, async (resp) => {
|
||||
const { sourceTarget, localPath, mountType, options } = req.body;
|
||||
const response = { ok: false }
|
||||
if(sourceTarget && localPath){
|
||||
try{
|
||||
const createDirResponse = await createMountPoint(localPath)
|
||||
response.createDirResponse = createDirResponse
|
||||
}catch(err){
|
||||
console.error(err)
|
||||
}
|
||||
try{
|
||||
const { exists } = await checkDiskPathExists(localPath)
|
||||
if(exists){
|
||||
const unmountResponse = await unmount(localPath)
|
||||
response.unmountResponse = unmountResponse
|
||||
}
|
||||
}catch(err){
|
||||
console.error(err)
|
||||
}
|
||||
const updateResponse = await update(sourceTarget, localPath, mountType, options);
|
||||
response.updateResponse = updateResponse
|
||||
response.ok = updateResponse.ok
|
||||
const remountResponse = await remount(localPath)
|
||||
response.remountResponse = remountResponse
|
||||
if(!remountResponse.ok){
|
||||
await remove(localPath);
|
||||
response.ok = false;
|
||||
response.error = remountResponse.error;
|
||||
}
|
||||
response.mount = {
|
||||
device: sourceTarget,
|
||||
mountPoint: localPath,
|
||||
type: mountType,
|
||||
options,
|
||||
}
|
||||
}else{
|
||||
response.error = lang['Invalid Data']
|
||||
}
|
||||
s.closeJsonResponse(res, response);
|
||||
},res,req);
|
||||
});
|
||||
/**
|
||||
* API : Remove Mount to fstab
|
||||
*/
|
||||
app.post(config.webPaths.superApiPrefix+':auth/mountManager/removeMount', function (req,res){
|
||||
s.superAuth(req.params, async (resp) => {
|
||||
const { localPath } = req.body;
|
||||
try{
|
||||
await unmount(localPath)
|
||||
if(config.videosDir.startsWith(localPath)){
|
||||
const configError = await modifyConfiguration({
|
||||
videosDir: '__DIR__/videos',
|
||||
}, true);
|
||||
}
|
||||
}catch(err){
|
||||
console.error(err)
|
||||
}
|
||||
const response = await remove(localPath);
|
||||
s.closeJsonResponse(res, response);
|
||||
},res,req);
|
||||
});
|
||||
/**
|
||||
* API : Set Mount Point as Videos Directory (videosDir)
|
||||
*/
|
||||
app.post(config.webPaths.superApiPrefix+':auth/mountManager/setVideosDir', function (req,res){
|
||||
s.superAuth(req.params, async (resp) => {
|
||||
const { localPath, pathInside } = req.body;
|
||||
const isDefaultDir = localPath === '__DIR__/videos';
|
||||
const response = { ok: false }
|
||||
try{
|
||||
const { exists } = isDefaultDir ? { exists: true } : await checkDiskPathExists(localPath)
|
||||
if(exists){
|
||||
const newVideosDirPath = pathInside ? path.join(localPath, pathInside) : localPath;
|
||||
const createDirResponse = isDefaultDir ? true : await createMountPoint(newVideosDirPath)
|
||||
const configError = await modifyConfiguration({
|
||||
videosDir: newVideosDirPath,
|
||||
}, true);
|
||||
response.ok = true;
|
||||
response.configError = configError;
|
||||
response.createDirResponse = createDirResponse;
|
||||
}
|
||||
}catch(err){
|
||||
console.error(err)
|
||||
}
|
||||
s.closeJsonResponse(res, response);
|
||||
},res,req);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
const fs = require('fs');
|
||||
const spawn = require('child_process').spawn;
|
||||
const {
|
||||
mergeDeep,
|
||||
} = require('../common.js')
|
||||
module.exports = (config) => {
|
||||
var currentlyUpdating = false
|
||||
return {
|
||||
|
|
@ -33,13 +36,20 @@ module.exports = (config) => {
|
|||
});
|
||||
});
|
||||
},
|
||||
modifyConfiguration: (postBody) => {
|
||||
modifyConfiguration: (postBody, useBase) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(config)
|
||||
|
||||
const configPath = config.thisIsDocker ? "/config/conf.json" : s.location.config;
|
||||
const configData = JSON.stringify(postBody,null,3);
|
||||
|
||||
let configToPost = postBody;
|
||||
if(useBase){
|
||||
try{
|
||||
const configBase = s.parseJSON(fs.readFileSync(configPath),{});
|
||||
configToPost = mergeDeep(configBase, postBody)
|
||||
}catch(err){
|
||||
console.error('modifyConfiguration : Failed to use Config base!')
|
||||
}
|
||||
}
|
||||
const configData = JSON.stringify(configToPost, null, 3);
|
||||
fs.writeFile(configPath, configData, resolve);
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ module.exports = function(s,config,lang,app,io){
|
|||
backblazeB2: require('./uploaders/backblazeB2.js'),
|
||||
amazonS3: require('./uploaders/amazonS3.js'),
|
||||
webdav: require('./uploaders/webdav.js'),
|
||||
//local storage
|
||||
mnt: require('./uploaders/mount.js'),
|
||||
//oauth
|
||||
googleDrive: require('./uploaders/googleDrive.js'),
|
||||
//simple storage
|
||||
|
|
@ -15,7 +17,9 @@ module.exports = function(s,config,lang,app,io){
|
|||
}
|
||||
Object.keys(loadedLibraries).forEach((key) => {
|
||||
var loadedLib = loadedLibraries[key](s,config,lang,app,io)
|
||||
loadedLib.isFormGroupGroup = true
|
||||
s.definitions["Account Settings"].blocks["Uploaders"].info.push(loadedLib)
|
||||
if(loadedLib.info){
|
||||
loadedLib.isFormGroupGroup = true
|
||||
s.definitions["Account Settings"].blocks["Uploaders"].info.push(loadedLib)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,316 @@
|
|||
const fs = require('fs').promises;
|
||||
const { createReadStream } = require('fs');
|
||||
const path = require('path');
|
||||
const {
|
||||
writeReadStream,
|
||||
checkDiskPathExists,
|
||||
} = require('node-fstab');
|
||||
module.exports = function(s,config,lang){
|
||||
if(s.isWin){
|
||||
return {}
|
||||
}
|
||||
function constructFilePath(groupKey, filePath){
|
||||
return path.join(s.group[groupKey].mnt, filePath)
|
||||
}
|
||||
const deleteObject = async (groupKey, filePath) => {
|
||||
const fullPath = constructFilePath(groupKey, filePath)
|
||||
const response = { ok: true }
|
||||
try{
|
||||
await fs.rm(fullPath)
|
||||
}catch(err){
|
||||
response.ok = false;
|
||||
response.err = err.toString();
|
||||
}
|
||||
return response
|
||||
};
|
||||
const uploadObject = async (groupKey, { filePath, readStream }) => {
|
||||
const fullPath = constructFilePath(groupKey, filePath)
|
||||
return await writeReadStream(readStream, fullPath);
|
||||
};
|
||||
const getObject = async (groupKey, filePath) => {
|
||||
const fullPath = constructFilePath(groupKey, filePath)
|
||||
return createReadStream(fullPath)
|
||||
};
|
||||
function beforeAccountSave(d){
|
||||
//d = save event
|
||||
d.formDetails.mnt_use_global = d.d.mnt_use_global
|
||||
d.formDetails.use_mnt = d.d.use_mnt
|
||||
}
|
||||
function cloudDiskUseStartup(group,userDetails){
|
||||
group.cloudDiskUse['mnt'].name = 'Mounted Drive'
|
||||
group.cloudDiskUse['mnt'].sizeLimitCheck = (userDetails.use_mnt_size_limit === '1')
|
||||
if(!userDetails.mnt_size_limit || userDetails.mnt_size_limit === ''){
|
||||
group.cloudDiskUse['mnt'].sizeLimit = 10000
|
||||
}else{
|
||||
group.cloudDiskUse['mnt'].sizeLimit = parseFloat(userDetails.mnt_size_limit)
|
||||
}
|
||||
}
|
||||
function loadGroupApp(e){
|
||||
// e = user
|
||||
var userDetails = JSON.parse(e.details)
|
||||
if(userDetails.mnt_use_global === '1' && config.cloudUploaders && config.cloudUploaders.WasabiHotCloudStorage){
|
||||
userDetails = Object.assign(userDetails,config.cloudUploaders.mountedDrive)
|
||||
}
|
||||
//Mounted Drive Storage
|
||||
if(
|
||||
!s.group[e.ke].mnt &&
|
||||
userDetails.mnt !== '0' &&
|
||||
userDetails.mnt_path &&
|
||||
userDetails.mnt_dir
|
||||
){
|
||||
checkDiskPathExists(userDetails.mnt_dir).then((response) => {
|
||||
if(response.exists){
|
||||
s.group[e.ke].mnt = userDetails.mnt_path;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
function unloadGroupApp(user){
|
||||
s.group[user.ke].mnt = null
|
||||
}
|
||||
function deleteVideo(e,video,callback){
|
||||
// e = user
|
||||
try{
|
||||
var videoDetails = JSON.parse(video.details)
|
||||
}catch(err){
|
||||
var videoDetails = video.details
|
||||
}
|
||||
if(video.type !== 'mnt'){
|
||||
callback()
|
||||
return
|
||||
}
|
||||
deleteObject(video.ke, videoDetails.location).then((response) => {
|
||||
if (response.err){
|
||||
console.error('Mounted Drive Storage DELETE Error')
|
||||
console.error(err);
|
||||
}
|
||||
callback()
|
||||
});
|
||||
}
|
||||
function uploadVideo(e,k,insertQuery){
|
||||
//e = video object
|
||||
//k = temporary values
|
||||
if(!k)k={};
|
||||
//cloud saver - Mounted Drive
|
||||
const groupKey = insertQuery.ke
|
||||
if(s.group[groupKey].mnt && s.group[groupKey].init.use_mnt !== '0' && s.group[groupKey].init.mnt_save === '1'){
|
||||
const filename = `${s.formattedTime(insertQuery.time)}.${insertQuery.ext}`
|
||||
var fileStream = createReadStream(k.dir + filename);
|
||||
var saveLocation = s.group[groupKey].init.mnt_dir+groupKey+'/'+e.mid+'/'+filename
|
||||
uploadObject(groupKey, {
|
||||
filePath: saveLocation,
|
||||
readStream: fileStream,
|
||||
}).then((response) => {
|
||||
if(response.err){
|
||||
s.userLog(e,{type:lang['Mounted Drive Storage Upload Error'],msg:response.err})
|
||||
}
|
||||
if(s.group[groupKey].init.mnt_log === '1' && response.ok){
|
||||
s.knexQuery({
|
||||
action: "insert",
|
||||
table: "Cloud Videos",
|
||||
insert: {
|
||||
mid: e.mid,
|
||||
ke: groupKey,
|
||||
ext: insertQuery.ext,
|
||||
time: insertQuery.time,
|
||||
status: 1,
|
||||
type : 'mnt',
|
||||
details: s.s({
|
||||
location : saveLocation
|
||||
}),
|
||||
size: k.filesize,
|
||||
end: k.endTime,
|
||||
href: ''
|
||||
}
|
||||
})
|
||||
s.setCloudDiskUsedForGroup(groupKey,{
|
||||
amount: k.filesizeMB,
|
||||
storageType: 'mnt'
|
||||
})
|
||||
s.purgeCloudDiskForGroup(e,'mnt')
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
function onInsertTimelapseFrame(monitorObject,queryInfo,filePath){
|
||||
var e = monitorObject
|
||||
if(s.group[e.ke].mnt && s.group[e.ke].init.use_mnt !== '0' && s.group[e.ke].init.mnt_save === '1'){
|
||||
var fileStream = createReadStream(filePath)
|
||||
fileStream.on('error', function (err) {
|
||||
console.error(err)
|
||||
})
|
||||
var saveLocation = s.group[e.ke].init.mnt_dir + e.ke + '/' + e.mid + '_timelapse/' + queryInfo.filename
|
||||
uploadObject(e.ke, {
|
||||
filePath: saveLocation,
|
||||
readStream: fileStream,
|
||||
}).then((response) => {
|
||||
if(response.err){
|
||||
s.userLog(e,{type:lang['Wasabi Hot Cloud Storage Upload Error'],msg:response.err})
|
||||
}
|
||||
if(s.group[e.ke].init.mnt_log === '1' && response.ok){
|
||||
s.knexQuery({
|
||||
action: "insert",
|
||||
table: "Cloud Timelapse Frames",
|
||||
insert: {
|
||||
mid: queryInfo.mid,
|
||||
ke: queryInfo.ke,
|
||||
time: queryInfo.time,
|
||||
filename: queryInfo.filename,
|
||||
type : 'mnt',
|
||||
details: s.s({
|
||||
location : saveLocation
|
||||
}),
|
||||
size: queryInfo.size,
|
||||
href: ''
|
||||
}
|
||||
})
|
||||
s.setCloudDiskUsedForGroup(e.ke,{
|
||||
amount : s.kilobyteToMegabyte(queryInfo.size),
|
||||
storageType : 'mnt'
|
||||
},'timelapseFrames')
|
||||
s.purgeCloudDiskForGroup(e,'mnt','timelapseFrames')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
function onDeleteTimelapseFrameFromCloud(e,frame,callback){
|
||||
// e = user
|
||||
try{
|
||||
var frameDetails = JSON.parse(frame.details)
|
||||
}catch(err){
|
||||
var frameDetails = frame.details
|
||||
}
|
||||
if(video.type !== 'mnt'){
|
||||
callback()
|
||||
return
|
||||
}
|
||||
if(!frameDetails.location){
|
||||
frameDetails.location = frame.href.split(locationUrl)[1]
|
||||
}
|
||||
deleteObject(e.ke, frameDetails.location).then((response) => {
|
||||
if (response.err){
|
||||
console.error('Mounted Drive Storage DELETE Error')
|
||||
console.error(err);
|
||||
}
|
||||
callback()
|
||||
});
|
||||
}
|
||||
async function onGetVideoData(video){
|
||||
const videoDetails = s.parseJSON(video.details)
|
||||
const saveLocation = videoDetails.location
|
||||
var fileStream = await getObject(video.ke, saveLocation);
|
||||
return fileStream
|
||||
}
|
||||
//Mounted Drive Storage
|
||||
s.addCloudUploader({
|
||||
name: 'mnt',
|
||||
loadGroupAppExtender: loadGroupApp,
|
||||
unloadGroupAppExtender: unloadGroupApp,
|
||||
insertCompletedVideoExtender: uploadVideo,
|
||||
deleteVideoFromCloudExtensions: deleteVideo,
|
||||
cloudDiskUseStartupExtensions: cloudDiskUseStartup,
|
||||
beforeAccountSave: beforeAccountSave,
|
||||
onAccountSave: cloudDiskUseStartup,
|
||||
onInsertTimelapseFrame: onInsertTimelapseFrame,
|
||||
onDeleteTimelapseFrameFromCloud: onDeleteTimelapseFrameFromCloud,
|
||||
onGetVideoData
|
||||
})
|
||||
//return fields that will appear in settings
|
||||
return {
|
||||
"evaluation": "details.use_mnt !== '0'",
|
||||
"name": lang["Mounted Drive Storage"],
|
||||
"color": "forestgreen",
|
||||
"info": [
|
||||
{
|
||||
"name": "detail=mnt_save",
|
||||
"selector":"autosave_mnt",
|
||||
"field": lang.Autosave,
|
||||
"description": "",
|
||||
"default": lang.No,
|
||||
"example": "",
|
||||
"fieldType": "select",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang.No,
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"hidden": true,
|
||||
"field": lang['Mount Point'],
|
||||
"name": "detail=mnt_path",
|
||||
"placeholder": "/mnt/yourdrive",
|
||||
"form-group-class": "autosave_mnt_input autosave_mnt_1",
|
||||
},
|
||||
{
|
||||
"hidden": true,
|
||||
"name": "detail=mnt_log",
|
||||
"field": lang['Save Links to Database'],
|
||||
"fieldType": "select",
|
||||
"selector": "h_mntsld",
|
||||
"form-group-class":"autosave_mnt_input autosave_mnt_1",
|
||||
"description": "",
|
||||
"default": "",
|
||||
"example": "",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang.No,
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"hidden": true,
|
||||
"name": "detail=use_mnt_size_limit",
|
||||
"field": lang['Use Max Storage Amount'],
|
||||
"fieldType": "select",
|
||||
"selector": "h_mntzl",
|
||||
"form-group-class":"autosave_mnt_input autosave_mnt_1",
|
||||
"form-group-class-pre-layer":"h_mntsld_input h_mntsld_1",
|
||||
"description": "",
|
||||
"default": "",
|
||||
"example": "",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang.No,
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"hidden": true,
|
||||
"name": "detail=mnt_size_limit",
|
||||
"field": lang['Max Storage Amount'],
|
||||
"form-group-class":"autosave_mnt_input autosave_mnt_1",
|
||||
"form-group-class-pre-layer":"h_mntsld_input h_mntsld_1",
|
||||
"description": "",
|
||||
"default": "10000",
|
||||
"example": "",
|
||||
"possible": ""
|
||||
},
|
||||
{
|
||||
"hidden": true,
|
||||
"name": "detail=mnt_dir",
|
||||
"field": lang['Save Directory'],
|
||||
"form-group-class":"autosave_mnt_input autosave_mnt_1",
|
||||
"description": "",
|
||||
"default": "/",
|
||||
"example": "",
|
||||
"possible": ""
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -42,6 +42,7 @@
|
|||
"mysql2": "^3.9.7",
|
||||
"node-abort-controller": "^3.0.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-fstab": "^1.0.12",
|
||||
"node-shinobi": "^1.0.4",
|
||||
"node-ssh": "^12.0.4",
|
||||
"nodemailer": "^6.7.1",
|
||||
|
|
|
|||
|
|
@ -343,6 +343,7 @@ $(document).ready(function () {
|
|||
schema: schema,
|
||||
}
|
||||
);
|
||||
window.configurationEditor = configurationEditor;
|
||||
|
||||
configurationEditor.setValue(data.config);
|
||||
|
||||
|
|
@ -404,5 +405,4 @@ $(document).ready(function () {
|
|||
}
|
||||
});
|
||||
|
||||
window.configurationEditor = configurationEditor;
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,195 @@
|
|||
$(document).ready(function(){
|
||||
const loadedMounts = {}
|
||||
const theEnclosure = $('#superMountManager')
|
||||
const theSearch = $('#mountManagerListSearch')
|
||||
const theTable = $('#mountManagerListTable tbody')
|
||||
const newMountForm = $('#mountManagerNewMount')
|
||||
function getMountId(theMount){
|
||||
return `${theMount.mountPoint.split('/').join('_')}`
|
||||
}
|
||||
function drawMountToTable(theMount){
|
||||
var html = `
|
||||
<tr row-mounted="${getMountId(theMount)}">
|
||||
<td class="align-middle">
|
||||
<div>${theMount.device}</div>
|
||||
<div><small>${theMount.mountPoint}</small></div>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
${theMount.type}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
${theMount.options}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<a class="btn btn-primary btn-sm cursor-pointer edit" title="${lang.Edit}"><i class="fa fa-pencil-square-o"></i></a>
|
||||
<a class="btn btn-success btn-sm cursor-pointer setVideosDir" title="${lang.videosDir}"><i class="fa fa-download"></i></a>
|
||||
<a class="btn btn-danger btn-sm cursor-pointer delete" title="${lang.Delete}"><i class="fa fa-trash-o"></i></a>
|
||||
</td>
|
||||
</tr>`
|
||||
theTable.append(html)
|
||||
}
|
||||
function drawMountsTable(data){
|
||||
theTable.empty()
|
||||
$.each(data,function(n,theMount){
|
||||
drawMountToTable(theMount)
|
||||
});
|
||||
}
|
||||
function filterMountsTable(theSearch = '') {
|
||||
var searchQuery = theSearch.trim().toLowerCase();
|
||||
if(searchQuery === ''){
|
||||
theTable.find(`[row-mounted]`).show()
|
||||
return;
|
||||
}
|
||||
var rows = Object.values(loadedMounts);
|
||||
var filtered = []
|
||||
rows.forEach((row) => {
|
||||
var searchInString = JSON.stringify(row).toLowerCase();
|
||||
var theElement = theTable.find(`[row-mounted="${getMountId(row)}"]`)
|
||||
if(searchInString.indexOf(searchQuery) > -1){
|
||||
theElement.show()
|
||||
}else{
|
||||
theElement.hide()
|
||||
}
|
||||
})
|
||||
return filtered
|
||||
}
|
||||
function loadMounts(callback) {
|
||||
return new Promise((resolve,reject) => {
|
||||
$.getJSON(superApiPrefix + $user.sessionKey + '/mountManager/list',function(data){
|
||||
$.each(data.mounts,function(n,theMount){
|
||||
loadedMounts[getMountId(theMount)] = theMount;
|
||||
})
|
||||
drawMountsTable(data.mounts)
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
function addMount(form) {
|
||||
return new Promise((resolve,reject) => {
|
||||
// const { sourceTarget, localPath, mountType, options } = form;
|
||||
$.post(superApiPrefix + $user.sessionKey + '/mountManager/mount', form,function(data){
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
function removeMount(localPath) {
|
||||
return new Promise((resolve,reject) => {
|
||||
$.post(superApiPrefix + $user.sessionKey + '/mountManager/removeMount',{
|
||||
localPath
|
||||
},function(data){
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
function setVideosDir(localPath, pathInside) {
|
||||
return new Promise((resolve,reject) => {
|
||||
$.post(superApiPrefix + $user.sessionKey + '/mountManager/setVideosDir',{
|
||||
localPath,
|
||||
pathInside
|
||||
},function(data){
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
function launchSetVideoDirConfirm(localPath){
|
||||
$.confirm.create({
|
||||
title: lang['Set New Videos Directory'],
|
||||
body: `<b>${lang['Mount Path']} : ${localPath}</b><br>${lang.setVideosDirWarning} ${lang.restartRequired}<br><br><input placeholder="${lang['Path Inside Mount']}" class="form-control" id="newVideosDirInnerPath">`,
|
||||
clickOptions: {
|
||||
class: 'btn-success',
|
||||
title: lang.Save,
|
||||
},
|
||||
clickCallback: async function(){
|
||||
const pathInside = $('#newVideosDirInnerPath').val().trim();
|
||||
const response = await setVideosDir(localPath, pathInside);
|
||||
if(response.ok){
|
||||
new PNotify({
|
||||
title: lang['New Videos Directory Set'],
|
||||
text: lang.restartRequired,
|
||||
type: 'success'
|
||||
})
|
||||
}else{
|
||||
new PNotify({
|
||||
title: lang['Action Failed'],
|
||||
text: lang['See System Logs'],
|
||||
type: 'danger'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
newMountForm.submit(async function(e){
|
||||
e.preventDefault();
|
||||
const form = newMountForm.serializeObject();
|
||||
$.each(form, function(key,val){form[key] = val.trim()});
|
||||
const response = await addMount(form);
|
||||
const notify = {
|
||||
title: lang['Mount Added'],
|
||||
text: lang.mountAddedText,
|
||||
type: 'success'
|
||||
}
|
||||
if(!response.ok){
|
||||
notify.title = lang['Failed to Add Mount']
|
||||
notify.text = response.error
|
||||
notify.type = 'danger'
|
||||
}else{
|
||||
const theMount = response.mount
|
||||
const mountId = getMountId(theMount);
|
||||
theTable.find(`[row-mounted="${mountId}"]`).remove()
|
||||
loadedMounts[mountId] = theMount;
|
||||
drawMountToTable(theMount);
|
||||
}
|
||||
new PNotify(notify)
|
||||
return false;
|
||||
});
|
||||
theTable.on('click','.delete', async function(e){
|
||||
const el = $(this).parents('[row-mounted]')
|
||||
const mountId = el.attr('row-mounted');
|
||||
const theMount = loadedMounts[mountId]
|
||||
const localPath = theMount.mountPoint
|
||||
$.confirm.create({
|
||||
title: lang['Delete Mount'],
|
||||
body: `<b>${lang['Mount Path']} : ${localPath} (${theMount.type})</b><br><small>${theMount.device}</small><br>${lang.setVideosDirWarning}`,
|
||||
clickOptions: {
|
||||
class: 'btn-danger',
|
||||
title: lang.Delete,
|
||||
},
|
||||
clickCallback: async function(){
|
||||
const response = await removeMount(localPath);
|
||||
if(response.ok){
|
||||
el.remove()
|
||||
}else{
|
||||
new PNotify({
|
||||
title: lang['Failed to Remove Mount'],
|
||||
text: lang['See System Logs'],
|
||||
type: 'danger'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
theTable.on('click','.edit', async function(e){
|
||||
const el = $(this).parents('[row-mounted]')
|
||||
const mountId = el.attr('row-mounted');
|
||||
const theMount = loadedMounts[mountId]
|
||||
newMountForm.find('[name="sourceTarget"]').val(theMount.device)
|
||||
newMountForm.find('[name="localPath"]').val(theMount.mountPoint)
|
||||
newMountForm.find('[name="mountType"]').val(theMount.type)
|
||||
newMountForm.find('[name="options"]').val(theMount.options)
|
||||
})
|
||||
theTable.on('click','.setVideosDir', function(e){
|
||||
const el = $(this).parents('[row-mounted]')
|
||||
const mountId = el.attr('row-mounted');
|
||||
const theMount = loadedMounts[mountId]
|
||||
const localPath = theMount.mountPoint
|
||||
launchSetVideoDirConfirm(localPath)
|
||||
})
|
||||
theEnclosure.on('click','.setDefaultVideosDir', function(e){
|
||||
launchSetVideoDirConfirm('__DIR__/videos')
|
||||
})
|
||||
theSearch.keydown(function(){
|
||||
const value = $(this).val().trim()
|
||||
filterMountsTable(value)
|
||||
})
|
||||
onInitSuccess(loadMounts)
|
||||
})
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form class="card bg-dark grey mt-1" id="mountManagerNewMount">
|
||||
<div class="card-header">
|
||||
<%- lang['Add Mount'] %>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label><%- lang.Source %></label>
|
||||
<input type="text" placeholder="//192.168.1.200/shared/folder" class="form-control" name="sourceTarget" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label><%- lang['Mount Path'] %></label>
|
||||
<input type="text" placeholder="/mnt/newmount" class="form-control" name="localPath" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label><%- lang['Mount Type'] %></label>
|
||||
<select type="text" class="form-control" name="mountType">
|
||||
<option value="cifs" selected>CIFS</option>
|
||||
<option value="nfs">NFS</option>
|
||||
<option value="ext4">ext4</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label><%- lang.Options %></label>
|
||||
<input type="text" placeholder="username=username,password=password,rw,iocharset=utf8,file_mode=0777,dir_mode=0777 0 0" value="username=username,password=password,rw,iocharset=utf8,file_mode=0777,dir_mode=0777 0 0" class="form-control" name="options" />
|
||||
</div>
|
||||
<div><button type="submit" class="btn btn-round btn-block btn-default mb-0"><i class="fa fa-check"></i> <%- lang.Save %></button></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-4 pb-0 m-0">
|
||||
<div id="mountManagerList">
|
||||
<div class="form-group">
|
||||
<a class="btn btn-block btn-info setDefaultVideosDir"><%- lang['Use Default Videos Directory'] %></a>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input id="mountManagerListSearch" type="text" placeholder="<%- lang.Search %>" class="form-control" />
|
||||
</div>
|
||||
<table class="table table-striped" id="mountManagerListTable">
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<link rel="stylesheet" href="<%-window.libURL%>assets/css/super.mountManager.css">
|
||||
<script src="<%-window.libURL%>assets/js/super.mountManager.js" type="text/javascript"></script>
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
<div class="row">
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card bg-dark grey mt-1">
|
||||
<div class="card-header">
|
||||
|
|
|
|||
|
|
@ -89,6 +89,9 @@
|
|||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" data-bs-target="#superPluginManager" role="tab"><%-lang['Plugin Manager']%></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" data-bs-target="#superMountManager" role="tab"><%-lang['Mount Manager']%></a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="card-body text-white" style="background:#263343">
|
||||
<!-- Tab panes -->
|
||||
|
|
@ -125,6 +128,9 @@
|
|||
<div class="tab-pane text-left" id="superPluginManager" role="tabpanel">
|
||||
<%- include('blocks/superPluginManager'); %>
|
||||
</div>
|
||||
<div class="tab-pane text-left" id="superMountManager" role="tabpanel">
|
||||
<%- include('blocks/superMountManager'); %>
|
||||
</div>
|
||||
<% customAutoLoad.superPageBlocks.forEach(function(block){ %>
|
||||
<%- include(block) %>
|
||||
<% }) %>
|
||||
|
|
@ -220,7 +226,10 @@ switch($user.lang){
|
|||
$.ccio.ws = io(`${location.origin.split('/super')[0]}/`,{
|
||||
path : tool.checkCorrectPathEnding(location.pathname)+'socket.io'
|
||||
});
|
||||
|
||||
const onInitSuccessExtensions = [];
|
||||
function onInitSuccess(theAction){
|
||||
onInitSuccessExtensions.push(theAction)
|
||||
}
|
||||
$.ccio.cx=function(x){return $.ccio.ws.emit('super',x)}
|
||||
$.ccio.ws.on('connect',function(d){
|
||||
$.ccio.cx({f:'init',mail:$user.mail,pass:$user.pass,machineId: `<%- config.machineId %>`})
|
||||
|
|
@ -232,6 +241,9 @@ $.ccio.ws.on('f',function(d){
|
|||
drawUserList()
|
||||
drawSystemLogs()
|
||||
drawSystemInfo()
|
||||
onInitSuccessExtensions.forEach((theAction) => {
|
||||
theAction()
|
||||
})
|
||||
break;
|
||||
case'log':
|
||||
$.ccio.tm(4,d.log,'#logs-list')
|
||||
|
|
|
|||
Loading…
Reference in New Issue