feat(droplet): Add one-drop content publisher
Simple drag-and-drop publishing for HTML/ZIP files: - Auto-detects content type (static/streamlit/hexo) - Creates vhosts at gk2.secubox.in by default - Registers with metablogizer or streamlit accordingly - CLI: dropletctl publish/list/remove/rename - LuCI drag-drop interface at Services > Droplet Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
296eac8562
commit
078e2dc01e
27
package/secubox/luci-app-droplet/Makefile
Normal file
27
package/secubox/luci-app-droplet/Makefile
Normal file
@ -0,0 +1,27 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-droplet
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
LUCI_TITLE:=LuCI Droplet Publisher - One-Drop Content Publishing
|
||||
LUCI_DEPENDS:=+secubox-app-droplet +luci-base
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
define Package/luci-app-droplet/install
|
||||
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-droplet.json $(1)/usr/share/luci/menu.d/
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-droplet.json $(1)/usr/share/rpcd/acl.d/
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.droplet $(1)/usr/libexec/rpcd/
|
||||
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/droplet
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/droplet/*.js $(1)/www/luci-static/resources/view/droplet/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,luci-app-droplet))
|
||||
@ -0,0 +1,352 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require fs';
|
||||
|
||||
var callDropletStatus = rpc.declare({
|
||||
object: 'luci.droplet',
|
||||
method: 'status',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callDropletList = rpc.declare({
|
||||
object: 'luci.droplet',
|
||||
method: 'list',
|
||||
expect: { droplets: [] }
|
||||
});
|
||||
|
||||
var callDropletUpload = rpc.declare({
|
||||
object: 'luci.droplet',
|
||||
method: 'upload',
|
||||
params: ['file', 'name', 'domain'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callDropletRemove = rpc.declare({
|
||||
object: 'luci.droplet',
|
||||
method: 'remove',
|
||||
params: ['name'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
callDropletStatus(),
|
||||
callDropletList()
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var status = data[0] || {};
|
||||
var droplets = data[1] || [];
|
||||
|
||||
var view = E('div', { 'class': 'cbi-map' }, [
|
||||
E('style', {}, `
|
||||
.droplet-container { max-width: 800px; margin: 0 auto; }
|
||||
.drop-zone {
|
||||
border: 3px dashed #00d4ff;
|
||||
border-radius: 16px;
|
||||
padding: 60px 40px;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, rgba(0,212,255,0.05), rgba(124,58,237,0.05));
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.drop-zone:hover, .drop-zone.drag-over {
|
||||
border-color: #7c3aed;
|
||||
background: linear-gradient(135deg, rgba(0,212,255,0.1), rgba(124,58,237,0.1));
|
||||
transform: scale(1.02);
|
||||
}
|
||||
.drop-zone h2 { color: #00d4ff; margin: 0 0 10px; font-size: 1.5em; }
|
||||
.drop-zone p { color: #888; margin: 0; }
|
||||
.drop-zone input[type="file"] { display: none; }
|
||||
.publish-form {
|
||||
display: none;
|
||||
background: #1a1a24;
|
||||
border: 1px solid #2a2a3e;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.publish-form.active { display: block; }
|
||||
.publish-form .field { margin-bottom: 15px; }
|
||||
.publish-form label { display: block; color: #888; margin-bottom: 5px; font-size: 0.9em; }
|
||||
.publish-form input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 10px 15px;
|
||||
background: #12121a;
|
||||
border: 1px solid #2a2a3e;
|
||||
border-radius: 8px;
|
||||
color: #e0e0e0;
|
||||
font-size: 1em;
|
||||
}
|
||||
.publish-form input:focus { outline: none; border-color: #00d4ff; }
|
||||
.publish-form .file-info {
|
||||
background: #12121a;
|
||||
padding: 10px 15px;
|
||||
border-radius: 8px;
|
||||
color: #00d4ff;
|
||||
font-family: monospace;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.publish-form .buttons { display: flex; gap: 10px; }
|
||||
.btn-publish {
|
||||
flex: 1;
|
||||
padding: 12px 24px;
|
||||
background: linear-gradient(135deg, #00d4ff, #7c3aed);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.btn-publish:hover { transform: translateY(-2px); }
|
||||
.btn-cancel {
|
||||
padding: 12px 24px;
|
||||
background: #2a2a3e;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
}
|
||||
.droplet-list { margin-top: 30px; }
|
||||
.droplet-list h3 { color: #e0e0e0; margin-bottom: 15px; }
|
||||
.droplet-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 15px;
|
||||
background: #1a1a24;
|
||||
border: 1px solid #2a2a3e;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.droplet-item:hover { border-color: #00d4ff; }
|
||||
.droplet-info { flex: 1; }
|
||||
.droplet-name { font-weight: 600; color: #e0e0e0; }
|
||||
.droplet-domain { font-size: 0.85em; color: #00d4ff; font-family: monospace; }
|
||||
.droplet-type {
|
||||
font-size: 0.75em;
|
||||
padding: 2px 8px;
|
||||
background: rgba(124,58,237,0.2);
|
||||
color: #7c3aed;
|
||||
border-radius: 4px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.droplet-actions button {
|
||||
padding: 6px 12px;
|
||||
background: #2a2a3e;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.droplet-actions button:hover { background: #3a3a4e; color: #e0e0e0; }
|
||||
.droplet-actions .btn-delete:hover { background: #ef4444; color: #fff; }
|
||||
.result-message {
|
||||
display: none;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.result-message.success { display: block; background: rgba(16,185,129,0.1); border: 1px solid #10b981; color: #10b981; }
|
||||
.result-message.error { display: block; background: rgba(239,68,68,0.1); border: 1px solid #ef4444; color: #ef4444; }
|
||||
.result-message a { color: inherit; }
|
||||
`),
|
||||
|
||||
E('div', { 'class': 'droplet-container' }, [
|
||||
E('h2', { 'style': 'color: #e0e0e0; margin-bottom: 20px;' }, [
|
||||
E('span', { 'style': 'color: #00d4ff;' }, 'Droplet'),
|
||||
' Publisher'
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'result-message', 'id': 'result-msg' }),
|
||||
|
||||
E('div', { 'class': 'drop-zone', 'id': 'drop-zone' }, [
|
||||
E('h2', {}, '📦 Drop to Publish'),
|
||||
E('p', {}, 'Drop HTML file or ZIP archive here'),
|
||||
E('p', { 'style': 'margin-top: 10px; font-size: 0.85em;' }, 'or click to browse'),
|
||||
E('input', { 'type': 'file', 'id': 'file-input', 'accept': '.html,.htm,.zip' })
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'publish-form', 'id': 'publish-form' }, [
|
||||
E('div', { 'class': 'file-info', 'id': 'file-info' }, ''),
|
||||
E('div', { 'class': 'field' }, [
|
||||
E('label', {}, 'Site Name'),
|
||||
E('input', { 'type': 'text', 'id': 'site-name', 'placeholder': 'mysite' })
|
||||
]),
|
||||
E('div', { 'class': 'field' }, [
|
||||
E('label', {}, 'Domain'),
|
||||
E('input', { 'type': 'text', 'id': 'site-domain', 'value': status.default_domain || 'gk2.secubox.in', 'placeholder': 'gk2.secubox.in' })
|
||||
]),
|
||||
E('div', { 'class': 'buttons' }, [
|
||||
E('button', { 'class': 'btn-cancel', 'id': 'btn-cancel' }, 'Cancel'),
|
||||
E('button', { 'class': 'btn-publish', 'id': 'btn-publish' }, '🚀 Publish')
|
||||
])
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'droplet-list' }, [
|
||||
E('h3', {}, 'Published Droplets (' + droplets.length + ')'),
|
||||
E('div', { 'id': 'droplet-items' },
|
||||
droplets.map(function(d) {
|
||||
return E('div', { 'class': 'droplet-item', 'data-name': d.name }, [
|
||||
E('div', { 'class': 'droplet-info' }, [
|
||||
E('span', { 'class': 'droplet-name' }, d.name),
|
||||
E('span', { 'class': 'droplet-type' }, d.type || 'static'),
|
||||
E('div', { 'class': 'droplet-domain' }, [
|
||||
E('a', { 'href': 'https://' + d.domain + '/', 'target': '_blank' }, d.domain)
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'droplet-actions' }, [
|
||||
E('button', { 'class': 'btn-open', 'data-url': 'https://' + d.domain + '/' }, '🔗'),
|
||||
E('button', { 'class': 'btn-delete', 'data-name': d.name }, '🗑')
|
||||
])
|
||||
]);
|
||||
})
|
||||
)
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
// Event handlers
|
||||
var dropZone = view.querySelector('#drop-zone');
|
||||
var fileInput = view.querySelector('#file-input');
|
||||
var publishForm = view.querySelector('#publish-form');
|
||||
var fileInfo = view.querySelector('#file-info');
|
||||
var siteName = view.querySelector('#site-name');
|
||||
var siteDomain = view.querySelector('#site-domain');
|
||||
var btnPublish = view.querySelector('#btn-publish');
|
||||
var btnCancel = view.querySelector('#btn-cancel');
|
||||
var resultMsg = view.querySelector('#result-msg');
|
||||
var selectedFile = null;
|
||||
|
||||
// Drag & drop
|
||||
dropZone.addEventListener('click', function() { fileInput.click(); });
|
||||
dropZone.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('drag-over');
|
||||
});
|
||||
dropZone.addEventListener('dragleave', function() {
|
||||
dropZone.classList.remove('drag-over');
|
||||
});
|
||||
dropZone.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('drag-over');
|
||||
if (e.dataTransfer.files.length) {
|
||||
handleFile(e.dataTransfer.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', function() {
|
||||
if (fileInput.files.length) {
|
||||
handleFile(fileInput.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
function handleFile(file) {
|
||||
selectedFile = file;
|
||||
fileInfo.textContent = '📄 ' + file.name + ' (' + formatSize(file.size) + ')';
|
||||
|
||||
// Auto-generate name from filename
|
||||
var name = file.name.replace(/\.(html?|zip)$/i, '').toLowerCase().replace(/[^a-z0-9_-]/g, '_');
|
||||
siteName.value = name;
|
||||
|
||||
publishForm.classList.add('active');
|
||||
dropZone.style.display = 'none';
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
btnCancel.addEventListener('click', function() {
|
||||
publishForm.classList.remove('active');
|
||||
dropZone.style.display = 'block';
|
||||
selectedFile = null;
|
||||
fileInput.value = '';
|
||||
});
|
||||
|
||||
btnPublish.addEventListener('click', function() {
|
||||
if (!selectedFile || !siteName.value) {
|
||||
showResult('error', 'Please select a file and enter a name');
|
||||
return;
|
||||
}
|
||||
|
||||
btnPublish.disabled = true;
|
||||
btnPublish.textContent = '⏳ Publishing...';
|
||||
|
||||
// Upload file first
|
||||
var formData = new FormData();
|
||||
formData.append('sessionid', rpc.getSessionID());
|
||||
formData.append('filename', '/tmp/droplet-upload/' + selectedFile.name);
|
||||
formData.append('filedata', selectedFile);
|
||||
|
||||
fetch('/cgi-bin/cgi-upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(function(res) { return res.json(); })
|
||||
.then(function(uploadRes) {
|
||||
if (uploadRes.size) {
|
||||
// File uploaded, now publish
|
||||
return callDropletUpload(selectedFile.name, siteName.value, siteDomain.value);
|
||||
} else {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
})
|
||||
.then(function(result) {
|
||||
if (result.success) {
|
||||
showResult('success', '✅ Published! <a href="' + result.url + '" target="_blank">' + result.url + '</a>');
|
||||
setTimeout(function() { location.reload(); }, 2000);
|
||||
} else {
|
||||
showResult('error', '❌ ' + (result.error || 'Failed to publish'));
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
showResult('error', '❌ ' + err.message);
|
||||
})
|
||||
.finally(function() {
|
||||
btnPublish.disabled = false;
|
||||
btnPublish.textContent = '🚀 Publish';
|
||||
});
|
||||
});
|
||||
|
||||
function showResult(type, msg) {
|
||||
resultMsg.className = 'result-message ' + type;
|
||||
resultMsg.innerHTML = msg;
|
||||
}
|
||||
|
||||
// Delete buttons
|
||||
view.querySelectorAll('.btn-delete').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var name = btn.dataset.name;
|
||||
if (confirm('Delete "' + name + '"?')) {
|
||||
callDropletRemove(name).then(function() {
|
||||
btn.closest('.droplet-item').remove();
|
||||
showResult('success', 'Deleted: ' + name);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Open buttons
|
||||
view.querySelectorAll('.btn-open').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
window.open(btn.dataset.url, '_blank');
|
||||
});
|
||||
});
|
||||
|
||||
return view;
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@ -0,0 +1,138 @@
|
||||
#!/bin/sh
|
||||
# RPCD handler for Droplet Publisher
|
||||
|
||||
. /usr/share/libubox/jshn.sh
|
||||
|
||||
UPLOAD_DIR="/tmp/droplet-upload"
|
||||
DEFAULT_DOMAIN="gk2.secubox.in"
|
||||
|
||||
case "$1" in
|
||||
list)
|
||||
echo '{"publish":{},"upload":{"file":"string","name":"string","domain":"string"},"list":{},"remove":{"name":"string"},"rename":{"old":"string","new":"string"},"status":{}}'
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
status)
|
||||
json_init
|
||||
json_add_string "upload_dir" "$UPLOAD_DIR"
|
||||
json_add_string "default_domain" "$DEFAULT_DOMAIN"
|
||||
json_add_int "sites_count" "$(uci show metablogizer 2>/dev/null | grep -c '=site$')"
|
||||
json_add_int "apps_count" "$(uci show streamlit 2>/dev/null | grep -c '=instance$')"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
list)
|
||||
json_init
|
||||
json_add_array "droplets"
|
||||
|
||||
# MetaBlog sites - use for loop to avoid subshell
|
||||
for name in $(uci show metablogizer 2>/dev/null | grep "=site$" | sed "s/metablogizer\.\(.*\)=site/\1/"); do
|
||||
domain=$(uci -q get "metablogizer.$name.domain")
|
||||
enabled=$(uci -q get "metablogizer.$name.enabled")
|
||||
[ -z "$enabled" ] && enabled="0"
|
||||
json_add_object ""
|
||||
json_add_string "name" "$name"
|
||||
json_add_string "domain" "$domain"
|
||||
json_add_string "type" "static"
|
||||
json_add_boolean "enabled" "$enabled"
|
||||
json_close_object
|
||||
done
|
||||
|
||||
# Streamlit apps
|
||||
for name in $(uci show streamlit 2>/dev/null | grep "=instance$" | sed "s/streamlit\.\(.*\)=instance/\1/"); do
|
||||
domain=$(uci -q get "streamlit.$name.domain")
|
||||
enabled=$(uci -q get "streamlit.$name.enabled")
|
||||
[ -z "$enabled" ] && enabled="0"
|
||||
json_add_object ""
|
||||
json_add_string "name" "$name"
|
||||
json_add_string "domain" "$domain"
|
||||
json_add_string "type" "streamlit"
|
||||
json_add_boolean "enabled" "$enabled"
|
||||
json_close_object
|
||||
done
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
;;
|
||||
|
||||
upload)
|
||||
# Read params
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var file file
|
||||
json_get_var name name
|
||||
json_get_var domain domain
|
||||
|
||||
[ -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"
|
||||
if [ ! -f "$upload_file" ]; then
|
||||
# Try direct path
|
||||
upload_file="$file"
|
||||
fi
|
||||
|
||||
[ ! -f "$upload_file" ] && { echo '{"error":"File not found"}'; exit 0; }
|
||||
|
||||
# Publish
|
||||
result=$(dropletctl publish "$upload_file" "$name" "$domain" 2>&1)
|
||||
exit_code=$?
|
||||
|
||||
# Extract vhost from result
|
||||
vhost=$(echo "$result" | grep -oE '[a-z0-9_-]+\.[a-z0-9.-]+' | tail -1)
|
||||
|
||||
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
|
||||
|
||||
# Cleanup
|
||||
rm -f "$upload_file"
|
||||
;;
|
||||
|
||||
remove)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var name name
|
||||
|
||||
[ -z "$name" ] && { echo '{"error":"Name required"}'; exit 0; }
|
||||
|
||||
result=$(dropletctl remove "$name" 2>&1)
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "Removed: $name"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
rename)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var old old
|
||||
json_get_var new new
|
||||
|
||||
[ -z "$old" ] || [ -z "$new" ] && { echo '{"error":"Old and new names required"}'; exit 0; }
|
||||
|
||||
result=$(dropletctl rename "$old" "$new" 2>&1)
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "Renamed: $old -> $new"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
*)
|
||||
echo '{"error":"Unknown method"}'
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
@ -0,0 +1,14 @@
|
||||
{
|
||||
"admin/services/droplet": {
|
||||
"title": "Droplet",
|
||||
"order": 45,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "droplet/overview"
|
||||
},
|
||||
"depends": {
|
||||
"acl": ["luci-app-droplet"],
|
||||
"uci": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
{
|
||||
"luci-app-droplet": {
|
||||
"description": "Droplet Publisher",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.droplet": ["status", "list"]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.droplet": ["upload", "remove", "rename"]
|
||||
},
|
||||
"cgi-io": ["upload"]
|
||||
}
|
||||
}
|
||||
}
|
||||
37
package/secubox/secubox-app-droplet/Makefile
Normal file
37
package/secubox/secubox-app-droplet/Makefile
Normal file
@ -0,0 +1,37 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=secubox-app-droplet
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_MAINTAINER:=SecuBox <secubox@cybermind.fr>
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/secubox-app-droplet
|
||||
SECTION:=secubox
|
||||
CATEGORY:=SecuBox
|
||||
TITLE:=Droplet Publisher - One-Drop Content Publishing
|
||||
DEPENDS:=+unzip
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/secubox-app-droplet/description
|
||||
One-drop content publisher for SecuBox.
|
||||
Drop HTML/ZIP files to instantly publish as sites with vhost configuration.
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/secubox-app-droplet/install
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_BIN) ./files/usr/sbin/dropletctl $(1)/usr/sbin/
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
touch $(1)/etc/config/droplet
|
||||
|
||||
$(INSTALL_DIR) $(1)/tmp/droplet-upload
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,secubox-app-droplet))
|
||||
@ -0,0 +1,3 @@
|
||||
config droplet 'main'
|
||||
option default_domain 'gk2.secubox.in'
|
||||
option upload_dir '/tmp/droplet-upload'
|
||||
292
package/secubox/secubox-app-droplet/files/usr/sbin/dropletctl
Normal file
292
package/secubox/secubox-app-droplet/files/usr/sbin/dropletctl
Normal file
@ -0,0 +1,292 @@
|
||||
#!/bin/sh
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Droplet Publisher - One-Drop Content Publishing
|
||||
# Drop HTML/ZIP → Get published site with vhost + Gitea versioning
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
DROPLET_DIR="/srv/droplet"
|
||||
SITES_DIR="/srv/metablogizer/sites"
|
||||
APPS_DIR="/srv/streamlit/apps"
|
||||
DEFAULT_DOMAIN="gk2.secubox.in"
|
||||
GITEA_REPO="gandalf/droplet-sites"
|
||||
GITEA_URL="https://git.gk2.secubox.in"
|
||||
|
||||
# Logging
|
||||
log_info() { logger -t droplet -p user.info "$*"; echo "[INFO] $*"; }
|
||||
log_error() { logger -t droplet -p user.error "$*"; echo "[ERROR] $*" >&2; }
|
||||
log_ok() { echo "[OK] $*"; }
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
# Detect content type from file/directory
|
||||
# Returns: static|streamlit|hexo|unknown
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
detect_type() {
|
||||
local path="$1"
|
||||
|
||||
# Check for Streamlit app
|
||||
if [ -f "$path/app.py" ] || [ -f "$path/main.py" ]; then
|
||||
grep -qE "import streamlit|from streamlit" "$path"/*.py 2>/dev/null && {
|
||||
echo "streamlit"
|
||||
return
|
||||
}
|
||||
fi
|
||||
|
||||
# Check for Hexo
|
||||
if [ -f "$path/_config.yml" ] && [ -d "$path/source" ]; then
|
||||
echo "hexo"
|
||||
return
|
||||
fi
|
||||
|
||||
# Check for static HTML
|
||||
if [ -f "$path/index.html" ] || [ -f "$path/index.htm" ]; then
|
||||
echo "static"
|
||||
return
|
||||
fi
|
||||
|
||||
# Single HTML file
|
||||
if [ -f "$path" ] && echo "$path" | grep -qiE '\.html?$'; then
|
||||
echo "static"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "unknown"
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
# Publish content
|
||||
# Usage: dropletctl publish <file> <name> [domain]
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
cmd_publish() {
|
||||
local file="$1"
|
||||
local name="$2"
|
||||
local domain="${3:-$DEFAULT_DOMAIN}"
|
||||
|
||||
[ -z "$file" ] && { log_error "Usage: dropletctl publish <file> <name> [domain]"; return 1; }
|
||||
[ -z "$name" ] && { log_error "Name required"; return 1; }
|
||||
[ ! -f "$file" ] && { log_error "File not found: $file"; return 1; }
|
||||
|
||||
# Sanitize name
|
||||
name=$(echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]/_/g')
|
||||
local vhost="${name}.${domain}"
|
||||
local tmp_dir="/tmp/droplet_$$"
|
||||
|
||||
mkdir -p "$tmp_dir"
|
||||
|
||||
log_info "Publishing: $file as $vhost"
|
||||
|
||||
# Extract if ZIP
|
||||
local content_type=$(file -b --mime-type "$file")
|
||||
if echo "$content_type" | grep -q "zip"; then
|
||||
log_info "Extracting ZIP..."
|
||||
unzip -q "$file" -d "$tmp_dir" || { log_error "Failed to extract ZIP"; rm -rf "$tmp_dir"; return 1; }
|
||||
|
||||
# Handle nested directory
|
||||
local nested=$(find "$tmp_dir" -mindepth 1 -maxdepth 1 -type d | head -1)
|
||||
if [ -n "$nested" ] && [ $(find "$tmp_dir" -mindepth 1 -maxdepth 1 | wc -l) -eq 1 ]; then
|
||||
mv "$nested"/* "$tmp_dir/" 2>/dev/null
|
||||
rmdir "$nested" 2>/dev/null
|
||||
fi
|
||||
elif echo "$content_type" | grep -qE "html|text"; then
|
||||
# Single HTML file
|
||||
cp "$file" "$tmp_dir/index.html"
|
||||
else
|
||||
log_error "Unsupported file type: $content_type"
|
||||
rm -rf "$tmp_dir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Detect content type
|
||||
local app_type=$(detect_type "$tmp_dir")
|
||||
log_info "Detected type: $app_type"
|
||||
|
||||
local target_dir=""
|
||||
local publish_method=""
|
||||
|
||||
case "$app_type" in
|
||||
streamlit)
|
||||
target_dir="$APPS_DIR/$name"
|
||||
publish_method="streamlit"
|
||||
;;
|
||||
static|hexo)
|
||||
target_dir="$SITES_DIR/$name"
|
||||
publish_method="metablog"
|
||||
;;
|
||||
*)
|
||||
# Default to static site
|
||||
target_dir="$SITES_DIR/$name"
|
||||
publish_method="metablog"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Deploy content
|
||||
log_info "Deploying to $target_dir..."
|
||||
mkdir -p "$target_dir"
|
||||
cp -r "$tmp_dir"/* "$target_dir/"
|
||||
|
||||
# Create vhost via haproxyctl
|
||||
log_info "Creating vhost: $vhost"
|
||||
if command -v haproxyctl >/dev/null 2>&1; then
|
||||
haproxyctl vhost add "$vhost" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Register with appropriate system
|
||||
if [ "$publish_method" = "streamlit" ]; then
|
||||
# Add to streamlit config
|
||||
local port=$(uci show streamlit 2>/dev/null | grep -oE "port='[0-9]+'" | grep -oE "[0-9]+" | sort -n | tail -1)
|
||||
port=$((${port:-8500} + 1))
|
||||
|
||||
uci set "streamlit.${name}=instance"
|
||||
uci set "streamlit.${name}.name=$name"
|
||||
uci set "streamlit.${name}.domain=$vhost"
|
||||
uci set "streamlit.${name}.port=$port"
|
||||
uci set "streamlit.${name}.enabled=1"
|
||||
uci commit streamlit
|
||||
|
||||
log_info "Registered Streamlit app on port $port"
|
||||
else
|
||||
# Add to metablogizer config
|
||||
uci set "metablogizer.${name}=site"
|
||||
uci set "metablogizer.${name}.name=$name"
|
||||
uci set "metablogizer.${name}.domain=$vhost"
|
||||
uci set "metablogizer.${name}.enabled=1"
|
||||
uci commit metablogizer
|
||||
|
||||
log_info "Registered MetaBlog site"
|
||||
fi
|
||||
|
||||
# Git commit if available
|
||||
if [ -d "$target_dir/.git" ] || command -v git >/dev/null 2>&1; then
|
||||
cd "$target_dir"
|
||||
if [ ! -d ".git" ]; then
|
||||
git init -q
|
||||
git remote add origin "${GITEA_URL}/${GITEA_REPO}/${name}.git" 2>/dev/null || true
|
||||
fi
|
||||
git add -A
|
||||
git commit -q -m "Droplet publish: $name" 2>/dev/null || true
|
||||
log_info "Committed to git"
|
||||
fi
|
||||
|
||||
# Reload HAProxy
|
||||
/etc/init.d/haproxy reload 2>/dev/null || true
|
||||
|
||||
# Cleanup
|
||||
rm -rf "$tmp_dir"
|
||||
|
||||
log_ok "Published: https://$vhost/"
|
||||
echo "$vhost"
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
# List published droplets
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
cmd_list() {
|
||||
echo "=== Published Droplets ==="
|
||||
|
||||
# MetaBlog sites
|
||||
uci show metablogizer 2>/dev/null | grep "=site$" | sed "s/metablogizer\.\(.*\)=site/\1/" | while read name; do
|
||||
domain=$(uci -q get "metablogizer.$name.domain")
|
||||
enabled=$(uci -q get "metablogizer.$name.enabled")
|
||||
[ "$enabled" = "1" ] && status="[ON]" || status="[OFF]"
|
||||
printf "%-30s %s %s\n" "$name" "$status" "https://$domain/"
|
||||
done
|
||||
|
||||
# Streamlit apps
|
||||
uci show streamlit 2>/dev/null | grep "=instance$" | sed "s/streamlit\.\(.*\)=instance/\1/" | while read name; do
|
||||
domain=$(uci -q get "streamlit.$name.domain")
|
||||
enabled=$(uci -q get "streamlit.$name.enabled")
|
||||
[ "$enabled" = "1" ] && status="[ON]" || status="[OFF]"
|
||||
printf "%-30s %s %s (streamlit)\n" "$name" "$status" "https://$domain/"
|
||||
done
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
# Remove a droplet
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
cmd_remove() {
|
||||
local name="$1"
|
||||
[ -z "$name" ] && { log_error "Usage: dropletctl remove <name>"; return 1; }
|
||||
|
||||
# Check metablogizer
|
||||
if uci -q get "metablogizer.$name" >/dev/null 2>&1; then
|
||||
uci delete "metablogizer.$name"
|
||||
uci commit metablogizer
|
||||
rm -rf "$SITES_DIR/$name"
|
||||
log_ok "Removed MetaBlog: $name"
|
||||
fi
|
||||
|
||||
# Check streamlit
|
||||
if uci -q get "streamlit.$name" >/dev/null 2>&1; then
|
||||
uci delete "streamlit.$name"
|
||||
uci commit streamlit
|
||||
rm -rf "$APPS_DIR/$name"
|
||||
log_ok "Removed Streamlit: $name"
|
||||
fi
|
||||
|
||||
# Remove vhost
|
||||
if command -v haproxyctl >/dev/null 2>&1; then
|
||||
haproxyctl vhost remove "${name}.${DEFAULT_DOMAIN}" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
/etc/init.d/haproxy reload 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
# Rename a droplet
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
cmd_rename() {
|
||||
local old="$1"
|
||||
local new="$2"
|
||||
[ -z "$old" ] || [ -z "$new" ] && { log_error "Usage: dropletctl rename <old> <new>"; return 1; }
|
||||
|
||||
new=$(echo "$new" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]/_/g')
|
||||
|
||||
# Check metablogizer
|
||||
if uci -q get "metablogizer.$old" >/dev/null 2>&1; then
|
||||
local domain="${new}.${DEFAULT_DOMAIN}"
|
||||
mv "$SITES_DIR/$old" "$SITES_DIR/$new" 2>/dev/null
|
||||
uci rename "metablogizer.$old=$new"
|
||||
uci set "metablogizer.$new.name=$new"
|
||||
uci set "metablogizer.$new.domain=$domain"
|
||||
uci commit metablogizer
|
||||
log_ok "Renamed MetaBlog: $old -> $new"
|
||||
fi
|
||||
|
||||
# Check streamlit
|
||||
if uci -q get "streamlit.$old" >/dev/null 2>&1; then
|
||||
local domain="${new}.${DEFAULT_DOMAIN}"
|
||||
mv "$APPS_DIR/$old" "$APPS_DIR/$new" 2>/dev/null
|
||||
uci rename "streamlit.$old=$new"
|
||||
uci set "streamlit.$new.name=$new"
|
||||
uci set "streamlit.$new.domain=$domain"
|
||||
uci commit streamlit
|
||||
log_ok "Renamed Streamlit: $old -> $new"
|
||||
fi
|
||||
|
||||
/etc/init.d/haproxy reload 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
# Main
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
case "$1" in
|
||||
publish) shift; cmd_publish "$@" ;;
|
||||
list) cmd_list ;;
|
||||
remove) shift; cmd_remove "$@" ;;
|
||||
rename) shift; cmd_rename "$@" ;;
|
||||
*)
|
||||
echo "Droplet Publisher - One-Drop Content Publishing"
|
||||
echo ""
|
||||
echo "Usage: dropletctl <command> [args]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " publish <file> <name> [domain] Publish HTML/ZIP as site"
|
||||
echo " list List published droplets"
|
||||
echo " remove <name> Remove a droplet"
|
||||
echo " rename <old> <new> Rename a droplet"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " dropletctl publish mysite.zip mysite"
|
||||
echo " dropletctl publish index.html landing"
|
||||
echo " dropletctl rename landing homepage"
|
||||
;;
|
||||
esac
|
||||
Loading…
Reference in New Issue
Block a user