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:
+13
-18
@@ -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
|
||||
|
||||
+155
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
+473
@@ -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.
Executable
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+60
-37
@@ -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 ""
|
||||
|
||||
+36
-28
@@ -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
|
||||
print_success "Release binary created: ginxsom-fcgi-linux-x86_64"
|
||||
else
|
||||
print_error "Binary not found after compilation"
|
||||
exit 1
|
||||
fi
|
||||
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 "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
|
||||
|
||||
@@ -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
|
||||
xxd -i < "${filepath}" >> "${OUTPUT_FILE}"
|
||||
# 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}"
|
||||
|
||||
+107
-44
@@ -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,34 +46,45 @@ 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';
|
||||
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 && value[0] != '\0' && strcmp(value, "http://localhost:9001") != 0) {
|
||||
strncpy(out, value, out_size - 1);
|
||||
out[out_size - 1] = '\0';
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_close(db);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
sqlite3_close(db);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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) {
|
||||
strncpy(out, value, out_size - 1);
|
||||
out[out_size - 1] = '\0';
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_close(db);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
sqlite3_close(db);
|
||||
|
||||
// Default fallback
|
||||
strncpy(out, "http://localhost:9001", out_size - 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,9 +648,12 @@ void handle_files_api(void) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Query recent files with pagination
|
||||
const char* sql = "SELECT sha256, size, type, uploaded_at, uploader_pubkey, filename "
|
||||
"FROM blobs ORDER BY uploaded_at DESC LIMIT ? OFFSET ?";
|
||||
// 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);
|
||||
if (rc != SQLITE_OK) {
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
+733097
-59244
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -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>
|
||||
|
||||
+1
-1
@@ -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');";
|
||||
|
||||
|
||||
+21
-5
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user