diff --git a/package/secubox/luci-app-droplet/htdocs/luci-static/resources/view/droplet/overview.js b/package/secubox/luci-app-droplet/htdocs/luci-static/resources/view/droplet/overview.js index e5ed1c37..e1dfe44f 100644 --- a/package/secubox/luci-app-droplet/htdocs/luci-static/resources/view/droplet/overview.js +++ b/package/secubox/luci-app-droplet/htdocs/luci-static/resources/view/droplet/overview.js @@ -30,6 +30,13 @@ var callDropletRemove = rpc.declare({ expect: {} }); +var callDropletJobStatus = rpc.declare({ + object: 'luci.droplet', + method: 'job_status', + params: ['job_id'], + expect: {} +}); + return view.extend({ load: function() { return Promise.all([ @@ -302,7 +309,11 @@ return view.extend({ } }) .then(function(result) { - if (result.success) { + // Handle async job response + if (result.status === 'started' && result.job_id) { + showResult('success', '⏳ Publishing ' + result.name + '...'); + return pollJobStatus(result.job_id); + } else if (result.success) { showResult('success', '✅ Published! ' + result.url + ''); setTimeout(function() { location.reload(); }, 2000); } else { @@ -323,6 +334,47 @@ return view.extend({ resultMsg.innerHTML = msg; } + function pollJobStatus(jobId) { + return new Promise(function(resolve, reject) { + var attempts = 0; + var maxAttempts = 60; // 60 * 2s = 2 minutes max + + function check() { + callDropletJobStatus(jobId).then(function(status) { + if (status.status === 'complete') { + if (status.success) { + showResult('success', '✅ Published! ' + status.url + ''); + setTimeout(function() { location.reload(); }, 2000); + } else { + showResult('error', '❌ ' + (status.error || 'Failed to publish')); + } + resolve(status); + } else if (status.status === 'running') { + attempts++; + if (attempts < maxAttempts) { + setTimeout(check, 2000); + } else { + showResult('error', '❌ Publish timed out'); + reject(new Error('Timeout')); + } + } else { + showResult('error', '❌ Job not found'); + reject(new Error('Job not found')); + } + }).catch(function(err) { + attempts++; + if (attempts < maxAttempts) { + setTimeout(check, 2000); + } else { + reject(err); + } + }); + } + + check(); + }); + } + // Delete buttons view.querySelectorAll('.btn-delete').forEach(function(btn) { btn.addEventListener('click', function() { diff --git a/package/secubox/luci-app-droplet/root/usr/libexec/rpcd/luci.droplet b/package/secubox/luci-app-droplet/root/usr/libexec/rpcd/luci.droplet index ed3a1f38..62049f23 100644 --- a/package/secubox/luci-app-droplet/root/usr/libexec/rpcd/luci.droplet +++ b/package/secubox/luci-app-droplet/root/usr/libexec/rpcd/luci.droplet @@ -5,10 +5,14 @@ UPLOAD_DIR="/tmp/droplet-upload" DEFAULT_DOMAIN="gk2.secubox.in" +JOB_DIR="/tmp/droplet-jobs" + +# Ensure job dir exists +mkdir -p "$JOB_DIR" case "$1" in list) - echo '{"publish":{},"upload":{"file":"string","name":"string","domain":"string"},"list":{},"remove":{"name":"string"},"rename":{"old":"string","new":"string"},"status":{}}' + echo '{"publish":{},"upload":{"file":"string","name":"string","domain":"string"},"list":{},"remove":{"name":"string"},"rename":{"old":"string","new":"string"},"status":{},"job_status":{"job_id":"string"}}' ;; call) case "$2" in @@ -21,6 +25,25 @@ case "$1" in json_dump ;; + job_status) + # Check status of an async publish job + read -r input + job_id=$(echo "$input" | jsonfilter -e '@.job_id' 2>/dev/null) + + if [ -z "$job_id" ]; then + echo '{"error":"Job ID required"}' + exit 0 + fi + + job_file="$JOB_DIR/$job_id" + if [ ! -f "$job_file" ]; then + echo '{"status":"not_found"}' + exit 0 + fi + + cat "$job_file" + ;; + list) json_init json_add_array "droplets" @@ -59,53 +82,57 @@ case "$1" in ;; upload) - # Read params + # Read params using jsonfilter for reliability read -r input - json_load "$input" - json_get_var file file - json_get_var name name - json_get_var domain domain + file=$(echo "$input" | jsonfilter -e '@.file' 2>/dev/null) + name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) + domain=$(echo "$input" | jsonfilter -e '@.domain' 2>/dev/null) [ -z "$name" ] && { echo '{"error":"Name required"}'; exit 0; } [ -z "$file" ] && { echo '{"error":"File required"}'; exit 0; } [ -z "$domain" ] && domain="$DEFAULT_DOMAIN" # File should be in upload dir (set by cgi-io) - local upload_file="$UPLOAD_DIR/$file" + upload_file="$UPLOAD_DIR/$file" if [ ! -f "$upload_file" ]; then # Try direct path upload_file="$file" fi - [ ! -f "$upload_file" ] && { echo '{"error":"File not found"}'; exit 0; } + [ ! -f "$upload_file" ] && { echo '{"error":"File not found: '"$file"'"}'; exit 0; } - # Publish - result=$(dropletctl publish "$upload_file" "$name" "$domain" 2>&1) - exit_code=$? + # Generate job ID and run publish async + job_id="$(date +%s)_$$" + job_file="$JOB_DIR/$job_id" - # Extract vhost from result - vhost=$(echo "$result" | grep -oE '[a-z0-9_-]+\.[a-z0-9.-]+' | tail -1) + # Initialize job status + echo '{"status":"running","name":"'"$name"'","domain":"'"$domain"'"}' > "$job_file" - json_init - if [ $exit_code -eq 0 ]; then - json_add_boolean "success" 1 - json_add_string "vhost" "$vhost" - json_add_string "url" "https://$vhost/" - json_add_string "message" "Published successfully" - else - json_add_boolean "success" 0 - json_add_string "error" "$result" - fi - json_dump + # Write background script - use file output to avoid pipe inheritance issues + bg_script="/tmp/droplet-bg-$job_id.sh" + result_file="/tmp/droplet-result-$job_id.txt" + printf '#!/bin/sh\nupload_file="$1"\nname="$2"\ndomain="$3"\njob_file="$4"\nresult_file="$5"\n' > "$bg_script" + # Write output to file instead of capturing in variable (avoids pipe inheritance) + printf 'dropletctl publish "$upload_file" "$name" "$domain" > "$result_file" 2>&1\nexit_code=$?\n' >> "$bg_script" + # Check for success marker and extract vhost + printf 'if grep -q "\\[OK\\] Published:" "$result_file"; then\n' >> "$bg_script" + printf ' vhost=$(tail -1 "$result_file")\n' >> "$bg_script" + printf ' printf '\''{"status":"complete","success":true,"vhost":"%%s","url":"https://%%s/","message":"Published successfully"}'\'' "$vhost" "$vhost" > "$job_file"\n' >> "$bg_script" + printf 'else\n error_msg=$(tail -10 "$result_file" | tr '\''\\n'\'' '\'' '\'' | sed '\''s/"/\\\\"/g'\'')\n' >> "$bg_script" + printf ' printf '\''{"status":"complete","success":false,"error":"%%s"}'\'' "$error_msg" > "$job_file"\nfi\n' >> "$bg_script" + printf 'rm -f "$upload_file" "$result_file" "$0"\nsleep 300\nrm -f "$job_file"\n' >> "$bg_script" + chmod +x "$bg_script" - # Cleanup - rm -f "$upload_file" + # Run background script detached + setsid "$bg_script" "$upload_file" "$name" "$domain" "$job_file" "$result_file" /dev/null 2>&1 & + + # Return immediately with job ID + echo '{"status":"started","job_id":"'"$job_id"'","name":"'"$name"'","domain":"'"$domain"'"}' ;; remove) read -r input - json_load "$input" - json_get_var name name + name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) [ -z "$name" ] && { echo '{"error":"Name required"}'; exit 0; } @@ -119,9 +146,8 @@ case "$1" in rename) read -r input - json_load "$input" - json_get_var old old - json_get_var new new + old=$(echo "$input" | jsonfilter -e '@.old' 2>/dev/null) + new=$(echo "$input" | jsonfilter -e '@.new' 2>/dev/null) [ -z "$old" ] || [ -z "$new" ] && { echo '{"error":"Old and new names required"}'; exit 0; } diff --git a/package/secubox/luci-app-droplet/root/usr/share/rpcd/acl.d/luci-app-droplet.json b/package/secubox/luci-app-droplet/root/usr/share/rpcd/acl.d/luci-app-droplet.json index 30a5d179..10112981 100644 --- a/package/secubox/luci-app-droplet/root/usr/share/rpcd/acl.d/luci-app-droplet.json +++ b/package/secubox/luci-app-droplet/root/usr/share/rpcd/acl.d/luci-app-droplet.json @@ -3,7 +3,7 @@ "description": "Droplet Publisher", "read": { "ubus": { - "luci.droplet": ["status", "list"] + "luci.droplet": ["status", "list", "job_status"] } }, "write": {