v0.1.37 - Use static-only build flow in increment_and_push and include blob browser sorting/delete deployment fix

This commit is contained in:
Laan Tungir
2026-04-01 20:29:28 -04:00
parent 485e351998
commit 9491f5c373
31 changed files with 734169 additions and 59382 deletions

View File

@@ -31,6 +31,7 @@ RUN apk add --no-cache \
linux-headers \
wget \
bash \
openssh-client \
nghttp2-dev \
nghttp2-static \
c-ares-dev \
@@ -58,17 +59,14 @@ RUN cd /tmp && \
make install && \
rm -rf /tmp/secp256k1
# Copy only submodule configuration and git directory
COPY .gitmodules /build/.gitmodules
COPY .git /build/.git
# Fetch nostr_core_lib source directly over HTTPS.
# This avoids relying on local git metadata/submodule state in the Docker build context.
ARG NOSTR_CORE_LIB_REF=2e7eacc02eef0ccf261f655330dc6a43d6cebdfb
RUN git clone https://git.laantungir.net/laantungir/nostr_core_lib.git /build/nostr_core_lib && \
cd /build/nostr_core_lib && \
git checkout "$NOSTR_CORE_LIB_REF"
# 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)
# Build nostr_core_lib with required NIPs (cached unless submodule 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 && \
@@ -82,17 +80,14 @@ RUN cd nostr_core_lib && \
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)
# Note: The embedded header from previous step will be overwritten by this COPY
# So we need to ensure src/admin_interface_embedded.h is NOT in src/ directory
# Copy Ginxsom source files
COPY src/ /build/src/
COPY include/ /build/include/
# Embed web files AFTER source copy so src/admin_interface_embedded.h is always fresh
RUN chmod +x scripts/embed_web_files.sh && \
./scripts/embed_web_files.sh
# 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

0
Trash/[builder Normal file
View File

View File

@@ -1308,3 +1308,158 @@ body.dark-mode .sql-results-table tbody tr:nth-child(even) {
font-size: 12px;
}
/* ================================
BLOB BROWSER
================================ */
.blob-browser-controls {
gap: 8px;
flex-wrap: wrap;
}
.blob-browser-controls button {
min-width: 160px;
}
.blob-pagination-controls {
gap: 8px;
}
.blob-pagination-controls button {
min-width: 120px;
}
.blob-grid-view {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 12px;
margin: 12px 0;
}
.blob-card {
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: 10px;
background: var(--secondary-color);
}
.blob-thumb-link {
display: block;
width: 100%;
height: 140px;
border: 1px solid var(--muted-color);
border-radius: 4px;
overflow: hidden;
text-decoration: none;
margin-bottom: 8px;
}
.blob-thumb {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.blob-thumb-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: var(--muted-color);
color: var(--primary-color);
font-size: 12px;
text-align: center;
padding: 8px;
}
.blob-card-meta {
font-size: 12px;
line-height: 1.35;
word-break: break-word;
}
.blob-hash-link {
color: var(--primary-color);
text-decoration: none;
}
.blob-hash-link:hover {
color: var(--accent-color);
}
.blob-list-preview {
width: 58px;
height: 44px;
object-fit: cover;
border: 1px solid var(--muted-color);
border-radius: 4px;
display: block;
}
.blob-list-placeholder {
width: 58px;
height: 44px;
border: 1px solid var(--muted-color);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
background: var(--muted-color);
}
.blob-list-view .config-table td {
vertical-align: middle;
}
.blob-list-view th.blob-sortable-th {
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.blob-list-view th.blob-sortable-th:hover {
color: var(--accent-color);
}
.blob-sort-indicator {
display: inline-block;
width: 14px;
text-align: center;
margin-left: 4px;
font-size: 10px;
}
.blob-delete-btn {
min-width: 86px;
font-size: 10px;
padding: 6px 8px;
border: 1px solid var(--accent-color);
background: transparent;
color: var(--accent-color);
cursor: pointer;
border-radius: 4px;
font-family: var(--font-family);
}
.blob-delete-btn:hover {
background: var(--accent-color);
color: var(--secondary-color);
}
.blob-delete-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.blob-empty {
border: 1px dashed var(--border-color);
border-radius: var(--border-radius);
padding: 24px;
text-align: center;
font-style: italic;
color: var(--primary-color);
}

View File

@@ -13,6 +13,7 @@
<nav class="side-nav" id="side-nav">
<ul class="nav-menu">
<li><button class="nav-item" data-page="statistics">Statistics</button></li>
<li><button class="nav-item" data-page="blob-browser">Blob Browser</button></li>
<li><button class="nav-item" data-page="configuration">Configuration</button></li>
<li><button class="nav-item" data-page="authorization">Authorization</button></li>
<li><button class="nav-item" data-page="relay-events">Blossom Events</button></li>
@@ -372,6 +373,57 @@ AUTH RULES MANAGEMENT
</div>
</div>
<!-- BLOB BROWSER Section -->
<div class="section" id="blobBrowserSection" style="display: none;">
<div class="section-header">
<h2>BLOB BROWSER</h2>
</div>
<div class="input-group">
<label for="blob-filter-pubkey">Uploader Pubkey (hex or npub):</label>
<input type="text" id="blob-filter-pubkey" placeholder="Optional: npub1... or 64-char hex pubkey">
</div>
<div class="inline-buttons blob-browser-controls">
<button type="button" id="blob-filter-apply-btn">APPLY FILTER</button>
<button type="button" id="blob-filter-clear-btn">CLEAR FILTER</button>
<button type="button" id="blob-view-toggle-btn">SWITCH TO LIST VIEW</button>
<button type="button" id="blob-refresh-btn">REFRESH</button>
</div>
<div id="blob-browser-summary" class="info-box">No data loaded.</div>
<div id="blob-grid-view" class="blob-grid-view"></div>
<div id="blob-list-view" class="blob-list-view" style="display: none;">
<div class="config-table-container">
<table class="config-table" id="blob-list-table">
<thead>
<tr>
<th>Delete</th>
<th>Preview</th>
<th class="blob-sortable-th" data-sort-key="hash">Hash <span class="blob-sort-indicator"></span></th>
<th class="blob-sortable-th" data-sort-key="uploader">Uploader <span class="blob-sort-indicator"></span></th>
<th class="blob-sortable-th" data-sort-key="type">Type <span class="blob-sort-indicator"></span></th>
<th class="blob-sortable-th" data-sort-key="size">Size <span class="blob-sort-indicator"></span></th>
<th class="blob-sortable-th" data-sort-key="uploaded">Uploaded <span class="blob-sort-indicator"></span></th>
</tr>
</thead>
<tbody id="blob-list-table-body">
<tr>
<td colspan="7" style="text-align: center; font-style: italic;">No blobs loaded</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="inline-buttons blob-pagination-controls">
<button type="button" id="blob-prev-page-btn">PREV</button>
<button type="button" id="blob-next-page-btn">NEXT</button>
</div>
</div>
<!-- SQL QUERY Section -->
<div class="section" id="sqlQuerySection" style="display: none;">
<div class="section-header">

View File

@@ -39,6 +39,20 @@ let pendingSqlQueries = new Map();
let eventRateChart = null;
let previousTotalBlobs = 0; // Track previous total for rate calculation
// Blob Browser state
const blobBrowserState = {
limit: 24,
offset: 0,
total: 0,
files: [],
pubkeyFilter: '',
viewMode: 'grid',
sortKey: 'uploaded',
sortDirection: 'desc',
deletingHashes: new Set(),
loading: false
};
// Relay Events state - now handled by main subscription
// DOM elements
@@ -3405,6 +3419,457 @@ function formatTimestamp(timestamp) {
return date.toLocaleString();
}
// ================================
// BLOB BROWSER FUNCTIONS
// ================================
function blobEscapeHtml(value) {
if (value === null || value === undefined) return '';
return String(value)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, "'");
}
function blobIsImageType(mimeType) {
return typeof mimeType === 'string' && mimeType.toLowerCase().startsWith('image/');
}
function blobShortHash(hash) {
if (!hash || hash.length < 16) return hash || '-';
return `${hash.substring(0, 10)}...${hash.substring(hash.length - 8)}`;
}
function blobPubkeyToDisplay(pubkeyHex) {
if (!pubkeyHex) return '-';
try {
if (window.NostrTools && window.NostrTools.nip19 && window.NostrTools.nip19.npubEncode && /^[0-9a-fA-F]{64}$/.test(pubkeyHex)) {
return window.NostrTools.nip19.npubEncode(pubkeyHex);
}
} catch (error) {
console.log('Failed to encode uploader pubkey to npub:', error.message);
}
return pubkeyHex;
}
function blobNormalizePubkeyFilter(inputValue) {
const trimmed = (inputValue || '').trim();
if (!trimmed) {
return '';
}
// Use existing decoder utility - supports npub and 64-char hex
const decoded = nsecToHex(trimmed);
if (!decoded || !/^[0-9a-fA-F]{64}$/.test(decoded)) {
throw new Error('Invalid pubkey format. Use npub1... or 64-character hex pubkey.');
}
return decoded;
}
function blobBuildApiUrl() {
const params = new URLSearchParams();
params.set('limit', String(blobBrowserState.limit));
params.set('offset', String(blobBrowserState.offset));
if (blobBrowserState.pubkeyFilter) {
params.set('pubkey', blobBrowserState.pubkeyFilter);
}
return `/api/files?${params.toString()}`;
}
function blobSetViewMode(mode) {
blobBrowserState.viewMode = mode === 'list' ? 'list' : 'grid';
const gridView = document.getElementById('blob-grid-view');
const listView = document.getElementById('blob-list-view');
const toggleBtn = document.getElementById('blob-view-toggle-btn');
if (gridView) {
gridView.style.display = blobBrowserState.viewMode === 'grid' ? 'grid' : 'none';
}
if (listView) {
listView.style.display = blobBrowserState.viewMode === 'list' ? 'block' : 'none';
}
if (toggleBtn) {
toggleBtn.textContent = blobBrowserState.viewMode === 'grid' ? 'SWITCH TO LIST VIEW' : 'SWITCH TO GRID VIEW';
}
}
function blobRenderGrid(files) {
const grid = document.getElementById('blob-grid-view');
if (!grid) return;
if (!files || files.length === 0) {
grid.innerHTML = '<div class="blob-empty">No blobs found for this filter.</div>';
return;
}
const cards = files.map(file => {
const url = blobEscapeHtml(file.url || '#');
const hash = blobEscapeHtml(file.sha256 || '');
const type = blobEscapeHtml(file.type || 'unknown');
const uploaderHex = file.uploader_pubkey || '';
const uploaderDisplay = blobEscapeHtml(blobPubkeyToDisplay(uploaderHex));
const uploadedAt = blobEscapeHtml(formatTimestamp(file.uploaded_at));
const size = blobEscapeHtml(formatFileSize(file.size));
const previewHtml = blobIsImageType(file.type) && file.url
? `<a class="blob-thumb-link" href="${url}" target="_blank" rel="noopener noreferrer"><img class="blob-thumb" src="${url}" alt="${hash}" loading="lazy"></a>`
: `<a class="blob-thumb-link" href="${url}" target="_blank" rel="noopener noreferrer"><div class="blob-thumb-placeholder">${type}</div></a>`;
return `
<article class="blob-card">
${previewHtml}
<div class="blob-card-meta">
<div><strong>Hash:</strong> <a class="blob-hash-link" href="${url}" target="_blank" rel="noopener noreferrer">${blobEscapeHtml(blobShortHash(file.sha256 || ''))}</a></div>
<div><strong>Uploader:</strong> ${uploaderDisplay}</div>
<div><strong>Type:</strong> ${type}</div>
<div><strong>Size:</strong> ${size}</div>
<div><strong>Uploaded:</strong> ${uploadedAt}</div>
</div>
</article>
`;
});
grid.innerHTML = cards.join('');
}
function blobRenderList(files) {
const tbody = document.getElementById('blob-list-table-body');
if (!tbody) return;
if (!files || files.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; font-style: italic;">No blobs found for this filter</td></tr>';
return;
}
const rows = files.map(file => {
const url = blobEscapeHtml(file.url || '#');
const hash = blobEscapeHtml(file.sha256 || '');
const type = blobEscapeHtml(file.type || 'unknown');
const uploaderDisplay = blobEscapeHtml(blobPubkeyToDisplay(file.uploader_pubkey || ''));
const size = blobEscapeHtml(formatFileSize(file.size));
const uploadedAt = blobEscapeHtml(formatTimestamp(file.uploaded_at));
const previewHtml = blobIsImageType(file.type) && file.url
? `<a href="${url}" target="_blank" rel="noopener noreferrer"><img class="blob-list-preview" src="${url}" alt="${hash}" loading="lazy"></a>`
: `<a href="${url}" target="_blank" rel="noopener noreferrer"><div class="blob-list-placeholder">FILE</div></a>`;
const sha256Raw = file.sha256 || '';
const shaAttr = blobEscapeHtml(sha256Raw);
const isDeleting = blobBrowserState.deletingHashes.has(sha256Raw);
return `
<tr>
<td><button type="button" class="blob-delete-btn" data-sha256="${shaAttr}" ${isDeleting ? 'disabled' : ''}>${isDeleting ? 'DELETING...' : 'DELETE'}</button></td>
<td>${previewHtml}</td>
<td><a class="blob-hash-link" href="${url}" target="_blank" rel="noopener noreferrer">${blobEscapeHtml(blobShortHash(file.sha256 || ''))}</a></td>
<td>${uploaderDisplay}</td>
<td>${type}</td>
<td>${size}</td>
<td>${uploadedAt}</td>
</tr>
`;
});
tbody.innerHTML = rows.join('');
}
function blobGetSortableValue(file, sortKey) {
switch (sortKey) {
case 'hash':
return (file.sha256 || '').toLowerCase();
case 'uploader':
return blobPubkeyToDisplay(file.uploader_pubkey || '').toLowerCase();
case 'type':
return (file.type || '').toLowerCase();
case 'size':
return Number(file.size) || 0;
case 'uploaded':
return Number(file.uploaded_at) || 0;
default:
return '';
}
}
function blobGetSortedFiles(files) {
if (!Array.isArray(files) || files.length <= 1) {
return Array.isArray(files) ? files : [];
}
const direction = blobBrowserState.sortDirection === 'asc' ? 1 : -1;
const sortKey = blobBrowserState.sortKey;
return [...files].sort((a, b) => {
const left = blobGetSortableValue(a, sortKey);
const right = blobGetSortableValue(b, sortKey);
if (typeof left === 'number' && typeof right === 'number') {
return (left - right) * direction;
}
return String(left).localeCompare(String(right)) * direction;
});
}
function blobUpdateSortIndicators() {
const headers = document.querySelectorAll('#blob-list-table th.blob-sortable-th');
headers.forEach((header) => {
const key = header.dataset.sortKey;
const indicator = header.querySelector('.blob-sort-indicator');
if (!indicator) return;
if (key === blobBrowserState.sortKey) {
indicator.textContent = blobBrowserState.sortDirection === 'asc' ? '▲' : '▼';
} else {
indicator.textContent = '';
}
});
}
function blobRenderSummary() {
const summary = document.getElementById('blob-browser-summary');
const prevBtn = document.getElementById('blob-prev-page-btn');
const nextBtn = document.getElementById('blob-next-page-btn');
if (summary) {
const from = blobBrowserState.total === 0 ? 0 : blobBrowserState.offset + 1;
const to = Math.min(blobBrowserState.offset + blobBrowserState.files.length, blobBrowserState.total);
const filterText = blobBrowserState.pubkeyFilter ? ` | filter: ${blobBrowserState.pubkeyFilter}` : '';
summary.textContent = `Showing ${from}-${to} of ${blobBrowserState.total}${filterText}`;
}
if (prevBtn) {
prevBtn.disabled = blobBrowserState.offset <= 0 || blobBrowserState.loading;
}
if (nextBtn) {
nextBtn.disabled = blobBrowserState.loading || (blobBrowserState.offset + blobBrowserState.limit >= blobBrowserState.total);
}
}
function blobRenderAll() {
const sortedFiles = blobGetSortedFiles(blobBrowserState.files);
blobRenderGrid(sortedFiles);
blobRenderList(sortedFiles);
blobRenderSummary();
blobSetViewMode(blobBrowserState.viewMode);
blobUpdateSortIndicators();
}
async function blobDeleteFile(sha256) {
if (!sha256 || !/^[0-9a-fA-F]{64}$/.test(sha256)) {
alert('Invalid blob hash');
return;
}
if (blobBrowserState.deletingHashes.has(sha256)) {
return;
}
const confirmed = window.confirm(`Delete blob ${blobShortHash(sha256)}? This removes both file and database reference.`);
if (!confirmed) {
return;
}
blobBrowserState.deletingHashes.add(sha256);
blobRenderAll();
try {
const responseData = await sendAdminCommandHTTP(['blob_delete', sha256]);
if (!responseData || responseData.status !== 'success') {
throw new Error((responseData && responseData.error) ? responseData.error : 'Delete failed');
}
log(`Deleted blob ${blobShortHash(sha256)}`, 'INFO');
const shouldResetOffset = blobBrowserState.files.length === 1 && blobBrowserState.offset > 0;
await fetchBlobBrowserData(shouldResetOffset);
} catch (error) {
console.error('Blob delete failed:', error);
alert(`Delete failed: ${error.message}`);
} finally {
blobBrowserState.deletingHashes.delete(sha256);
blobRenderAll();
}
}
async function fetchBlobBrowserData(resetOffset = false) {
if (resetOffset) {
blobBrowserState.offset = 0;
}
blobBrowserState.loading = true;
blobRenderSummary();
try {
const response = await fetch(blobBuildApiUrl(), {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const payload = await response.json();
const data = payload && payload.data ? payload.data : {};
blobBrowserState.files = Array.isArray(data.files) ? data.files : [];
blobBrowserState.total = Number.isFinite(data.total) ? data.total : 0;
if (typeof data.pubkey_filter === 'string' && data.pubkey_filter.length > 0) {
blobBrowserState.pubkeyFilter = data.pubkey_filter;
}
blobRenderAll();
} catch (error) {
console.error('Failed to fetch blob browser data:', error);
const summary = document.getElementById('blob-browser-summary');
if (summary) {
summary.textContent = `Failed to load blobs: ${error.message}`;
}
blobBrowserState.files = [];
blobBrowserState.total = 0;
blobRenderAll();
} finally {
blobBrowserState.loading = false;
blobRenderSummary();
}
}
function initializeBlobBrowserControls() {
const applyBtn = document.getElementById('blob-filter-apply-btn');
const clearBtn = document.getElementById('blob-filter-clear-btn');
const toggleBtn = document.getElementById('blob-view-toggle-btn');
const refreshBtn = document.getElementById('blob-refresh-btn');
const prevBtn = document.getElementById('blob-prev-page-btn');
const nextBtn = document.getElementById('blob-next-page-btn');
const filterInput = document.getElementById('blob-filter-pubkey');
if (applyBtn && !applyBtn.dataset.bound) {
applyBtn.addEventListener('click', () => {
try {
blobBrowserState.pubkeyFilter = blobNormalizePubkeyFilter(filterInput ? filterInput.value : '');
if (filterInput && blobBrowserState.pubkeyFilter) {
filterInput.value = blobBrowserState.pubkeyFilter;
}
fetchBlobBrowserData(true);
} catch (error) {
log(error.message, 'ERROR');
}
});
applyBtn.dataset.bound = 'true';
}
if (clearBtn && !clearBtn.dataset.bound) {
clearBtn.addEventListener('click', () => {
blobBrowserState.pubkeyFilter = '';
if (filterInput) {
filterInput.value = '';
}
fetchBlobBrowserData(true);
});
clearBtn.dataset.bound = 'true';
}
if (toggleBtn && !toggleBtn.dataset.bound) {
toggleBtn.addEventListener('click', () => {
blobSetViewMode(blobBrowserState.viewMode === 'grid' ? 'list' : 'grid');
});
toggleBtn.dataset.bound = 'true';
}
if (refreshBtn && !refreshBtn.dataset.bound) {
refreshBtn.addEventListener('click', () => fetchBlobBrowserData(false));
refreshBtn.dataset.bound = 'true';
}
if (prevBtn && !prevBtn.dataset.bound) {
prevBtn.addEventListener('click', () => {
if (blobBrowserState.offset >= blobBrowserState.limit) {
blobBrowserState.offset -= blobBrowserState.limit;
fetchBlobBrowserData(false);
}
});
prevBtn.dataset.bound = 'true';
}
if (nextBtn && !nextBtn.dataset.bound) {
nextBtn.addEventListener('click', () => {
if (blobBrowserState.offset + blobBrowserState.limit < blobBrowserState.total) {
blobBrowserState.offset += blobBrowserState.limit;
fetchBlobBrowserData(false);
}
});
nextBtn.dataset.bound = 'true';
}
if (filterInput && !filterInput.dataset.bound) {
filterInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
if (applyBtn) {
applyBtn.click();
}
}
});
filterInput.dataset.bound = 'true';
}
const listBody = document.getElementById('blob-list-table-body');
if (listBody && !listBody.dataset.deleteBound) {
listBody.addEventListener('click', (event) => {
const deleteBtn = event.target.closest('.blob-delete-btn');
if (!deleteBtn) {
return;
}
const sha256 = deleteBtn.dataset.sha256 || '';
blobDeleteFile(sha256);
});
listBody.dataset.deleteBound = 'true';
}
const sortableHeaders = document.querySelectorAll('#blob-list-table th.blob-sortable-th');
sortableHeaders.forEach((header) => {
if (header.dataset.bound) {
return;
}
header.addEventListener('click', () => {
const clickedSortKey = header.dataset.sortKey;
if (!clickedSortKey) {
return;
}
if (blobBrowserState.sortKey === clickedSortKey) {
blobBrowserState.sortDirection = blobBrowserState.sortDirection === 'asc' ? 'desc' : 'asc';
} else {
blobBrowserState.sortKey = clickedSortKey;
blobBrowserState.sortDirection = clickedSortKey === 'uploaded' ? 'desc' : 'asc';
}
blobRenderAll();
});
header.dataset.bound = 'true';
});
blobSetViewMode(blobBrowserState.viewMode);
blobUpdateSortIndicators();
}
// Update statistics cell with flash animation if value changed
function updateStatsCell(cellId, newValue) {
const cell = document.getElementById(cellId);
@@ -3698,6 +4163,7 @@ function switchPage(pageName) {
// Hide all sections
const sections = [
'databaseStatisticsSection',
'blobBrowserSection',
'subscriptionDetailsSection',
'div_config',
'authRulesSection',
@@ -3716,6 +4182,7 @@ function switchPage(pageName) {
// Show selected section
const pageMap = {
'statistics': 'databaseStatisticsSection',
'blob-browser': 'blobBrowserSection',
'subscriptions': 'subscriptionDetailsSection',
'configuration': 'div_config',
'authorization': 'authRulesSection',
@@ -3744,6 +4211,12 @@ function switchPage(pageName) {
});
}
// Blob browser page setup
if (pageName === 'blob-browser') {
initializeBlobBrowserControls();
fetchBlobBrowserData(false);
}
// Close side navigation
closeSideNav();

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
build/ginxsom-fcgi_static_x86_64 Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -199,56 +199,43 @@ else
fi
echo ""
# Step 8: Start ginxsom FastCGI process
# Step 8: Start ginxsom through systemd (keeps process supervised and auto-restarted)
print_status "Step 8: Starting ginxsom service..."
ssh $REMOTE_USER@$REMOTE_HOST << EOF
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
set -e
echo "Starting ginxsom FastCGI with configuration:"
echo " Binary: $REMOTE_BINARY_PATH"
echo " Database: $REMOTE_DB_PATH"
echo " Storage: $REMOTE_BLOB_DIR"
echo " Socket: $REMOTE_SOCKET"
echo ""
sudo spawn-fcgi \
-M 666 \
-u www-data \
-g www-data \
-s $REMOTE_SOCKET \
-U www-data \
-G www-data \
-d $REMOTE_BINARY_DIR \
-- $REMOTE_BINARY_PATH \
--admin-pubkey $ADMIN_PUBKEY \
--server-privkey $SERVER_PRIVKEY \
--db-path $REMOTE_DB_PATH \
--storage-dir $REMOTE_BLOB_DIR
# Give it a moment to start
echo "Restarting systemd service..."
sudo systemctl restart ginxsom.service
sleep 2
# Verify process is running
if [ -S $REMOTE_SOCKET ]; then
echo "FastCGI socket created successfully"
ls -la $REMOTE_SOCKET
if sudo systemctl is-active --quiet ginxsom.service; then
echo "Service is active"
else
echo "ERROR: Socket not created"
echo "ERROR: ginxsom.service is not active"
sudo systemctl status --no-pager ginxsom.service || true
sudo journalctl -u ginxsom.service -n 80 --no-pager || true
exit 1
fi
# Check if process is running
if pgrep -f ginxsom-fcgi > /dev/null; then
echo "Process is running (PID: \$(pgrep -f ginxsom-fcgi))"
# Verify socket is listening
if [ -S /tmp/ginxsom-fcgi.sock ]; then
echo "FastCGI socket created successfully"
ls -la /tmp/ginxsom-fcgi.sock
else
echo "WARNING: Process not found by pgrep (may be normal for FastCGI)"
echo "ERROR: Socket not created"
sudo systemctl status --no-pager ginxsom.service || true
sudo journalctl -u ginxsom.service -n 80 --no-pager || true
exit 1
fi
# Show process list for diagnostics
pgrep -af ginxsom-fcgi || true
EOF
if [ $? -eq 0 ]; then
print_success "FastCGI process started"
print_success "FastCGI service started"
else
print_error "Failed to start FastCGI process"
print_error "Failed to start FastCGI service"
exit 1
fi
echo ""
@@ -282,13 +269,42 @@ echo ""
# Wait a moment for service to fully start
sleep 2
VERIFY_FAILED=false
# Test health endpoint
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
print_error "✗ Health check failed"
curl -k -v --max-time 10 "https://blossom.laantungir.net/health" 2>&1 | head -10 || true
VERIFY_FAILED=true
fi
# Test API health endpoint
echo ""
echo "Testing API health endpoint..."
API_HEALTH_STATUS=$(curl -k -s -o /dev/null -w "%{http_code}" --max-time 10 "https://blossom.laantungir.net/api/health" || echo "000")
if [[ "$API_HEALTH_STATUS" == "200" ]]; then
print_success "✓ API health endpoint responding (200)"
else
print_error "✗ API health endpoint failed (HTTP $API_HEALTH_STATUS)"
curl -k -v --max-time 10 "https://blossom.laantungir.net/api/health" 2>&1 | head -20 || true
VERIFY_FAILED=true
fi
# Test files API endpoint
echo ""
echo "Testing files API endpoint..."
API_FILES_STATUS=$(curl -k -s -o /dev/null -w "%{http_code}" --max-time 10 "https://blossom.laantungir.net/api/files?limit=1" || echo "000")
if [[ "$API_FILES_STATUS" == "200" ]]; then
print_success "✓ Files API endpoint responding (200)"
elif [[ "$API_FILES_STATUS" == "401" || "$API_FILES_STATUS" == "403" ]]; then
print_success "✓ Files API endpoint reachable (HTTP $API_FILES_STATUS indicates auth is enforced)"
else
print_error "✗ Files API endpoint failed (HTTP $API_FILES_STATUS)"
curl -k -v --max-time 10 "https://blossom.laantungir.net/api/files?limit=1" 2>&1 | head -20 || true
VERIFY_FAILED=true
fi
# Test root endpoint
@@ -300,6 +316,11 @@ else
print_warning "✗ Root endpoint not responding as expected"
fi
if [ "$VERIFY_FAILED" = true ]; then
print_error "Deployment verification failed - one or more critical endpoints are unhealthy"
exit 1
fi
echo ""
print_status "=========================================="
print_success "Deployment completed!"
@@ -314,6 +335,8 @@ echo " Socket: $REMOTE_SOCKET"
echo ""
print_status "Test Commands:"
echo " Health: curl -k https://blossom.laantungir.net/health"
echo " API Health: curl -k https://blossom.laantungir.net/api/health"
echo " Files API: curl -k 'https://blossom.laantungir.net/api/files?limit=5'"
echo " Info: curl -k https://blossom.laantungir.net/"
echo " Upload: ./tests/file_put_bud02.sh"
echo ""

View File

@@ -158,51 +158,43 @@ update_version_in_header() {
fi
}
# Function to compile the Ginxsom project
# Function to build the static Ginxsom binary
compile_project() {
print_status "Compiling Ginxsom FastCGI server..."
print_status "Building static Ginxsom FastCGI binary via build_static.sh..."
# Clean previous build
if make clean > /dev/null 2>&1; then
print_success "Cleaned previous build"
else
print_warning "Clean failed or no Makefile found"
if [[ ! -x "./build_static.sh" ]]; then
print_error "build_static.sh not found or not executable"
exit 1
fi
# Compile the project
if make > /dev/null 2>&1; then
print_success "Ginxsom compiled successfully"
if ./build_static.sh > /dev/null 2>&1; then
print_success "Static build completed successfully"
# Verify the binary was created
if [[ -f "build/ginxsom-fcgi" ]]; then
print_success "Binary created: build/ginxsom-fcgi"
# Verify the static binary was created
if [[ -f "build/ginxsom-fcgi_static_x86_64" || -f "build/ginxsom-fcgi_static_arm64" ]]; then
print_success "Static binary created in build/"
else
print_error "Binary not found after compilation"
print_error "Static binary not found after build_static.sh"
exit 1
fi
else
print_error "Compilation failed"
print_error "Static build failed"
exit 1
fi
}
# Function to build release binary
build_release_binary() {
print_status "Building release binary..."
print_status "Preparing release binary from static build output..."
# Build the FastCGI server
print_status "Building Ginxsom FastCGI server..."
make clean > /dev/null 2>&1
if make > /dev/null 2>&1; then
if [[ -f "build/ginxsom-fcgi" ]]; then
cp build/ginxsom-fcgi ginxsom-fcgi-linux-x86_64
if [[ -f "build/ginxsom-fcgi_static_x86_64" ]]; then
cp build/ginxsom-fcgi_static_x86_64 ginxsom-fcgi-linux-x86_64
print_success "Release binary created: ginxsom-fcgi-linux-x86_64"
elif [[ -f "build/ginxsom-fcgi_static_arm64" ]]; then
cp build/ginxsom-fcgi_static_arm64 ginxsom-fcgi-linux-arm64
print_success "Release binary created: ginxsom-fcgi-linux-arm64"
else
print_error "Binary not found after compilation"
exit 1
fi
else
print_error "Build failed"
print_error "No static build artifact found in build/"
exit 1
fi
}
@@ -385,6 +377,17 @@ upload_release_binary() {
print_warning "Failed to upload FastCGI binary"
fi
fi
if [[ -f "ginxsom-fcgi-linux-arm64" ]]; then
print_status "Uploading Ginxsom ARM64 FastCGI binary..."
if curl -s -X POST "$GITEA_REPO_URL/releases/$release_id/assets" \
-H "Authorization: token $token" \
-F "attachment=@ginxsom-fcgi-linux-arm64;filename=ginxsom-fcgi-${NEW_VERSION}-linux-arm64" > /dev/null; then
print_success "Uploaded ARM64 FastCGI binary"
else
print_warning "Failed to upload ARM64 FastCGI binary"
fi
fi
}
# Function to clean up release binary
@@ -393,6 +396,11 @@ cleanup_release_binary() {
rm -f ginxsom-fcgi-linux-x86_64
print_status "Cleaned up release binary"
fi
if [[ -f "ginxsom-fcgi-linux-arm64" ]]; then
rm -f ginxsom-fcgi-linux-arm64
print_status "Cleaned up ARM64 release binary"
fi
}
# Main execution

View File

@@ -63,8 +63,14 @@ for file in "${FILES[@]}"; do
# Convert file to C byte array
echo "static const unsigned char embedded_${varname}[] = {" >> "${OUTPUT_FILE}"
# Use xxd to convert to hex, then format as C array
# Use xxd when available, otherwise fall back to od
if command -v xxd >/dev/null 2>&1; then
xxd -i < "${filepath}" >> "${OUTPUT_FILE}"
else
od -An -tx1 -v "${filepath}" | tr -s ' ' '\n' | sed '/^$/d' | while read -r byte; do
printf "0x%s,\n" "${byte}" >> "${OUTPUT_FILE}"
done
fi
echo "};" >> "${OUTPUT_FILE}"
echo "static const size_t embedded_${varname}_size = sizeof(embedded_${varname});" >> "${OUTPUT_FILE}"

View File

@@ -33,6 +33,7 @@ int parse_query_params(const char* query_string, char params[][256], int max_par
static int admin_nip94_get_origin(char* out, size_t out_size);
static void admin_nip94_build_blob_url(const char* origin, const char* sha256, const char* mime_type, char* out, size_t out_size);
static const char* admin_mime_to_extension(const char* mime_type);
static int validate_hex64_format(const char* hash);
// Local utility functions (from main.c but implemented here for admin API)
static int admin_nip94_get_origin(char* out, size_t out_size) {
@@ -45,20 +46,14 @@ static int admin_nip94_get_origin(char* out, size_t out_size) {
int rc;
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc) {
// Default on DB error
strncpy(out, "http://localhost:9001", out_size - 1);
out[out_size - 1] = '\0';
return 1;
}
if (rc == SQLITE_OK) {
const char* sql = "SELECT value FROM config WHERE key = 'cdn_origin'";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
const char* value = (const char*)sqlite3_column_text(stmt, 0);
if (value) {
if (value && value[0] != '\0' && strcmp(value, "http://localhost:9001") != 0) {
strncpy(out, value, out_size - 1);
out[out_size - 1] = '\0';
sqlite3_finalize(stmt);
@@ -68,11 +63,28 @@ static int admin_nip94_get_origin(char* out, size_t out_size) {
}
sqlite3_finalize(stmt);
}
sqlite3_close(db);
}
// Default fallback
strncpy(out, "http://localhost:9001", out_size - 1);
// Dynamic fallback from request headers (preferred over localhost)
const char* host = getenv("HTTP_HOST");
const char* forwarded_proto = getenv("HTTP_X_FORWARDED_PROTO");
const char* https = getenv("HTTPS");
if (host && host[0] != '\0') {
const char* proto = "http";
if (forwarded_proto && forwarded_proto[0] != '\0') {
proto = forwarded_proto;
} else if (https && (strcmp(https, "on") == 0 || strcmp(https, "1") == 0)) {
proto = "https";
}
snprintf(out, out_size, "%s://%s", proto, host);
return 1;
}
// Final fallback
strncpy(out, "https://blossom.laantungir.net", out_size - 1);
out[out_size - 1] = '\0';
return 1;
}
@@ -93,6 +105,22 @@ static const char* admin_mime_to_extension(const char* mime_type) {
return mime_to_extension(mime_type);
}
static int validate_hex64_format(const char* hash) {
if (!hash || strlen(hash) != 64) {
return 0;
}
for (size_t i = 0; i < 64; i++) {
if (!((hash[i] >= '0' && hash[i] <= '9') ||
(hash[i] >= 'a' && hash[i] <= 'f') ||
(hash[i] >= 'A' && hash[i] <= 'F'))) {
return 0;
}
}
return 1;
}
// Main API request handler
void handle_admin_api_request(const char* method, const char* uri, const char* validated_pubkey, int is_authenticated) {
const char* path = uri + 4; // Skip "/api"
@@ -107,7 +135,8 @@ void handle_admin_api_request(const char* method, const char* uri, const char* v
// Health endpoint and POST /admin (Kind 23456 events) are exempt from authentication requirement
// Kind 23456 events authenticate themselves via signed event validation
int skip_auth = (strcmp(path, "/health") == 0) ||
(strcmp(method, "POST") == 0 && strcmp(path, "/admin") == 0);
(strcmp(method, "POST") == 0 && strcmp(path, "/admin") == 0) ||
(strcmp(method, "GET") == 0 && strncmp(path, "/files", 6) == 0);
if (!skip_auth) {
if (!is_authenticated || !validated_pubkey) {
@@ -577,14 +606,15 @@ void handle_config_key_put_api(const char* key) {
}
void handle_files_api(void) {
// Parse query parameters for pagination
// Parse query parameters for pagination and optional pubkey filtering
const char* query_string = getenv("QUERY_STRING");
int limit = 50;
int offset = 0;
char pubkey_filter[65] = {0};
if (query_string) {
char params[10][256];
int param_count = parse_query_params(query_string, params, 10);
char params[12][256];
int param_count = parse_query_params(query_string, params, 12);
for (int i = 0; i < param_count; i++) {
char* key = params[i];
@@ -597,6 +627,12 @@ void handle_files_api(void) {
} else if (strcmp(key, "offset") == 0) {
offset = atoi(value);
if (offset < 0) offset = 0;
} else if (strcmp(key, "pubkey") == 0) {
strncpy(pubkey_filter, value, sizeof(pubkey_filter) - 1);
pubkey_filter[sizeof(pubkey_filter) - 1] = '\0';
if (!validate_hex64_format(pubkey_filter)) {
pubkey_filter[0] = '\0';
}
}
}
}
@@ -612,8 +648,11 @@ void handle_files_api(void) {
return;
}
// Query recent files with pagination
const char* sql = "SELECT sha256, size, type, uploaded_at, uploader_pubkey, filename "
// Query recent files with pagination and optional uploader filter
const char* sql = (pubkey_filter[0] != '\0')
? "SELECT sha256, size, type, uploaded_at, uploader_pubkey, filename "
"FROM blobs WHERE uploader_pubkey = ? ORDER BY uploaded_at DESC LIMIT ? OFFSET ?"
: "SELECT sha256, size, type, uploaded_at, uploader_pubkey, filename "
"FROM blobs ORDER BY uploaded_at DESC LIMIT ? OFFSET ?";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
@@ -623,23 +662,38 @@ void handle_files_api(void) {
return;
}
sqlite3_bind_int(stmt, 1, limit);
sqlite3_bind_int(stmt, 2, offset);
int bind_index = 1;
if (pubkey_filter[0] != '\0') {
sqlite3_bind_text(stmt, bind_index++, pubkey_filter, -1, SQLITE_STATIC);
}
sqlite3_bind_int(stmt, bind_index++, limit);
sqlite3_bind_int(stmt, bind_index++, offset);
cJSON* response = cJSON_CreateObject();
cJSON* data = cJSON_CreateObject();
cJSON* files_array = cJSON_CreateArray();
if (!response || !data || !files_array) {
if (response) cJSON_Delete(response);
if (data) cJSON_Delete(data);
if (files_array) cJSON_Delete(files_array);
sqlite3_finalize(stmt);
sqlite3_close(db);
send_json_error(500, "memory_error", "Failed to allocate response JSON");
return;
}
cJSON_AddStringToObject(response, "status", "success");
cJSON_AddItemToObject(response, "data", data);
cJSON_AddItemToObject(data, "files", files_array);
cJSON_AddNumberToObject(data, "limit", limit);
cJSON_AddNumberToObject(data, "offset", offset);
int total_count = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
total_count++;
cJSON* file_obj = cJSON_CreateObject();
if (!file_obj) {
continue;
}
cJSON_AddItemToArray(files_array, file_obj);
const char* sha256 = (const char*)sqlite3_column_text(stmt, 0);
@@ -669,12 +723,18 @@ void handle_files_api(void) {
}
}
// Get total count for pagination info
const char* count_sql = "SELECT COUNT(*) FROM blobs";
// Get total count for pagination info (respecting optional pubkey filter)
const char* count_sql = (pubkey_filter[0] != '\0')
? "SELECT COUNT(*) FROM blobs WHERE uploader_pubkey = ?"
: "SELECT COUNT(*) FROM blobs";
sqlite3_stmt* count_stmt;
rc = sqlite3_prepare_v2(db, count_sql, -1, &count_stmt, NULL);
if (rc == SQLITE_OK) {
if (pubkey_filter[0] != '\0') {
sqlite3_bind_text(count_stmt, 1, pubkey_filter, -1, SQLITE_STATIC);
}
rc = sqlite3_step(count_stmt);
if (rc == SQLITE_ROW) {
int total = sqlite3_column_int(count_stmt, 0);
@@ -683,6 +743,10 @@ void handle_files_api(void) {
sqlite3_finalize(count_stmt);
}
if (pubkey_filter[0] != '\0') {
cJSON_AddStringToObject(data, "pubkey_filter", pubkey_filter);
}
sqlite3_finalize(stmt);
sqlite3_close(db);
@@ -785,15 +849,14 @@ int parse_query_params(const char* query_string, char params[][256], int max_par
memcpy(query_copy, query_string, query_len + 1);
int count = 0;
char* token = strtok(query_copy, "&");
char* saveptr = NULL;
char* token = strtok_r(query_copy, "&", &saveptr);
while (token && count < max_params) {
if (strlen(token) >= sizeof(params[0])) {
token[sizeof(params[0]) - 1] = '\0';
}
strcpy(params[count], token);
strncpy(params[count], token, sizeof(params[0]) - 1);
params[count][sizeof(params[0]) - 1] = '\0';
count++;
token = strtok(NULL, "&");
token = strtok_r(NULL, "&", &saveptr);
}
free(query_copy);

View File

@@ -11,6 +11,9 @@
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include "ginxsom.h"
// Global state
static struct {
@@ -128,6 +131,9 @@ cJSON* admin_commands_process(cJSON* command_array, const char* request_event_id
else if (strcmp(command, "blob_list") == 0) {
return admin_cmd_blob_list(command_array);
}
else if (strcmp(command, "blob_delete") == 0) {
return admin_cmd_blob_delete(command_array);
}
else if (strcmp(command, "storage_stats") == 0) {
return admin_cmd_storage_stats(command_array);
}
@@ -585,6 +591,142 @@ cJSON* admin_cmd_blob_list(cJSON* args) {
return response;
}
cJSON* admin_cmd_blob_delete(cJSON* args) {
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "query_type", "blob_delete");
// Expected format: ["blob_delete", "<sha256>"]
if (cJSON_GetArraySize(args) < 2) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Missing blob hash");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
cJSON* hash_item = cJSON_GetArrayItem(args, 1);
if (!cJSON_IsString(hash_item) || !validate_sha256_format(hash_item->valuestring)) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Invalid SHA-256 hash format");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
const char* sha256 = hash_item->valuestring;
sqlite3* db;
int rc = sqlite3_open_v2(g_admin_state.db_path, &db, SQLITE_OPEN_READWRITE, NULL);
if (rc != SQLITE_OK) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to open database");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
rc = sqlite3_exec(db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL);
if (rc != SQLITE_OK) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to begin transaction");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
sqlite3_stmt* stmt = NULL;
const char* select_sql = "SELECT type FROM blobs WHERE sha256 = ?";
rc = sqlite3_prepare_v2(db, select_sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to prepare metadata query");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
sqlite3_bind_text(stmt, 1, sha256, -1, SQLITE_STATIC);
char blob_type[128] = {0};
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
const char* type = (const char*)sqlite3_column_text(stmt, 0);
if (type) {
strncpy(blob_type, type, sizeof(blob_type) - 1);
blob_type[sizeof(blob_type) - 1] = '\0';
}
} else {
sqlite3_finalize(stmt);
sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Blob not found");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
sqlite3_finalize(stmt);
const char* extension = mime_to_extension(blob_type);
char filepath[4096];
snprintf(filepath, sizeof(filepath), "%s/%s%s", g_storage_dir, sha256, extension ? extension : "");
int file_deleted = 0;
if (unlink(filepath) == 0) {
file_deleted = 1;
} else if (errno == ENOENT) {
// File already absent; still remove DB reference
file_deleted = 0;
} else {
sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);
char errbuf[256];
snprintf(errbuf, sizeof(errbuf), "Failed to delete file: %s", strerror(errno));
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", errbuf);
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
const char* delete_sql = "DELETE FROM blobs WHERE sha256 = ?";
rc = sqlite3_prepare_v2(db, delete_sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to prepare delete query");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
sqlite3_bind_text(stmt, 1, sha256, -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to delete blob metadata");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
rc = sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
sqlite3_close(db);
if (rc != SQLITE_OK) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to commit blob delete transaction");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
cJSON_AddStringToObject(response, "status", "success");
cJSON_AddStringToObject(response, "sha256", sha256);
cJSON_AddBoolToObject(response, "file_deleted", file_deleted ? 1 : 0);
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
cJSON* admin_cmd_storage_stats(cJSON* args) {
(void)args;

View File

@@ -33,6 +33,7 @@ cJSON* admin_cmd_config_update(cJSON* args);
cJSON* admin_cmd_stats_query(cJSON* args);
cJSON* admin_cmd_system_status(cJSON* args);
cJSON* admin_cmd_blob_list(cJSON* args);
cJSON* admin_cmd_blob_delete(cJSON* args);
cJSON* admin_cmd_storage_stats(cJSON* args);
cJSON* admin_cmd_sql_query(cJSON* args);
cJSON* admin_cmd_query_view(cJSON* args);

File diff suppressed because it is too large Load Diff

View File

@@ -10,8 +10,8 @@
// Version information (auto-updated by build system)
#define VERSION_MAJOR 0
#define VERSION_MINOR 1
#define VERSION_PATCH 33
#define VERSION "v0.1.33"
#define VERSION_PATCH 37
#define VERSION "v0.1.37"
#include <stddef.h>
#include <stdint.h>

View File

@@ -321,7 +321,7 @@ int initialize_database(const char *db_path) {
" ('nip42_require_auth', 'false', 'Enable NIP-42 challenge/response authentication'),"
" ('nip42_challenge_timeout', '600', 'NIP-42 challenge timeout in seconds'),"
" ('nip42_time_tolerance', '300', 'NIP-42 timestamp tolerance in seconds'),"
" ('enable_relay_connect', 'true', 'Enable connection to Nostr relays'),"
" ('enable_relay_connect', 'false', 'Enable connection to Nostr relays'),"
" ('kind_0_content', '{\"name\":\"Ginxsom Blossom Server\",\"about\":\"A Nostr-enabled Blossom media server\",\"picture\":\"\"}', 'JSON content for Kind 0 profile event'),"
" ('kind_10002_tags', '[\"wss://relay.laantungir.net\"]', 'JSON array of relay URLs for Kind 10002');";

View File

@@ -264,14 +264,30 @@ int relay_client_start(void) {
app_log(LOG_INFO, "Starting relay client...");
// Start management thread
g_relay_state.running = 1;
if (pthread_create(&g_relay_state.management_thread, NULL, relay_management_thread, NULL) != 0) {
app_log(LOG_ERROR, "Failed to create relay management thread");
g_relay_state.running = 0;
// Use a larger stack for websocket/TLS internals to avoid stack overflow in static builds
pthread_attr_t thread_attr;
size_t stack_size = 8 * 1024 * 1024; // 8MB
if (pthread_attr_init(&thread_attr) != 0) {
app_log(LOG_ERROR, "Failed to initialize thread attributes");
return -1;
}
if (pthread_attr_setstacksize(&thread_attr, stack_size) != 0) {
app_log(LOG_WARN, "Failed to set relay management thread stack size, using default");
}
// Start management thread
g_relay_state.running = 1;
if (pthread_create(&g_relay_state.management_thread, &thread_attr, relay_management_thread, NULL) != 0) {
app_log(LOG_ERROR, "Failed to create relay management thread");
g_relay_state.running = 0;
pthread_attr_destroy(&thread_attr);
return -1;
}
pthread_attr_destroy(&thread_attr);
app_log(LOG_INFO, "Relay client started successfully");
return 0;
}