Mount Manager in Superuser Panel

merge-requests/521/merge
Moe 2024-11-02 21:50:22 +00:00
parent 954609108e
commit f42f3831f9
14 changed files with 1948 additions and 5392 deletions

View File

@ -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

View File

@ -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",

133
libs/mountManager.js Normal file
View File

@ -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);
});
}

View File

@ -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);
});
},

View File

@ -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)
}
})
}

316
libs/uploaders/mount.js Normal file
View File

@ -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": ""
},
]
}
}

6581
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

View File

@ -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;
})

View File

@ -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)
})

View File

@ -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>

View File

@ -1,5 +1,4 @@
<div class="row">
<div class="col-md-6">
<div class="card bg-dark grey mt-1">
<div class="card-header">

View File

@ -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')