feat(streamlit): KISS upload - auto-detect ZIP, extract app.py, install deps
Streamlit upload now matches MetaBlogizer KISS pattern: - Auto-detects ZIP files by magic bytes (PK header) - Extracts app.py from ZIP archives automatically - Adds UTF-8 encoding declaration to Python files - Installs requirements.txt dependencies in background - Restarts instance on re-upload for immediate update Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
af1564821f
commit
20cf959185
@ -517,7 +517,7 @@ get_app() {
|
||||
json_close_obj
|
||||
}
|
||||
|
||||
# Upload app (receive base64 content)
|
||||
# Upload app (receive base64 content) - KISS: auto-detects ZIP or .py
|
||||
# NOTE: uhttpd-mod-ubus has a 64KB JSON body limit.
|
||||
# Small files (<40KB) go through RPC directly.
|
||||
# Larger files use chunked upload: upload_chunk + upload_finalize.
|
||||
@ -548,27 +548,115 @@ upload_app() {
|
||||
local data_path
|
||||
config_load "$CONFIG"
|
||||
config_get data_path main data_path "/srv/streamlit"
|
||||
|
||||
local app_file="$data_path/apps/${name}.py"
|
||||
mkdir -p "$data_path/apps"
|
||||
|
||||
base64 -d < "$b64file" > "$app_file" 2>/dev/null
|
||||
# Decode content to temp file first
|
||||
local tmpfile="/tmp/upload_${name}_$$.bin"
|
||||
base64 -d < "$b64file" > "$tmpfile" 2>/dev/null
|
||||
local rc=$?
|
||||
rm -f "$b64file"
|
||||
|
||||
if [ $rc -eq 0 ] && [ -s "$app_file" ]; then
|
||||
uci set "${CONFIG}.${name}=app"
|
||||
uci set "${CONFIG}.${name}.name=$name"
|
||||
uci set "${CONFIG}.${name}.path=${name}.py"
|
||||
uci set "${CONFIG}.${name}.enabled=1"
|
||||
uci commit "$CONFIG"
|
||||
# Auto-create Gitea repo and push (background)
|
||||
streamlitctl gitea push "$name" >/dev/null 2>&1 &
|
||||
json_success "App uploaded: $name"
|
||||
else
|
||||
rm -f "$app_file"
|
||||
json_error "Failed to decode app content"
|
||||
if [ $rc -ne 0 ] || [ ! -s "$tmpfile" ]; then
|
||||
rm -f "$tmpfile"
|
||||
json_error "Failed to decode content"
|
||||
return
|
||||
fi
|
||||
|
||||
# KISS: Auto-detect ZIP by magic bytes (PK = 0x504B)
|
||||
local is_zip_file=0
|
||||
local magic=$(head -c2 "$tmpfile" 2>/dev/null | od -An -tx1 | tr -d ' ')
|
||||
[ "$magic" = "504b" ] && is_zip_file=1
|
||||
|
||||
local app_file="$data_path/apps/${name}.py"
|
||||
|
||||
if [ "$is_zip_file" = "1" ]; then
|
||||
# Extract app.py from ZIP archive
|
||||
local tmpdir="/tmp/extract_${name}_$$"
|
||||
mkdir -p "$tmpdir"
|
||||
|
||||
python3 -c "
|
||||
import zipfile, sys
|
||||
try:
|
||||
z = zipfile.ZipFile('$tmpfile')
|
||||
names = z.namelist()
|
||||
app_py = None
|
||||
req_txt = None
|
||||
for n in names:
|
||||
bn = n.split('/')[-1]
|
||||
if bn == 'app.py':
|
||||
app_py = n
|
||||
elif bn == 'requirements.txt':
|
||||
req_txt = n
|
||||
elif bn.endswith('.py') and app_py is None:
|
||||
app_py = n
|
||||
if app_py:
|
||||
content = z.read(app_py).decode('utf-8', errors='replace')
|
||||
if not content.startswith('# -*- coding'):
|
||||
content = '# -*- coding: utf-8 -*-\n' + content
|
||||
with open('$app_file', 'w') as f:
|
||||
f.write(content)
|
||||
if req_txt:
|
||||
z.extract(req_txt, '$tmpdir')
|
||||
import shutil
|
||||
shutil.move('$tmpdir/' + req_txt, '$tmpdir/requirements.txt')
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
except:
|
||||
sys.exit(1)
|
||||
" 2>/dev/null
|
||||
rc=$?
|
||||
rm -f "$tmpfile"
|
||||
|
||||
if [ $rc -ne 0 ] || [ ! -s "$app_file" ]; then
|
||||
rm -rf "$tmpdir"
|
||||
json_error "No Python file found in ZIP"
|
||||
return
|
||||
fi
|
||||
|
||||
# Install requirements if found
|
||||
if [ -f "$tmpdir/requirements.txt" ] && lxc_running; then
|
||||
cp "$tmpdir/requirements.txt" "$data_path/apps/${name}_requirements.txt"
|
||||
lxc-attach -n "$LXC_NAME" -- pip3 install --break-system-packages \
|
||||
-r "/srv/apps/${name}_requirements.txt" >/dev/null 2>&1 &
|
||||
fi
|
||||
rm -rf "$tmpdir"
|
||||
else
|
||||
# Plain .py file - add encoding declaration if needed
|
||||
local first_line=$(head -c 50 "$tmpfile" 2>/dev/null)
|
||||
if ! echo "$first_line" | grep -q "coding"; then
|
||||
printf '# -*- coding: utf-8 -*-\n' > "$app_file"
|
||||
cat "$tmpfile" >> "$app_file"
|
||||
else
|
||||
mv "$tmpfile" "$app_file"
|
||||
fi
|
||||
rm -f "$tmpfile"
|
||||
fi
|
||||
|
||||
if [ ! -s "$app_file" ]; then
|
||||
json_error "Failed to create app file"
|
||||
return
|
||||
fi
|
||||
|
||||
uci set "${CONFIG}.${name}=app"
|
||||
uci set "${CONFIG}.${name}.name=$name"
|
||||
uci set "${CONFIG}.${name}.path=${name}.py"
|
||||
uci set "${CONFIG}.${name}.enabled=1"
|
||||
uci commit "$CONFIG"
|
||||
|
||||
# Restart instance if running (for re-uploads)
|
||||
if lxc_running; then
|
||||
local port=$(uci -q get "${CONFIG}.${name}.port")
|
||||
if [ -n "$port" ]; then
|
||||
lxc-attach -n "$LXC_NAME" -- pkill -f "port=$port" 2>/dev/null
|
||||
sleep 1
|
||||
streamlitctl instance start "$name" >/dev/null 2>&1 &
|
||||
fi
|
||||
fi
|
||||
|
||||
# Auto-create Gitea repo and push (background)
|
||||
streamlitctl gitea push "$name" >/dev/null 2>&1 &
|
||||
json_success "App uploaded: $name"
|
||||
}
|
||||
|
||||
# Chunked upload: receive a base64 chunk and append to temp file
|
||||
@ -1553,7 +1641,7 @@ get_emancipation() {
|
||||
json_close_obj
|
||||
}
|
||||
|
||||
# One-click upload with auto instance creation
|
||||
# One-click upload with auto instance creation (KISS: handles ZIP or .py)
|
||||
upload_and_deploy() {
|
||||
local tmpinput="/tmp/rpcd_deploy_$$.json"
|
||||
cat > "$tmpinput"
|
||||
@ -1584,17 +1672,99 @@ upload_and_deploy() {
|
||||
config_get data_path main data_path "/srv/streamlit"
|
||||
mkdir -p "$data_path/apps"
|
||||
|
||||
local app_file="$data_path/apps/${name}.py"
|
||||
base64 -d < "$b64file" > "$app_file" 2>/dev/null
|
||||
# Decode content to temp file first
|
||||
local tmpfile="/tmp/upload_${name}_$$.bin"
|
||||
base64 -d < "$b64file" > "$tmpfile" 2>/dev/null
|
||||
local rc=$?
|
||||
rm -f "$b64file"
|
||||
|
||||
if [ $rc -ne 0 ] || [ ! -s "$app_file" ]; then
|
||||
rm -f "$app_file"
|
||||
if [ $rc -ne 0 ] || [ ! -s "$tmpfile" ]; then
|
||||
rm -f "$tmpfile"
|
||||
json_error "Failed to decode content"
|
||||
return
|
||||
fi
|
||||
|
||||
# KISS: Auto-detect ZIP by magic bytes (PK = 0x504B)
|
||||
local is_zip_file=0
|
||||
local magic=$(head -c2 "$tmpfile" 2>/dev/null | od -An -tx1 | tr -d ' ')
|
||||
[ "$magic" = "504b" ] && is_zip_file=1
|
||||
|
||||
local app_file="$data_path/apps/${name}.py"
|
||||
|
||||
if [ "$is_zip_file" = "1" ]; then
|
||||
# Extract app.py from ZIP archive
|
||||
local tmpdir="/tmp/extract_${name}_$$"
|
||||
mkdir -p "$tmpdir"
|
||||
|
||||
# Use Python to extract (unzip may not be available)
|
||||
python3 -c "
|
||||
import zipfile, sys
|
||||
try:
|
||||
z = zipfile.ZipFile('$tmpfile')
|
||||
names = z.namelist()
|
||||
# Find app.py or main .py file
|
||||
app_py = None
|
||||
req_txt = None
|
||||
for n in names:
|
||||
bn = n.split('/')[-1]
|
||||
if bn == 'app.py':
|
||||
app_py = n
|
||||
elif bn == 'requirements.txt':
|
||||
req_txt = n
|
||||
elif bn.endswith('.py') and app_py is None:
|
||||
app_py = n
|
||||
if app_py:
|
||||
content = z.read(app_py).decode('utf-8', errors='replace')
|
||||
# Add UTF-8 encoding if not present
|
||||
if not content.startswith('# -*- coding'):
|
||||
content = '# -*- coding: utf-8 -*-\n' + content
|
||||
with open('$app_file', 'w') as f:
|
||||
f.write(content)
|
||||
# Extract requirements.txt if present
|
||||
if req_txt:
|
||||
z.extract(req_txt, '$tmpdir')
|
||||
import shutil
|
||||
shutil.move('$tmpdir/' + req_txt, '$tmpdir/requirements.txt')
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(str(e), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
" 2>/dev/null
|
||||
rc=$?
|
||||
rm -f "$tmpfile"
|
||||
|
||||
if [ $rc -ne 0 ] || [ ! -s "$app_file" ]; then
|
||||
rm -rf "$tmpdir"
|
||||
json_error "No Python file found in ZIP"
|
||||
return
|
||||
fi
|
||||
|
||||
# Install requirements if found
|
||||
if [ -f "$tmpdir/requirements.txt" ] && lxc_running; then
|
||||
cp "$tmpdir/requirements.txt" "$data_path/apps/${name}_requirements.txt"
|
||||
lxc-attach -n "$LXC_NAME" -- pip3 install --break-system-packages \
|
||||
-r "/srv/apps/${name}_requirements.txt" >/dev/null 2>&1 &
|
||||
fi
|
||||
rm -rf "$tmpdir"
|
||||
else
|
||||
# Plain .py file - add encoding declaration if needed
|
||||
local first_line=$(head -c 50 "$tmpfile" 2>/dev/null)
|
||||
if ! echo "$first_line" | grep -q "coding"; then
|
||||
printf '# -*- coding: utf-8 -*-\n' > "$app_file"
|
||||
cat "$tmpfile" >> "$app_file"
|
||||
else
|
||||
mv "$tmpfile" "$app_file"
|
||||
fi
|
||||
rm -f "$tmpfile"
|
||||
fi
|
||||
|
||||
if [ ! -s "$app_file" ]; then
|
||||
json_error "Failed to create app file"
|
||||
return
|
||||
fi
|
||||
|
||||
# Register app in UCI
|
||||
uci set "${CONFIG}.${name}=app"
|
||||
uci set "${CONFIG}.${name}.name=$name"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user