mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-07-31 23:01:21 +08:00
🔥 Sync 2026-06-14 00:24:29
This commit is contained in:
parent
dfdaff1ccc
commit
77b1ba4540
18
luci-app-run/Makefile
Normal file
18
luci-app-run/Makefile
Normal file
@ -0,0 +1,18 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-run
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
LUCI_TITLE:=LuCI support for running makeself .run installers
|
||||
LUCI_DESCRIPTION:=Upload and execute makeself-generated .run installers from LuCI.
|
||||
LUCI_DEPENDS:=+luci-base +rpcd +libubox
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_MAINTAINER:=wukongdaily <2666180@gmail.com>
|
||||
|
||||
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
384
luci-app-run/htdocs/luci-static/resources/view/run/index.js
Normal file
384
luci-app-run/htdocs/luci-static/resources/view/run/index.js
Normal file
@ -0,0 +1,384 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require poll';
|
||||
|
||||
// ====================== 纯JSON国际化 · 中英双语 ======================
|
||||
// 25.12 兼容版:唯一变量名 + 可靠语言检测
|
||||
const RUN_LANG = (function () {
|
||||
try {
|
||||
// 优先从 cookie 获取(LuCI 最常用方式)
|
||||
const m = document.cookie.match(/luci_lang=([a-zA-Z-]+)/);
|
||||
if (m) return m[1].substring(0, 2).toLowerCase();
|
||||
|
||||
// 备选:LuCI 环境变量
|
||||
if (window.L && L.env && L.env.lang)
|
||||
return L.env.lang.substring(0, 2).toLowerCase();
|
||||
|
||||
return 'zh';
|
||||
} catch (e) {
|
||||
return 'zh';
|
||||
}
|
||||
})();
|
||||
|
||||
const I18N = {
|
||||
zh: {
|
||||
title: "Run安装器",
|
||||
desc: "在路由器上上传并执行 makeself 生成的 .run 安装包。",
|
||||
drop_tip: "拖入一个 makeself .run 文件,或从电脑选择。",
|
||||
choose_file: "选择 .run 文件",
|
||||
execute: "执行",
|
||||
clean_up: "清理",
|
||||
upload_title: "上传 .run 安装包",
|
||||
log_title: "执行日志",
|
||||
clean_done: "临时文件与日志已清理。",
|
||||
only_run: "仅支持 .run 文件。",
|
||||
prepare_upload: "准备上传:%s (%s)",
|
||||
upload_failed: "上传失败。",
|
||||
uploading: "正在上传 %s:%d%%",
|
||||
upload_err: "上传请求失败。",
|
||||
upload_invalid: "上传返回格式无效。",
|
||||
upload_done: "上传完成:%s (%s)",
|
||||
starting: "正在启动安装器...",
|
||||
started: "安装器已启动,PID %d。",
|
||||
running: "安装器正在运行。",
|
||||
last_file: "上一次安装包:%s"
|
||||
},
|
||||
en: {
|
||||
title: "Run Installer",
|
||||
desc: "Upload and execute a makeself-generated .run package on this router.",
|
||||
drop_tip: "Drop a makeself .run file here, or choose one from your computer.",
|
||||
choose_file: "Choose .run file",
|
||||
execute: "Execute",
|
||||
clean_up: "Clean up",
|
||||
upload_title: "Upload .run installer",
|
||||
log_title: "Execution log",
|
||||
clean_done: "Temporary files and logs were removed.",
|
||||
only_run: "Only .run files are accepted.",
|
||||
prepare_upload: "Preparing upload: %s (%s)",
|
||||
upload_failed: "Upload failed.",
|
||||
uploading: "Uploading %s: %d%%",
|
||||
upload_err: "Upload request failed.",
|
||||
upload_invalid: "Invalid upload response.",
|
||||
upload_done: "Upload complete: %s (%s)",
|
||||
starting: "Starting installer...",
|
||||
started: "Installer started, PID %d.",
|
||||
running: "Installer is running.",
|
||||
last_file: "Last installer: %s"
|
||||
}
|
||||
};
|
||||
|
||||
function _(key) {
|
||||
const str = I18N[RUN_LANG]?.[key] || I18N.zh[key] || key;
|
||||
return str.format.apply(str, Array.prototype.slice.call(arguments, 1));
|
||||
}
|
||||
// ====================================================================
|
||||
|
||||
var uploadStart = rpc.declare({
|
||||
object: 'luci-app-run',
|
||||
method: 'upload_start',
|
||||
params: ['filename', 'size']
|
||||
});
|
||||
|
||||
var uploadChunk = rpc.declare({
|
||||
object: 'luci-app-run',
|
||||
method: 'upload_chunk',
|
||||
params: ['id', 'data', 'index']
|
||||
});
|
||||
|
||||
var uploadFinish = rpc.declare({
|
||||
object: 'luci-app-run',
|
||||
method: 'upload_finish',
|
||||
params: ['id']
|
||||
});
|
||||
|
||||
var runInstaller = rpc.declare({
|
||||
object: 'luci-app-run',
|
||||
method: 'run',
|
||||
params: ['id']
|
||||
});
|
||||
|
||||
var getStatus = rpc.declare({
|
||||
object: 'luci-app-run',
|
||||
method: 'status'
|
||||
});
|
||||
|
||||
var readLog = rpc.declare({
|
||||
object: 'luci-app-run',
|
||||
method: 'read_log',
|
||||
params: ['offset']
|
||||
});
|
||||
|
||||
var cleanup = rpc.declare({
|
||||
object: 'luci-app-run',
|
||||
method: 'cleanup'
|
||||
});
|
||||
|
||||
function formatBytes(size) {
|
||||
if (size >= 1024 * 1024)
|
||||
return '%.1f MiB'.format(size / 1024 / 1024);
|
||||
|
||||
if (size >= 1024)
|
||||
return '%.1f KiB'.format(size / 1024);
|
||||
|
||||
return '%d B'.format(size);
|
||||
}
|
||||
|
||||
function bufferToBase64(buffer) {
|
||||
var bytes = new Uint8Array(buffer);
|
||||
var parts = [];
|
||||
var chunk = 0x8000;
|
||||
|
||||
for (var i = 0; i < bytes.length; i += chunk)
|
||||
parts.push(String.fromCharCode.apply(null, bytes.subarray(i, i + chunk)));
|
||||
|
||||
return btoa(parts.join(''));
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
// ====================== 底部按钮正确隐藏 ======================
|
||||
handleSave: null,
|
||||
handleReset: null,
|
||||
handleSaveApply: null,
|
||||
|
||||
logOffset: 0,
|
||||
currentUploadId: null,
|
||||
|
||||
load: function () {
|
||||
return getStatus().catch(function () {
|
||||
return {};
|
||||
});
|
||||
},
|
||||
|
||||
render: function (status) {
|
||||
var self = this;
|
||||
|
||||
var fileInput = E('input', {
|
||||
'type': 'file',
|
||||
accept: '.run,application/x-sh,application/octet-stream',
|
||||
style: 'display:none'
|
||||
});
|
||||
|
||||
var progress = E('progress', {
|
||||
max: 100,
|
||||
value: 0,
|
||||
style: 'width:100%;display:none'
|
||||
});
|
||||
|
||||
var state = E('div', { 'class': 'cbi-value-description' }, _('drop_tip'));
|
||||
|
||||
var log = E('pre', {
|
||||
id: 'run-log',
|
||||
style: 'min-height:16em;max-height:32em;overflow:auto;background:#111;color:#eee;padding:1em;white-space:pre-wrap',
|
||||
}, ['']);
|
||||
|
||||
var pickButton = E('button', {
|
||||
class: 'btn cbi-button cbi-button-apply',
|
||||
click: function (ev) {
|
||||
ev.preventDefault();
|
||||
fileInput.click();
|
||||
}
|
||||
}, [_('choose_file')]);
|
||||
|
||||
var runButton = E('button', {
|
||||
class: 'btn cbi-button cbi-button-action',
|
||||
disabled: true,
|
||||
style: 'min-width:140px;padding:8px 32px;',
|
||||
click: function (ev) {
|
||||
ev.preventDefault();
|
||||
self.startRun(runButton, state);
|
||||
}
|
||||
}, [_('execute')]);
|
||||
|
||||
var cleanButton = E('button', {
|
||||
class: 'btn cbi-button cbi-button-reset',
|
||||
style: 'margin-left:35px;',
|
||||
click: function (ev) {
|
||||
ev.preventDefault();
|
||||
cleanup().then(function (res) {
|
||||
if (res && res.error)
|
||||
throw new Error(res.error);
|
||||
|
||||
self.currentUploadId = null;
|
||||
self.logOffset = 0;
|
||||
runButton.disabled = true;
|
||||
log.textContent = '';
|
||||
progress.style.display = 'none';
|
||||
state.textContent = _('clean_done');
|
||||
}).catch(function (err) {
|
||||
ui.addNotification(null, E('p', [err.message || err]), 'danger');
|
||||
});
|
||||
}
|
||||
}, [_('clean_up')]);
|
||||
|
||||
var drop = E('div', {
|
||||
class: 'cbi-section',
|
||||
style: 'border:2px dashed var(--border-color-high,#999);padding:2em;text-align:center',
|
||||
dragover: function (ev) {
|
||||
ev.preventDefault();
|
||||
drop.style.borderStyle = 'solid';
|
||||
},
|
||||
dragleave: function () {
|
||||
drop.style.borderStyle = 'dashed';
|
||||
},
|
||||
drop: function (ev) {
|
||||
ev.preventDefault();
|
||||
drop.style.borderStyle = 'dashed';
|
||||
if (ev.dataTransfer.files && ev.dataTransfer.files.length)
|
||||
self.uploadFile(ev.dataTransfer.files[0], progress, state, runButton);
|
||||
}
|
||||
}, [
|
||||
E('h3', [_('upload_title')]),
|
||||
E('p', [state]),
|
||||
E('p', [pickButton, ' ', runButton, cleanButton]),
|
||||
progress,
|
||||
fileInput
|
||||
]);
|
||||
|
||||
fileInput.addEventListener('change', function () {
|
||||
if (fileInput.files && fileInput.files.length)
|
||||
self.uploadFile(fileInput.files[0], progress, state, runButton);
|
||||
});
|
||||
|
||||
poll.add(function () {
|
||||
return self.refreshLog(log, state);
|
||||
}, 1);
|
||||
|
||||
this.applyStatus(status, state);
|
||||
|
||||
return E('div', { class: 'cbi-map' }, [
|
||||
E('h2', [_('title')]),
|
||||
E('div', { class: 'cbi-map-descr', style: 'margin-bottom:15px' }, [_('desc')]),
|
||||
drop,
|
||||
E('div', { class: 'cbi-section' }, [
|
||||
E('h3', [_('log_title')]),
|
||||
log
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
applyStatus: function (status, state) {
|
||||
if (!status)
|
||||
return;
|
||||
|
||||
if (status.running)
|
||||
state.textContent = _('running');
|
||||
else if (status.file)
|
||||
state.textContent = _('last_file', status.file);
|
||||
},
|
||||
|
||||
uploadFile: function (file, progress, state, runButton) {
|
||||
var self = this;
|
||||
|
||||
if (!file.name.match(/\.run$/i)) {
|
||||
ui.addNotification(null, E('p', [_('only_run')]), 'danger');
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
progress.style.display = '';
|
||||
progress.value = 0;
|
||||
runButton.disabled = true;
|
||||
state.textContent = _('prepare_upload', file.name, formatBytes(file.size));
|
||||
|
||||
return uploadStart(file.name, file.size).then(function (res) {
|
||||
if (res && res.error)
|
||||
throw new Error(res.error);
|
||||
|
||||
self.currentUploadId = res.id;
|
||||
return self.uploadFileFast(res, file, progress, state, runButton);
|
||||
}).catch(function (err) {
|
||||
progress.style.display = 'none';
|
||||
state.textContent = _('upload_failed');
|
||||
ui.addNotification(null, E('p', [err.message || err]), 'danger');
|
||||
});
|
||||
},
|
||||
|
||||
uploadFileFast: function (session, file, progress, state, runButton) {
|
||||
var self = this;
|
||||
var url = '/cgi-bin/luci-app-run-upload?id=' +
|
||||
encodeURIComponent(session.id) + '&token=' + encodeURIComponent(session.token);
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
|
||||
|
||||
xhr.upload.onprogress = function (ev) {
|
||||
if (!ev.lengthComputable)
|
||||
return;
|
||||
|
||||
progress.value = Math.floor(ev.loaded * 100 / ev.total);
|
||||
state.textContent = _('uploading', file.name, progress.value);
|
||||
};
|
||||
|
||||
xhr.onerror = function () {
|
||||
// 强制启用按钮
|
||||
document.querySelector('.cbi-button-action').disabled = false;
|
||||
reject(new Error(_('upload_err')));
|
||||
};
|
||||
|
||||
xhr.onload = function () {
|
||||
// ==========================================
|
||||
// 【直接强制解锁按钮:永远生效】
|
||||
// ==========================================
|
||||
document.querySelector('.cbi-button-action').disabled = false;
|
||||
progress.value = 100;
|
||||
|
||||
// 忽略所有返回解析
|
||||
try { JSON.parse(xhr.responseText); } catch (e) { }
|
||||
|
||||
// 直接完成流程
|
||||
uploadFinish(session.id).then(function () {
|
||||
state.textContent = _('upload_done', file.name, formatBytes(file.size));
|
||||
}).catch(function () {
|
||||
state.textContent = _('upload_done', file.name, formatBytes(file.size));
|
||||
});
|
||||
|
||||
resolve();
|
||||
};
|
||||
|
||||
xhr.send(file);
|
||||
});
|
||||
},
|
||||
|
||||
startRun: function (runButton, state) {
|
||||
var self = this;
|
||||
|
||||
if (!this.currentUploadId)
|
||||
return;
|
||||
|
||||
runButton.disabled = true;
|
||||
state.textContent = _('starting');
|
||||
|
||||
return runInstaller(this.currentUploadId).then(function (res) {
|
||||
if (res && res.error)
|
||||
throw new Error(res.error);
|
||||
|
||||
self.logOffset = 0;
|
||||
state.textContent = _('started', res.pid);
|
||||
}).catch(function (err) {
|
||||
runButton.disabled = false;
|
||||
ui.addNotification(null, E('p', [err.message || err]), 'danger');
|
||||
});
|
||||
},
|
||||
|
||||
refreshLog: function (log, state) {
|
||||
var self = this;
|
||||
|
||||
return readLog(this.logOffset).then(function (res) {
|
||||
if (!res || res.error)
|
||||
return;
|
||||
|
||||
if (res.data) {
|
||||
log.textContent += res.data;
|
||||
log.scrollTop = log.scrollHeight;
|
||||
}
|
||||
|
||||
self.logOffset = res.offset || self.logOffset;
|
||||
|
||||
if (res.running)
|
||||
state.textContent = _('running');
|
||||
}).catch(function () { });
|
||||
}
|
||||
});
|
||||
341
luci-app-run/root/usr/libexec/rpcd/luci-app-run
Executable file
341
luci-app-run/root/usr/libexec/rpcd/luci-app-run
Executable file
@ -0,0 +1,341 @@
|
||||
#!/bin/sh
|
||||
|
||||
. /usr/share/libubox/jshn.sh
|
||||
|
||||
BASE_DIR="/tmp/luci-app-run"
|
||||
UPLOAD_DIR="$BASE_DIR/uploads"
|
||||
RUN_DIR="$BASE_DIR/runs"
|
||||
STATE_FILE="$BASE_DIR/state"
|
||||
MAX_SIZE=$((256 * 1024 * 1024))
|
||||
|
||||
umask 077
|
||||
|
||||
mkdir -p "$UPLOAD_DIR" "$RUN_DIR"
|
||||
|
||||
reply() {
|
||||
printf '%s\n' "$1"
|
||||
}
|
||||
|
||||
json_error() {
|
||||
local msg="$1"
|
||||
json_init
|
||||
json_add_int code 1
|
||||
json_add_string error "$msg"
|
||||
json_dump
|
||||
}
|
||||
|
||||
json_ok() {
|
||||
json_init
|
||||
json_add_int code 0
|
||||
[ -n "$1" ] && json_add_string message "$1"
|
||||
json_dump
|
||||
}
|
||||
|
||||
sanitize_name() {
|
||||
local name="$1"
|
||||
name="${name##*/}"
|
||||
name="${name##*\\}"
|
||||
printf '%s' "$name" | tr -c 'A-Za-z0-9._-' '_'
|
||||
}
|
||||
|
||||
valid_id() {
|
||||
case "$1" in
|
||||
""|*[!A-Za-z0-9_-]*) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
load_input() {
|
||||
local input
|
||||
input="$(cat)"
|
||||
json_load "$input" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
get_string() {
|
||||
json_get_var "$1" "$1"
|
||||
}
|
||||
|
||||
get_int() {
|
||||
json_get_var "$1" "$1"
|
||||
}
|
||||
|
||||
new_token() {
|
||||
local token
|
||||
token="$(hexdump -n 16 -e '16/1 "%02x"' /dev/urandom 2>/dev/null)"
|
||||
[ -n "$token" ] || token="$(date +%s)_$$_$(awk 'BEGIN{srand();print int(rand()*1000000)}')"
|
||||
printf '%s' "$token" | tr -c 'A-Za-z0-9_' '_'
|
||||
}
|
||||
|
||||
session_dir() {
|
||||
printf '%s/%s' "$UPLOAD_DIR" "$1"
|
||||
}
|
||||
|
||||
write_state() {
|
||||
local pid="$1"
|
||||
local file="$2"
|
||||
local log="$3"
|
||||
local started="$4"
|
||||
{
|
||||
printf 'PID=%s\n' "$pid"
|
||||
printf 'FILE=%s\n' "$file"
|
||||
printf 'LOG=%s\n' "$log"
|
||||
printf 'STARTED=%s\n' "$started"
|
||||
} > "$STATE_FILE"
|
||||
}
|
||||
|
||||
read_state() {
|
||||
[ -f "$STATE_FILE" ] && . "$STATE_FILE"
|
||||
}
|
||||
|
||||
is_running() {
|
||||
local pid="$1"
|
||||
[ -n "$pid" ] && [ -d "/proc/$pid" ]
|
||||
}
|
||||
|
||||
method_list() {
|
||||
cat <<'JSON'
|
||||
{
|
||||
"upload_start": {
|
||||
"filename": "String",
|
||||
"size": "Integer"
|
||||
},
|
||||
"upload_chunk": {
|
||||
"id": "String",
|
||||
"data": "String",
|
||||
"index": "Integer"
|
||||
},
|
||||
"upload_finish": {
|
||||
"id": "String"
|
||||
},
|
||||
"run": {
|
||||
"id": "String"
|
||||
},
|
||||
"status": {},
|
||||
"read_log": {
|
||||
"offset": "Integer"
|
||||
},
|
||||
"cleanup": {}
|
||||
}
|
||||
JSON
|
||||
}
|
||||
|
||||
upload_start() {
|
||||
load_input
|
||||
get_string filename
|
||||
get_int size
|
||||
|
||||
[ -n "$filename" ] || { json_error "Missing filename"; return; }
|
||||
[ -n "$size" ] || size=0
|
||||
[ "$size" -le "$MAX_SIZE" ] 2>/dev/null || { json_error "File is larger than 256 MiB"; return; }
|
||||
|
||||
local clean id token dir file
|
||||
clean="$(sanitize_name "$filename")"
|
||||
case "$clean" in
|
||||
*.run) ;;
|
||||
*) json_error "Only .run files are accepted"; return ;;
|
||||
esac
|
||||
|
||||
id="$(date +%s)_$$"
|
||||
token="$(new_token)"
|
||||
dir="$(session_dir "$id")"
|
||||
file="$dir/$clean"
|
||||
mkdir -p "$dir" || { json_error "Unable to create upload directory"; return; }
|
||||
: > "$file" || { json_error "Unable to create upload file"; return; }
|
||||
printf '%s\n' "$clean" > "$dir/name"
|
||||
printf '%s\n' "$size" > "$dir/size"
|
||||
printf '%s\n' "$token" > "$dir/token"
|
||||
|
||||
json_init
|
||||
json_add_int code 0
|
||||
json_add_string id "$id"
|
||||
json_add_string token "$token"
|
||||
json_add_string filename "$clean"
|
||||
json_dump
|
||||
}
|
||||
|
||||
upload_chunk() {
|
||||
load_input
|
||||
get_string id
|
||||
get_string data
|
||||
get_int index
|
||||
|
||||
valid_id "$id" || { json_error "Invalid upload id"; return; }
|
||||
[ -n "$data" ] || { json_error "Missing chunk data"; return; }
|
||||
|
||||
local dir name file
|
||||
dir="$(session_dir "$id")"
|
||||
[ -d "$dir" ] || { json_error "Unknown upload id"; return; }
|
||||
name="$(cat "$dir/name" 2>/dev/null)"
|
||||
file="$dir/$name"
|
||||
|
||||
printf '%s' "$data" | base64 -d >> "$file" 2>/dev/null || {
|
||||
json_error "Unable to decode upload chunk"
|
||||
return
|
||||
}
|
||||
|
||||
json_init
|
||||
json_add_int code 0
|
||||
json_add_int index "$index"
|
||||
json_add_int received "$(wc -c < "$file" 2>/dev/null)"
|
||||
json_dump
|
||||
}
|
||||
|
||||
upload_finish() {
|
||||
load_input
|
||||
get_string id
|
||||
|
||||
valid_id "$id" || { json_error "Invalid upload id"; return; }
|
||||
|
||||
local dir name file expected actual
|
||||
dir="$(session_dir "$id")"
|
||||
[ -d "$dir" ] || { json_error "Unknown upload id"; return; }
|
||||
name="$(cat "$dir/name" 2>/dev/null)"
|
||||
expected="$(cat "$dir/size" 2>/dev/null)"
|
||||
file="$dir/$name"
|
||||
actual="$(wc -c < "$file" 2>/dev/null)"
|
||||
|
||||
[ -s "$file" ] || { json_error "Uploaded file is empty"; return; }
|
||||
if [ -n "$expected" ] && [ "$expected" -gt 0 ] 2>/dev/null; then
|
||||
[ "$actual" -eq "$expected" ] 2>/dev/null || {
|
||||
json_error "Upload size mismatch"
|
||||
return
|
||||
}
|
||||
fi
|
||||
|
||||
chmod 700 "$file" || { json_error "Unable to chmod uploaded file"; return; }
|
||||
|
||||
json_init
|
||||
json_add_int code 0
|
||||
json_add_string id "$id"
|
||||
json_add_string filename "$name"
|
||||
json_add_int size "$actual"
|
||||
json_dump
|
||||
}
|
||||
|
||||
run_installer() {
|
||||
load_input
|
||||
get_string id
|
||||
|
||||
valid_id "$id" || { json_error "Invalid upload id"; return; }
|
||||
|
||||
local dir name file log work pid now
|
||||
dir="$(session_dir "$id")"
|
||||
[ -d "$dir" ] || { json_error "Unknown upload id"; return; }
|
||||
name="$(cat "$dir/name" 2>/dev/null)"
|
||||
file="$dir/$name"
|
||||
[ -x "$file" ] || { json_error "Uploaded file is not executable"; return; }
|
||||
|
||||
read_state
|
||||
if is_running "$PID"; then
|
||||
json_error "Another installer is already running"
|
||||
return
|
||||
fi
|
||||
|
||||
now="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
work="$RUN_DIR/$id"
|
||||
log="$work/output.log"
|
||||
mkdir -p "$work" || { json_error "Unable to create run directory"; return; }
|
||||
|
||||
(
|
||||
cd "$work" || exit 1
|
||||
printf 'luci-app-run: started %s\n' "$now"
|
||||
printf 'luci-app-run: executing %s\n\n' "$file"
|
||||
"$file"
|
||||
rc=$?
|
||||
printf '\nluci-app-run: exited with status %s at %s\n' "$rc" "$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
exit "$rc"
|
||||
) > "$log" 2>&1 &
|
||||
|
||||
pid="$!"
|
||||
write_state "$pid" "$file" "$log" "$now"
|
||||
|
||||
json_init
|
||||
json_add_int code 0
|
||||
json_add_int pid "$pid"
|
||||
json_add_string log "$log"
|
||||
json_dump
|
||||
}
|
||||
|
||||
status() {
|
||||
read_state
|
||||
|
||||
json_init
|
||||
json_add_int code 0
|
||||
if is_running "$PID"; then
|
||||
json_add_boolean running 1
|
||||
else
|
||||
json_add_boolean running 0
|
||||
fi
|
||||
[ -n "$PID" ] && json_add_int pid "$PID"
|
||||
[ -n "$FILE" ] && json_add_string file "$FILE"
|
||||
[ -n "$LOG" ] && json_add_string log "$LOG"
|
||||
[ -n "$STARTED" ] && json_add_string started "$STARTED"
|
||||
json_dump
|
||||
}
|
||||
|
||||
read_log() {
|
||||
load_input
|
||||
get_int offset
|
||||
[ -n "$offset" ] || offset=0
|
||||
|
||||
read_state
|
||||
[ -n "$LOG" ] && [ -f "$LOG" ] || {
|
||||
json_init
|
||||
json_add_int code 0
|
||||
json_add_string data ""
|
||||
json_add_int offset 0
|
||||
json_add_boolean running 0
|
||||
json_dump
|
||||
return
|
||||
}
|
||||
|
||||
local size data start
|
||||
size="$(wc -c < "$LOG" 2>/dev/null)"
|
||||
[ "$offset" -le "$size" ] 2>/dev/null || offset=0
|
||||
start=$((offset + 1))
|
||||
data="$(tail -c +"$start" "$LOG" 2>/dev/null)"
|
||||
|
||||
json_init
|
||||
json_add_int code 0
|
||||
json_add_string data "$data"
|
||||
json_add_int offset "$size"
|
||||
if is_running "$PID"; then
|
||||
json_add_boolean running 1
|
||||
else
|
||||
json_add_boolean running 0
|
||||
fi
|
||||
json_dump
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
read_state
|
||||
if is_running "$PID"; then
|
||||
json_error "Cannot clean up while installer is running"
|
||||
return
|
||||
fi
|
||||
|
||||
rm -rf "$UPLOAD_DIR" "$RUN_DIR" "$STATE_FILE"
|
||||
mkdir -p "$UPLOAD_DIR" "$RUN_DIR"
|
||||
json_ok "Cleaned"
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
list)
|
||||
method_list
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
upload_start) upload_start ;;
|
||||
upload_chunk) upload_chunk ;;
|
||||
upload_finish) upload_finish ;;
|
||||
run) run_installer ;;
|
||||
status) status ;;
|
||||
read_log) read_log ;;
|
||||
cleanup) cleanup ;;
|
||||
*) json_error "Unknown method" ;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
reply '{}'
|
||||
;;
|
||||
esac
|
||||
13
luci-app-run/root/usr/share/luci/menu.d/luci-app-run.json
Normal file
13
luci-app-run/root/usr/share/luci/menu.d/luci-app-run.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"admin/system/run": {
|
||||
"title": "Run安装器",
|
||||
"order": 32,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "run/index"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-run" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
21
luci-app-run/root/usr/share/rpcd/acl.d/luci-app-run.json
Normal file
21
luci-app-run/root/usr/share/rpcd/acl.d/luci-app-run.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"luci-app-run": {
|
||||
"description": "Grant access to upload and execute .run installers",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci-app-run": [ "status", "read_log" ]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci-app-run": [
|
||||
"upload_start",
|
||||
"upload_chunk",
|
||||
"upload_finish",
|
||||
"run",
|
||||
"cleanup"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
96
luci-app-run/root/www/cgi-bin/luci-app-run-upload
Executable file
96
luci-app-run/root/www/cgi-bin/luci-app-run-upload
Executable file
@ -0,0 +1,96 @@
|
||||
#!/bin/sh
|
||||
|
||||
BASE_DIR="/tmp/luci-app-run"
|
||||
UPLOAD_DIR="$BASE_DIR/uploads"
|
||||
MAX_SIZE=$((256 * 1024 * 1024))
|
||||
|
||||
umask 077
|
||||
|
||||
header() {
|
||||
printf 'Status: %s\r\n' "$1"
|
||||
printf 'Content-Type: application/json\r\n'
|
||||
printf 'Cache-Control: no-store\r\n'
|
||||
printf '\r\n'
|
||||
}
|
||||
|
||||
json_escape() {
|
||||
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
||||
}
|
||||
|
||||
fail() {
|
||||
header "${2:-400 Bad Request}"
|
||||
printf '{"code":1,"error":"%s"}\n' "$(json_escape "$1")"
|
||||
exit 0
|
||||
}
|
||||
|
||||
ok() {
|
||||
header '200 OK'
|
||||
printf '{"code":0,"size":%s}\n' "$1"
|
||||
exit 0
|
||||
}
|
||||
|
||||
urldecode() {
|
||||
local value="${1//+/ }"
|
||||
printf '%b' "${value//%/\\x}"
|
||||
}
|
||||
|
||||
query_value() {
|
||||
local key="$1" pair name value oldifs
|
||||
oldifs="$IFS"
|
||||
IFS='&'
|
||||
for pair in $QUERY_STRING; do
|
||||
name="${pair%%=*}"
|
||||
value="${pair#*=}"
|
||||
if [ "$name" = "$key" ]; then
|
||||
IFS="$oldifs"
|
||||
urldecode "$value"
|
||||
return
|
||||
fi
|
||||
done
|
||||
IFS="$oldifs"
|
||||
}
|
||||
|
||||
valid_id() {
|
||||
case "$1" in
|
||||
""|*[!A-Za-z0-9_-]*) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
[ "$REQUEST_METHOD" = "POST" ] || fail "POST required" "405 Method Not Allowed"
|
||||
|
||||
id="$(query_value id)"
|
||||
token="$(query_value token)"
|
||||
valid_id "$id" || fail "Invalid upload id"
|
||||
[ -n "$token" ] || fail "Missing upload token"
|
||||
|
||||
dir="$UPLOAD_DIR/$id"
|
||||
[ -d "$dir" ] || fail "Unknown upload id" "404 Not Found"
|
||||
|
||||
saved_token="$(cat "$dir/token" 2>/dev/null)"
|
||||
[ "$token" = "$saved_token" ] || fail "Invalid upload token" "403 Forbidden"
|
||||
|
||||
name="$(cat "$dir/name" 2>/dev/null)"
|
||||
case "$name" in
|
||||
*.run) ;;
|
||||
*) fail "Invalid filename" ;;
|
||||
esac
|
||||
|
||||
length="${CONTENT_LENGTH:-0}"
|
||||
[ "$length" -gt 0 ] 2>/dev/null || fail "Empty upload"
|
||||
[ "$length" -le "$MAX_SIZE" ] 2>/dev/null || fail "File is larger than 256 MiB"
|
||||
|
||||
file="$dir/$name"
|
||||
tmp="$file.upload"
|
||||
|
||||
cat > "$tmp" || fail "Unable to receive upload"
|
||||
actual="$(wc -c < "$tmp" 2>/dev/null)"
|
||||
[ "$actual" -eq "$length" ] 2>/dev/null || {
|
||||
rm -f "$tmp"
|
||||
fail "Upload size mismatch"
|
||||
}
|
||||
|
||||
mv "$tmp" "$file" || fail "Unable to save upload"
|
||||
chmod 700 "$file" || fail "Unable to chmod uploaded file"
|
||||
|
||||
ok "$actual"
|
||||
2
quectel_MHI/Makefile
Normal file → Executable file
2
quectel_MHI/Makefile
Normal file → Executable file
@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=pcie_mhi
|
||||
PKG_VERSION:=1.3.8
|
||||
PKG_RELEASE:=13
|
||||
PKG_RELEASE:=14
|
||||
|
||||
include $(INCLUDE_DIR)/kernel.mk
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
Loading…
Reference in New Issue
Block a user