💐 Sync 2026-06-25 20:51:16

This commit is contained in:
github-actions[bot] committed 2026-06-25 20:51:16 +08:00
1 parent 3232f1d574
commit 7a3941cfa5
18 files changed
+734 -55

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-run
PKG_VERSION:=1.0.0
PKG_RELEASE:=6
PKG_RELEASE:=7
LUCI_TITLE:=LuCI support for running installers and packages
LUCI_DESCRIPTION:=Upload and execute .run, .sh, .ipk and .apk files from LuCI.
@@ -28,7 +28,11 @@
"args_hint": "您可以输入脚本参数或留空",
"cancel": "取消",
"confirm": "确定",
"auto_cleaned": "执行完毕,已自动清理临时文件。"
"auto_cleaned": "执行完毕,已自动清理临时文件。",
"download_run": "下载并执行 .run",
"download_url": "请输入 .run 文件的下载地址",
"downloading": "正在下载...",
"only_run": "仅支持 .run 文件下载"
},
"en": {
"title": "Run Installer",
@@ -59,6 +63,10 @@
"args_hint": "You can enter script arguments or leave it blank",
"cancel": "Cancel",
"confirm": "OK",
"auto_cleaned": "Execution complete, temporary files cleaned automatically."
"auto_cleaned": "Execution complete, temporary files cleaned automatically.",
"download_run": "Download and execute .run",
"download_url": "Enter .run file download URL",
"downloading": "Downloading...",
"only_run": "Only .run files are supported"
}
}
@@ -89,7 +89,11 @@ function getDefaultText(key) {
'args_hint': '您可以输入脚本参数或留空',
'cancel': '取消',
'confirm': '确定',
'auto_cleaned': '执行完毕,已自动清理临时文件。'
'auto_cleaned': '执行完毕,已自动清理临时文件。',
'download_run': '下载并执行 .run',
'download_url': '请输入 .run 文件的下载地址',
'downloading': '正在下载...',
'only_run': '仅支持 .run 文件下载'
};
return defaults[key] || null;
}
@@ -144,6 +148,12 @@ var cleanup = rpc.declare({
method: 'cleanup'
});
var downloadRun = rpc.declare({
object: 'luci-app-run',
method: 'download_run',
params: ['url']
});
function formatBytes(size) {
if (size >= 1024 * 1024)
return '%.1f MiB'.format(size / 1024 / 1024);
@@ -290,6 +300,20 @@ return view.extend({
}
}, [_('choose_apk')]);
var downloadButton = E('button', {
class: 'cbi-button cbi-button-action run-btn',
style: 'margin-left:10px;background:#E65100!important;background-color:#E65100!important;background-image:none!important;color:#fff!important;border-color:#E65100!important;box-shadow:none!important;text-shadow:none!important;opacity:1!important',
click: function (ev) {
ev.preventDefault();
self.showDownloadDialog(function (url) {
if (url !== null) {
log.textContent = '';
self.startDownload(runButton, state, url.trim());
}
});
}
}, [_('download_run')]);
var runButton = E('button', {
class: 'cbi-button cbi-button-action run-btn',
disabled: true,
@@ -354,7 +378,7 @@ return view.extend({
E('h3', [_('upload_title')]),
E('p', [state]),
E('p', [pickButton, ipkButton, apkButton]),
E('p', { style: 'margin-top:10px' }, [runButton, cleanButton]),
E('p', { style: 'margin-top:10px' }, [downloadButton, runButton, cleanButton]),
progress,
fileInput,
ipkInput,
@@ -543,6 +567,81 @@ return view.extend({
document.getElementById('run-args-dialog-input').focus();
},
showDownloadDialog: function (callback) {
var dialog = E('div', {
style: 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999'
}, [
E('div', {
style: 'background:#fff;border-radius:8px;padding:20px;width:400px;box-shadow:0 4px 20px rgba(0,0,0,0.3)'
}, [
E('input', {
type: 'text',
placeholder: _('download_url'),
style: 'width:100%;padding:10px;margin-bottom:15px;border:1px solid #ccc;border-radius:4px;box-sizing:border-box;font-size:14px',
id: 'run-download-url-input'
}),
E('div', {
style: 'display:flex;justify-content:flex-end;gap:10px'
}, [
E('button', {
class: 'cbi-button cbi-button-reset',
style: 'padding:8px 20px;text-transform:none',
click: function () {
document.body.removeChild(dialog);
callback(null);
}
}, [_('cancel')]),
E('button', {
class: 'cbi-button cbi-button-action',
style: 'padding:8px 20px;text-transform:none',
click: function () {
var url = document.getElementById('run-download-url-input').value.trim();
if (!url) {
document.body.removeChild(dialog);
callback(null);
return;
}
var filename = url.split('/').pop().split('?')[0];
if (!filename.match(/\.run$/i)) {
ui.addNotification(null, E('p', [_('only_run')]), 'danger');
return;
}
document.body.removeChild(dialog);
callback(url);
}
}, [_('confirm')])
])
])
]);
document.body.appendChild(dialog);
document.getElementById('run-download-url-input').focus();
},
startDownload: function (runButton, state, url) {
var self = this;
runButton.disabled = true;
state.textContent = _('downloading');
self.prevRunning = false;
self.autoCleanType = null;
return downloadRun(url).then(function (res) {
if (res && res.error) {
throw new Error(res.error);
}
self.logOffset = 0;
self.prevRunning = true;
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;
@@ -75,11 +75,13 @@ write_state() {
local file="$2"
local log="$3"
local started="$4"
local type="$5"
{
printf 'PID=%s\n' "$pid"
printf 'FILE=%s\n' "$file"
printf 'LOG=%s\n' "$log"
printf 'STARTED=%s\n' "$started"
printf 'TYPE=%s\n' "$type"
} > "$STATE_FILE"
}
@@ -136,6 +138,9 @@ method_list() {
"read_log": {
"offset": "Integer"
},
"download_run": {
"url": "String"
},
"cleanup": {},
"version": {},
"capabilities": {}
@@ -236,6 +241,86 @@ upload_finish() {
json_dump
}
download_run() {
load_input
get_string url
[ -n "$url" ] || { json_error "Missing URL"; return; }
local filename id dir file log work pid now
# Extract filename from URL - get everything after the last /
filename="${url##*/}"
# Remove query parameters if any
filename="${filename%%\?*}"
case "$filename" in
*.run) ;;
*) json_error "Only .run files are supported for download"; return ;;
esac
read_state
if [ -n "$PID" ] && ! is_running "$PID"; then
rm -f "$STATE_FILE"
unset PID
fi
if is_running "$PID"; then
json_error "Another installer is already running"
return
fi
id="download_$(date +%s)"
dir="$RUN_DIR/$id"
file="$dir/$filename"
log="$dir/output.log"
mkdir -p "$dir" || { json_error "Unable to create download directory"; return; }
now="$(date '+%Y-%m-%d %H:%M:%S')"
(
cd "$dir" || exit 1
printf 'luci-app-run: started %s\n' "$now"
printf 'luci-app-run: downloading %s\n\n' "$url"
if command -v curl >/dev/null 2>&1; then
curl -L -o "$filename" "$url"
elif command -v wget >/dev/null 2>&1; then
wget -O "$filename" "$url"
else
printf 'luci-app-run: error: neither curl nor wget is available\n'
exit 1
fi
rc=$?
if [ "$rc" -ne 0 ]; then
printf '\nluci-app-run: download failed with status %s at %s\n' "$rc" "$(date '+%Y-%m-%d %H:%M:%S')"
exit "$rc"
fi
printf '\nluci-app-run: download completed at %s\n' "$(date '+%Y-%m-%d %H:%M:%S')"
printf '\nluci-app-run: executing %s\n\n' "$file"
chmod 700 "$file" 2>/dev/null || true
"$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" "download"
json_init
json_add_int code 0
json_add_int pid "$pid"
json_add_string log "$log"
json_add_string filename "$filename"
json_dump
}
run_installer() {
load_input
get_string id
@@ -317,7 +402,7 @@ run_installer() {
esac
pid="$!"
write_state "$pid" "$file" "$log" "$now"
write_state "$pid" "$file" "$log" "$now" "upload"
json_init
json_add_int code 0
@@ -433,6 +518,7 @@ case "$1" in
upload_chunk) upload_chunk ;;
upload_finish) upload_finish ;;
run) run_installer ;;
download_run) download_run ;;
status) status ;;
read_log) read_log ;;
cleanup) cleanup ;;
@@ -13,6 +13,7 @@
"upload_chunk",
"upload_finish",
"run",
"download_run",
"cleanup"
]
}