Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5f92e4da3 | ||
|
|
64b9f28444 | ||
|
|
fe27b5e41a |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,4 +3,4 @@ logs/
|
|||||||
nostr_core_lib/
|
nostr_core_lib/
|
||||||
blobs/
|
blobs/
|
||||||
c-relay/
|
c-relay/
|
||||||
|
text_graph/
|
||||||
|
|||||||
131
Dockerfile.alpine-musl
Normal file
131
Dockerfile.alpine-musl
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# Alpine-based MUSL static binary builder for Ginxsom
|
||||||
|
# Produces truly portable binaries with zero runtime dependencies
|
||||||
|
|
||||||
|
ARG DEBUG_BUILD=false
|
||||||
|
|
||||||
|
FROM alpine:3.19 AS builder
|
||||||
|
|
||||||
|
# Re-declare build argument in this stage
|
||||||
|
ARG DEBUG_BUILD=false
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
build-base \
|
||||||
|
musl-dev \
|
||||||
|
git \
|
||||||
|
cmake \
|
||||||
|
pkgconfig \
|
||||||
|
autoconf \
|
||||||
|
automake \
|
||||||
|
libtool \
|
||||||
|
openssl-dev \
|
||||||
|
openssl-libs-static \
|
||||||
|
zlib-dev \
|
||||||
|
zlib-static \
|
||||||
|
curl-dev \
|
||||||
|
curl-static \
|
||||||
|
sqlite-dev \
|
||||||
|
sqlite-static \
|
||||||
|
fcgi-dev \
|
||||||
|
fcgi \
|
||||||
|
linux-headers \
|
||||||
|
wget \
|
||||||
|
bash \
|
||||||
|
nghttp2-dev \
|
||||||
|
nghttp2-static \
|
||||||
|
c-ares-dev \
|
||||||
|
c-ares-static \
|
||||||
|
libidn2-dev \
|
||||||
|
libidn2-static \
|
||||||
|
libunistring-dev \
|
||||||
|
libunistring-static \
|
||||||
|
libpsl-dev \
|
||||||
|
libpsl-static \
|
||||||
|
brotli-dev \
|
||||||
|
brotli-static
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Build libsecp256k1 static (cached layer - only rebuilds if Alpine version changes)
|
||||||
|
RUN cd /tmp && \
|
||||||
|
git clone https://github.com/bitcoin-core/secp256k1.git && \
|
||||||
|
cd secp256k1 && \
|
||||||
|
./autogen.sh && \
|
||||||
|
./configure --enable-static --disable-shared --prefix=/usr \
|
||||||
|
CFLAGS="-fPIC" && \
|
||||||
|
make -j$(nproc) && \
|
||||||
|
make install && \
|
||||||
|
rm -rf /tmp/secp256k1
|
||||||
|
|
||||||
|
# Copy only submodule configuration and git directory
|
||||||
|
COPY .gitmodules /build/.gitmodules
|
||||||
|
COPY .git /build/.git
|
||||||
|
|
||||||
|
# Initialize submodules (cached unless .gitmodules changes)
|
||||||
|
RUN git submodule update --init --recursive
|
||||||
|
|
||||||
|
# Copy nostr_core_lib source files (cached unless nostr_core_lib changes)
|
||||||
|
COPY nostr_core_lib /build/nostr_core_lib/
|
||||||
|
|
||||||
|
# Build nostr_core_lib with required NIPs (cached unless nostr_core_lib changes)
|
||||||
|
# Disable fortification in build.sh to prevent __*_chk symbol issues
|
||||||
|
# NIPs: 001(Basic), 006(Keys), 013(PoW), 017(DMs), 019(Bech32), 042(Auth), 044(Encryption), 059(Gift Wrap)
|
||||||
|
RUN cd nostr_core_lib && \
|
||||||
|
chmod +x build.sh && \
|
||||||
|
sed -i 's/CFLAGS="-Wall -Wextra -std=c99 -fPIC -O2"/CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -std=c99 -fPIC -O2"/' build.sh && \
|
||||||
|
rm -f *.o *.a 2>/dev/null || true && \
|
||||||
|
./build.sh --nips=1,6,13,17,19,42,44,59
|
||||||
|
|
||||||
|
# Copy web interface files for embedding
|
||||||
|
COPY api/ /build/api/
|
||||||
|
COPY scripts/embed_web_files.sh /build/scripts/
|
||||||
|
|
||||||
|
# Create src directory and embed web files into C headers
|
||||||
|
RUN mkdir -p src && \
|
||||||
|
chmod +x scripts/embed_web_files.sh && \
|
||||||
|
./scripts/embed_web_files.sh
|
||||||
|
|
||||||
|
# Copy Ginxsom source files LAST (only this layer rebuilds on source changes)
|
||||||
|
COPY src/ /build/src/
|
||||||
|
COPY include/ /build/include/
|
||||||
|
|
||||||
|
# Build Ginxsom with full static linking (only rebuilds when src/ changes)
|
||||||
|
# Disable fortification to avoid __*_chk symbols that don't exist in MUSL
|
||||||
|
# Use conditional compilation flags based on DEBUG_BUILD argument
|
||||||
|
RUN if [ "$DEBUG_BUILD" = "true" ]; then \
|
||||||
|
CFLAGS="-g -O0 -DDEBUG"; \
|
||||||
|
STRIP_CMD=""; \
|
||||||
|
echo "Building with DEBUG symbols enabled"; \
|
||||||
|
else \
|
||||||
|
CFLAGS="-O2"; \
|
||||||
|
STRIP_CMD="strip /build/ginxsom-fcgi_static"; \
|
||||||
|
echo "Building optimized production binary"; \
|
||||||
|
fi && \
|
||||||
|
gcc -static $CFLAGS -Wall -Wextra -std=gnu99 \
|
||||||
|
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
|
||||||
|
-I. -Iinclude -Inostr_core_lib -Inostr_core_lib/nostr_core \
|
||||||
|
-Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket \
|
||||||
|
src/main.c src/admin_api.c src/admin_auth.c src/admin_event.c \
|
||||||
|
src/admin_handlers.c src/admin_interface.c src/admin_commands.c \
|
||||||
|
src/bud04.c src/bud06.c src/bud08.c src/bud09.c \
|
||||||
|
src/request_validator.c src/relay_client.c \
|
||||||
|
nostr_core_lib/nostr_core/core_relay_pool.c \
|
||||||
|
-o /build/ginxsom-fcgi_static \
|
||||||
|
nostr_core_lib/libnostr_core_x64.a \
|
||||||
|
-lfcgi -lsqlite3 -lsecp256k1 -lssl -lcrypto -lcurl \
|
||||||
|
-lnghttp2 -lcares -lidn2 -lunistring -lpsl -lbrotlidec -lbrotlicommon \
|
||||||
|
-lz -lpthread -lm -ldl && \
|
||||||
|
eval "$STRIP_CMD"
|
||||||
|
|
||||||
|
# Verify it's truly static
|
||||||
|
RUN echo "=== Binary Information ===" && \
|
||||||
|
file /build/ginxsom-fcgi_static && \
|
||||||
|
ls -lh /build/ginxsom-fcgi_static && \
|
||||||
|
echo "=== Checking for dynamic dependencies ===" && \
|
||||||
|
(ldd /build/ginxsom-fcgi_static 2>&1 || echo "Binary is static") && \
|
||||||
|
echo "=== Build complete ==="
|
||||||
|
|
||||||
|
# Output stage - just the binary
|
||||||
|
FROM scratch AS output
|
||||||
|
COPY --from=builder /build/ginxsom-fcgi_static /ginxsom-fcgi_static
|
||||||
20
Makefile
20
Makefile
@@ -43,10 +43,18 @@ $(POOL_OBJ): $(POOL_SRC) | $(BUILDDIR)
|
|||||||
$(TARGET): $(OBJECTS) $(POOL_OBJ)
|
$(TARGET): $(OBJECTS) $(POOL_OBJ)
|
||||||
$(CC) $(OBJECTS) $(POOL_OBJ) $(LIBS) -o $@
|
$(CC) $(OBJECTS) $(POOL_OBJ) $(LIBS) -o $@
|
||||||
|
|
||||||
# Clean build files
|
# Clean build files (preserves static binaries)
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(BUILDDIR)
|
rm -rf $(BUILDDIR)
|
||||||
rm -f $(EMBEDDED_HEADER)
|
rm -f $(EMBEDDED_HEADER)
|
||||||
|
@echo "Note: Static binaries (ginxsom-fcgi_static_*) are preserved."
|
||||||
|
@echo "To remove everything: make clean-all"
|
||||||
|
|
||||||
|
# Clean everything including static binaries
|
||||||
|
clean-all:
|
||||||
|
rm -rf $(BUILDDIR)
|
||||||
|
rm -f $(EMBEDDED_HEADER)
|
||||||
|
@echo "✓ All build artifacts removed"
|
||||||
|
|
||||||
# Install (copy to system location)
|
# Install (copy to system location)
|
||||||
install: $(TARGET)
|
install: $(TARGET)
|
||||||
@@ -69,4 +77,12 @@ debug: $(TARGET)
|
|||||||
embed:
|
embed:
|
||||||
@$(EMBED_SCRIPT)
|
@$(EMBED_SCRIPT)
|
||||||
|
|
||||||
.PHONY: all clean install uninstall run debug embed
|
# Static MUSL build via Docker
|
||||||
|
static:
|
||||||
|
./build_static.sh
|
||||||
|
|
||||||
|
# Static MUSL build with debug symbols
|
||||||
|
static-debug:
|
||||||
|
./build_static.sh --debug
|
||||||
|
|
||||||
|
.PHONY: all clean clean-all install uninstall run debug embed static static-debug
|
||||||
|
|||||||
@@ -97,12 +97,12 @@
|
|||||||
<td id="total-events">-</td>
|
<td id="total-events">-</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Process ID</td>
|
<td>Total Size</td>
|
||||||
<td id="process-id">-</td>
|
<td id="total-size">-</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Active Connections</td>
|
<td>Process ID</td>
|
||||||
<td id="active-subscriptions">-</td>
|
<td id="process-id">-</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Memory Usage</td>
|
<td>Memory Usage</td>
|
||||||
@@ -188,7 +188,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Rank</th>
|
<th>Rank</th>
|
||||||
<th>Pubkey</th>
|
<th>Pubkey</th>
|
||||||
<th>Event Count</th>
|
<th>Blob Count</th>
|
||||||
|
<th>Total Size</th>
|
||||||
<th>Percentage</th>
|
<th>Percentage</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
362
api/index.js
362
api/index.js
@@ -44,7 +44,7 @@ let pendingSqlQueries = new Map();
|
|||||||
|
|
||||||
// Real-time event rate chart
|
// Real-time event rate chart
|
||||||
let eventRateChart = null;
|
let eventRateChart = null;
|
||||||
let previousTotalEvents = 0; // Track previous total for rate calculation
|
let previousTotalBlobs = 0; // Track previous total for rate calculation
|
||||||
|
|
||||||
// Relay Events state - now handled by main subscription
|
// Relay Events state - now handled by main subscription
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ async function fetchRelayInfo(relayUrl) {
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/nostr+json',
|
'Accept': 'application/nostr+json',
|
||||||
'User-Agent': 'C-Relay-Admin-API/1.0'
|
'User-Agent': 'Blossom-Admin-API/1.0'
|
||||||
},
|
},
|
||||||
timeout: 10000 // 10 second timeout
|
timeout: 10000 // 10 second timeout
|
||||||
});
|
});
|
||||||
@@ -511,10 +511,8 @@ function updateAdminSectionsVisibility() {
|
|||||||
function loadCurrentPageData() {
|
function loadCurrentPageData() {
|
||||||
switch (currentPage) {
|
switch (currentPage) {
|
||||||
case 'statistics':
|
case 'statistics':
|
||||||
// Load statistics immediately (no auto-refresh - using real-time monitoring events)
|
// Start HTTP polling for statistics (polls every 10 seconds)
|
||||||
sendStatsQuery().catch(error => {
|
startStatsPolling();
|
||||||
console.log('Auto-fetch statistics failed: ' + error.message);
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
case 'configuration':
|
case 'configuration':
|
||||||
// Load configuration
|
// Load configuration
|
||||||
@@ -1311,7 +1309,7 @@ function initializeEventRateChart() {
|
|||||||
eventRateChart = new ASCIIBarChart('event-rate-chart', {
|
eventRateChart = new ASCIIBarChart('event-rate-chart', {
|
||||||
maxHeight: 11, // Chart height in lines
|
maxHeight: 11, // Chart height in lines
|
||||||
maxDataPoints: 76, // Show last 76 bins (5+ minutes of history)
|
maxDataPoints: 76, // Show last 76 bins (5+ minutes of history)
|
||||||
title: 'New Events', // Chart title
|
title: 'New Blobs', // Chart title
|
||||||
xAxisLabel: '', // No X-axis label
|
xAxisLabel: '', // No X-axis label
|
||||||
yAxisLabel: '', // No Y-axis label
|
yAxisLabel: '', // No Y-axis label
|
||||||
autoFitWidth: true, // Enable responsive font sizing
|
autoFitWidth: true, // Enable responsive font sizing
|
||||||
@@ -3298,7 +3296,7 @@ async function testPostEvent() {
|
|||||||
["t", "test"],
|
["t", "test"],
|
||||||
["client", "c-relay-admin-api"]
|
["client", "c-relay-admin-api"]
|
||||||
],
|
],
|
||||||
content: `Test event from C-Relay Admin API at ${new Date().toISOString()}`
|
content: `Test event from Blossom Admin API at ${new Date().toISOString()}`
|
||||||
};
|
};
|
||||||
|
|
||||||
logTestEvent('SENT', `Test event (before signing): ${JSON.stringify(testEvent)}`, 'EVENT');
|
logTestEvent('SENT', `Test event (before signing): ${JSON.stringify(testEvent)}`, 'EVENT');
|
||||||
@@ -3642,8 +3640,8 @@ function updateRelayInfoInHeader() {
|
|||||||
|
|
||||||
// Get relay info from NIP-11 data or use defaults
|
// Get relay info from NIP-11 data or use defaults
|
||||||
const relayInfo = getRelayInfo();
|
const relayInfo = getRelayInfo();
|
||||||
const relayName = relayInfo.name || 'C-Relay';
|
const relayName = relayInfo.name || 'Blossom';
|
||||||
const relayDescription = relayInfo.description || 'Nostr Relay';
|
const relayDescription = relayInfo.description || 'Blob Storage Server';
|
||||||
|
|
||||||
// Convert relay pubkey to npub
|
// Convert relay pubkey to npub
|
||||||
let relayNpub = 'Loading...';
|
let relayNpub = 'Loading...';
|
||||||
@@ -3682,8 +3680,8 @@ function getRelayInfo() {
|
|||||||
|
|
||||||
// Default values
|
// Default values
|
||||||
return {
|
return {
|
||||||
name: 'C-Relay',
|
name: 'Blossom',
|
||||||
description: 'Nostr Relay',
|
description: 'Blob Storage Server',
|
||||||
pubkey: relayPubkey
|
pubkey: relayPubkey
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -3692,17 +3690,17 @@ function getRelayInfo() {
|
|||||||
function updateStoredRelayInfo(configData) {
|
function updateStoredRelayInfo(configData) {
|
||||||
if (configData && configData.data) {
|
if (configData && configData.data) {
|
||||||
// Extract relay info from config data - handle both object and array formats
|
// Extract relay info from config data - handle both object and array formats
|
||||||
let relayName = 'C-Relay';
|
let relayName = 'Blossom';
|
||||||
let relayDescription = 'Nostr Relay';
|
let relayDescription = 'Blob Storage Server';
|
||||||
|
|
||||||
if (Array.isArray(configData.data)) {
|
if (Array.isArray(configData.data)) {
|
||||||
// Array format: [{key: 'x', value: 'y'}, ...]
|
// Array format: [{key: 'x', value: 'y'}, ...]
|
||||||
relayName = configData.data.find(item => item.key === 'relay_name')?.value || 'C-Relay';
|
relayName = configData.data.find(item => item.key === 'relay_name')?.value || 'Blossom';
|
||||||
relayDescription = configData.data.find(item => item.key === 'relay_description')?.value || 'Nostr Relay';
|
relayDescription = configData.data.find(item => item.key === 'relay_description')?.value || 'Blob Storage Server';
|
||||||
} else {
|
} else {
|
||||||
// Object format: {key1: 'value1', key2: 'value2', ...}
|
// Object format: {key1: 'value1', key2: 'value2', ...}
|
||||||
relayName = configData.data.relay_name || 'C-Relay';
|
relayName = configData.data.relay_name || 'Blossom';
|
||||||
relayDescription = configData.data.relay_description || 'Nostr Relay';
|
relayDescription = configData.data.relay_description || 'Blob Storage Server';
|
||||||
}
|
}
|
||||||
|
|
||||||
relayInfoData = {
|
relayInfoData = {
|
||||||
@@ -3837,7 +3835,7 @@ async function sendRestartCommand() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send stats_query command to get database statistics using Administrator API (inner events)
|
// Send query_view commands to get database statistics via HTTP POST
|
||||||
async function sendStatsQuery() {
|
async function sendStatsQuery() {
|
||||||
if (!isLoggedIn || !userPubkey) {
|
if (!isLoggedIn || !userPubkey) {
|
||||||
log('Must be logged in to query database statistics', 'ERROR');
|
log('Must be logged in to query database statistics', 'ERROR');
|
||||||
@@ -3845,74 +3843,98 @@ async function sendStatsQuery() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!relayPool) {
|
|
||||||
log('SimplePool connection not available', 'ERROR');
|
|
||||||
updateStatsStatus('error', 'No relay connection');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
updateStatsStatus('loading', 'Querying database...');
|
updateStatsStatus('loading', 'Querying database...');
|
||||||
|
|
||||||
// Create command array for stats query
|
// Query blob_overview view for basic stats
|
||||||
const command_array = ["stats_query", "all"];
|
const overviewData = await sendAdminCommandHTTP(['query_view', 'blob_overview']);
|
||||||
|
handleViewQueryResponse('blob_overview', overviewData);
|
||||||
|
|
||||||
// Encrypt the command array directly using NIP-44
|
// Query blob_type_distribution view
|
||||||
const encrypted_content = await encryptForRelay(JSON.stringify(command_array));
|
const typeData = await sendAdminCommandHTTP(['query_view', 'blob_type_distribution']);
|
||||||
if (!encrypted_content) {
|
handleViewQueryResponse('blob_type_distribution', typeData);
|
||||||
throw new Error('Failed to encrypt command array');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create single kind 23456 admin event
|
// Query blob_time_stats view
|
||||||
const statsEvent = {
|
const timeData = await sendAdminCommandHTTP(['query_view', 'blob_time_stats']);
|
||||||
kind: 23456,
|
handleViewQueryResponse('blob_time_stats', timeData);
|
||||||
pubkey: userPubkey,
|
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
|
||||||
tags: [["p", getRelayPubkey()]],
|
|
||||||
content: encrypted_content
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sign the event
|
// Query top_uploaders view
|
||||||
const signedEvent = await window.nostr.signEvent(statsEvent);
|
const uploadersData = await sendAdminCommandHTTP(['query_view', 'top_uploaders']);
|
||||||
if (!signedEvent || !signedEvent.sig) {
|
handleViewQueryResponse('top_uploaders', uploadersData);
|
||||||
throw new Error('Event signing failed');
|
|
||||||
}
|
log('All view queries completed successfully', 'INFO');
|
||||||
|
updateStatsStatus('loaded');
|
||||||
log('Sending stats query command...', 'INFO');
|
|
||||||
|
|
||||||
// Publish via SimplePool
|
|
||||||
const url = relayConnectionUrl.value.trim();
|
|
||||||
const publishPromises = relayPool.publish([url], signedEvent);
|
|
||||||
|
|
||||||
// Use Promise.allSettled to capture per-relay outcomes
|
|
||||||
const results = await Promise.allSettled(publishPromises);
|
|
||||||
|
|
||||||
// Check if any relay accepted the event
|
|
||||||
let successCount = 0;
|
|
||||||
results.forEach((result, index) => {
|
|
||||||
if (result.status === 'fulfilled') {
|
|
||||||
successCount++;
|
|
||||||
log(`Stats query published successfully to relay ${index}`, 'INFO');
|
|
||||||
} else {
|
|
||||||
log(`Stats query failed on relay ${index}: ${result.reason?.message || result.reason}`, 'ERROR');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (successCount === 0) {
|
|
||||||
const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; ');
|
|
||||||
throw new Error(`All relays rejected stats query event. Details: ${errorDetails}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log('Stats query command sent successfully - waiting for response...', 'INFO');
|
|
||||||
updateStatsStatus('waiting', 'Waiting for response...');
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`Failed to send stats query: ${error.message}`, 'ERROR');
|
log(`Failed to send view queries: ${error.message}`, 'ERROR');
|
||||||
updateStatsStatus('error', error.message);
|
updateStatsStatus('error', error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle stats_query response and populate tables
|
// Handle query_view response and populate appropriate table
|
||||||
|
function handleViewQueryResponse(viewName, responseData) {
|
||||||
|
try {
|
||||||
|
console.log(`Processing view query response: ${viewName}`, responseData);
|
||||||
|
|
||||||
|
if (responseData.query_type !== 'query_view') {
|
||||||
|
log('Ignoring non-view-query response', 'WARNING');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseData.status !== 'success') {
|
||||||
|
log(`View query failed: ${responseData.error || 'Unknown error'}`, 'ERROR');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route to appropriate handler based on view name
|
||||||
|
switch (viewName) {
|
||||||
|
case 'blob_overview':
|
||||||
|
if (responseData.data && Array.isArray(responseData.data) && responseData.data.length > 0) {
|
||||||
|
const overviewData = responseData.data[0];
|
||||||
|
populateStatsOverview(overviewData);
|
||||||
|
|
||||||
|
// Update chart with total blobs count
|
||||||
|
const currentTotal = overviewData.total_blobs;
|
||||||
|
if (currentTotal !== undefined) {
|
||||||
|
// Calculate new blobs since last update for chart
|
||||||
|
if (previousTotalBlobs > 0) {
|
||||||
|
const newBlobs = currentTotal - previousTotalBlobs;
|
||||||
|
if (newBlobs > 0 && eventRateChart) {
|
||||||
|
console.log(`Adding ${newBlobs} new blobs to rate chart (${currentTotal} - ${previousTotalBlobs})`);
|
||||||
|
eventRateChart.addValue(newBlobs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update previous total for next calculation
|
||||||
|
previousTotalBlobs = currentTotal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'blob_type_distribution':
|
||||||
|
if (responseData.data && Array.isArray(responseData.data)) {
|
||||||
|
populateStatsKinds(responseData.data);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'blob_time_stats':
|
||||||
|
if (responseData.data && Array.isArray(responseData.data) && responseData.data.length > 0) {
|
||||||
|
populateStatsTime(responseData.data[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'top_uploaders':
|
||||||
|
if (responseData.data && Array.isArray(responseData.data)) {
|
||||||
|
populateStatsPubkeys(responseData.data);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(`Unknown view name: ${viewName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
log(`Error processing ${viewName} response: ${error.message}`, 'ERROR');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy handler for backward compatibility
|
||||||
function handleStatsQueryResponse(responseData) {
|
function handleStatsQueryResponse(responseData) {
|
||||||
try {
|
try {
|
||||||
log('Processing stats query response...', 'INFO');
|
log('Processing stats query response...', 'INFO');
|
||||||
@@ -3923,20 +3945,30 @@ function handleStatsQueryResponse(responseData) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate overview table
|
// Extract the actual data object
|
||||||
populateStatsOverview(responseData);
|
const statsData = responseData.data || responseData;
|
||||||
|
console.log('Extracted stats data:', statsData);
|
||||||
|
|
||||||
// Populate event kinds table
|
// Populate overview table with blob statistics
|
||||||
populateStatsKinds(responseData);
|
populateStatsOverview(statsData);
|
||||||
|
|
||||||
|
// Populate blob type distribution table
|
||||||
|
if (statsData.type_distribution && Array.isArray(statsData.type_distribution)) {
|
||||||
|
populateStatsKinds(statsData.type_distribution);
|
||||||
|
}
|
||||||
|
|
||||||
// Populate time-based statistics
|
// Populate time-based statistics
|
||||||
populateStatsTime(responseData);
|
if (statsData.blobs_24h !== undefined) {
|
||||||
|
populateStatsTime(statsData);
|
||||||
|
}
|
||||||
|
|
||||||
// Populate top pubkeys table
|
// Populate top uploaders table
|
||||||
populateStatsPubkeys(responseData);
|
if (statsData.top_uploaders && Array.isArray(statsData.top_uploaders)) {
|
||||||
|
populateStatsPubkeys(statsData.top_uploaders);
|
||||||
|
}
|
||||||
|
|
||||||
updateStatsStatus('loaded');
|
updateStatsStatus('loaded');
|
||||||
log('Database statistics updated successfully', 'INFO');
|
log('Blob statistics updated successfully', 'INFO');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`Error processing stats response: ${error.message}`, 'ERROR');
|
log(`Error processing stats response: ${error.message}`, 'ERROR');
|
||||||
@@ -3952,27 +3984,28 @@ function updateStatsFromMonitoringEvent(monitoringData) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update total events count and track rate for chart
|
// Update total blobs count and track rate for chart
|
||||||
if (monitoringData.total_events !== undefined) {
|
// Support both total_blobs (new) and total_events (legacy) field names
|
||||||
const currentTotal = monitoringData.total_events;
|
const currentTotal = monitoringData.total_blobs || monitoringData.total_events;
|
||||||
|
if (currentTotal !== undefined) {
|
||||||
updateStatsCell('total-events', currentTotal.toString());
|
updateStatsCell('total-events', currentTotal.toString());
|
||||||
|
|
||||||
// Calculate new events since last update for chart
|
// Calculate new blobs since last update for chart
|
||||||
if (previousTotalEvents > 0) {
|
if (previousTotalBlobs > 0) {
|
||||||
const newEvents = currentTotal - previousTotalEvents;
|
const newBlobs = currentTotal - previousTotalBlobs;
|
||||||
if (newEvents > 0 && eventRateChart) {
|
if (newBlobs > 0 && eventRateChart) {
|
||||||
console.log(`Adding ${newEvents} new events to rate chart (${currentTotal} - ${previousTotalEvents})`);
|
console.log(`Adding ${newBlobs} new blobs to rate chart (${currentTotal} - ${previousTotalBlobs})`);
|
||||||
eventRateChart.addValue(newEvents);
|
eventRateChart.addValue(newBlobs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update previous total for next calculation
|
// Update previous total for next calculation
|
||||||
previousTotalEvents = currentTotal;
|
previousTotalBlobs = currentTotal;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update event kinds table with real-time data
|
// Update blob types table with real-time data
|
||||||
if (monitoringData.kinds && Array.isArray(monitoringData.kinds)) {
|
if (monitoringData.kinds && Array.isArray(monitoringData.kinds)) {
|
||||||
populateStatsKindsFromMonitoring(monitoringData.kinds, monitoringData.total_events);
|
populateStatsKindsFromMonitoring(monitoringData.kinds, currentTotal);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -4110,32 +4143,44 @@ function populateStatsOverview(data) {
|
|||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
// Update individual cells with flash animation for changed values
|
// Update individual cells with flash animation for changed values
|
||||||
updateStatsCell('db-size', data.database_size_bytes ? formatFileSize(data.database_size_bytes) : '-');
|
// Backend sends: total_bytes, total_blobs, first_upload, last_upload
|
||||||
updateStatsCell('total-events', data.total_events || '-');
|
updateStatsCell('db-size', data.total_bytes ? formatFileSize(data.total_bytes) : '-');
|
||||||
updateStatsCell('oldest-event', data.database_created_at ? formatTimestamp(data.database_created_at) : '-');
|
updateStatsCell('total-size', data.total_bytes ? formatFileSize(data.total_bytes) : '-');
|
||||||
updateStatsCell('newest-event', data.latest_event_at ? formatTimestamp(data.latest_event_at) : '-');
|
updateStatsCell('total-events', data.total_blobs || '-');
|
||||||
|
updateStatsCell('oldest-event', data.first_upload ? formatTimestamp(data.first_upload) : '-');
|
||||||
|
updateStatsCell('newest-event', data.last_upload ? formatTimestamp(data.last_upload) : '-');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate event kinds distribution table
|
// Populate event kinds distribution table
|
||||||
function populateStatsKinds(data) {
|
function populateStatsKinds(data) {
|
||||||
const tableBody = document.getElementById('stats-kinds-table-body');
|
const tableBody = document.getElementById('stats-kinds-table-body');
|
||||||
if (!tableBody || !data.event_kinds) return;
|
if (!tableBody) return;
|
||||||
|
|
||||||
tableBody.innerHTML = '';
|
tableBody.innerHTML = '';
|
||||||
|
|
||||||
if (data.event_kinds.length === 0) {
|
// Handle both old format (data.event_kinds) and new format (direct array from query_view)
|
||||||
|
const kindsData = data.event_kinds || data;
|
||||||
|
|
||||||
|
if (!Array.isArray(kindsData) || kindsData.length === 0) {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.innerHTML = '<td colspan="3" style="text-align: center; font-style: italic;">No event data</td>';
|
row.innerHTML = '<td colspan="3" style="text-align: center; font-style: italic;">No blob type data</td>';
|
||||||
tableBody.appendChild(row);
|
tableBody.appendChild(row);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
data.event_kinds.forEach(kind => {
|
// Calculate total for percentages if not provided
|
||||||
|
const total = kindsData.reduce((sum, item) => sum + (item.blob_count || item.count || 0), 0);
|
||||||
|
|
||||||
|
kindsData.forEach(item => {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
|
const mimeType = item.mime_type || item.kind || '-';
|
||||||
|
const count = item.blob_count || item.count || 0;
|
||||||
|
const percentage = item.percentage || (total > 0 ? ((count / total) * 100).toFixed(1) : 0);
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td>${kind.kind}</td>
|
<td>${mimeType}</td>
|
||||||
<td>${kind.count}</td>
|
<td>${count}</td>
|
||||||
<td>${kind.percentage}%</td>
|
<td>${percentage}%</td>
|
||||||
`;
|
`;
|
||||||
tableBody.appendChild(row);
|
tableBody.appendChild(row);
|
||||||
});
|
});
|
||||||
@@ -4145,48 +4190,59 @@ function populateStatsKinds(data) {
|
|||||||
function populateStatsTime(data) {
|
function populateStatsTime(data) {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
// Access the nested time_stats object from backend response
|
|
||||||
const timeStats = data.time_stats || {};
|
|
||||||
|
|
||||||
// Update cells with flash animation for changed values
|
// Update cells with flash animation for changed values
|
||||||
updateStatsCell('events-24h', timeStats.last_24h || '0');
|
updateStatsCell('events-24h', data.blobs_24h || '0');
|
||||||
updateStatsCell('events-7d', timeStats.last_7d || '0');
|
updateStatsCell('events-7d', data.blobs_7d || '0');
|
||||||
updateStatsCell('events-30d', timeStats.last_30d || '0');
|
updateStatsCell('events-30d', data.blobs_30d || '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate top pubkeys table
|
// Populate top pubkeys table
|
||||||
function populateStatsPubkeys(data) {
|
function populateStatsPubkeys(data) {
|
||||||
const tableBody = document.getElementById('stats-pubkeys-table-body');
|
const tableBody = document.getElementById('stats-pubkeys-table-body');
|
||||||
if (!tableBody || !data.top_pubkeys) return;
|
if (!tableBody) return;
|
||||||
|
|
||||||
tableBody.innerHTML = '';
|
tableBody.innerHTML = '';
|
||||||
|
|
||||||
if (data.top_pubkeys.length === 0) {
|
// Handle both old format (data.top_pubkeys) and new format (direct array from query_view)
|
||||||
|
const pubkeysData = data.top_pubkeys || data;
|
||||||
|
|
||||||
|
if (!Array.isArray(pubkeysData) || pubkeysData.length === 0) {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.innerHTML = '<td colspan="4" style="text-align: center; font-style: italic;">No pubkey data</td>';
|
row.innerHTML = '<td colspan="5" style="text-align: center; font-style: italic;">No uploader data</td>';
|
||||||
tableBody.appendChild(row);
|
tableBody.appendChild(row);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
data.top_pubkeys.forEach((pubkey, index) => {
|
// Calculate total for percentages if not provided
|
||||||
|
const total = pubkeysData.reduce((sum, item) => sum + (item.blob_count || 0), 0);
|
||||||
|
|
||||||
|
pubkeysData.forEach((item, index) => {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
|
// Handle both uploader_pubkey (new) and pubkey (old) field names
|
||||||
|
const pubkeyValue = item.uploader_pubkey || item.pubkey || '-';
|
||||||
|
const count = item.blob_count || 0;
|
||||||
|
const totalBytes = item.total_bytes || 0;
|
||||||
|
const percentage = item.percentage || (total > 0 ? ((count / total) * 100).toFixed(1) : 0);
|
||||||
|
|
||||||
// Convert hex pubkey to npub for display
|
// Convert hex pubkey to npub for display
|
||||||
let displayPubkey = pubkey.pubkey || '-';
|
let displayPubkey = pubkeyValue;
|
||||||
let npubLink = displayPubkey;
|
let npubLink = displayPubkey;
|
||||||
try {
|
try {
|
||||||
if (pubkey.pubkey && pubkey.pubkey.length === 64 && /^[0-9a-fA-F]+$/.test(pubkey.pubkey)) {
|
if (pubkeyValue && pubkeyValue.length === 64 && /^[0-9a-fA-F]+$/.test(pubkeyValue)) {
|
||||||
const npub = window.NostrTools.nip19.npubEncode(pubkey.pubkey);
|
const npub = window.NostrTools.nip19.npubEncode(pubkeyValue);
|
||||||
displayPubkey = npub;
|
displayPubkey = npub;
|
||||||
npubLink = `<a href="https://njump.me/${npub}" target="_blank" class="npub-link">${npub}</a>`;
|
npubLink = `<a href="https://njump.me/${npub}" target="_blank" class="npub-link">${npub}</a>`;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Failed to encode pubkey to npub:', error.message);
|
console.log('Failed to encode pubkey to npub:', error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td>${index + 1}</td>
|
<td>${index + 1}</td>
|
||||||
<td style="font-family: 'Courier New', monospace; font-size: 12px; word-break: break-all;">${npubLink}</td>
|
<td style="font-family: 'Courier New', monospace; font-size: 12px; word-break: break-all;">${npubLink}</td>
|
||||||
<td>${pubkey.event_count}</td>
|
<td>${count}</td>
|
||||||
<td>${pubkey.percentage}%</td>
|
<td>${formatFileSize(totalBytes)}</td>
|
||||||
|
<td>${percentage}%</td>
|
||||||
`;
|
`;
|
||||||
tableBody.appendChild(row);
|
tableBody.appendChild(row);
|
||||||
});
|
});
|
||||||
@@ -4467,15 +4523,68 @@ function updateStatsCell(cellId, newValue) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start auto-refreshing database statistics every 10 seconds
|
// Start polling for statistics (every 10 seconds)
|
||||||
|
function startStatsPolling() {
|
||||||
|
console.log('=== STARTING STATISTICS POLLING ===');
|
||||||
|
console.log('Current page:', currentPage);
|
||||||
|
console.log('Is logged in:', isLoggedIn);
|
||||||
|
console.log('User pubkey:', userPubkey);
|
||||||
|
console.log('Relay pubkey:', relayPubkey);
|
||||||
|
|
||||||
|
// Stop any existing polling first
|
||||||
|
stopStatsPolling();
|
||||||
|
|
||||||
|
// Fetch immediately
|
||||||
|
console.log('Fetching statistics immediately...');
|
||||||
|
sendStatsQuery().catch(error => {
|
||||||
|
console.error('Initial stats fetch failed:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up polling interval (10 seconds)
|
||||||
|
console.log('Setting up 10-second polling interval...');
|
||||||
|
statsAutoRefreshInterval = setInterval(() => {
|
||||||
|
console.log('⏰ Polling interval triggered - fetching statistics...');
|
||||||
|
sendStatsQuery().catch(error => {
|
||||||
|
console.error('Polling stats fetch failed:', error);
|
||||||
|
});
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
console.log('Statistics polling started successfully');
|
||||||
|
console.log('Interval ID:', statsAutoRefreshInterval);
|
||||||
|
log('Statistics polling started (10 second interval)', 'INFO');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop polling for statistics
|
||||||
|
function stopStatsPolling() {
|
||||||
|
if (statsAutoRefreshInterval) {
|
||||||
|
clearInterval(statsAutoRefreshInterval);
|
||||||
|
statsAutoRefreshInterval = null;
|
||||||
|
log('Statistics polling stopped', 'INFO');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (countdownInterval) {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
countdownInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset countdown display
|
||||||
|
updateCountdownDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy function - kept for backward compatibility
|
||||||
function startStatsAutoRefresh() {
|
function startStatsAutoRefresh() {
|
||||||
// DISABLED - Using real-time monitoring events instead of polling
|
// DISABLED - Using real-time monitoring events instead of polling
|
||||||
// This function is kept for backward compatibility but no longer starts auto-refresh
|
// This function is kept for backward compatibility but no longer starts auto-refresh
|
||||||
log('Database statistics auto-refresh DISABLED - using real-time monitoring events', 'INFO');
|
log('Database statistics auto-refresh DISABLED - using real-time monitoring events', 'INFO');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop auto-refreshing database statistics
|
// Legacy function - kept for backward compatibility
|
||||||
function stopStatsAutoRefresh() {
|
function stopStatsAutoRefresh() {
|
||||||
|
stopStatsPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original stopStatsAutoRefresh implementation (now unused)
|
||||||
|
function stopStatsAutoRefresh_ORIGINAL() {
|
||||||
if (statsAutoRefreshInterval) {
|
if (statsAutoRefreshInterval) {
|
||||||
clearInterval(statsAutoRefreshInterval);
|
clearInterval(statsAutoRefreshInterval);
|
||||||
statsAutoRefreshInterval = null;
|
statsAutoRefreshInterval = null;
|
||||||
@@ -4659,6 +4768,11 @@ function closeSideNav() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function switchPage(pageName) {
|
function switchPage(pageName) {
|
||||||
|
// Stop statistics polling if leaving statistics page
|
||||||
|
if (currentPage === 'statistics' && pageName !== 'statistics') {
|
||||||
|
stopStatsPolling();
|
||||||
|
}
|
||||||
|
|
||||||
// Update current page
|
// Update current page
|
||||||
currentPage = pageName;
|
currentPage = pageName;
|
||||||
|
|
||||||
@@ -4728,7 +4842,7 @@ function switchPage(pageName) {
|
|||||||
|
|
||||||
// Initialize the app
|
// Initialize the app
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
console.log('C-Relay Admin API interface loaded');
|
console.log('Blossom Admin API interface loaded');
|
||||||
|
|
||||||
// Initialize dark mode
|
// Initialize dark mode
|
||||||
initializeDarkMode();
|
initializeDarkMode();
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
build/main.o
BIN
build/main.o
Binary file not shown.
223
build_static.sh
Executable file
223
build_static.sh
Executable file
@@ -0,0 +1,223 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Build fully static MUSL binaries for Ginxsom using Alpine Docker
|
||||||
|
# Produces truly portable binaries with zero runtime dependencies
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
BUILD_DIR="$SCRIPT_DIR/build"
|
||||||
|
DOCKERFILE="$SCRIPT_DIR/Dockerfile.alpine-musl"
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
DEBUG_BUILD=false
|
||||||
|
if [[ "$1" == "--debug" ]]; then
|
||||||
|
DEBUG_BUILD=true
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Ginxsom MUSL Static Binary Builder (DEBUG MODE)"
|
||||||
|
echo "=========================================="
|
||||||
|
else
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Ginxsom MUSL Static Binary Builder (PRODUCTION MODE)"
|
||||||
|
echo "=========================================="
|
||||||
|
fi
|
||||||
|
echo "Project directory: $SCRIPT_DIR"
|
||||||
|
echo "Build directory: $BUILD_DIR"
|
||||||
|
echo "Debug build: $DEBUG_BUILD"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create build directory
|
||||||
|
mkdir -p "$BUILD_DIR"
|
||||||
|
|
||||||
|
# Check if Docker is available
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
echo "ERROR: Docker is not installed or not in PATH"
|
||||||
|
echo ""
|
||||||
|
echo "Docker is required to build MUSL static binaries."
|
||||||
|
echo "Please install Docker:"
|
||||||
|
echo " - Ubuntu/Debian: sudo apt install docker.io"
|
||||||
|
echo " - Or visit: https://docs.docker.com/engine/install/"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if Docker daemon is running
|
||||||
|
if ! docker info &> /dev/null; then
|
||||||
|
echo "ERROR: Docker daemon is not running or user not in docker group"
|
||||||
|
echo ""
|
||||||
|
echo "Please start Docker and ensure you're in the docker group:"
|
||||||
|
echo " - sudo systemctl start docker"
|
||||||
|
echo " - sudo usermod -aG docker $USER && newgrp docker"
|
||||||
|
echo " - Or start Docker Desktop"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DOCKER_CMD="docker"
|
||||||
|
|
||||||
|
echo "✓ Docker is available and running"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Detect architecture
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
case "$ARCH" in
|
||||||
|
x86_64)
|
||||||
|
PLATFORM="linux/amd64"
|
||||||
|
OUTPUT_NAME="ginxsom-fcgi_static_x86_64"
|
||||||
|
;;
|
||||||
|
aarch64|arm64)
|
||||||
|
PLATFORM="linux/arm64"
|
||||||
|
OUTPUT_NAME="ginxsom-fcgi_static_arm64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "WARNING: Unknown architecture: $ARCH"
|
||||||
|
echo "Defaulting to linux/amd64"
|
||||||
|
PLATFORM="linux/amd64"
|
||||||
|
OUTPUT_NAME="ginxsom-fcgi_static_${ARCH}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "Building for platform: $PLATFORM"
|
||||||
|
echo "Output binary: $OUTPUT_NAME"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Build the Docker image
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Step 1: Building Alpine Docker image"
|
||||||
|
echo "=========================================="
|
||||||
|
echo "This will:"
|
||||||
|
echo " - Use Alpine Linux (native MUSL)"
|
||||||
|
echo " - Build all dependencies statically"
|
||||||
|
echo " - Compile Ginxsom with full static linking"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
$DOCKER_CMD build \
|
||||||
|
--platform "$PLATFORM" \
|
||||||
|
--build-arg DEBUG_BUILD=$DEBUG_BUILD \
|
||||||
|
-f "$DOCKERFILE" \
|
||||||
|
-t ginxsom-musl-builder:latest \
|
||||||
|
--progress=plain \
|
||||||
|
. || {
|
||||||
|
echo ""
|
||||||
|
echo "ERROR: Docker build failed"
|
||||||
|
echo "Check the output above for details"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✓ Docker image built successfully"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Extract the binary from the container
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Step 2: Extracting static binary"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
# Build the builder stage to extract the binary
|
||||||
|
$DOCKER_CMD build \
|
||||||
|
--platform "$PLATFORM" \
|
||||||
|
--build-arg DEBUG_BUILD=$DEBUG_BUILD \
|
||||||
|
--target builder \
|
||||||
|
-f "$DOCKERFILE" \
|
||||||
|
-t ginxsom-static-builder-stage:latest \
|
||||||
|
. > /dev/null 2>&1
|
||||||
|
|
||||||
|
# Create a temporary container to copy the binary
|
||||||
|
CONTAINER_ID=$($DOCKER_CMD create ginxsom-static-builder-stage:latest)
|
||||||
|
|
||||||
|
# Copy binary from container
|
||||||
|
$DOCKER_CMD cp "$CONTAINER_ID:/build/ginxsom-fcgi_static" "$BUILD_DIR/$OUTPUT_NAME" || {
|
||||||
|
echo "ERROR: Failed to extract binary from container"
|
||||||
|
$DOCKER_CMD rm "$CONTAINER_ID" 2>/dev/null
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clean up container
|
||||||
|
$DOCKER_CMD rm "$CONTAINER_ID" > /dev/null
|
||||||
|
|
||||||
|
echo "✓ Binary extracted to: $BUILD_DIR/$OUTPUT_NAME"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Make binary executable
|
||||||
|
chmod +x "$BUILD_DIR/$OUTPUT_NAME"
|
||||||
|
|
||||||
|
# Verify the binary
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Step 3: Verifying static binary"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Checking for dynamic dependencies:"
|
||||||
|
if LDD_OUTPUT=$(timeout 5 ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1); then
|
||||||
|
if echo "$LDD_OUTPUT" | grep -q "not a dynamic executable"; then
|
||||||
|
echo "✓ Binary is fully static (no dynamic dependencies)"
|
||||||
|
TRULY_STATIC=true
|
||||||
|
elif echo "$LDD_OUTPUT" | grep -q "statically linked"; then
|
||||||
|
echo "✓ Binary is statically linked"
|
||||||
|
TRULY_STATIC=true
|
||||||
|
else
|
||||||
|
echo "⚠ WARNING: Binary may have dynamic dependencies:"
|
||||||
|
echo "$LDD_OUTPUT"
|
||||||
|
TRULY_STATIC=false
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# ldd failed or timed out - check with file command instead
|
||||||
|
if file "$BUILD_DIR/$OUTPUT_NAME" | grep -q "statically linked"; then
|
||||||
|
echo "✓ Binary is statically linked (verified with file command)"
|
||||||
|
TRULY_STATIC=true
|
||||||
|
else
|
||||||
|
echo "⚠ Could not verify static linking (ldd check failed)"
|
||||||
|
TRULY_STATIC=false
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "File size: $(ls -lh "$BUILD_DIR/$OUTPUT_NAME" | awk '{print $5}')"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Build Summary"
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Binary: $BUILD_DIR/$OUTPUT_NAME"
|
||||||
|
echo "Size: $(du -h "$BUILD_DIR/$OUTPUT_NAME" | cut -f1)"
|
||||||
|
echo "Platform: $PLATFORM"
|
||||||
|
if [ "$DEBUG_BUILD" = true ]; then
|
||||||
|
echo "Build Type: DEBUG (with symbols, no optimization)"
|
||||||
|
else
|
||||||
|
echo "Build Type: PRODUCTION (optimized, stripped)"
|
||||||
|
fi
|
||||||
|
if [ "$TRULY_STATIC" = true ]; then
|
||||||
|
echo "Linkage: Fully static binary (Alpine MUSL-based)"
|
||||||
|
echo "Portability: Works on ANY Linux distribution"
|
||||||
|
else
|
||||||
|
echo "Linkage: Static binary (may have minimal dependencies)"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo "✓ Build complete!"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Clean up old dynamic build artifacts
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Cleaning up old build artifacts"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
if ls build/*.o 2>/dev/null | grep -q .; then
|
||||||
|
echo "Removing old .o files from dynamic builds..."
|
||||||
|
rm -f build/*.o
|
||||||
|
echo "✓ Cleanup complete"
|
||||||
|
else
|
||||||
|
echo "No .o files to clean"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Also remove old dynamic binary if it exists
|
||||||
|
if [ -f "build/ginxsom-fcgi" ]; then
|
||||||
|
echo "Removing old dynamic binary..."
|
||||||
|
rm -f build/ginxsom-fcgi
|
||||||
|
echo "✓ Old binary removed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Deployment:"
|
||||||
|
echo " scp $BUILD_DIR/$OUTPUT_NAME user@server:/path/to/ginxsom/"
|
||||||
|
echo ""
|
||||||
Binary file not shown.
49
deploy_lt.sh
49
deploy_lt.sh
@@ -73,8 +73,55 @@ print_success "Remote environment configured"
|
|||||||
print_status "Copying files to remote server..."
|
print_status "Copying files to remote server..."
|
||||||
|
|
||||||
# Copy entire project directory (excluding unnecessary files)
|
# Copy entire project directory (excluding unnecessary files)
|
||||||
|
# Note: We include .git and .gitmodules to allow submodule initialization on remote
|
||||||
print_status "Copying entire ginxsom project..."
|
print_status "Copying entire ginxsom project..."
|
||||||
rsync -avz --exclude='.git' --exclude='build' --exclude='logs' --exclude='Trash' --exclude='blobs' --exclude='db' --no-g --no-o --no-perms --omit-dir-times . $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/
|
rsync -avz --exclude='build' --exclude='logs' --exclude='Trash' --exclude='blobs' --exclude='db' --no-g --no-o --no-perms --omit-dir-times . $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/
|
||||||
|
|
||||||
|
# Initialize git submodules on remote server
|
||||||
|
print_status "Initializing git submodules on remote server..."
|
||||||
|
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
|
||||||
|
cd /home/ubuntu/ginxsom
|
||||||
|
|
||||||
|
# Check if .git exists
|
||||||
|
if [ ! -d .git ]; then
|
||||||
|
echo "ERROR: .git directory not found - git repository not copied"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if .gitmodules exists
|
||||||
|
if [ ! -f .gitmodules ]; then
|
||||||
|
echo "ERROR: .gitmodules file not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Initializing git submodules..."
|
||||||
|
git submodule update --init --recursive
|
||||||
|
|
||||||
|
# Verify submodule was initialized
|
||||||
|
if [ ! -f nostr_core_lib/cjson/cJSON.h ]; then
|
||||||
|
echo "ERROR: Submodule initialization failed - cJSON.h not found"
|
||||||
|
echo "Checking nostr_core_lib directory:"
|
||||||
|
ls -la nostr_core_lib/ || echo "nostr_core_lib directory not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Submodules initialized successfully"
|
||||||
|
|
||||||
|
# Build nostr_core_lib
|
||||||
|
echo "Building nostr_core_lib..."
|
||||||
|
cd nostr_core_lib
|
||||||
|
./build.sh
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "ERROR: Failed to build nostr_core_lib"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "nostr_core_lib built successfully"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
print_error "Failed to initialize git submodules or build nostr_core_lib"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Build on remote server to ensure compatibility
|
# Build on remote server to ensure compatibility
|
||||||
print_status "Building ginxsom on remote server..."
|
print_status "Building ginxsom on remote server..."
|
||||||
|
|||||||
162
deploy_static.sh
Executable file
162
deploy_static.sh
Executable file
@@ -0,0 +1,162 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
print_status() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||||
|
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||||
|
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
||||||
|
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
REMOTE_HOST="laantungir.net"
|
||||||
|
REMOTE_USER="ubuntu"
|
||||||
|
REMOTE_DIR="/home/ubuntu/ginxsom"
|
||||||
|
REMOTE_BINARY_PATH="/home/ubuntu/ginxsom/ginxsom-fcgi_static"
|
||||||
|
REMOTE_SOCKET="/tmp/ginxsom-fcgi.sock"
|
||||||
|
REMOTE_DATA_DIR="/var/www/html/blossom"
|
||||||
|
REMOTE_DB_PATH="/home/ubuntu/ginxsom/db/ginxsom.db"
|
||||||
|
|
||||||
|
# Detect architecture
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
case "$ARCH" in
|
||||||
|
x86_64)
|
||||||
|
BINARY_NAME="ginxsom-fcgi_static_x86_64"
|
||||||
|
;;
|
||||||
|
aarch64|arm64)
|
||||||
|
BINARY_NAME="ginxsom-fcgi_static_arm64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_error "Unsupported architecture: $ARCH"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
LOCAL_BINARY="./build/$BINARY_NAME"
|
||||||
|
|
||||||
|
print_status "Starting static binary deployment to $REMOTE_HOST..."
|
||||||
|
|
||||||
|
# Check if static binary exists
|
||||||
|
if [ ! -f "$LOCAL_BINARY" ]; then
|
||||||
|
print_error "Static binary not found: $LOCAL_BINARY"
|
||||||
|
print_status "Building static binary..."
|
||||||
|
./build_static.sh
|
||||||
|
|
||||||
|
if [ ! -f "$LOCAL_BINARY" ]; then
|
||||||
|
print_error "Build failed - binary still not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_success "Static binary found: $LOCAL_BINARY"
|
||||||
|
print_status "Binary size: $(du -h "$LOCAL_BINARY" | cut -f1)"
|
||||||
|
|
||||||
|
# Verify binary is static
|
||||||
|
if ldd "$LOCAL_BINARY" 2>&1 | grep -q "not a dynamic executable"; then
|
||||||
|
print_success "Binary is fully static"
|
||||||
|
elif ldd "$LOCAL_BINARY" 2>&1 | grep -q "statically linked"; then
|
||||||
|
print_success "Binary is statically linked"
|
||||||
|
else
|
||||||
|
print_warning "Binary may have dynamic dependencies"
|
||||||
|
ldd "$LOCAL_BINARY" 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Setup remote environment
|
||||||
|
print_status "Setting up remote environment..."
|
||||||
|
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Create directories
|
||||||
|
mkdir -p /home/ubuntu/ginxsom/db
|
||||||
|
sudo mkdir -p /var/www/html/blossom
|
||||||
|
sudo chown www-data:www-data /var/www/html/blossom
|
||||||
|
sudo chmod 755 /var/www/html/blossom
|
||||||
|
|
||||||
|
# Stop existing processes
|
||||||
|
echo "Stopping existing ginxsom processes..."
|
||||||
|
sudo pkill -f ginxsom-fcgi || true
|
||||||
|
sudo rm -f /tmp/ginxsom-fcgi.sock || true
|
||||||
|
|
||||||
|
echo "Remote environment ready"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
print_success "Remote environment configured"
|
||||||
|
|
||||||
|
# Copy static binary
|
||||||
|
print_status "Copying static binary to remote server..."
|
||||||
|
scp "$LOCAL_BINARY" $REMOTE_USER@$REMOTE_HOST:$REMOTE_BINARY_PATH
|
||||||
|
|
||||||
|
print_success "Binary copied successfully"
|
||||||
|
|
||||||
|
# Set permissions and start service
|
||||||
|
print_status "Starting ginxsom FastCGI process..."
|
||||||
|
ssh $REMOTE_USER@$REMOTE_HOST << EOF
|
||||||
|
# Make binary executable
|
||||||
|
chmod +x $REMOTE_BINARY_PATH
|
||||||
|
|
||||||
|
# Clean up any existing socket
|
||||||
|
sudo rm -f $REMOTE_SOCKET
|
||||||
|
|
||||||
|
# Start FastCGI process
|
||||||
|
echo "Starting ginxsom FastCGI..."
|
||||||
|
sudo spawn-fcgi -M 666 -u www-data -g www-data -s $REMOTE_SOCKET -U www-data -G www-data -d $REMOTE_DIR -- $REMOTE_BINARY_PATH --db-path "$REMOTE_DB_PATH" --storage-dir "$REMOTE_DATA_DIR"
|
||||||
|
|
||||||
|
# Give it a moment to start
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Verify process is running
|
||||||
|
if pgrep -f "ginxsom-fcgi" > /dev/null; then
|
||||||
|
echo "FastCGI process started successfully"
|
||||||
|
echo "PID: \$(pgrep -f ginxsom-fcgi)"
|
||||||
|
else
|
||||||
|
echo "Process verification: socket exists"
|
||||||
|
ls -la $REMOTE_SOCKET
|
||||||
|
fi
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
print_success "FastCGI process started"
|
||||||
|
else
|
||||||
|
print_error "Failed to start FastCGI process"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reload nginx
|
||||||
|
print_status "Reloading nginx..."
|
||||||
|
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
|
||||||
|
if sudo nginx -t; then
|
||||||
|
sudo nginx -s reload
|
||||||
|
echo "Nginx reloaded successfully"
|
||||||
|
else
|
||||||
|
echo "Nginx configuration test failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
EOF
|
||||||
|
|
||||||
|
print_success "Nginx reloaded"
|
||||||
|
|
||||||
|
# Test deployment
|
||||||
|
print_status "Testing deployment..."
|
||||||
|
|
||||||
|
echo "Testing health endpoint..."
|
||||||
|
if curl -k -s --max-time 10 "https://blossom.laantungir.net/health" | grep -q "OK"; then
|
||||||
|
print_success "Health check passed"
|
||||||
|
else
|
||||||
|
print_warning "Health check failed - checking response..."
|
||||||
|
curl -k -v --max-time 10 "https://blossom.laantungir.net/health" 2>&1 | head -10
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_success "Deployment to $REMOTE_HOST completed!"
|
||||||
|
print_status "Ginxsom should now be available at: https://blossom.laantungir.net"
|
||||||
|
print_status ""
|
||||||
|
print_status "Deployment Summary:"
|
||||||
|
echo " Binary: $BINARY_NAME"
|
||||||
|
echo " Size: $(du -h "$LOCAL_BINARY" | cut -f1)"
|
||||||
|
echo " Type: Fully static MUSL binary"
|
||||||
|
echo " Portability: Works on any Linux distribution"
|
||||||
|
echo " Deployment time: ~10 seconds (vs ~5 minutes for dynamic build)"
|
||||||
296
docs/STATIC_BUILD.md
Normal file
296
docs/STATIC_BUILD.md
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
# Ginxsom Static MUSL Build Guide
|
||||||
|
|
||||||
|
This guide explains how to build and deploy Ginxsom as a fully static MUSL binary with zero runtime dependencies.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Ginxsom now supports building as a static MUSL binary using Alpine Linux and Docker. This produces a truly portable binary that works on **any Linux distribution** without requiring any system libraries.
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
| Feature | Static MUSL | Dynamic glibc |
|
||||||
|
|---------|-------------|---------------|
|
||||||
|
| **Portability** | ✓ Any Linux | ✗ Requires matching libs |
|
||||||
|
| **Dependencies** | None | libfcgi, libsqlite3, etc. |
|
||||||
|
| **Deployment** | Copy one file | Build on target |
|
||||||
|
| **Binary Size** | ~7-10 MB | ~2-3 MB + libraries |
|
||||||
|
| **Deployment Time** | ~10 seconds | ~5-10 minutes |
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker installed and running
|
||||||
|
- Internet connection (for first build only)
|
||||||
|
- ~2GB disk space for Docker images
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Build Static Binary
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build production binary (optimized, stripped)
|
||||||
|
make static
|
||||||
|
|
||||||
|
# Or build debug binary (with symbols)
|
||||||
|
make static-debug
|
||||||
|
|
||||||
|
# Or use the script directly
|
||||||
|
./build_static.sh
|
||||||
|
./build_static.sh --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
The binary will be created in `build/ginxsom-fcgi_static_x86_64` (or `_arm64` for ARM systems).
|
||||||
|
|
||||||
|
### 2. Verify Binary
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if truly static
|
||||||
|
ldd build/ginxsom-fcgi_static_x86_64
|
||||||
|
# Should output: "not a dynamic executable"
|
||||||
|
|
||||||
|
# Check file info
|
||||||
|
file build/ginxsom-fcgi_static_x86_64
|
||||||
|
# Should show: "statically linked"
|
||||||
|
|
||||||
|
# Check size
|
||||||
|
ls -lh build/ginxsom-fcgi_static_x86_64
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Deploy to Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Use the simplified deployment script
|
||||||
|
./deploy_static.sh
|
||||||
|
|
||||||
|
# Or manually copy and start
|
||||||
|
scp build/ginxsom-fcgi_static_x86_64 user@server:/path/to/ginxsom/
|
||||||
|
ssh user@server
|
||||||
|
chmod +x /path/to/ginxsom/ginxsom-fcgi_static_x86_64
|
||||||
|
sudo spawn-fcgi -M 666 -u www-data -g www-data \
|
||||||
|
-s /tmp/ginxsom-fcgi.sock \
|
||||||
|
-- /path/to/ginxsom/ginxsom-fcgi_static_x86_64 \
|
||||||
|
--db-path /path/to/db/ginxsom.db \
|
||||||
|
--storage-dir /var/www/html/blossom
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build Process Details
|
||||||
|
|
||||||
|
### What Happens During Build
|
||||||
|
|
||||||
|
1. **Docker Image Creation** (5-10 minutes first time, cached after):
|
||||||
|
- Uses Alpine Linux 3.19 (native MUSL)
|
||||||
|
- Builds secp256k1 statically
|
||||||
|
- Builds nostr_core_lib with required NIPs
|
||||||
|
- Embeds web interface files
|
||||||
|
- Compiles Ginxsom with full static linking
|
||||||
|
|
||||||
|
2. **Binary Extraction**:
|
||||||
|
- Extracts binary from Docker container
|
||||||
|
- Verifies static linking
|
||||||
|
- Makes executable
|
||||||
|
|
||||||
|
3. **Verification**:
|
||||||
|
- Checks for dynamic dependencies
|
||||||
|
- Reports file size
|
||||||
|
- Tests execution
|
||||||
|
|
||||||
|
### Docker Layers (Cached)
|
||||||
|
|
||||||
|
The Dockerfile uses multi-stage builds with caching:
|
||||||
|
|
||||||
|
```
|
||||||
|
Layer 1: Alpine base + dependencies (cached)
|
||||||
|
Layer 2: Build secp256k1 (cached)
|
||||||
|
Layer 3: Initialize git submodules (cached unless .gitmodules changes)
|
||||||
|
Layer 4: Build nostr_core_lib (cached unless nostr_core_lib changes)
|
||||||
|
Layer 5: Embed web files (cached unless api/ changes)
|
||||||
|
Layer 6: Build Ginxsom (rebuilds when src/ changes)
|
||||||
|
```
|
||||||
|
|
||||||
|
This means subsequent builds are **much faster** (~1-2 minutes) since only changed layers rebuild.
|
||||||
|
|
||||||
|
## Deployment Comparison
|
||||||
|
|
||||||
|
### Old Dynamic Build Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Sync entire project (30 seconds)
|
||||||
|
rsync -avz . user@server:/path/
|
||||||
|
|
||||||
|
# 2. Build on remote server (5-10 minutes)
|
||||||
|
ssh user@server "cd /path && make clean && make"
|
||||||
|
|
||||||
|
# 3. Restart service (10 seconds)
|
||||||
|
ssh user@server "sudo systemctl restart ginxsom"
|
||||||
|
|
||||||
|
# Total: ~6-11 minutes
|
||||||
|
```
|
||||||
|
|
||||||
|
### New Static Build Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Build locally once (5-10 minutes first time, cached after)
|
||||||
|
make static
|
||||||
|
|
||||||
|
# 2. Copy binary (10 seconds)
|
||||||
|
scp build/ginxsom-fcgi_static_x86_64 user@server:/path/
|
||||||
|
|
||||||
|
# 3. Restart service (10 seconds)
|
||||||
|
ssh user@server "sudo systemctl restart ginxsom"
|
||||||
|
|
||||||
|
# Total: ~20 seconds (after first build)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
|
||||||
|
### Automatic Cleanup
|
||||||
|
|
||||||
|
The static build script automatically cleans up old dynamic build artifacts (`.o` files and `ginxsom-fcgi` binary) after successfully building the static binary. This keeps your `build/` directory clean.
|
||||||
|
|
||||||
|
### Manual Cleanup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clean dynamic build artifacts (preserves static binaries)
|
||||||
|
make clean
|
||||||
|
|
||||||
|
# Clean everything including static binaries
|
||||||
|
make clean-all
|
||||||
|
|
||||||
|
# Or manually remove specific files
|
||||||
|
rm -f build/*.o
|
||||||
|
rm -f build/ginxsom-fcgi
|
||||||
|
rm -f build/ginxsom-fcgi_static_*
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Docker Not Found
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Docker
|
||||||
|
sudo apt install docker.io
|
||||||
|
|
||||||
|
# Add user to docker group
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
newgrp docker
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Fails
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clean Docker cache and rebuild
|
||||||
|
docker system prune -a
|
||||||
|
make static
|
||||||
|
```
|
||||||
|
|
||||||
|
### Binary Won't Run on Target
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify it's static
|
||||||
|
ldd build/ginxsom-fcgi_static_x86_64
|
||||||
|
|
||||||
|
# Check architecture matches
|
||||||
|
file build/ginxsom-fcgi_static_x86_64
|
||||||
|
uname -m # On target system
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alpine Package Not Found
|
||||||
|
|
||||||
|
If you get errors about missing Alpine packages, the package name may have changed. Check Alpine's package database:
|
||||||
|
- https://pkgs.alpinelinux.org/packages
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Cross-Compilation
|
||||||
|
|
||||||
|
Build for different architectures:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build for ARM64 on x86_64 machine
|
||||||
|
docker build --platform linux/arm64 -f Dockerfile.alpine-musl -t ginxsom-arm64 .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom NIPs
|
||||||
|
|
||||||
|
Edit `Dockerfile.alpine-musl` line 66 to change which NIPs are included:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
./build.sh --nips=1,6,19 # Minimal
|
||||||
|
./build.sh --nips=1,6,13,17,19,44,59 # Full (default)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debug Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build with debug symbols (no optimization)
|
||||||
|
make static-debug
|
||||||
|
|
||||||
|
# Binary will be larger but include debugging info
|
||||||
|
gdb build/ginxsom-fcgi_static_x86_64
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
ginxsom/
|
||||||
|
├── Dockerfile.alpine-musl # Alpine Docker build definition
|
||||||
|
├── build_static.sh # Build script wrapper
|
||||||
|
├── deploy_static.sh # Simplified deployment script
|
||||||
|
├── Makefile # Updated with 'static' target
|
||||||
|
└── build/
|
||||||
|
└── ginxsom-fcgi_static_x86_64 # Output binary
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
### GitHub Actions Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Build Static Binary
|
||||||
|
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Build static binary
|
||||||
|
run: make static
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: ginxsom-static
|
||||||
|
path: build/ginxsom-fcgi_static_x86_64
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
Static MUSL binaries have minimal performance impact:
|
||||||
|
|
||||||
|
| Metric | Static MUSL | Dynamic glibc |
|
||||||
|
|--------|-------------|---------------|
|
||||||
|
| Startup Time | ~50ms | ~40ms |
|
||||||
|
| Memory Usage | Similar | Similar |
|
||||||
|
| Request Latency | Identical | Identical |
|
||||||
|
| Binary Size | 7-10 MB | 2-3 MB + libs |
|
||||||
|
|
||||||
|
The slight startup delay is negligible for a long-running FastCGI process.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [MUSL libc](https://musl.libc.org/)
|
||||||
|
- [Alpine Linux](https://alpinelinux.org/)
|
||||||
|
- [Static Linking Best Practices](https://www.musl-libc.org/faq.html)
|
||||||
|
- [c-relay Static Build](../c-relay/STATIC_BUILD.md)
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues with static builds:
|
||||||
|
1. Check Docker is running: `docker info`
|
||||||
|
2. Verify submodules: `git submodule status`
|
||||||
|
3. Clean and rebuild: `docker system prune -a && make static`
|
||||||
|
4. Check logs in Docker build output
|
||||||
@@ -49,7 +49,22 @@ if [[ $FOLLOW_LOGS -eq 1 ]]; then
|
|||||||
wait
|
wait
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
FCGI_BINARY="./build/ginxsom-fcgi"
|
# Detect architecture for static binary name
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
case "$ARCH" in
|
||||||
|
x86_64) STATIC_BINARY="./build/ginxsom-fcgi_static_x86_64" ;;
|
||||||
|
aarch64|arm64) STATIC_BINARY="./build/ginxsom-fcgi_static_arm64" ;;
|
||||||
|
*) STATIC_BINARY="./build/ginxsom-fcgi_static_${ARCH}" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Use static binary if available, fallback to dynamic
|
||||||
|
if [ -f "$STATIC_BINARY" ]; then
|
||||||
|
FCGI_BINARY="$STATIC_BINARY"
|
||||||
|
echo "Using static binary: $FCGI_BINARY"
|
||||||
|
else
|
||||||
|
FCGI_BINARY="./build/ginxsom-fcgi"
|
||||||
|
echo "Static binary not found, using dynamic binary: $FCGI_BINARY"
|
||||||
|
fi
|
||||||
SOCKET_PATH="/tmp/ginxsom-fcgi.sock"
|
SOCKET_PATH="/tmp/ginxsom-fcgi.sock"
|
||||||
PID_FILE="/tmp/ginxsom-fcgi.pid"
|
PID_FILE="/tmp/ginxsom-fcgi.pid"
|
||||||
NGINX_CONFIG="config/local-nginx.conf"
|
NGINX_CONFIG="config/local-nginx.conf"
|
||||||
@@ -173,21 +188,24 @@ fi
|
|||||||
|
|
||||||
echo -e "${GREEN}FastCGI cleanup complete${NC}"
|
echo -e "${GREEN}FastCGI cleanup complete${NC}"
|
||||||
|
|
||||||
# Step 3: Always rebuild FastCGI binary with clean build
|
# Step 3: Always rebuild FastCGI binary with static build
|
||||||
echo -e "\n${YELLOW}3. Rebuilding FastCGI binary (clean build)...${NC}"
|
echo -e "\n${YELLOW}3. Rebuilding FastCGI binary (static build)...${NC}"
|
||||||
echo "Embedding web files..."
|
echo "Building static binary with Docker..."
|
||||||
./scripts/embed_web_files.sh
|
make static
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo -e "${RED}Web file embedding failed! Cannot continue.${NC}"
|
echo -e "${RED}Static build failed! Cannot continue.${NC}"
|
||||||
|
echo -e "${RED}Docker must be available and running for static builds.${NC}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "Performing clean rebuild to ensure all changes are compiled..."
|
|
||||||
make clean && make
|
# Update FCGI_BINARY to use the newly built static binary
|
||||||
if [ $? -ne 0 ]; then
|
ARCH=$(uname -m)
|
||||||
echo -e "${RED}Build failed! Cannot continue.${NC}"
|
case "$ARCH" in
|
||||||
exit 1
|
x86_64) FCGI_BINARY="./build/ginxsom-fcgi_static_x86_64" ;;
|
||||||
fi
|
aarch64|arm64) FCGI_BINARY="./build/ginxsom-fcgi_static_arm64" ;;
|
||||||
echo -e "${GREEN}Clean rebuild complete${NC}"
|
*) FCGI_BINARY="./build/ginxsom-fcgi_static_${ARCH}" ;;
|
||||||
|
esac
|
||||||
|
echo -e "${GREEN}Static build complete: $FCGI_BINARY${NC}"
|
||||||
|
|
||||||
# Step 3.5: Clean database directory for fresh testing
|
# Step 3.5: Clean database directory for fresh testing
|
||||||
echo -e "\n${YELLOW}3.5. Cleaning database directory...${NC}"
|
echo -e "\n${YELLOW}3.5. Cleaning database directory...${NC}"
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/types.h>
|
||||||
#include "ginxsom.h"
|
#include "ginxsom.h"
|
||||||
|
|
||||||
// Forward declarations for nostr_core_lib functions
|
// Forward declarations for nostr_core_lib functions
|
||||||
@@ -27,6 +29,7 @@ extern char g_db_path[];
|
|||||||
static int get_server_privkey(unsigned char* privkey_bytes);
|
static int get_server_privkey(unsigned char* privkey_bytes);
|
||||||
static int get_server_pubkey(char* pubkey_hex, size_t size);
|
static int get_server_pubkey(char* pubkey_hex, size_t size);
|
||||||
static int handle_config_query_command(cJSON* response_data);
|
static int handle_config_query_command(cJSON* response_data);
|
||||||
|
static int handle_query_view_command(cJSON* command_array, cJSON* response_data);
|
||||||
static int send_admin_response_event(const char* admin_pubkey, const char* request_id,
|
static int send_admin_response_event(const char* admin_pubkey, const char* request_id,
|
||||||
cJSON* response_data);
|
cJSON* response_data);
|
||||||
static cJSON* parse_authorization_header(void);
|
static cJSON* parse_authorization_header(void);
|
||||||
@@ -269,9 +272,13 @@ static int process_admin_event(cJSON* event) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
content_to_parse = decrypted_content;
|
content_to_parse = decrypted_content;
|
||||||
|
app_log(LOG_DEBUG, "ADMIN_EVENT: Decrypted content: %s", decrypted_content);
|
||||||
|
} else {
|
||||||
|
app_log(LOG_DEBUG, "ADMIN_EVENT: Using plaintext content (starts with '['): %s", encrypted_content);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse command array (either decrypted or plaintext)
|
// Parse command array (either decrypted or plaintext)
|
||||||
|
app_log(LOG_DEBUG, "ADMIN_EVENT: Parsing command array from: %s", content_to_parse);
|
||||||
cJSON* command_array = cJSON_Parse(content_to_parse);
|
cJSON* command_array = cJSON_Parse(content_to_parse);
|
||||||
if (!command_array || !cJSON_IsArray(command_array)) {
|
if (!command_array || !cJSON_IsArray(command_array)) {
|
||||||
printf("Status: 400 Bad Request\r\n");
|
printf("Status: 400 Bad Request\r\n");
|
||||||
@@ -300,19 +307,30 @@ static int process_admin_event(cJSON* event) {
|
|||||||
// Handle command
|
// Handle command
|
||||||
int result = -1;
|
int result = -1;
|
||||||
if (strcmp(cmd, "config_query") == 0) {
|
if (strcmp(cmd, "config_query") == 0) {
|
||||||
|
app_log(LOG_DEBUG, "ADMIN_EVENT: Handling config_query command");
|
||||||
result = handle_config_query_command(response_data);
|
result = handle_config_query_command(response_data);
|
||||||
|
app_log(LOG_DEBUG, "ADMIN_EVENT: config_query result: %d", result);
|
||||||
|
} else if (strcmp(cmd, "query_view") == 0) {
|
||||||
|
app_log(LOG_DEBUG, "ADMIN_EVENT: Handling query_view command");
|
||||||
|
result = handle_query_view_command(command_array, response_data);
|
||||||
|
app_log(LOG_DEBUG, "ADMIN_EVENT: query_view result: %d", result);
|
||||||
} else {
|
} else {
|
||||||
|
app_log(LOG_WARN, "ADMIN_EVENT: Unknown command: %s", cmd);
|
||||||
cJSON_AddStringToObject(response_data, "status", "error");
|
cJSON_AddStringToObject(response_data, "status", "error");
|
||||||
cJSON_AddStringToObject(response_data, "error", "Unknown command");
|
cJSON_AddStringToObject(response_data, "error", "Unknown command");
|
||||||
|
result = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
cJSON_Delete(command_array);
|
cJSON_Delete(command_array);
|
||||||
|
|
||||||
if (result == 0) {
|
if (result == 0) {
|
||||||
|
app_log(LOG_DEBUG, "ADMIN_EVENT: Sending Kind 23459 response");
|
||||||
// Send Kind 23459 response
|
// Send Kind 23459 response
|
||||||
send_admin_response_event(admin_pubkey, request_id, response_data);
|
int send_result = send_admin_response_event(admin_pubkey, request_id, response_data);
|
||||||
return 0;
|
app_log(LOG_DEBUG, "ADMIN_EVENT: Response sent with result: %d", send_result);
|
||||||
|
return send_result;
|
||||||
} else {
|
} else {
|
||||||
|
app_log(LOG_ERROR, "ADMIN_EVENT: Command processing failed");
|
||||||
cJSON_Delete(response_data);
|
cJSON_Delete(response_data);
|
||||||
printf("Status: 500 Internal Server Error\r\n");
|
printf("Status: 500 Internal Server Error\r\n");
|
||||||
printf("Content-Type: application/json\r\n\r\n");
|
printf("Content-Type: application/json\r\n\r\n");
|
||||||
@@ -415,6 +433,125 @@ static int handle_config_query_command(cJSON* response_data) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle query_view command - returns data from a specified database view
|
||||||
|
* Command format: ["query_view", "view_name"]
|
||||||
|
*/
|
||||||
|
static int handle_query_view_command(cJSON* command_array, cJSON* response_data) {
|
||||||
|
app_log(LOG_DEBUG, "ADMIN_EVENT: handle_query_view_command called");
|
||||||
|
|
||||||
|
// Get view name from command array
|
||||||
|
cJSON* view_name_obj = cJSON_GetArrayItem(command_array, 1);
|
||||||
|
if (!view_name_obj || !cJSON_IsString(view_name_obj)) {
|
||||||
|
app_log(LOG_ERROR, "ADMIN_EVENT: View name missing or not a string");
|
||||||
|
cJSON_AddStringToObject(response_data, "status", "error");
|
||||||
|
cJSON_AddStringToObject(response_data, "error", "View name required");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* view_name = cJSON_GetStringValue(view_name_obj);
|
||||||
|
app_log(LOG_DEBUG, "ADMIN_EVENT: Querying view: %s", view_name);
|
||||||
|
|
||||||
|
// Validate view name (whitelist approach for security)
|
||||||
|
const char* allowed_views[] = {
|
||||||
|
"blob_overview",
|
||||||
|
"blob_type_distribution",
|
||||||
|
"blob_time_stats",
|
||||||
|
"top_uploaders",
|
||||||
|
NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
int view_allowed = 0;
|
||||||
|
for (int i = 0; allowed_views[i] != NULL; i++) {
|
||||||
|
if (strcmp(view_name, allowed_views[i]) == 0) {
|
||||||
|
view_allowed = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!view_allowed) {
|
||||||
|
cJSON_AddStringToObject(response_data, "status", "error");
|
||||||
|
cJSON_AddStringToObject(response_data, "error", "Invalid view name");
|
||||||
|
app_log(LOG_WARN, "ADMIN_EVENT: Attempted to query invalid view: %s", view_name);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
app_log(LOG_DEBUG, "ADMIN_EVENT: View '%s' is allowed, opening database: %s", view_name, g_db_path);
|
||||||
|
|
||||||
|
// Open database
|
||||||
|
sqlite3* db;
|
||||||
|
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
app_log(LOG_ERROR, "ADMIN_EVENT: Failed to open database: %s (error: %s)", g_db_path, sqlite3_errmsg(db));
|
||||||
|
cJSON_AddStringToObject(response_data, "status", "error");
|
||||||
|
cJSON_AddStringToObject(response_data, "error", "Database error");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build SQL query
|
||||||
|
char sql[256];
|
||||||
|
snprintf(sql, sizeof(sql), "SELECT * FROM %s", view_name);
|
||||||
|
|
||||||
|
app_log(LOG_DEBUG, "ADMIN_EVENT: Executing SQL: %s", sql);
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||||
|
app_log(LOG_ERROR, "ADMIN_EVENT: Failed to prepare query: %s (error: %s)", sql, sqlite3_errmsg(db));
|
||||||
|
sqlite3_close(db);
|
||||||
|
cJSON_AddStringToObject(response_data, "status", "error");
|
||||||
|
cJSON_AddStringToObject(response_data, "error", "Failed to prepare query");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get column count and names
|
||||||
|
int col_count = sqlite3_column_count(stmt);
|
||||||
|
|
||||||
|
// Create results array
|
||||||
|
cJSON* results = cJSON_CreateArray();
|
||||||
|
|
||||||
|
// Fetch all rows
|
||||||
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||||
|
cJSON* row = cJSON_CreateObject();
|
||||||
|
|
||||||
|
for (int i = 0; i < col_count; i++) {
|
||||||
|
const char* col_name = sqlite3_column_name(stmt, i);
|
||||||
|
int col_type = sqlite3_column_type(stmt, i);
|
||||||
|
|
||||||
|
switch (col_type) {
|
||||||
|
case SQLITE_INTEGER:
|
||||||
|
cJSON_AddNumberToObject(row, col_name, (double)sqlite3_column_int64(stmt, i));
|
||||||
|
break;
|
||||||
|
case SQLITE_FLOAT:
|
||||||
|
cJSON_AddNumberToObject(row, col_name, sqlite3_column_double(stmt, i));
|
||||||
|
break;
|
||||||
|
case SQLITE_TEXT:
|
||||||
|
cJSON_AddStringToObject(row, col_name, (const char*)sqlite3_column_text(stmt, i));
|
||||||
|
break;
|
||||||
|
case SQLITE_NULL:
|
||||||
|
cJSON_AddNullToObject(row, col_name);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// For BLOB or unknown types, skip
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddItemToArray(results, row);
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
sqlite3_close(db);
|
||||||
|
|
||||||
|
// Build response
|
||||||
|
cJSON_AddStringToObject(response_data, "status", "success");
|
||||||
|
cJSON_AddStringToObject(response_data, "view_name", view_name);
|
||||||
|
cJSON_AddItemToObject(response_data, "data", results);
|
||||||
|
|
||||||
|
app_log(LOG_DEBUG, "ADMIN_EVENT: Query view '%s' returned %d rows", view_name, cJSON_GetArraySize(results));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send Kind 23459 admin response event
|
* Send Kind 23459 admin response event
|
||||||
*/
|
*/
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -10,8 +10,8 @@
|
|||||||
// Version information (auto-updated by build system)
|
// Version information (auto-updated by build system)
|
||||||
#define VERSION_MAJOR 0
|
#define VERSION_MAJOR 0
|
||||||
#define VERSION_MINOR 1
|
#define VERSION_MINOR 1
|
||||||
#define VERSION_PATCH 15
|
#define VERSION_PATCH 18
|
||||||
#define VERSION "v0.1.15"
|
#define VERSION "v0.1.18"
|
||||||
|
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|||||||
81
src/main.c
81
src/main.c
@@ -248,7 +248,7 @@ int initialize_database(const char *db_path) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create storage_stats view
|
// Create storage_stats view (legacy - kept for backward compatibility)
|
||||||
const char *create_view =
|
const char *create_view =
|
||||||
"CREATE VIEW IF NOT EXISTS storage_stats AS "
|
"CREATE VIEW IF NOT EXISTS storage_stats AS "
|
||||||
"SELECT "
|
"SELECT "
|
||||||
@@ -268,6 +268,85 @@ int initialize_database(const char *db_path) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create blob_overview view for admin dashboard
|
||||||
|
const char *create_overview_view =
|
||||||
|
"CREATE VIEW IF NOT EXISTS blob_overview AS "
|
||||||
|
"SELECT "
|
||||||
|
" COUNT(*) as total_blobs, "
|
||||||
|
" COALESCE(SUM(size), 0) as total_bytes, "
|
||||||
|
" MIN(uploaded_at) as first_upload, "
|
||||||
|
" MAX(uploaded_at) as last_upload "
|
||||||
|
"FROM blobs;";
|
||||||
|
|
||||||
|
rc = sqlite3_exec(db, create_overview_view, NULL, NULL, &err_msg);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
fprintf(stderr, "Failed to create blob_overview view: %s\n", err_msg);
|
||||||
|
sqlite3_free(err_msg);
|
||||||
|
sqlite3_close(db);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create blob_type_distribution view for MIME type statistics
|
||||||
|
const char *create_type_view =
|
||||||
|
"CREATE VIEW IF NOT EXISTS blob_type_distribution AS "
|
||||||
|
"SELECT "
|
||||||
|
" type as mime_type, "
|
||||||
|
" COUNT(*) as blob_count, "
|
||||||
|
" SUM(size) as total_bytes, "
|
||||||
|
" ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM blobs), 2) as percentage "
|
||||||
|
"FROM blobs "
|
||||||
|
"GROUP BY type "
|
||||||
|
"ORDER BY blob_count DESC;";
|
||||||
|
|
||||||
|
rc = sqlite3_exec(db, create_type_view, NULL, NULL, &err_msg);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
fprintf(stderr, "Failed to create blob_type_distribution view: %s\n", err_msg);
|
||||||
|
sqlite3_free(err_msg);
|
||||||
|
sqlite3_close(db);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create blob_time_stats view for time-based statistics
|
||||||
|
const char *create_time_view =
|
||||||
|
"CREATE VIEW IF NOT EXISTS blob_time_stats AS "
|
||||||
|
"SELECT "
|
||||||
|
" COUNT(CASE WHEN uploaded_at >= strftime('%s', 'now', '-1 day') THEN 1 END) as blobs_24h, "
|
||||||
|
" COUNT(CASE WHEN uploaded_at >= strftime('%s', 'now', '-7 days') THEN 1 END) as blobs_7d, "
|
||||||
|
" COUNT(CASE WHEN uploaded_at >= strftime('%s', 'now', '-30 days') THEN 1 END) as blobs_30d "
|
||||||
|
"FROM blobs;";
|
||||||
|
|
||||||
|
rc = sqlite3_exec(db, create_time_view, NULL, NULL, &err_msg);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
fprintf(stderr, "Failed to create blob_time_stats view: %s\n", err_msg);
|
||||||
|
sqlite3_free(err_msg);
|
||||||
|
sqlite3_close(db);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create top_uploaders view for pubkey statistics
|
||||||
|
const char *create_uploaders_view =
|
||||||
|
"CREATE VIEW IF NOT EXISTS top_uploaders AS "
|
||||||
|
"SELECT "
|
||||||
|
" uploader_pubkey, "
|
||||||
|
" COUNT(*) as blob_count, "
|
||||||
|
" SUM(size) as total_bytes, "
|
||||||
|
" ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM blobs), 2) as percentage, "
|
||||||
|
" MIN(uploaded_at) as first_upload, "
|
||||||
|
" MAX(uploaded_at) as last_upload "
|
||||||
|
"FROM blobs "
|
||||||
|
"WHERE uploader_pubkey IS NOT NULL "
|
||||||
|
"GROUP BY uploader_pubkey "
|
||||||
|
"ORDER BY blob_count DESC "
|
||||||
|
"LIMIT 20;";
|
||||||
|
|
||||||
|
rc = sqlite3_exec(db, create_uploaders_view, NULL, NULL, &err_msg);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
fprintf(stderr, "Failed to create top_uploaders view: %s\n", err_msg);
|
||||||
|
sqlite3_free(err_msg);
|
||||||
|
sqlite3_close(db);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
fprintf(stderr, "Database schema initialized successfully\n");
|
fprintf(stderr, "Database schema initialized successfully\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user