mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-12 17:37:51 +08:00
🐶 Sync 2026-11-08 23:51:11
This commit is contained in:
1 parent
5a5196e2d6
commit
534c8d6ab2
73 files changed
+12872
-2580
No files matched your search
@@ -6,8 +6,17 @@
|
||||
"iwinfo": [ "assoclist" ],
|
||||
"luci": [ "getVersion" ],
|
||||
"luci-rpc": [ "getDHCPLeases" ],
|
||||
"luci.fluentdashboard": [ "load" ],
|
||||
"system": [ "board", "info" ]
|
||||
}
|
||||
}
|
||||
},
|
||||
"luci-mod-fluentdashboard-edit": {
|
||||
"description": "Grant permission to publish the shared Fluent dashboard layout",
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.fluentdashboard": [ "save" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
'use strict';
|
||||
|
||||
import { open, readfile, writefile, rename, unlink, mkdtemp, rmdir } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
|
||||
const config = '/etc/config/fluentdashboard';
|
||||
const max_size = 262144;
|
||||
function require_value(ok, message) { if (!ok) die(message); }
|
||||
function integer(v, min, max) { return type(v) == 'int' && v >= min && v <= max; }
|
||||
function id(v) { return type(v) == 'string' && match(v, /^[A-Za-z0-9_-]{1,64}$/); }
|
||||
function safe_json(v, depth) {
|
||||
if (depth > 16) return false;
|
||||
if (type(v) == 'object' || type(v) == 'array') {
|
||||
for (let k, item in v) {
|
||||
if (k in [ '__proto__', 'constructor', 'prototype' ] || !safe_json(item, depth + 1)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function validate(doc) {
|
||||
require_value(type(doc) == 'object' && doc.schemaVersion == 1 && safe_json(doc, 0), 'Invalid schema');
|
||||
require_value(integer(doc.revision, 0, 9007199254740990), 'Invalid revision');
|
||||
require_value(type(doc.dashboards) == 'array' && length(doc.dashboards) >= 1 && length(doc.dashboards) <= 16, 'Invalid dashboards');
|
||||
let dashboards = {};
|
||||
for (let dashboard in doc.dashboards) {
|
||||
require_value(type(dashboard) == 'object' && id(dashboard.id) && !exists(dashboards, dashboard.id), 'Invalid dashboard id');
|
||||
dashboards[dashboard.id] = true;
|
||||
require_value(type(dashboard.name) == 'string' && length(trim(dashboard.name)) && length(dashboard.name) <= 128, 'Invalid name');
|
||||
require_value(type(dashboard.nodes) == 'array' && length(dashboard.nodes) <= 120, 'Too many nodes');
|
||||
let nodes = {};
|
||||
for (let node in dashboard.nodes) {
|
||||
require_value(type(node) == 'object' && id(node.id) && !exists(nodes, node.id), 'Invalid node id');
|
||||
nodes[node.id] = node;
|
||||
require_value(node.kind in [ 'widget', 'group' ] && exists(node, 'parentId') && (node.parentId == null || id(node.parentId)), 'Invalid node');
|
||||
for (let flag in [ 'hidden', 'locked', 'collapsed' ]) require_value(!exists(node, flag) || type(node[flag]) == 'bool', 'Invalid flag');
|
||||
require_value(!exists(node, 'title') || (type(node.title) == 'string' && length(node.title) <= 256), 'Invalid title');
|
||||
let t = node.tile;
|
||||
require_value(type(t) == 'object' && t.size in [ 'small', 'medium', 'wide', 'large', 'xlarge', 'full', 'custom' ], 'Invalid tile');
|
||||
require_value(integer(t.x, 0, 11) && integer(t.y, 0, 10000) && integer(t.w, 1, 12) && integer(t.h, 1, 48) && integer(t.order, 0, 100000), 'Invalid coordinates');
|
||||
if (node.kind == 'widget')
|
||||
require_value(type(node.widgetType) == 'string' && match(node.widgetType, /^[A-Za-z0-9_.-]{1,128}$/) && integer(node.widgetVersion, 1, 100000) && type(node.config) == 'object', 'Invalid widget');
|
||||
else {
|
||||
require_value(type(node.title) == 'string', 'Invalid group title');
|
||||
require_value(!exists(node, 'description') || (type(node.description) == 'string' && length(node.description) <= 2048), 'Invalid description');
|
||||
}
|
||||
}
|
||||
for (let node in dashboard.nodes) {
|
||||
let seen = {}; seen[node.id] = true;
|
||||
let parent = node.parentId, depth = node.kind == 'group' ? 1 : 0;
|
||||
while (parent != null) {
|
||||
require_value(!exists(seen, parent) && nodes[parent]?.kind == 'group', 'Invalid parent graph');
|
||||
seen[parent] = true;
|
||||
require_value(++depth <= 2, 'Too many group levels');
|
||||
parent = nodes[parent].parentId;
|
||||
}
|
||||
require_value(node.tile.x + node.tile.w <= (node.parentId == null ? 12 : nodes[node.parentId].tile.w), 'Tile outside parent');
|
||||
}
|
||||
}
|
||||
require_value(type(doc.defaultDashboardId) == 'string' && exists(dashboards, doc.defaultDashboardId), 'Invalid default dashboard');
|
||||
require_value(!exists(doc, 'meta') || type(doc.meta) == 'object', 'Invalid metadata');
|
||||
}
|
||||
|
||||
function load_document() {
|
||||
const uci = cursor('/etc/config', '/var/lock', '');
|
||||
require_value(uci.load('fluentdashboard'), 'Cannot read dashboard configuration');
|
||||
let raw = uci.get('fluentdashboard', 'global', 'layout');
|
||||
let revision = int(uci.get('fluentdashboard', 'global', 'revision'));
|
||||
require_value(integer(revision, 0, 9007199254740990), 'Invalid stored revision');
|
||||
if (!raw && revision == 0) return { document: null, revision: 0 };
|
||||
require_value(type(raw) == 'string' && length(raw) <= max_size, 'Invalid stored layout');
|
||||
let doc = json(raw);
|
||||
validate(doc);
|
||||
require_value(doc.revision == revision, 'Stored revision mismatch');
|
||||
return { document: doc, revision };
|
||||
}
|
||||
|
||||
function save_document(args) {
|
||||
require_value(type(args.layout) == 'string' && length(args.layout) <= max_size, 'Layout too large');
|
||||
let doc = json(args.layout);
|
||||
validate(doc);
|
||||
require_value(integer(args.expectedRevision, 0, 9007199254740989) && doc.revision == args.expectedRevision, 'Invalid expected revision');
|
||||
const current = load_document();
|
||||
if (current.revision != args.expectedRevision) return { error: 'conflict', revision: current.revision };
|
||||
doc.revision = current.revision + 1;
|
||||
// Isolated UCI cursor: no global /tmp/.uci deltas, commit then atomic same-filesystem rename.
|
||||
const staging = mkdtemp('/etc/config/.fluentdashboard-XXXXXX');
|
||||
require_value(staging, 'Cannot create staging directory');
|
||||
let result;
|
||||
try {
|
||||
const old = readfile(config);
|
||||
require_value(old != null && writefile(staging + '/fluentdashboard', old) == length(old), 'Cannot stage config');
|
||||
const uci = cursor(staging, staging, '');
|
||||
require_value(uci.load('fluentdashboard'), 'Cannot load staged config');
|
||||
require_value(uci.set('fluentdashboard', 'global', 'layout', sprintf('%J', doc)) && uci.set('fluentdashboard', 'global', 'revision', sprintf('%d', doc.revision)) && uci.set('fluentdashboard', 'global', 'schema_version', '1'), 'Cannot set config');
|
||||
require_value(uci.commit('fluentdashboard'), 'Cannot commit config');
|
||||
require_value(writefile(staging + '/previous', old) == length(old) && rename(staging + '/previous', config + '.previous'), 'Cannot preserve previous config');
|
||||
require_value(rename(staging + '/fluentdashboard', config), 'Cannot publish config');
|
||||
result = { document: doc, revision: doc.revision };
|
||||
} catch (error) { result = { error: 'storage', message: `${error}` }; }
|
||||
unlink(staging + '/fluentdashboard'); unlink(staging + '/previous'); rmdir(staging);
|
||||
return result;
|
||||
}
|
||||
|
||||
return {
|
||||
'luci.fluentdashboard': {
|
||||
load: { call: function() { try { return load_document(); } catch (error) { return { error: 'corrupt', message: `${error}` }; } } },
|
||||
save: {
|
||||
args: { layout: '', expectedRevision: 0 },
|
||||
call: function(request) {
|
||||
const lock = open('/var/lock/fluentdashboard.lock', 'a', 0600);
|
||||
if (!lock) return { error: 'storage', message: 'Cannot open lock' };
|
||||
if (!lock.lock('xn')) { lock.close(); return { error: 'busy', message: 'Another save is in progress' }; }
|
||||
let result;
|
||||
try { result = save_document(request.args); } catch (error) { result = { error: 'invalid', message: `${error}` }; }
|
||||
lock.close();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user