Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1ea256076 | ||
|
|
09654fa557 | ||
|
|
469d2180c1 | ||
|
|
fa5f5e60cb | ||
|
|
015094949e | ||
|
|
9491f5c373 | ||
|
|
485e351998 | ||
|
|
66ac92b4a0 | ||
|
|
ba8b51d1c6 | ||
|
|
b9e9a08e9e | ||
|
|
d400744f68 | ||
|
|
4b12cb19dc | ||
|
|
e5b1556a80 |
+3
-2
@@ -4,5 +4,6 @@ nostr_core_lib/
|
||||
blobs/
|
||||
c-relay/
|
||||
text_graph/
|
||||
.test_keys/
|
||||
ginxsom-local.service
|
||||
.test_keys
|
||||
ginxsom-local.service
|
||||
*.tar.gz
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
description: "increment and push"
|
||||
---
|
||||
|
||||
Run increment_and_push.sh adding a good comment on the command line following the command.
|
||||
Run increment_and_push.sh adding a good comment on the command line following the command. Don't run any other git commands.
|
||||
+23
-20
@@ -31,6 +31,7 @@ RUN apk add --no-cache \
|
||||
linux-headers \
|
||||
wget \
|
||||
bash \
|
||||
openssh-client \
|
||||
nghttp2-dev \
|
||||
nghttp2-static \
|
||||
c-ares-dev \
|
||||
@@ -58,41 +59,43 @@ RUN cd /tmp && \
|
||||
make install && \
|
||||
rm -rf /tmp/secp256k1
|
||||
|
||||
# Copy only submodule configuration and git directory
|
||||
COPY .gitmodules /build/.gitmodules
|
||||
COPY .git /build/.git
|
||||
# Copy nostr_core_lib from local submodule (avoids remote git dependency)
|
||||
COPY nostr_core_lib /build/nostr_core_lib
|
||||
|
||||
# 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 && \
|
||||
chmod +x build.sh && \
|
||||
sed -i 's/CFLAGS="-Wall -Wextra -std=c99 -fPIC -O2"/CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -std=c99 -fPIC -O2"/' build.sh && \
|
||||
if [ "$(uname -m)" = "aarch64" ] && ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \
|
||||
ln -sf /usr/bin/gcc /usr/local/bin/aarch64-linux-gnu-gcc; \
|
||||
fi && \
|
||||
rm -f *.o *.a 2>/dev/null || true && \
|
||||
./build.sh --nips=1,6,13,17,19,42,44,59
|
||||
./build.sh --nips=1,6,13,17,19,42,44,59 && \
|
||||
if [ -f libnostr_core_arm64.a ]; then \
|
||||
cp libnostr_core_arm64.a libnostr_core.a; \
|
||||
elif [ -f libnostr_core_x64.a ]; then \
|
||||
cp libnostr_core_x64.a libnostr_core.a; \
|
||||
else \
|
||||
echo "ERROR: No supported nostr_core static library produced"; \
|
||||
ls -la *.a 2>/dev/null || true; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# Copy web interface files for embedding
|
||||
# Note: Changes to api/ files will trigger rebuild from this point
|
||||
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
|
||||
@@ -115,7 +118,7 @@ RUN if [ "$DEBUG_BUILD" = "true" ]; then \
|
||||
src/request_validator.c src/relay_client.c \
|
||||
nostr_core_lib/nostr_core/core_relay_pool.c \
|
||||
-o /build/ginxsom-fcgi_static \
|
||||
nostr_core_lib/libnostr_core_x64.a \
|
||||
nostr_core_lib/libnostr_core.a \
|
||||
-lfcgi -lsqlite3 -lsecp256k1 -lssl -lcrypto -lcurl \
|
||||
-lnghttp2 -lcares -lidn2 -lunistring -lpsl -lbrotlidec -lbrotlicommon \
|
||||
-lz -lpthread -lm -ldl && \
|
||||
|
||||
+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.
Executable
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+106
-4
@@ -11,8 +11,40 @@ DOCKERFILE="$SCRIPT_DIR/Dockerfile.alpine-musl"
|
||||
|
||||
# Parse command line arguments
|
||||
DEBUG_BUILD=false
|
||||
if [[ "$1" == "--debug" ]]; then
|
||||
DEBUG_BUILD=true
|
||||
TARGET_ARCH=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--debug)
|
||||
DEBUG_BUILD=true
|
||||
shift
|
||||
;;
|
||||
--arch)
|
||||
if [[ -z "$2" ]]; then
|
||||
echo "ERROR: --arch requires a value"
|
||||
echo "Usage: $0 [--debug] [--arch <arm64|armv7|x86_64>]"
|
||||
exit 1
|
||||
fi
|
||||
case "$2" in
|
||||
arm64|armv7|x86_64)
|
||||
TARGET_ARCH="$2"
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unsupported architecture '$2'"
|
||||
echo "Supported values: arm64, armv7, x86_64"
|
||||
exit 1
|
||||
esac
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unknown argument '$1'"
|
||||
echo "Usage: $0 [--debug] [--arch <arm64|armv7|x86_64>]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$DEBUG_BUILD" = true ]; then
|
||||
echo "=========================================="
|
||||
echo "Ginxsom MUSL Static Binary Builder (DEBUG MODE)"
|
||||
echo "=========================================="
|
||||
@@ -24,6 +56,9 @@ fi
|
||||
echo "Project directory: $SCRIPT_DIR"
|
||||
echo "Build directory: $BUILD_DIR"
|
||||
echo "Debug build: $DEBUG_BUILD"
|
||||
if [[ -n "$TARGET_ARCH" ]]; then
|
||||
echo "Requested target architecture: $TARGET_ARCH"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Create build directory
|
||||
@@ -58,17 +93,43 @@ DOCKER_CMD="docker"
|
||||
echo "✓ Docker is available and running"
|
||||
echo ""
|
||||
|
||||
# Detect architecture
|
||||
# Detect host architecture
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
HOST_ARCH="x86_64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
HOST_ARCH="arm64"
|
||||
;;
|
||||
armv7l|armv7)
|
||||
HOST_ARCH="armv7"
|
||||
;;
|
||||
*)
|
||||
HOST_ARCH="unknown"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Resolve target architecture
|
||||
if [[ -n "$TARGET_ARCH" ]]; then
|
||||
ARCH="$TARGET_ARCH"
|
||||
fi
|
||||
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
PLATFORM="linux/amd64"
|
||||
OUTPUT_NAME="ginxsom-fcgi_static_x86_64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
ARCH="arm64"
|
||||
PLATFORM="linux/arm64"
|
||||
OUTPUT_NAME="ginxsom-fcgi_static_arm64"
|
||||
;;
|
||||
armv7l|armv7)
|
||||
ARCH="armv7"
|
||||
PLATFORM="linux/arm/v7"
|
||||
OUTPUT_NAME="ginxsom-fcgi_static_armv7"
|
||||
;;
|
||||
*)
|
||||
echo "WARNING: Unknown architecture: $ARCH"
|
||||
echo "Defaulting to linux/amd64"
|
||||
@@ -77,10 +138,51 @@ case "$ARCH" in
|
||||
;;
|
||||
esac
|
||||
|
||||
# When cross-building, ensure buildx exists and warn about QEMU/binfmt setup
|
||||
if [[ "$HOST_ARCH" != "unknown" && "$ARCH" != "$HOST_ARCH" ]]; then
|
||||
echo "NOTE: Cross-building from host '$HOST_ARCH' to target '$ARCH'."
|
||||
echo " Docker QEMU/binfmt support is required for this to work."
|
||||
if ! docker buildx version >/dev/null 2>&1; then
|
||||
echo "ERROR: docker buildx is required for cross-architecture builds but is not available."
|
||||
echo "Install/enable buildx and binfmt support, then retry."
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ docker buildx is available for cross-architecture build"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Append _debug suffix to output name for debug builds so production binary is never overwritten
|
||||
if [ "$DEBUG_BUILD" = true ]; then
|
||||
OUTPUT_NAME="${OUTPUT_NAME}_debug"
|
||||
fi
|
||||
|
||||
echo "Building for platform: $PLATFORM"
|
||||
echo "Output binary: $OUTPUT_NAME"
|
||||
echo ""
|
||||
|
||||
# Check if Alpine base image is cached
|
||||
echo "Checking for cached Alpine Docker image..."
|
||||
if ! docker images alpine:3.19 --format "{{.Repository}}:{{.Tag}}" | grep -q "alpine:3.19"; then
|
||||
echo "⚠ Alpine 3.19 image not found in cache"
|
||||
echo "Attempting to pull Alpine 3.19 image..."
|
||||
if ! docker pull alpine:3.19; then
|
||||
echo ""
|
||||
echo "ERROR: Failed to pull Alpine 3.19 image"
|
||||
echo "This is required for the static build."
|
||||
echo ""
|
||||
echo "Possible solutions:"
|
||||
echo " 1. Check your internet connection"
|
||||
echo " 2. Try again later (Docker Hub may be temporarily unavailable)"
|
||||
echo " 3. If you have IPv6 issues, disable IPv6 for Docker"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Alpine 3.19 image pulled successfully"
|
||||
else
|
||||
echo "✓ Alpine 3.19 image found in cache"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Build the Docker image
|
||||
echo "=========================================="
|
||||
echo "Step 1: Building Alpine Docker image"
|
||||
@@ -220,4 +322,4 @@ fi
|
||||
echo ""
|
||||
echo "Deployment:"
|
||||
echo " scp $BUILD_DIR/$OUTPUT_NAME user@server:/path/to/ginxsom/"
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
+103
-32
@@ -26,51 +26,122 @@ server {
|
||||
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-SHA-256, X-Content-Type, X-Content-Length" always;
|
||||
add_header Access-Control-Max-Age 86400 always;
|
||||
|
||||
# Handle preflight requests
|
||||
location / {
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header Access-Control-Allow-Methods "GET, HEAD, PUT, DELETE, OPTIONS";
|
||||
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-SHA-256, X-Content-Type, X-Content-Length";
|
||||
add_header Access-Control-Max-Age 86400;
|
||||
add_header Content-Length 0;
|
||||
add_header Content-Type text/plain;
|
||||
return 204;
|
||||
}
|
||||
|
||||
# Pass all requests to FastCGI
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/tmp/ginxsom.sock;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
# Root directory for blob storage
|
||||
root /home/teknari/Storage/Blossom;
|
||||
|
||||
# Maximum upload size
|
||||
client_max_body_size 100M;
|
||||
|
||||
# OPTIONS preflight handler
|
||||
if ($request_method = OPTIONS) {
|
||||
return 204;
|
||||
}
|
||||
|
||||
# All dynamic endpoints go through FastCGI
|
||||
location ~ ^/(upload|list|delete|mirror|media|report|health|admin) {
|
||||
# PUT /upload - File uploads
|
||||
location = /upload {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/tmp/ginxsom.sock;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
|
||||
# Handle large uploads
|
||||
client_max_body_size 100M;
|
||||
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
|
||||
fastcgi_read_timeout 300s;
|
||||
|
||||
# Disable buffering for large uploads
|
||||
fastcgi_request_buffering off;
|
||||
fastcgi_buffering off;
|
||||
}
|
||||
|
||||
# Blob serving - SHA256 hashes
|
||||
location ~ "^/([a-fA-F0-9]{64})" {
|
||||
# GET /list/<pubkey> - List user blobs
|
||||
location ~ "^/list/([a-f0-9]{64})$" {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/tmp/ginxsom.sock;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
|
||||
}
|
||||
|
||||
# PUT /mirror - Mirror content
|
||||
location = /mirror {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/tmp/ginxsom.sock;
|
||||
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
|
||||
}
|
||||
|
||||
# PUT /report - Report content
|
||||
location = /report {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/tmp/ginxsom.sock;
|
||||
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
|
||||
}
|
||||
|
||||
# GET /auth - NIP-42 challenges
|
||||
location = /auth {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/tmp/ginxsom.sock;
|
||||
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
|
||||
}
|
||||
|
||||
# GET /health - Health check
|
||||
location = /health {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/tmp/ginxsom.sock;
|
||||
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
|
||||
}
|
||||
|
||||
# Admin API
|
||||
location /api/ {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/tmp/ginxsom.sock;
|
||||
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
|
||||
}
|
||||
|
||||
# Admin interface
|
||||
location /admin {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/tmp/ginxsom.sock;
|
||||
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
|
||||
}
|
||||
|
||||
# Blob serving - SHA256 patterns (serve directly from filesystem)
|
||||
location ~ "^/([a-f0-9]{64})(\.[a-zA-Z0-9]+)?$" {
|
||||
# Handle DELETE via rewrite to internal location
|
||||
if ($request_method = DELETE) {
|
||||
rewrite ^/(.*)$ /fcgi-delete/$1 last;
|
||||
}
|
||||
|
||||
# Enable range requests for video streaming
|
||||
add_header Accept-Ranges bytes always;
|
||||
# Handle HEAD via rewrite to internal location
|
||||
if ($request_method = HEAD) {
|
||||
rewrite ^/(.*)$ /fcgi-head/$1 last;
|
||||
}
|
||||
|
||||
# Cache static blobs
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable" always;
|
||||
# GET requests - serve directly from filesystem
|
||||
if ($request_method != GET) {
|
||||
return 405;
|
||||
}
|
||||
|
||||
try_files /blobs/$1.html /blobs/$1.js /blobs/$1.mjs /blobs/$1.css /blobs/$1.txt /blobs/$1.jpg /blobs/$1.jpeg /blobs/$1.png /blobs/$1.webp /blobs/$1.gif /blobs/$1.pdf /blobs/$1.mp4 /blobs/$1.mp3 /blobs/$1.md =404;
|
||||
|
||||
# Cache control for blobs
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
}
|
||||
|
||||
# Internal FastCGI handler for DELETE
|
||||
location ~ "^/fcgi-delete/([a-f0-9]{64}).*$" {
|
||||
internal;
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/tmp/ginxsom.sock;
|
||||
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
|
||||
fastcgi_param REQUEST_URI /$1;
|
||||
}
|
||||
|
||||
# Internal FastCGI handler for HEAD
|
||||
location ~ "^/fcgi-head/([a-f0-9]{64}).*$" {
|
||||
internal;
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/tmp/ginxsom.sock;
|
||||
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
|
||||
fastcgi_param REQUEST_URI /$1;
|
||||
}
|
||||
|
||||
# Root endpoint - server info
|
||||
location = / {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/tmp/ginxsom.sock;
|
||||
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
|
||||
}
|
||||
|
||||
# Deny access to sensitive files
|
||||
|
||||
@@ -298,7 +298,7 @@ http {
|
||||
}
|
||||
|
||||
# GET requests - serve files directly with extension fallback
|
||||
try_files /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md =404;
|
||||
try_files /$1.html /$1.js /$1.mjs /$1.css /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md =404;
|
||||
|
||||
# Cache headers for blob content
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
@@ -678,7 +678,7 @@ http {
|
||||
}
|
||||
|
||||
# GET requests - serve files directly with extension fallback
|
||||
try_files /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md =404;
|
||||
try_files /$1.html /$1.js /$1.mjs /$1.css /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md =404;
|
||||
|
||||
# Cache headers for blob content
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
|
||||
Binary file not shown.
+6
-2
@@ -87,6 +87,10 @@ if [ -f "$SCRIPT_DIR/.admin_keys" ]; then
|
||||
chmod 600 "$DEPLOY_DIR/.admin_keys"
|
||||
echo " - .admin_keys"
|
||||
fi
|
||||
if [ -f "$SCRIPT_DIR/config/fastcgi_params" ]; then
|
||||
cp "$SCRIPT_DIR/config/fastcgi_params" "$DEPLOY_DIR/"
|
||||
echo " - fastcgi_params"
|
||||
fi
|
||||
echo -e "${GREEN}✓ Configuration files copied${NC}"
|
||||
echo ""
|
||||
|
||||
@@ -195,5 +199,5 @@ echo " Reload: sudo systemctl reload nginx"
|
||||
echo " Logs: sudo tail -f /var/log/nginx/error.log"
|
||||
echo ""
|
||||
echo "Test the server:"
|
||||
echo " curl http://localhost:$NGINX_PORT/health"
|
||||
echo " curl http://localhost:$NGINX_PORT/"
|
||||
echo " curl -k https://localhost:$NGINX_PORT/health"
|
||||
echo " curl -k https://localhost:$NGINX_PORT/"
|
||||
|
||||
+63
-40
@@ -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 ""
|
||||
|
||||
+354
-230
@@ -8,17 +8,65 @@ YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
print_status() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
||||
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
print_status() { echo -e "${BLUE}[INFO]${NC} $1" >&2; }
|
||||
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" >&2; }
|
||||
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" >&2; }
|
||||
print_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
|
||||
|
||||
# Global variables
|
||||
COMMIT_MESSAGE=""
|
||||
RELEASE_MODE=false
|
||||
VERSION_INCREMENT_TYPE="patch" # "patch", "minor", or "major"
|
||||
VERSION_INCREMENT_EXPLICIT=false
|
||||
|
||||
# TODO: Update this URL to match your actual Gitea repository
|
||||
GITEA_REPO_URL="https://git.example.com/api/v1/repos/username/ginxsom"
|
||||
GITEA_REPO_URL="https://git.laantungir.net/api/v1/repos/laantungir/ginxsom"
|
||||
|
||||
show_usage() {
|
||||
echo "Ginxsom Increment and Push Script"
|
||||
echo ""
|
||||
echo "USAGE:"
|
||||
echo " $0 [OPTIONS] \"commit message\""
|
||||
echo ""
|
||||
echo "COMMANDS:"
|
||||
echo " $0 \"commit message\" Default: increment patch, commit & push"
|
||||
echo " $0 -p \"commit message\" Increment patch version"
|
||||
echo " $0 -m \"commit message\" Increment minor version"
|
||||
echo " $0 -M \"commit message\" Increment major version"
|
||||
echo " $0 -r \"commit message\" Create release with assets (no version increment)"
|
||||
echo " $0 -r -p \"commit message\" Create release with patch version increment"
|
||||
echo " $0 -r -m \"commit message\" Create release with minor version increment"
|
||||
echo " $0 -h Show this help message"
|
||||
echo ""
|
||||
echo "OPTIONS:"
|
||||
echo " -p, --patch Increment patch version (default)"
|
||||
echo " -m, --minor Increment minor version"
|
||||
echo " -M, --major Increment major version"
|
||||
echo " -r, --release Create release with assets"
|
||||
echo " -h, --help Show this help message"
|
||||
echo ""
|
||||
echo "EXAMPLES:"
|
||||
echo " $0 \"Fixed authentication bug\""
|
||||
echo " $0 -m \"Added new features\""
|
||||
echo " $0 -M \"Breaking API changes\""
|
||||
echo " $0 -r \"Release current version\""
|
||||
echo " $0 -r -p \"Release with patch increment\""
|
||||
echo " $0 -r -m \"Release with minor increment\""
|
||||
echo ""
|
||||
echo "VERSION INCREMENT MODES:"
|
||||
echo " -p, --patch (default): Increment patch version (v1.2.3 → v1.2.4)"
|
||||
echo " -m, --minor: Increment minor version, zero patch (v1.2.3 → v1.3.0)"
|
||||
echo " -M, --major: Increment major version, zero minor+patch (v1.2.3 → v2.0.0)"
|
||||
echo ""
|
||||
echo "RELEASE MODE (-r flag):"
|
||||
echo " - Build static binaries for x86_64 and ARM64 using build_static.sh"
|
||||
echo " - Create source tarball"
|
||||
echo " - Git add, commit, push, and create Gitea release with assets"
|
||||
echo " - Can be combined with version increment flags"
|
||||
echo ""
|
||||
echo "REQUIREMENTS FOR RELEASE MODE:"
|
||||
echo " - Gitea token in ~/.gitea_token for release uploads"
|
||||
echo " - Docker installed for static binary builds"
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
@@ -27,6 +75,21 @@ while [[ $# -gt 0 ]]; do
|
||||
RELEASE_MODE=true
|
||||
shift
|
||||
;;
|
||||
-p|--patch)
|
||||
VERSION_INCREMENT_TYPE="patch"
|
||||
VERSION_INCREMENT_EXPLICIT=true
|
||||
shift
|
||||
;;
|
||||
-m|--minor)
|
||||
VERSION_INCREMENT_TYPE="minor"
|
||||
VERSION_INCREMENT_EXPLICIT=true
|
||||
shift
|
||||
;;
|
||||
-M|--major)
|
||||
VERSION_INCREMENT_TYPE="major"
|
||||
VERSION_INCREMENT_EXPLICIT=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
show_usage
|
||||
exit 0
|
||||
@@ -41,32 +104,6 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
done
|
||||
|
||||
show_usage() {
|
||||
echo "Ginxsom Build and Push Script"
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " $0 \"commit message\" - Default: compile, increment patch, commit & push"
|
||||
echo " $0 -r \"commit message\" - Release: compile, increment minor, create release"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 \"Fixed authentication bug\""
|
||||
echo " $0 --release \"Major release with admin API\""
|
||||
echo ""
|
||||
echo "Default Mode (patch increment):"
|
||||
echo " - Compile Ginxsom FastCGI server"
|
||||
echo " - Increment patch version (v1.2.3 → v1.2.4)"
|
||||
echo " - Git add, commit with message, and push"
|
||||
echo ""
|
||||
echo "Release Mode (-r flag):"
|
||||
echo " - Compile Ginxsom FastCGI server"
|
||||
echo " - Increment minor version, zero patch (v1.2.3 → v1.3.0)"
|
||||
echo " - Git add, commit, push, and create Gitea release"
|
||||
echo ""
|
||||
echo "Requirements for Release Mode:"
|
||||
echo " - Gitea token in ~/.gitea_token for release uploads"
|
||||
echo " - Update GITEA_REPO_URL in script for your repository"
|
||||
}
|
||||
|
||||
# Validate inputs
|
||||
if [[ -z "$COMMIT_MESSAGE" ]]; then
|
||||
print_error "Commit message is required"
|
||||
@@ -85,20 +122,20 @@ check_git_repo() {
|
||||
|
||||
# Function to get current version and increment appropriately
|
||||
increment_version() {
|
||||
local increment_type="$1" # "patch" or "minor"
|
||||
|
||||
local increment_type="$1" # "patch", "minor", or "major"
|
||||
|
||||
print_status "Getting current version..."
|
||||
|
||||
|
||||
# Get the highest version tag (not chronologically latest)
|
||||
LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || echo "")
|
||||
if [[ -z "$LATEST_TAG" ]]; then
|
||||
LATEST_TAG="v0.0.0"
|
||||
print_warning "No version tags found, starting from $LATEST_TAG"
|
||||
fi
|
||||
|
||||
|
||||
# Extract version components (remove 'v' prefix)
|
||||
VERSION=${LATEST_TAG#v}
|
||||
|
||||
|
||||
# Parse major.minor.patch using regex
|
||||
if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||
MAJOR=${BASH_REMATCH[1]}
|
||||
@@ -109,121 +146,74 @@ increment_version() {
|
||||
print_error "Expected format: v0.1.0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# Increment version based on type
|
||||
if [[ "$increment_type" == "minor" ]]; then
|
||||
# Minor release: increment minor, zero patch
|
||||
if [[ "$increment_type" == "major" ]]; then
|
||||
NEW_MAJOR=$((MAJOR + 1))
|
||||
NEW_MINOR=0
|
||||
NEW_PATCH=0
|
||||
NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
|
||||
print_status "Major version increment: incrementing major version"
|
||||
elif [[ "$increment_type" == "minor" ]]; then
|
||||
NEW_MAJOR=$MAJOR
|
||||
NEW_MINOR=$((MINOR + 1))
|
||||
NEW_PATCH=0
|
||||
NEW_VERSION="v${MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
|
||||
print_status "Release mode: incrementing minor version"
|
||||
NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
|
||||
print_status "Minor version increment: incrementing minor version"
|
||||
else
|
||||
# Default: increment patch
|
||||
NEW_MAJOR=$MAJOR
|
||||
NEW_MINOR=$MINOR
|
||||
NEW_PATCH=$((PATCH + 1))
|
||||
NEW_VERSION="v${MAJOR}.${MINOR}.${NEW_PATCH}"
|
||||
print_status "Default mode: incrementing patch version"
|
||||
NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
|
||||
print_status "Patch version increment: incrementing patch version"
|
||||
fi
|
||||
|
||||
|
||||
print_status "Current version: $LATEST_TAG"
|
||||
print_status "New version: $NEW_VERSION"
|
||||
|
||||
|
||||
# Update version in src/ginxsom.h
|
||||
update_version_in_header "$NEW_VERSION" "$NEW_MAJOR" "$NEW_MINOR" "$NEW_PATCH"
|
||||
|
||||
# Export for use in other functions
|
||||
export NEW_VERSION
|
||||
}
|
||||
|
||||
# Function to update version in header file
|
||||
# Function to update version macros in src/ginxsom.h
|
||||
update_version_in_header() {
|
||||
local version="$1"
|
||||
print_status "Updating version in src/ginxsom.h to $version..."
|
||||
local new_version="$1"
|
||||
local major="$2"
|
||||
local minor="$3"
|
||||
local patch="$4"
|
||||
|
||||
# Extract version components (remove 'v' prefix)
|
||||
local version_no_v=${version#v}
|
||||
print_status "Updating version in src/ginxsom.h..."
|
||||
|
||||
# Parse major.minor.patch using regex
|
||||
if [[ $version_no_v =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||
local major=${BASH_REMATCH[1]}
|
||||
local minor=${BASH_REMATCH[2]}
|
||||
local patch=${BASH_REMATCH[3]}
|
||||
|
||||
# Update the header file
|
||||
sed -i "s/#define VERSION_MAJOR [0-9]\+/#define VERSION_MAJOR $major/" src/ginxsom.h
|
||||
sed -i "s/#define VERSION_MINOR [0-9]\+/#define VERSION_MINOR $minor/" src/ginxsom.h
|
||||
sed -i "s/#define VERSION_PATCH [0-9]\+/#define VERSION_PATCH $patch/" src/ginxsom.h
|
||||
sed -i "s/#define VERSION \"v[0-9]\+\.[0-9]\+\.[0-9]\+\"/#define VERSION \"$version\"/" src/ginxsom.h
|
||||
|
||||
print_success "Updated version in header file"
|
||||
else
|
||||
print_error "Invalid version format: $version"
|
||||
if [[ ! -f "src/ginxsom.h" ]]; then
|
||||
print_error "src/ginxsom.h not found"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to compile the Ginxsom project
|
||||
compile_project() {
|
||||
print_status "Compiling Ginxsom FastCGI server..."
|
||||
sed -i "s/#define VERSION_MAJOR [0-9]\+/#define VERSION_MAJOR $major/" src/ginxsom.h
|
||||
sed -i "s/#define VERSION_MINOR [0-9]\+/#define VERSION_MINOR $minor/" src/ginxsom.h
|
||||
sed -i "s/#define VERSION_PATCH [0-9]\+/#define VERSION_PATCH $patch/" src/ginxsom.h
|
||||
sed -i "s/#define VERSION \"v[0-9]\+\.[0-9]\+\.[0-9]\+\"/#define VERSION \"$new_version\"/" src/ginxsom.h
|
||||
|
||||
# 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"
|
||||
fi
|
||||
|
||||
# Compile the project
|
||||
if make > /dev/null 2>&1; then
|
||||
print_success "Ginxsom compiled successfully"
|
||||
|
||||
# Verify the binary was created
|
||||
if [[ -f "build/ginxsom-fcgi" ]]; then
|
||||
print_success "Binary created: build/ginxsom-fcgi"
|
||||
else
|
||||
print_error "Binary not found after compilation"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
print_error "Compilation failed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to build release binary
|
||||
build_release_binary() {
|
||||
print_status "Building release binary..."
|
||||
|
||||
# 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
|
||||
else
|
||||
print_error "Build failed"
|
||||
exit 1
|
||||
fi
|
||||
print_success "Updated version in src/ginxsom.h to $new_version"
|
||||
}
|
||||
|
||||
# Function to commit and push changes
|
||||
git_commit_and_push() {
|
||||
print_status "Preparing git commit..."
|
||||
|
||||
# Stage all changes
|
||||
|
||||
if git add . > /dev/null 2>&1; then
|
||||
print_success "Staged all changes"
|
||||
else
|
||||
print_error "Failed to stage changes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if there are changes to commit
|
||||
|
||||
if git diff --staged --quiet; then
|
||||
print_warning "No changes to commit"
|
||||
else
|
||||
# Commit changes
|
||||
if git commit -m "$NEW_VERSION - $COMMIT_MESSAGE" > /dev/null 2>&1; then
|
||||
print_success "Committed changes"
|
||||
else
|
||||
@@ -231,15 +221,13 @@ git_commit_and_push() {
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create new git tag
|
||||
|
||||
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Created tag: $NEW_VERSION"
|
||||
else
|
||||
print_warning "Tag $NEW_VERSION already exists"
|
||||
fi
|
||||
|
||||
# Push changes and tags
|
||||
|
||||
print_status "Pushing to remote repository..."
|
||||
if git push > /dev/null 2>&1; then
|
||||
print_success "Pushed changes"
|
||||
@@ -247,8 +235,7 @@ git_commit_and_push() {
|
||||
print_error "Failed to push changes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Push only the new tag to avoid conflicts with existing tags
|
||||
|
||||
if git push origin "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Pushed tag: $NEW_VERSION"
|
||||
else
|
||||
@@ -265,20 +252,17 @@ git_commit_and_push() {
|
||||
# Function to commit and push changes without creating a tag (tag already created)
|
||||
git_commit_and_push_no_tag() {
|
||||
print_status "Preparing git commit..."
|
||||
|
||||
# Stage all changes
|
||||
|
||||
if git add . > /dev/null 2>&1; then
|
||||
print_success "Staged all changes"
|
||||
else
|
||||
print_error "Failed to stage changes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if there are changes to commit
|
||||
|
||||
if git diff --staged --quiet; then
|
||||
print_warning "No changes to commit"
|
||||
else
|
||||
# Commit changes
|
||||
if git commit -m "$NEW_VERSION - $COMMIT_MESSAGE" > /dev/null 2>&1; then
|
||||
print_success "Committed changes"
|
||||
else
|
||||
@@ -286,8 +270,7 @@ git_commit_and_push_no_tag() {
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Push changes and tags
|
||||
|
||||
print_status "Pushing to remote repository..."
|
||||
if git push > /dev/null 2>&1; then
|
||||
print_success "Pushed changes"
|
||||
@@ -295,8 +278,7 @@ git_commit_and_push_no_tag() {
|
||||
print_error "Failed to push changes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Push only the new tag to avoid conflicts with existing tags
|
||||
|
||||
if git push origin "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Pushed tag: $NEW_VERSION"
|
||||
else
|
||||
@@ -310,42 +292,198 @@ git_commit_and_push_no_tag() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to build release binaries
|
||||
build_release_binary() {
|
||||
print_status "Building release binaries..."
|
||||
|
||||
if [[ ! -f "build_static.sh" ]]; then
|
||||
print_error "build_static.sh not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Build x86_64 first (required for success)
|
||||
if ./build_static.sh > /dev/null 2>&1; then
|
||||
print_success "Built x86_64 static binary successfully"
|
||||
else
|
||||
print_error "Failed to build x86_64 static binary"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Build arm64 second (warning-only on failure)
|
||||
if ./build_static.sh --arch arm64 > /dev/null 2>&1; then
|
||||
print_success "Built ARM64 static binary successfully"
|
||||
else
|
||||
print_warning "Failed to build ARM64 static binary (continuing with release)"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to create source tarball
|
||||
create_source_tarball() {
|
||||
print_status "Creating source tarball..."
|
||||
|
||||
local tarball_name="ginxsom-${NEW_VERSION#v}.tar.gz"
|
||||
|
||||
# Remove any existing tarball first to avoid self-inclusion
|
||||
rm -f "$tarball_name"
|
||||
|
||||
if tar -czf "$tarball_name" \
|
||||
--exclude='./build' \
|
||||
--exclude='./.git' \
|
||||
--exclude='./.gitmodules' \
|
||||
--exclude='./db/*.db' \
|
||||
--exclude='./db/*.db-*' \
|
||||
--exclude='./*.log' \
|
||||
--exclude="./$tarball_name" \
|
||||
--exclude='./Trash' \
|
||||
--exclude='./*.tar.gz' \
|
||||
. 2>/dev/null; then
|
||||
print_success "Created source tarball: $tarball_name"
|
||||
echo "$tarball_name"
|
||||
return 0
|
||||
else
|
||||
print_error "Failed to create source tarball"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to upload release assets to Gitea
|
||||
upload_release_assets() {
|
||||
local release_id="$1"
|
||||
local binary_path_x86="$2"
|
||||
local tarball_path="$3"
|
||||
local binary_path_arm64="$4"
|
||||
|
||||
print_status "Uploading release assets..."
|
||||
|
||||
if [[ ! -f "$HOME/.gitea_token" ]]; then
|
||||
print_warning "No ~/.gitea_token found. Skipping asset uploads."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local token=$(cat "$HOME/.gitea_token" | tr -d '\n\r')
|
||||
local assets_url="$GITEA_REPO_URL/releases/$release_id/assets"
|
||||
print_status "Assets URL: $assets_url"
|
||||
|
||||
# Upload x86_64 binary
|
||||
if [[ -f "$binary_path_x86" ]]; then
|
||||
print_status "Uploading binary: $(basename "$binary_path_x86")"
|
||||
|
||||
local max_attempts=3
|
||||
local attempt=1
|
||||
while [[ $attempt -le $max_attempts ]]; do
|
||||
print_status "Upload attempt $attempt/$max_attempts"
|
||||
local binary_response=$(curl -fS -X POST "$assets_url" \
|
||||
-H "Authorization: token $token" \
|
||||
-F "attachment=@$binary_path_x86;filename=$(basename "$binary_path_x86")" \
|
||||
-F "name=$(basename "$binary_path_x86")")
|
||||
|
||||
if echo "$binary_response" | grep -q '"id"'; then
|
||||
print_success "Uploaded x86_64 binary successfully"
|
||||
break
|
||||
else
|
||||
print_warning "Upload attempt $attempt failed"
|
||||
if [[ $attempt -lt $max_attempts ]]; then
|
||||
print_status "Retrying in 2 seconds..."
|
||||
sleep 2
|
||||
else
|
||||
print_error "Failed to upload x86_64 binary after $max_attempts attempts"
|
||||
print_error "Response: $binary_response"
|
||||
fi
|
||||
fi
|
||||
((attempt++))
|
||||
done
|
||||
fi
|
||||
|
||||
# Upload ARM64 binary
|
||||
if [[ -f "$binary_path_arm64" ]]; then
|
||||
print_status "Uploading binary: $(basename "$binary_path_arm64")"
|
||||
|
||||
local max_attempts=3
|
||||
local attempt=1
|
||||
while [[ $attempt -le $max_attempts ]]; do
|
||||
print_status "Upload attempt $attempt/$max_attempts"
|
||||
local binary_response=$(curl -fS -X POST "$assets_url" \
|
||||
-H "Authorization: token $token" \
|
||||
-F "attachment=@$binary_path_arm64;filename=$(basename "$binary_path_arm64")" \
|
||||
-F "name=$(basename "$binary_path_arm64")")
|
||||
|
||||
if echo "$binary_response" | grep -q '"id"'; then
|
||||
print_success "Uploaded ARM64 binary successfully"
|
||||
break
|
||||
else
|
||||
print_warning "Upload attempt $attempt failed"
|
||||
if [[ $attempt -lt $max_attempts ]]; then
|
||||
print_status "Retrying in 2 seconds..."
|
||||
sleep 2
|
||||
else
|
||||
print_error "Failed to upload ARM64 binary after $max_attempts attempts"
|
||||
print_error "Response: $binary_response"
|
||||
fi
|
||||
fi
|
||||
((attempt++))
|
||||
done
|
||||
fi
|
||||
|
||||
# Upload source tarball
|
||||
if [[ -f "$tarball_path" ]]; then
|
||||
print_status "Uploading source tarball: $(basename "$tarball_path")"
|
||||
local tarball_response=$(curl -s -X POST "$GITEA_REPO_URL/releases/$release_id/assets" \
|
||||
-H "Authorization: token $token" \
|
||||
-F "attachment=@$tarball_path;filename=$(basename "$tarball_path")")
|
||||
|
||||
if echo "$tarball_response" | grep -q '"id"'; then
|
||||
print_success "Uploaded source tarball successfully"
|
||||
else
|
||||
print_warning "Failed to upload source tarball: $tarball_response"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to create Gitea release
|
||||
create_gitea_release() {
|
||||
print_status "Creating Gitea release..."
|
||||
|
||||
# Check for Gitea token
|
||||
|
||||
if [[ ! -f "$HOME/.gitea_token" ]]; then
|
||||
print_warning "No ~/.gitea_token found. Skipping release creation."
|
||||
print_warning "Create ~/.gitea_token with your Gitea access token to enable releases."
|
||||
return 0
|
||||
fi
|
||||
|
||||
|
||||
local token=$(cat "$HOME/.gitea_token" | tr -d '\n\r')
|
||||
|
||||
# Create release
|
||||
|
||||
print_status "Creating release $NEW_VERSION..."
|
||||
local response=$(curl -s -X POST "$GITEA_REPO_URL/releases" \
|
||||
-H "Authorization: token $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\": \"$NEW_VERSION\", \"name\": \"$NEW_VERSION\", \"body\": \"$COMMIT_MESSAGE\"}")
|
||||
|
||||
-H "Authorization: token $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\": \"$NEW_VERSION\", \"name\": \"$NEW_VERSION\", \"body\": \"$COMMIT_MESSAGE\"}")
|
||||
|
||||
if echo "$response" | grep -q '"id"'; then
|
||||
print_success "Created release $NEW_VERSION"
|
||||
upload_release_binary "$token"
|
||||
local release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
|
||||
echo $release_id
|
||||
elif echo "$response" | grep -q "already exists"; then
|
||||
print_warning "Release $NEW_VERSION already exists"
|
||||
upload_release_binary "$token"
|
||||
local check_response=$(curl -s -H "Authorization: token $token" "$GITEA_REPO_URL/releases/tags/$NEW_VERSION")
|
||||
if echo "$check_response" | grep -q '"id"'; then
|
||||
local release_id=$(echo "$check_response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
|
||||
print_status "Using existing release ID: $release_id"
|
||||
echo $release_id
|
||||
else
|
||||
print_error "Could not find existing release ID"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
print_error "Failed to create release $NEW_VERSION"
|
||||
print_error "Response: $response"
|
||||
|
||||
# Try to check if the release exists anyway
|
||||
|
||||
print_status "Checking if release exists..."
|
||||
local check_response=$(curl -s -H "Authorization: token $token" "$GITEA_REPO_URL/releases/tags/$NEW_VERSION")
|
||||
if echo "$check_response" | grep -q '"id"'; then
|
||||
print_warning "Release exists but creation response was unexpected"
|
||||
upload_release_binary "$token"
|
||||
local release_id=$(echo "$check_response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
|
||||
echo $release_id
|
||||
else
|
||||
print_error "Release does not exist and creation failed"
|
||||
return 1
|
||||
@@ -353,62 +491,26 @@ create_gitea_release() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to upload release binary
|
||||
upload_release_binary() {
|
||||
local token="$1"
|
||||
|
||||
# Get release ID with more robust parsing
|
||||
print_status "Getting release ID for $NEW_VERSION..."
|
||||
local response=$(curl -s -H "Authorization: token $token" "$GITEA_REPO_URL/releases/tags/$NEW_VERSION")
|
||||
local release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -n1 | cut -d: -f2)
|
||||
|
||||
if [[ -z "$release_id" ]]; then
|
||||
print_error "Could not get release ID for $NEW_VERSION"
|
||||
print_error "API Response: $response"
|
||||
|
||||
# Try to list all releases to debug
|
||||
print_status "Available releases:"
|
||||
curl -s -H "Authorization: token $token" "$GITEA_REPO_URL/releases" | grep -o '"tag_name":"[^"]*"' | head -5
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_success "Found release ID: $release_id"
|
||||
|
||||
# Upload FastCGI binary
|
||||
if [[ -f "ginxsom-fcgi-linux-x86_64" ]]; then
|
||||
print_status "Uploading Ginxsom FastCGI binary..."
|
||||
if curl -s -X POST "$GITEA_REPO_URL/releases/$release_id/assets" \
|
||||
-H "Authorization: token $token" \
|
||||
-F "attachment=@ginxsom-fcgi-linux-x86_64;filename=ginxsom-fcgi-${NEW_VERSION}-linux-x86_64" > /dev/null; then
|
||||
print_success "Uploaded FastCGI binary"
|
||||
else
|
||||
print_warning "Failed to upload FastCGI binary"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to clean up release binary
|
||||
cleanup_release_binary() {
|
||||
if [[ -f "ginxsom-fcgi-linux-x86_64" ]]; then
|
||||
rm -f ginxsom-fcgi-linux-x86_64
|
||||
print_status "Cleaned up release binary"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
print_status "Ginxsom Build and Push Script"
|
||||
|
||||
print_status "Ginxsom Increment and Push Script"
|
||||
|
||||
# Check prerequisites
|
||||
check_git_repo
|
||||
|
||||
|
||||
if [[ "$RELEASE_MODE" == true ]]; then
|
||||
print_status "=== RELEASE MODE ==="
|
||||
|
||||
# Increment minor version for releases
|
||||
increment_version "minor"
|
||||
|
||||
# Create new git tag BEFORE compilation
|
||||
|
||||
# Only increment version if explicitly requested
|
||||
if [[ "$VERSION_INCREMENT_EXPLICIT" == true ]]; then
|
||||
increment_version "$VERSION_INCREMENT_TYPE"
|
||||
else
|
||||
LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || echo "v0.0.0")
|
||||
NEW_VERSION="$LATEST_TAG"
|
||||
export NEW_VERSION
|
||||
fi
|
||||
|
||||
# Create new git tag BEFORE compilation so version is picked up
|
||||
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Created tag: $NEW_VERSION"
|
||||
else
|
||||
@@ -416,35 +518,63 @@ main() {
|
||||
git tag -d "$NEW_VERSION" > /dev/null 2>&1
|
||||
git tag "$NEW_VERSION" > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
# Update version in header file
|
||||
update_version_in_header "$NEW_VERSION"
|
||||
|
||||
# Compile project
|
||||
compile_project
|
||||
|
||||
# Build release binary
|
||||
build_release_binary
|
||||
|
||||
# Commit and push (but skip tag creation since we already did it)
|
||||
git_commit_and_push_no_tag
|
||||
|
||||
# Create Gitea release with binary
|
||||
create_gitea_release
|
||||
|
||||
# Cleanup
|
||||
cleanup_release_binary
|
||||
|
||||
print_success "Release $NEW_VERSION completed successfully!"
|
||||
print_status "Binary uploaded to Gitea release"
|
||||
|
||||
|
||||
# Build release binaries
|
||||
local binary_path_x86=""
|
||||
local binary_path_arm64=""
|
||||
if build_release_binary; then
|
||||
if [[ -f "build/ginxsom-fcgi_static_x86_64" ]]; then
|
||||
binary_path_x86="build/ginxsom-fcgi_static_x86_64"
|
||||
fi
|
||||
if [[ -f "build/ginxsom-fcgi_static_arm64" ]]; then
|
||||
binary_path_arm64="build/ginxsom-fcgi_static_arm64"
|
||||
fi
|
||||
else
|
||||
print_warning "x86_64 binary build failed, continuing with release creation"
|
||||
if [[ -f "build/ginxsom-fcgi_static_x86_64" ]]; then
|
||||
print_status "Using existing x86_64 binary from previous build"
|
||||
binary_path_x86="build/ginxsom-fcgi_static_x86_64"
|
||||
fi
|
||||
if [[ -f "build/ginxsom-fcgi_static_arm64" ]]; then
|
||||
print_status "Using existing ARM64 binary from previous build"
|
||||
binary_path_arm64="build/ginxsom-fcgi_static_arm64"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create source tarball
|
||||
local tarball_path=""
|
||||
if tarball_path=$(create_source_tarball); then
|
||||
: # tarball_path is set by the function
|
||||
else
|
||||
print_warning "Source tarball creation failed, continuing with release creation"
|
||||
fi
|
||||
|
||||
# Create Gitea release
|
||||
local release_id=""
|
||||
if release_id=$(create_gitea_release); then
|
||||
if [[ "$release_id" =~ ^[0-9]+$ ]]; then
|
||||
if [[ -n "$release_id" && (-n "$binary_path_x86" || -n "$binary_path_arm64" || -n "$tarball_path") ]]; then
|
||||
upload_release_assets "$release_id" "$binary_path_x86" "$tarball_path" "$binary_path_arm64"
|
||||
fi
|
||||
print_success "Release $NEW_VERSION completed successfully!"
|
||||
else
|
||||
print_error "Invalid release_id: $release_id"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
print_error "Release creation failed"
|
||||
fi
|
||||
|
||||
else
|
||||
print_status "=== DEFAULT MODE ==="
|
||||
|
||||
# Increment patch version for regular commits
|
||||
increment_version "patch"
|
||||
|
||||
# Create new git tag BEFORE compilation
|
||||
|
||||
# Increment version based on type (default to patch)
|
||||
increment_version "$VERSION_INCREMENT_TYPE"
|
||||
|
||||
# Create new git tag BEFORE compilation so version is picked up
|
||||
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Created tag: $NEW_VERSION"
|
||||
else
|
||||
@@ -452,17 +582,11 @@ main() {
|
||||
git tag -d "$NEW_VERSION" > /dev/null 2>&1
|
||||
git tag "$NEW_VERSION" > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
# Update version in header file
|
||||
update_version_in_header "$NEW_VERSION"
|
||||
|
||||
# Compile project
|
||||
compile_project
|
||||
|
||||
# Commit and push (but skip tag creation since we already did it)
|
||||
git_commit_and_push_no_tag
|
||||
|
||||
print_success "Build and push completed successfully!"
|
||||
|
||||
print_success "Increment and push completed successfully!"
|
||||
print_status "Version $NEW_VERSION pushed to repository"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
# Fix: Ginxsom 100% CPU Busy-Wait Bug
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
The ginxsom-fcgi process consumes 100% CPU due to a **busy-wait spin loop** in the relay management thread.
|
||||
|
||||
### The Bug Chain
|
||||
|
||||
1. **`relay_management_thread()`** in `src/relay_client.c:328` runs a `while` loop calling `nostr_relay_pool_poll()` with a 1000ms timeout
|
||||
2. **`nostr_relay_pool_poll()`** in `nostr_core_lib/nostr_core/core_relay_pool.c:1699` iterates over all relays, but **skips disconnected relays** at line 1725-1728 — it only calls `nostr_ws_receive()` (which actually blocks) for connected relays
|
||||
3. **When no relays are connected** (or all are in error/disconnected state), the entire poll function returns 0 instantly with zero blocking
|
||||
4. **The caller has no sleep** for the `events_processed == 0` case — it immediately loops back and calls poll again
|
||||
5. This creates a **tight spin loop** consuming 100% of a CPU core
|
||||
|
||||
### Additional Issues Found
|
||||
|
||||
- **No signal handling**: `main.c` has zero `signal()`, `SIGTERM`, or `SIGINT` handling. When systemd sends SIGTERM to stop the service, the process ignores it and must be SIGKILL'd after timeout (confirmed in journal: `State 'stop-sigterm' timed out. Killing.`)
|
||||
- **`relay_client_stop()` never called**: The cleanup function exists but is never invoked — there's no shutdown path
|
||||
- **`g_relay_state.running` not volatile**: The `running` flag is read by the management thread but set by the main thread (or signal handler). Without `volatile`, the compiler may optimize away the check
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["relay_management_thread()"] -->|"while running"| B["nostr_relay_pool_poll(pool, 1000)"]
|
||||
B --> C{"Any relays connected?"}
|
||||
C -->|"Yes"| D["nostr_ws_receive() blocks up to timeout_ms"]
|
||||
C -->|"No - all disconnected"| E["Returns 0 immediately"]
|
||||
D --> F{"events_processed > 0?"}
|
||||
E --> F
|
||||
F -->|"> 0"| A
|
||||
F -->|"== 0"| G["No sleep! Loops immediately"]
|
||||
F -->|"< 0"| H["sleep(1) then loop"]
|
||||
G -->|"BUSY WAIT"| A
|
||||
```
|
||||
|
||||
## Fixes Required
|
||||
|
||||
### Fix 1: Add idle sleep to poll loop (`src/relay_client.c`)
|
||||
|
||||
**File:** `src/relay_client.c`, lines 327-337
|
||||
|
||||
**Before:**
|
||||
```c
|
||||
while (g_relay_state.running) {
|
||||
int events_processed = nostr_relay_pool_poll(g_relay_state.pool, 1000);
|
||||
if (events_processed < 0) {
|
||||
app_log(LOG_ERROR, "Error polling relay pool");
|
||||
sleep(1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**After:**
|
||||
```c
|
||||
while (g_relay_state.running) {
|
||||
int events_processed = nostr_relay_pool_poll(g_relay_state.pool, 1000);
|
||||
if (events_processed < 0) {
|
||||
app_log(LOG_ERROR, "Error polling relay pool");
|
||||
sleep(1);
|
||||
} else if (events_processed == 0) {
|
||||
// Prevent busy-wait when no relays are connected or no events pending.
|
||||
// nostr_relay_pool_poll() returns immediately if all relays are disconnected
|
||||
// because it skips the blocking nostr_ws_receive() call for non-connected relays.
|
||||
usleep(100000); // 100ms idle sleep
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fix 2: Make `running` flag volatile (`src/relay_client.c`)
|
||||
|
||||
**File:** `src/relay_client.c`, line 39
|
||||
|
||||
**Before:**
|
||||
```c
|
||||
static struct {
|
||||
int enabled;
|
||||
int initialized;
|
||||
int running;
|
||||
...
|
||||
```
|
||||
|
||||
**After:**
|
||||
```c
|
||||
static struct {
|
||||
int enabled;
|
||||
int initialized;
|
||||
volatile int running;
|
||||
...
|
||||
```
|
||||
|
||||
### Fix 3: Add signal handler for graceful shutdown (`src/main.c`)
|
||||
|
||||
**File:** `src/main.c`
|
||||
|
||||
Add near the top (after includes):
|
||||
```c
|
||||
#include <signal.h>
|
||||
|
||||
static volatile sig_atomic_t g_shutdown_requested = 0;
|
||||
|
||||
static void shutdown_handler(int signum) {
|
||||
(void)signum;
|
||||
g_shutdown_requested = 1;
|
||||
}
|
||||
```
|
||||
|
||||
Register the handler before the FCGI loop (before line 2664):
|
||||
```c
|
||||
// Register signal handlers for graceful shutdown
|
||||
signal(SIGTERM, shutdown_handler);
|
||||
signal(SIGINT, shutdown_handler);
|
||||
```
|
||||
|
||||
### Fix 4: Call `relay_client_stop()` on shutdown (`src/main.c`)
|
||||
|
||||
**File:** `src/main.c`
|
||||
|
||||
Modify the FCGI loop condition (line 2667) to also check the shutdown flag:
|
||||
```c
|
||||
while (FCGI_Accept() >= 0 && !g_shutdown_requested) {
|
||||
```
|
||||
|
||||
Add cleanup after the FCGI loop exits (before `return 0` at line 3068):
|
||||
```c
|
||||
// Graceful shutdown
|
||||
app_log(LOG_INFO, "Shutting down ginxsom...");
|
||||
relay_client_stop();
|
||||
app_log(LOG_INFO, "Ginxsom shutdown complete");
|
||||
```
|
||||
|
||||
Also add the `relay_client.h` include at the top of `main.c`.
|
||||
|
||||
## Summary of Changes
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `src/relay_client.c:328-336` | Add `usleep(100000)` when poll returns 0 | Fix 100% CPU busy-wait |
|
||||
| `src/relay_client.c:39` | Make `running` field `volatile` | Thread-safe flag checking |
|
||||
| `src/main.c` (top) | Add signal handler function | Respond to SIGTERM/SIGINT |
|
||||
| `src/main.c` (before FCGI loop) | Register signal handlers | Enable graceful shutdown |
|
||||
| `src/main.c:2667` | Add `&& !g_shutdown_requested` to loop | Exit FCGI loop on signal |
|
||||
| `src/main.c` (after FCGI loop) | Call `relay_client_stop()` | Clean up relay thread |
|
||||
|
||||
## Testing
|
||||
|
||||
1. Build locally: `make clean && make`
|
||||
2. Run locally and verify CPU usage stays near 0% when idle
|
||||
3. Send SIGTERM and verify clean shutdown (no SIGKILL needed)
|
||||
4. Deploy to remote server via `deploy_lt.sh`
|
||||
5. Monitor with `top` to confirm CPU usage is normal
|
||||
@@ -0,0 +1,142 @@
|
||||
# Unified Logging Migration Plan
|
||||
|
||||
## Overview
|
||||
|
||||
Migrate ginxsom to use nostr_core_lib v0.4.13's callback-based logging API and unify all application logging into a single sink (`logs/app/app.log`).
|
||||
|
||||
## Architecture Context
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Current State - v0.4.8
|
||||
A[ginxsom app] -->|fprintf stderr ~122 calls| B[nginx error_log]
|
||||
A -->|app_log ~80+ calls| C[logs/app/app.log]
|
||||
A -->|validator_debug_log ~10 calls| D[logs/app/debug.log]
|
||||
E[nostr_core_lib] -->|direct file write| F[nostr_core_lib/debug.log]
|
||||
G[systemd journald] -->|spawn-fcgi parent only| H[journal]
|
||||
end
|
||||
```
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Target State - v0.4.13
|
||||
A2[ginxsom app] -->|stderr: pre-init fatal only ~10 calls| B2[nginx error_log]
|
||||
A2 -->|app_log: all operational logs| C2[logs/app/app.log]
|
||||
E2[nostr_core_lib] -->|callback| A2
|
||||
G2[systemd journald] -->|spawn-fcgi parent only| H2[journal]
|
||||
end
|
||||
```
|
||||
|
||||
## Logging Policy
|
||||
|
||||
| Sink | Purpose | When Used |
|
||||
|------|---------|-----------|
|
||||
| `stderr` -> nginx error_log | Pre-init fatal errors only | Before `app_log` is available, or truly unrecoverable errors during early `main` |
|
||||
| `logs/app/app.log` via `app_log` | All application operational logging | Everything after logger init: app events, nostr_core_lib callback, validator, relay, admin |
|
||||
| `logs/app/debug.log` | **ELIMINATED** | Replaced by `app_log(LOG_DEBUG, ...)` |
|
||||
| `nostr_core_lib/debug.log` | **ELIMINATED** | v0.4.13 removes direct file logging; callback replaces it |
|
||||
|
||||
## Log Levels
|
||||
|
||||
```c
|
||||
typedef enum {
|
||||
LOG_TRACE = 0, // NEW: maps to NOSTR_LOG_LEVEL_TRACE
|
||||
LOG_DEBUG = 1, // maps to NOSTR_LOG_LEVEL_DEBUG
|
||||
LOG_INFO = 2, // maps to NOSTR_LOG_LEVEL_INFO
|
||||
LOG_WARN = 3, // maps to NOSTR_LOG_LEVEL_WARN
|
||||
LOG_ERROR = 4 // maps to NOSTR_LOG_LEVEL_ERROR
|
||||
} log_level_t;
|
||||
```
|
||||
|
||||
**Note:** Existing code uses `LOG_DEBUG=0, LOG_INFO=1, LOG_WARN=2, LOG_ERROR=3`. Adding `LOG_TRACE` at 0 shifts all values up by 1. All existing call sites use the enum names (not raw integers), so this is safe.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Update nostr_core_lib submodule
|
||||
|
||||
1. Update the `nostr_core_lib` git submodule to v0.4.13
|
||||
2. Rebuild `libnostr_core_x64.a` from the updated source
|
||||
3. Verify the new headers exist: `nostr_core/nostr_log.h` (or equivalent in `nostr_core.h`)
|
||||
4. Verify build still compiles with `make clean && make`
|
||||
|
||||
### Phase 2: Enhance app_log in ginxsom.h and main.c
|
||||
|
||||
5. Add `LOG_TRACE` level to `log_level_t` enum in `ginxsom.h`
|
||||
6. Add a global `g_log_level` variable to control minimum log level
|
||||
7. Update `app_log()` in `main.c` to:
|
||||
- Check message level against `g_log_level` before writing
|
||||
- Handle the new `LOG_TRACE` level string
|
||||
- Keep the existing file-based sink (`logs/app/app.log`)
|
||||
|
||||
### Phase 3: Register nostr_core_lib log callback
|
||||
|
||||
8. Add `#include "nostr_core/nostr_core.h"` (or `nostr_log.h`) to `main.c` if not already present
|
||||
9. Create `nostr_log_cb()` callback function that maps nostr log levels to `app_log()` levels and prefixes messages with `[nostr:component]`
|
||||
10. In `main()`, after `nostr_init()` / `nostr_crypto_init()`, call:
|
||||
- `nostr_set_log_callback(nostr_log_cb, NULL)`
|
||||
- `nostr_set_log_level(NOSTR_LOG_LEVEL_INFO)` (or TRACE for debug builds)
|
||||
11. In shutdown path, call `nostr_set_log_callback(NULL, NULL)` before `nostr_cleanup()`
|
||||
|
||||
### Phase 4: Eliminate debug.log
|
||||
|
||||
12. Remove `validator_debug_log()` function from `request_validator.c`
|
||||
13. Replace all `validator_debug_log()` calls with `app_log(LOG_DEBUG, "VALIDATOR: %s", ...)` calls
|
||||
14. Delete stale `logs/app/debug.log` and `nostr_core_lib/debug.log` references from `.gitignore` or docs if present
|
||||
15. Remove `debug.log` from project root if it was generated by nostr_core_lib
|
||||
|
||||
### Phase 5: Migrate fprintf(stderr) calls to app_log
|
||||
|
||||
Migrate post-startup `fprintf(stderr, ...)` calls file-by-file. Keep only pre-init and truly fatal stderr calls in early `main()`.
|
||||
|
||||
**Files to migrate (by priority):**
|
||||
|
||||
16. `src/admin_auth.c` (~28 fprintf calls) - Replace with `app_log(LOG_ERROR, "AUTH: ...")` etc.
|
||||
17. `src/request_validator.c` (~2 fprintf calls) - Replace with `app_log(LOG_DEBUG, "VALIDATOR: ...")`
|
||||
18. `src/main.c` - Categorize ~90 fprintf calls:
|
||||
- **Keep as stderr** (~10): Pre-logger-init errors in early `main()`, help text output, keypair display banner
|
||||
- **Migrate to app_log** (~80): All post-init startup messages, database operations, request logging, upload handling, shutdown messages
|
||||
|
||||
### Phase 6: Remove duplicate log_level_t declarations
|
||||
|
||||
19. Remove the duplicate `typedef enum { LOG_DEBUG... } log_level_t` and `void app_log(...)` forward declarations from `src/relay_client.c` and `src/admin_commands.c` — these files should just `#include "ginxsom.h"` instead
|
||||
|
||||
### Phase 7: Update nostr_crypto_init to nostr_init
|
||||
|
||||
20. Replace `nostr_crypto_init()` calls with `nostr_init()` in `main.c` and `request_validator.c` if v0.4.13 consolidates initialization under `nostr_init()`
|
||||
21. Add `nostr_cleanup()` call in shutdown path if not already present
|
||||
|
||||
### Phase 8: Update Makefile and build
|
||||
|
||||
22. Verify `Makefile` CFLAGS and LIBS are compatible with v0.4.13 (check if new link flags needed)
|
||||
23. Remove any `LOGGING_FLAGS` build-time defines that are no longer needed
|
||||
24. Run `make clean && make` and verify clean build
|
||||
|
||||
### Phase 9: Verify and test
|
||||
|
||||
25. Run the application and verify:
|
||||
- `logs/app/app.log` contains both app logs and `[nostr:websocket]` / `[nostr:nip013]` tagged lines
|
||||
- `logs/app/debug.log` is no longer created
|
||||
- `nostr_core_lib/debug.log` is no longer created
|
||||
- `logs/nginx/error.log` only contains pre-init errors and nginx's own messages
|
||||
26. Run existing test suite to verify no regressions
|
||||
|
||||
## Files Modified
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `nostr_core_lib/` | Submodule updated to v0.4.13 |
|
||||
| `src/ginxsom.h` | Add LOG_TRACE, add g_log_level extern |
|
||||
| `src/main.c` | Enhanced app_log, nostr_log_cb, callback registration, fprintf migration |
|
||||
| `src/request_validator.c` | Remove validator_debug_log, replace with app_log |
|
||||
| `src/admin_auth.c` | Replace fprintf(stderr) with app_log |
|
||||
| `src/relay_client.c` | Remove duplicate log_level_t typedef |
|
||||
| `src/admin_commands.c` | Remove duplicate log_level_t typedef |
|
||||
| `Makefile` | Verify/update build flags |
|
||||
| `.gitignore` | Add/update debug.log exclusions |
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
- **Backward compatibility**: Enum names are used everywhere, not raw integers, so shifting values is safe
|
||||
- **Build breakage**: Phase 1 verifies build before any code changes
|
||||
- **Runtime breakage**: Callback registration is additive — if it fails, app still works, just without nostr_core_lib log routing
|
||||
- **stderr removal safety**: Only post-init calls are migrated; pre-init fatal errors stay on stderr for nginx capture
|
||||
+1
-1
@@ -184,7 +184,7 @@ server {
|
||||
return 405;
|
||||
}
|
||||
|
||||
try_files /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md =404;
|
||||
try_files /$1.html /$1.js /$1.mjs /$1.css /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md =404;
|
||||
|
||||
# Cache headers
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
|
||||
@@ -63,9 +63,15 @@ 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}"
|
||||
done
|
||||
|
||||
+106
-69
@@ -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;
|
||||
}
|
||||
@@ -88,35 +100,25 @@ static void admin_nip94_build_blob_url(const char* origin, const char* sha256,
|
||||
snprintf(out, out_size, "%s/%s%s", origin, sha256, extension);
|
||||
}
|
||||
|
||||
// Centralized MIME type to file extension mapping (from main.c)
|
||||
// Centralized MIME type to file extension mapping (delegates to shared BUD-08 mapping)
|
||||
static const char* admin_mime_to_extension(const char* mime_type) {
|
||||
if (!mime_type) {
|
||||
return ".bin";
|
||||
return mime_to_extension(mime_type);
|
||||
}
|
||||
|
||||
static int validate_hex64_format(const char* hash) {
|
||||
if (!hash || strlen(hash) != 64) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strstr(mime_type, "image/jpeg")) {
|
||||
return ".jpg";
|
||||
} else if (strstr(mime_type, "image/webp")) {
|
||||
return ".webp";
|
||||
} else if (strstr(mime_type, "image/png")) {
|
||||
return ".png";
|
||||
} else if (strstr(mime_type, "image/gif")) {
|
||||
return ".gif";
|
||||
} else if (strstr(mime_type, "video/mp4")) {
|
||||
return ".mp4";
|
||||
} else if (strstr(mime_type, "video/webm")) {
|
||||
return ".webm";
|
||||
} else if (strstr(mime_type, "audio/mpeg")) {
|
||||
return ".mp3";
|
||||
} else if (strstr(mime_type, "audio/ogg")) {
|
||||
return ".ogg";
|
||||
} else if (strstr(mime_type, "text/plain")) {
|
||||
return ".txt";
|
||||
} else if (strstr(mime_type, "application/pdf")) {
|
||||
return ".pdf";
|
||||
} else {
|
||||
return ".bin";
|
||||
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
|
||||
@@ -133,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) {
|
||||
@@ -603,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];
|
||||
@@ -623,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';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -638,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) {
|
||||
@@ -649,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);
|
||||
@@ -695,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);
|
||||
@@ -709,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);
|
||||
|
||||
@@ -811,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);
|
||||
|
||||
+27
-27
@@ -28,7 +28,7 @@ static int g_keys_loaded = 0; // Whether keys have been loaded
|
||||
static int ensure_keys_loaded(void) {
|
||||
if (!g_keys_loaded) {
|
||||
if (get_blossom_private_key(g_blossom_seckey, sizeof(g_blossom_seckey)) != 0) {
|
||||
fprintf(stderr, "ERROR: Cannot load blossom private key for admin auth\n");
|
||||
app_log(LOG_ERROR, "ERROR: Cannot load blossom private key for admin auth\n");
|
||||
return -1;
|
||||
}
|
||||
g_keys_loaded = 1;
|
||||
@@ -83,13 +83,13 @@ int validate_admin_event(cJSON *event) {
|
||||
cJSON *sig = cJSON_GetObjectItem(event, "sig");
|
||||
|
||||
if (!cJSON_IsString(pubkey) || !cJSON_IsString(sig)) {
|
||||
fprintf(stderr, "AUTH: Invalid event format - missing pubkey or sig\n");
|
||||
app_log(LOG_ERROR, "AUTH: Invalid event format - missing pubkey or sig\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check if pubkey matches configured admin pubkey
|
||||
if (!validate_admin_pubkey(pubkey->valuestring)) {
|
||||
fprintf(stderr, "AUTH: Pubkey %s is not authorized admin\n", pubkey->valuestring);
|
||||
app_log(LOG_ERROR, "AUTH: Pubkey %s is not authorized admin\n", pubkey->valuestring);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -114,14 +114,14 @@ int decrypt_admin_command(cJSON *event, char **decrypted_command_out) {
|
||||
// Get admin pubkey from event
|
||||
cJSON *admin_pubkey_json = cJSON_GetObjectItem(event, "pubkey");
|
||||
if (!cJSON_IsString(admin_pubkey_json)) {
|
||||
fprintf(stderr, "AUTH: Missing or invalid pubkey in event\n");
|
||||
app_log(LOG_ERROR, "AUTH: Missing or invalid pubkey in event\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Get encrypted content
|
||||
cJSON *content = cJSON_GetObjectItem(event, "content");
|
||||
if (!cJSON_IsString(content)) {
|
||||
fprintf(stderr, "AUTH: Missing or invalid content in event\n");
|
||||
app_log(LOG_ERROR, "AUTH: Missing or invalid content in event\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -130,12 +130,12 @@ int decrypt_admin_command(cJSON *event, char **decrypted_command_out) {
|
||||
unsigned char admin_public_key[32];
|
||||
|
||||
if (nostr_hex_to_bytes(g_blossom_seckey, blossom_private_key, 32) != 0) {
|
||||
fprintf(stderr, "AUTH: Failed to parse blossom private key\n");
|
||||
app_log(LOG_ERROR, "AUTH: Failed to parse blossom private key\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (nostr_hex_to_bytes(admin_pubkey_json->valuestring, admin_public_key, 32) != 0) {
|
||||
fprintf(stderr, "AUTH: Failed to parse admin public key\n");
|
||||
app_log(LOG_ERROR, "AUTH: Failed to parse admin public key\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -152,14 +152,14 @@ int decrypt_admin_command(cJSON *event, char **decrypted_command_out) {
|
||||
);
|
||||
|
||||
if (result != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "AUTH: NIP-44 decryption failed with error code %d\n", result);
|
||||
app_log(LOG_ERROR, "AUTH: NIP-44 decryption failed with error code %d\n", result);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Allocate and copy decrypted content
|
||||
*decrypted_command_out = malloc(strlen(decrypted_buffer) + 1);
|
||||
if (!*decrypted_command_out) {
|
||||
fprintf(stderr, "AUTH: Failed to allocate memory for decrypted content\n");
|
||||
app_log(LOG_ERROR, "AUTH: Failed to allocate memory for decrypted content\n");
|
||||
return -1;
|
||||
}
|
||||
strcpy(*decrypted_command_out, decrypted_buffer);
|
||||
@@ -176,19 +176,19 @@ int parse_admin_command(const char *decrypted_content, char ***command_array_out
|
||||
// Parse the decrypted content as JSON array
|
||||
cJSON *content_json = cJSON_Parse(decrypted_content);
|
||||
if (!content_json) {
|
||||
fprintf(stderr, "AUTH: Failed to parse decrypted content as JSON\n");
|
||||
app_log(LOG_ERROR, "AUTH: Failed to parse decrypted content as JSON\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!cJSON_IsArray(content_json)) {
|
||||
fprintf(stderr, "AUTH: Decrypted content is not a JSON array\n");
|
||||
app_log(LOG_ERROR, "AUTH: Decrypted content is not a JSON array\n");
|
||||
cJSON_Delete(content_json);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int array_size = cJSON_GetArraySize(content_json);
|
||||
if (array_size < 1) {
|
||||
fprintf(stderr, "AUTH: Command array is empty\n");
|
||||
app_log(LOG_ERROR, "AUTH: Command array is empty\n");
|
||||
cJSON_Delete(content_json);
|
||||
return -1;
|
||||
}
|
||||
@@ -196,7 +196,7 @@ int parse_admin_command(const char *decrypted_content, char ***command_array_out
|
||||
// Allocate command array
|
||||
char **command_array = malloc(array_size * sizeof(char *));
|
||||
if (!command_array) {
|
||||
fprintf(stderr, "AUTH: Failed to allocate command array\n");
|
||||
app_log(LOG_ERROR, "AUTH: Failed to allocate command array\n");
|
||||
cJSON_Delete(content_json);
|
||||
return -1;
|
||||
}
|
||||
@@ -205,7 +205,7 @@ int parse_admin_command(const char *decrypted_content, char ***command_array_out
|
||||
for (int i = 0; i < array_size; i++) {
|
||||
cJSON *item = cJSON_GetArrayItem(content_json, i);
|
||||
if (!cJSON_IsString(item)) {
|
||||
fprintf(stderr, "AUTH: Command array element %d is not a string\n", i);
|
||||
app_log(LOG_ERROR, "AUTH: Command array element %d is not a string\n", i);
|
||||
// Clean up allocated strings
|
||||
for (int j = 0; j < i; j++) {
|
||||
free(command_array[j]);
|
||||
@@ -217,7 +217,7 @@ int parse_admin_command(const char *decrypted_content, char ***command_array_out
|
||||
|
||||
command_array[i] = malloc(strlen(item->valuestring) + 1);
|
||||
if (!command_array[i]) {
|
||||
fprintf(stderr, "AUTH: Failed to allocate command string\n");
|
||||
app_log(LOG_ERROR, "AUTH: Failed to allocate command string\n");
|
||||
// Clean up allocated strings
|
||||
for (int j = 0; j < i; j++) {
|
||||
free(command_array[j]);
|
||||
@@ -228,7 +228,7 @@ int parse_admin_command(const char *decrypted_content, char ***command_array_out
|
||||
}
|
||||
strcpy(command_array[i], item->valuestring);
|
||||
if (!command_array[i]) {
|
||||
fprintf(stderr, "AUTH: Failed to duplicate command string\n");
|
||||
app_log(LOG_ERROR, "AUTH: Failed to duplicate command string\n");
|
||||
// Clean up allocated strings
|
||||
for (int j = 0; j < i; j++) {
|
||||
free(command_array[j]);
|
||||
@@ -274,7 +274,7 @@ int process_admin_command(cJSON *event, char ***command_array_out, int *command_
|
||||
sqlite3_close(db);
|
||||
|
||||
if (strlen(blossom_pubkey) != 64) {
|
||||
fprintf(stderr, "ERROR: Cannot determine blossom pubkey for admin auth\n");
|
||||
app_log(LOG_ERROR, "ERROR: Cannot determine blossom pubkey for admin auth\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -296,7 +296,7 @@ int process_admin_command(cJSON *event, char ***command_array_out, int *command_
|
||||
|
||||
*admin_pubkey_out = malloc(strlen(admin_pubkey_json->valuestring) + 1);
|
||||
if (!*admin_pubkey_out) {
|
||||
fprintf(stderr, "AUTH: Failed to allocate admin pubkey string\n");
|
||||
app_log(LOG_ERROR, "AUTH: Failed to allocate admin pubkey string\n");
|
||||
return -1;
|
||||
}
|
||||
strcpy(*admin_pubkey_out, admin_pubkey_json->valuestring);
|
||||
@@ -386,7 +386,7 @@ int create_admin_response(const char *response_json, const char *admin_pubkey, c
|
||||
sqlite3_close(db);
|
||||
|
||||
if (strlen(blossom_pubkey) != 64) {
|
||||
fprintf(stderr, "ERROR: Cannot determine blossom pubkey for response\n");
|
||||
app_log(LOG_ERROR, "ERROR: Cannot determine blossom pubkey for response\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -395,12 +395,12 @@ int create_admin_response(const char *response_json, const char *admin_pubkey, c
|
||||
unsigned char admin_public_key[32];
|
||||
|
||||
if (nostr_hex_to_bytes(g_blossom_seckey, blossom_private_key, 32) != 0) {
|
||||
fprintf(stderr, "AUTH: Failed to parse blossom private key\n");
|
||||
app_log(LOG_ERROR, "AUTH: Failed to parse blossom private key\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (nostr_hex_to_bytes(admin_pubkey, admin_public_key, 32) != 0) {
|
||||
fprintf(stderr, "AUTH: Failed to parse admin public key\n");
|
||||
app_log(LOG_ERROR, "AUTH: Failed to parse admin public key\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -415,14 +415,14 @@ int create_admin_response(const char *response_json, const char *admin_pubkey, c
|
||||
);
|
||||
|
||||
if (result != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "AUTH: NIP-44 encryption failed with error code %d\n", result);
|
||||
app_log(LOG_ERROR, "AUTH: NIP-44 encryption failed with error code %d\n", result);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Create Kind 23457 response event
|
||||
cJSON *response_event = cJSON_CreateObject();
|
||||
if (!response_event) {
|
||||
fprintf(stderr, "AUTH: Failed to create response event JSON\n");
|
||||
app_log(LOG_ERROR, "AUTH: Failed to create response event JSON\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -444,7 +444,7 @@ int create_admin_response(const char *response_json, const char *admin_pubkey, c
|
||||
// Convert private key hex to bytes
|
||||
unsigned char blossom_private_key_bytes[32];
|
||||
if (nostr_hex_to_bytes(g_blossom_seckey, blossom_private_key_bytes, 32) != 0) {
|
||||
fprintf(stderr, "AUTH: Failed to parse blossom private key for signing\n");
|
||||
app_log(LOG_ERROR, "AUTH: Failed to parse blossom private key for signing\n");
|
||||
cJSON_Delete(response_event);
|
||||
return -1;
|
||||
}
|
||||
@@ -452,7 +452,7 @@ int create_admin_response(const char *response_json, const char *admin_pubkey, c
|
||||
// Create a temporary event structure for signing
|
||||
cJSON* temp_event = cJSON_Duplicate(response_event, 1);
|
||||
if (!temp_event) {
|
||||
fprintf(stderr, "AUTH: Failed to create temp event for signing\n");
|
||||
app_log(LOG_ERROR, "AUTH: Failed to create temp event for signing\n");
|
||||
cJSON_Delete(response_event);
|
||||
return -1;
|
||||
}
|
||||
@@ -467,7 +467,7 @@ int create_admin_response(const char *response_json, const char *admin_pubkey, c
|
||||
);
|
||||
|
||||
if (!signed_event) {
|
||||
fprintf(stderr, "AUTH: Failed to sign admin response event\n");
|
||||
app_log(LOG_ERROR, "AUTH: Failed to sign admin response event\n");
|
||||
cJSON_Delete(response_event);
|
||||
cJSON_Delete(temp_event);
|
||||
return -1;
|
||||
@@ -481,7 +481,7 @@ int create_admin_response(const char *response_json, const char *admin_pubkey, c
|
||||
cJSON_AddStringToObject(response_event, "id", cJSON_GetStringValue(signed_id));
|
||||
cJSON_AddStringToObject(response_event, "sig", cJSON_GetStringValue(signed_sig));
|
||||
} else {
|
||||
fprintf(stderr, "AUTH: Signed event missing id or sig\n");
|
||||
app_log(LOG_ERROR, "AUTH: Signed event missing id or sig\n");
|
||||
cJSON_Delete(response_event);
|
||||
cJSON_Delete(signed_event);
|
||||
cJSON_Delete(temp_event);
|
||||
|
||||
+144
-11
@@ -3,23 +3,17 @@
|
||||
*/
|
||||
|
||||
#include "admin_commands.h"
|
||||
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
#include "app_log.h"
|
||||
#include "../nostr_core_lib/nostr_core/nip044.h"
|
||||
#include <sqlite3.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <time.h>
|
||||
|
||||
// Forward declare app_log
|
||||
typedef enum {
|
||||
LOG_DEBUG = 0,
|
||||
LOG_INFO = 1,
|
||||
LOG_WARN = 2,
|
||||
LOG_ERROR = 3
|
||||
} log_level_t;
|
||||
|
||||
void app_log(log_level_t level, const char* format, ...);
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include "ginxsom.h"
|
||||
|
||||
// Global state
|
||||
static struct {
|
||||
@@ -137,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);
|
||||
}
|
||||
@@ -594,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
@@ -0,0 +1,24 @@
|
||||
#ifndef APP_LOG_H
|
||||
#define APP_LOG_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Centralized application logging (writes to logs/app/app.log)
|
||||
typedef enum {
|
||||
LOG_TRACE = 0,
|
||||
LOG_DEBUG = 1,
|
||||
LOG_INFO = 2,
|
||||
LOG_WARN = 3,
|
||||
LOG_ERROR = 4
|
||||
} log_level_t;
|
||||
|
||||
extern log_level_t g_log_level;
|
||||
void app_log(log_level_t level, const char* format, ...);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // APP_LOG_H
|
||||
+48
-23
@@ -95,35 +95,60 @@ int nip94_get_origin(char* out, size_t out_size) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
#define GINXSOM_MIME_EXTENSION_ENTRIES(X) \
|
||||
X("image/jpeg", ".jpg") \
|
||||
X("image/webp", ".webp") \
|
||||
X("image/png", ".png") \
|
||||
X("image/gif", ".gif") \
|
||||
X("video/mp4", ".mp4") \
|
||||
X("video/webm", ".webm") \
|
||||
X("audio/mpeg", ".mp3") \
|
||||
X("audio/ogg", ".ogg") \
|
||||
X("text/plain", ".txt") \
|
||||
X("text/html", ".html") \
|
||||
X("text/css", ".css") \
|
||||
X("application/javascript", ".js") \
|
||||
X("text/javascript", ".mjs") \
|
||||
X("application/pdf", ".pdf")
|
||||
|
||||
typedef struct {
|
||||
const char* mime;
|
||||
const char* extension;
|
||||
} mime_extension_entry_t;
|
||||
|
||||
static const mime_extension_entry_t g_mime_extension_map[] = {
|
||||
#define MAKE_MIME_ENTRY(mime, ext) {mime, ext},
|
||||
GINXSOM_MIME_EXTENSION_ENTRIES(MAKE_MIME_ENTRY)
|
||||
#undef MAKE_MIME_ENTRY
|
||||
};
|
||||
|
||||
static const char* g_supported_mime_types[] = {
|
||||
#define MAKE_MIME_ONLY(mime, ext) mime,
|
||||
GINXSOM_MIME_EXTENSION_ENTRIES(MAKE_MIME_ONLY)
|
||||
#undef MAKE_MIME_ONLY
|
||||
};
|
||||
|
||||
const char* const* ginxsom_supported_mime_types(size_t* count) {
|
||||
if (count) {
|
||||
*count = sizeof(g_supported_mime_types) / sizeof(g_supported_mime_types[0]);
|
||||
}
|
||||
return g_supported_mime_types;
|
||||
}
|
||||
|
||||
// Centralized MIME type to file extension mapping
|
||||
const char* mime_to_extension(const char* mime_type) {
|
||||
if (!mime_type) {
|
||||
return ".bin";
|
||||
}
|
||||
|
||||
if (strstr(mime_type, "image/jpeg")) {
|
||||
return ".jpg";
|
||||
} else if (strstr(mime_type, "image/webp")) {
|
||||
return ".webp";
|
||||
} else if (strstr(mime_type, "image/png")) {
|
||||
return ".png";
|
||||
} else if (strstr(mime_type, "image/gif")) {
|
||||
return ".gif";
|
||||
} else if (strstr(mime_type, "video/mp4")) {
|
||||
return ".mp4";
|
||||
} else if (strstr(mime_type, "video/webm")) {
|
||||
return ".webm";
|
||||
} else if (strstr(mime_type, "audio/mpeg")) {
|
||||
return ".mp3";
|
||||
} else if (strstr(mime_type, "audio/ogg")) {
|
||||
return ".ogg";
|
||||
} else if (strstr(mime_type, "text/plain")) {
|
||||
return ".txt";
|
||||
} else if (strstr(mime_type, "application/pdf")) {
|
||||
return ".pdf";
|
||||
} else {
|
||||
return ".bin";
|
||||
|
||||
size_t count = sizeof(g_mime_extension_map) / sizeof(g_mime_extension_map[0]);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
if (strstr(mime_type, g_mime_extension_map[i].mime)) {
|
||||
return g_mime_extension_map[i].extension;
|
||||
}
|
||||
}
|
||||
|
||||
return ".bin";
|
||||
}
|
||||
|
||||
// Build canonical blob URL from origin + sha256 + extension
|
||||
|
||||
+6
-12
@@ -9,9 +9,9 @@
|
||||
#define GINXSOM_H
|
||||
// Version information (auto-updated by build system)
|
||||
#define VERSION_MAJOR 0
|
||||
#define VERSION_MINOR 1
|
||||
#define VERSION_PATCH 26
|
||||
#define VERSION "v0.1.26"
|
||||
#define VERSION_MINOR 2
|
||||
#define VERSION_PATCH 3
|
||||
#define VERSION "v0.2.3"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
@@ -20,6 +20,8 @@
|
||||
#include <sqlite3.h>
|
||||
#include "../nostr_core_lib/cjson/cJSON.h"
|
||||
|
||||
#include "app_log.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -226,6 +228,7 @@ int nip94_get_origin(char* out, size_t out_size);
|
||||
|
||||
// MIME type and file extension handling
|
||||
const char* mime_to_extension(const char* mime_type);
|
||||
const char* const* ginxsom_supported_mime_types(size_t* count);
|
||||
void nip94_build_blob_url(const char* origin, const char* sha256, const char* mime_type, char* out, size_t out_size);
|
||||
|
||||
// Image dimension parsing
|
||||
@@ -250,15 +253,6 @@ void send_json_response(int status_code, const char* json_content);
|
||||
// Logging utilities
|
||||
void log_request(const char* method, const char* uri, const char* auth_status, int status_code);
|
||||
|
||||
// Centralized application logging (writes to logs/app/app.log)
|
||||
typedef enum {
|
||||
LOG_DEBUG = 0,
|
||||
LOG_INFO = 1,
|
||||
LOG_WARN = 2,
|
||||
LOG_ERROR = 3
|
||||
} log_level_t;
|
||||
|
||||
void app_log(log_level_t level, const char* format, ...);
|
||||
|
||||
// SHA-256 validation helper (used by multiple BUDs)
|
||||
int validate_sha256_format(const char* sha256);
|
||||
|
||||
+393
-503
File diff suppressed because it is too large
Load Diff
+24
-12
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "relay_client.h"
|
||||
#include "admin_commands.h"
|
||||
#include "app_log.h"
|
||||
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
#include <sqlite3.h>
|
||||
#include <stdio.h>
|
||||
@@ -15,16 +16,6 @@
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
|
||||
// Forward declare app_log to avoid including ginxsom.h (which has typedef conflicts)
|
||||
typedef enum {
|
||||
LOG_DEBUG = 0,
|
||||
LOG_INFO = 1,
|
||||
LOG_WARN = 2,
|
||||
LOG_ERROR = 3
|
||||
} log_level_t;
|
||||
|
||||
void app_log(log_level_t level, const char* format, ...);
|
||||
|
||||
// Maximum number of relays to connect to
|
||||
#define MAX_RELAYS 10
|
||||
|
||||
@@ -36,7 +27,7 @@ void app_log(log_level_t level, const char* format, ...);
|
||||
static struct {
|
||||
int enabled;
|
||||
int initialized;
|
||||
int running;
|
||||
volatile int running;
|
||||
char db_path[512];
|
||||
nostr_relay_pool_t* pool;
|
||||
char** relay_urls;
|
||||
@@ -272,14 +263,30 @@ int relay_client_start(void) {
|
||||
}
|
||||
|
||||
app_log(LOG_INFO, "Starting relay client...");
|
||||
|
||||
// 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, NULL, relay_management_thread, NULL) != 0) {
|
||||
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;
|
||||
@@ -332,6 +339,11 @@ static void *relay_management_thread(void *arg) {
|
||||
if (events_processed < 0) {
|
||||
app_log(LOG_ERROR, "Error polling relay pool");
|
||||
sleep(1);
|
||||
} else if (events_processed == 0) {
|
||||
// Prevent busy-wait: nostr_relay_pool_poll() returns immediately when all
|
||||
// relays are disconnected because it skips the blocking nostr_ws_receive()
|
||||
// call for non-connected relays. Without this sleep, the loop spins at 100% CPU.
|
||||
usleep(100000); // 100ms idle sleep
|
||||
}
|
||||
// Pool handles all connection management, reconnection, and message processing
|
||||
}
|
||||
|
||||
+67
-31
@@ -92,14 +92,25 @@ struct {
|
||||
} g_last_rule_violation = {0};
|
||||
|
||||
/**
|
||||
* Helper function for consistent debug logging to our debug.log file
|
||||
* Helper function for consistent validator debug logging via app logger
|
||||
*/
|
||||
static void validator_debug_log(const char *message) {
|
||||
FILE *debug_log = fopen("logs/app/debug.log", "a");
|
||||
if (debug_log) {
|
||||
fprintf(debug_log, "%ld %s", (long)time(NULL), message);
|
||||
fclose(debug_log);
|
||||
const char *msg = message ? message : "";
|
||||
size_t msg_len = strlen(msg);
|
||||
|
||||
if (msg_len > 0 && msg[msg_len - 1] == '\n') {
|
||||
char trimmed[1024];
|
||||
size_t copy_len = msg_len - 1;
|
||||
if (copy_len >= sizeof(trimmed)) {
|
||||
copy_len = sizeof(trimmed) - 1;
|
||||
}
|
||||
memcpy(trimmed, msg, copy_len);
|
||||
trimmed[copy_len] = '\0';
|
||||
app_log(LOG_DEBUG, "%s", trimmed);
|
||||
return;
|
||||
}
|
||||
|
||||
app_log(LOG_DEBUG, "%s", msg);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
@@ -143,9 +154,9 @@ int ginxsom_request_validator_init(const char *db_path, const char *app_name) {
|
||||
}
|
||||
|
||||
// Initialize nostr_core_lib if not already done
|
||||
if (nostr_crypto_init() != NOSTR_SUCCESS) {
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
validator_debug_log(
|
||||
"VALIDATOR: Failed to initialize nostr crypto system\n");
|
||||
"VALIDATOR: Failed to initialize nostr core system\n");
|
||||
return NOSTR_ERROR_CRYPTO_INIT;
|
||||
}
|
||||
|
||||
@@ -285,7 +296,50 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
|
||||
// PHASE 2: NOSTR EVENT VALIDATION (CPU Intensive ~2ms)
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Check if authentication is disabled first (regardless of header presence)
|
||||
// Global authentication bypass for all non-admin operations.
|
||||
// This makes blob upload/download/list/delete anonymous by default while
|
||||
// keeping admin endpoints gated by the normal validation flow below.
|
||||
int is_admin_operation =
|
||||
(request->operation &&
|
||||
(strcmp(request->operation, "admin") == 0 ||
|
||||
strcmp(request->operation, "admin_event") == 0));
|
||||
|
||||
if (!is_admin_operation) {
|
||||
validator_debug_log(
|
||||
"VALIDATOR_DEBUG: STEP 4 PASSED - Authentication bypassed for non-admin operation\n");
|
||||
result->valid = 1;
|
||||
result->error_code = NOSTR_SUCCESS;
|
||||
strcpy(result->reason, "Authentication bypassed for non-admin operation");
|
||||
|
||||
// Preserve uploader attribution when a valid auth header is optionally
|
||||
// provided by extracting the pubkey without requiring it.
|
||||
if (request->auth_header) {
|
||||
char optional_event_json[4096];
|
||||
int optional_parse = parse_authorization_header(
|
||||
request->auth_header, optional_event_json, sizeof(optional_event_json));
|
||||
if (optional_parse == NOSTR_SUCCESS) {
|
||||
cJSON *optional_event = cJSON_Parse(optional_event_json);
|
||||
if (optional_event) {
|
||||
if (nostr_validate_event(optional_event) == NOSTR_SUCCESS) {
|
||||
char optional_pubkey[65] = {0};
|
||||
int optional_extract = extract_pubkey_from_event(
|
||||
optional_event, optional_pubkey, sizeof(optional_pubkey));
|
||||
if (optional_extract == NOSTR_SUCCESS && strlen(optional_pubkey) == 64) {
|
||||
strncpy(result->pubkey, optional_pubkey, 64);
|
||||
result->pubkey[64] = '\0';
|
||||
strcpy(result->reason,
|
||||
"Authentication bypassed (pubkey extracted from optional auth header)");
|
||||
}
|
||||
}
|
||||
cJSON_Delete(optional_event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
// For admin operations, keep config-driven authentication behavior.
|
||||
if (!g_auth_cache.auth_required) {
|
||||
validator_debug_log("VALIDATOR_DEBUG: STEP 4 PASSED - Authentication "
|
||||
"disabled, allowing request\n");
|
||||
@@ -305,25 +359,7 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
// Check if this is a GET blob request - allow public blob retrieval
|
||||
if (request->operation && strcmp(request->operation, "get_blob") == 0) {
|
||||
result->valid = 1;
|
||||
result->error_code = NOSTR_SUCCESS;
|
||||
strcpy(result->reason, "Public blob retrieval - no authentication required");
|
||||
validator_debug_log("VALIDATOR_DEBUG: GET blob detected, bypassing authentication\n");
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
// Check if this is a HEAD request - allow public metadata retrieval
|
||||
if (request->operation && (strcmp(request->operation, "head") == 0 || strcmp(request->operation, "head_upload") == 0)) {
|
||||
result->valid = 1;
|
||||
result->error_code = NOSTR_SUCCESS;
|
||||
strcpy(result->reason, "Public HEAD request - no authentication required");
|
||||
validator_debug_log("VALIDATOR_DEBUG: HEAD request detected, bypassing authentication\n");
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
// Check if authentication header is provided (required for non-public operations)
|
||||
// Check if authentication header is provided (required for non-report operations)
|
||||
if (!request->auth_header) {
|
||||
|
||||
result->valid = 0;
|
||||
@@ -1160,15 +1196,15 @@ static int reload_auth_config(void) {
|
||||
}
|
||||
|
||||
// Debug logging
|
||||
fprintf(stderr,
|
||||
app_log(LOG_DEBUG,
|
||||
"VALIDATOR: Configuration loaded from unified config table - "
|
||||
"auth_required: %d, max_file_size: %ld, nip42_mode: %d, "
|
||||
"cache_timeout: %d\n",
|
||||
"cache_timeout: %d",
|
||||
g_auth_cache.auth_required, g_auth_cache.max_file_size,
|
||||
g_auth_cache.nip42_mode, cache_timeout);
|
||||
fprintf(stderr,
|
||||
app_log(LOG_DEBUG,
|
||||
"VALIDATOR: NIP-42 mode details - nip42_mode=%d (0=disabled, "
|
||||
"1=optional/enabled, 2=required)\n",
|
||||
"1=optional/enabled, 2=required)",
|
||||
g_auth_cache.nip42_mode);
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>test_html</title>
|
||||
/* payload type: text/html */
|
||||
console.log("test_html");
|
||||
Executable
+266
@@ -0,0 +1,266 @@
|
||||
#!/bin/bash
|
||||
|
||||
# file_put_production.sh - Test script for production Ginxsom Blossom server
|
||||
# Tests upload functionality on blossom.laantungir.net
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
# Configuration
|
||||
SERVER_URL="https://blossom.laantungir.net"
|
||||
UPLOAD_ENDPOINT="${SERVER_URL}/upload"
|
||||
TEST_FILE="test_blob_$(date +%s).html"
|
||||
CLEANUP_FILES=()
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Cleanup function
|
||||
cleanup() {
|
||||
echo -e "${YELLOW}Cleaning up temporary files...${NC}"
|
||||
for file in "${CLEANUP_FILES[@]}"; do
|
||||
if [[ -f "$file" ]]; then
|
||||
rm -f "$file"
|
||||
echo "Removed: $file"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Set up cleanup on exit
|
||||
trap cleanup EXIT
|
||||
|
||||
# Helper functions
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
# Check prerequisites
|
||||
check_prerequisites() {
|
||||
log_info "Checking prerequisites..."
|
||||
|
||||
# Check if nak is installed
|
||||
if ! command -v nak &> /dev/null; then
|
||||
log_error "nak command not found. Please install nak first."
|
||||
log_info "Install with: go install github.com/fiatjaf/nak@latest"
|
||||
exit 1
|
||||
fi
|
||||
log_success "nak is installed"
|
||||
|
||||
# Check if curl is available
|
||||
if ! command -v curl &> /dev/null; then
|
||||
log_error "curl command not found. Please install curl."
|
||||
exit 1
|
||||
fi
|
||||
log_success "curl is available"
|
||||
|
||||
# Check if sha256sum is available
|
||||
if ! command -v sha256sum &> /dev/null; then
|
||||
log_error "sha256sum command not found."
|
||||
exit 1
|
||||
fi
|
||||
log_success "sha256sum is available"
|
||||
|
||||
# Check if base64 is available
|
||||
if ! command -v base64 &> /dev/null; then
|
||||
log_error "base64 command not found."
|
||||
exit 1
|
||||
fi
|
||||
log_success "base64 is available"
|
||||
}
|
||||
|
||||
# Check if server is running
|
||||
check_server() {
|
||||
log_info "Checking if server is running..."
|
||||
|
||||
if curl -s -f "${SERVER_URL}/health" > /dev/null 2>&1; then
|
||||
log_success "Server is running at ${SERVER_URL}"
|
||||
else
|
||||
log_error "Server is not responding at ${SERVER_URL}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Create test file
|
||||
create_test_file() {
|
||||
log_info "Creating test file: ${TEST_FILE}"
|
||||
|
||||
# Create test content with timestamp and random data
|
||||
cat > "${TEST_FILE}" << EOF
|
||||
Test blob content for Ginxsom Blossom server (PRODUCTION)
|
||||
Timestamp: $(date -Iseconds)
|
||||
Random data: $(openssl rand -hex 32)
|
||||
Test message: Hello from production test!
|
||||
|
||||
This file is used to test the upload functionality
|
||||
of the Ginxsom Blossom server on blossom.laantungir.net
|
||||
EOF
|
||||
|
||||
CLEANUP_FILES+=("${TEST_FILE}")
|
||||
log_success "Created test file with $(wc -c < "${TEST_FILE}") bytes"
|
||||
}
|
||||
|
||||
# Calculate file hash
|
||||
calculate_hash() {
|
||||
log_info "Calculating SHA-256 hash..."
|
||||
|
||||
HASH=$(sha256sum "${TEST_FILE}" | cut -d' ' -f1)
|
||||
|
||||
log_success "Data to hash: ${TEST_FILE}"
|
||||
log_success "File hash: ${HASH}"
|
||||
}
|
||||
|
||||
# Generate nostr event
|
||||
generate_nostr_event() {
|
||||
log_info "Generating kind 24242 nostr event with nak using WSB's private key..."
|
||||
|
||||
# Calculate expiration time (1 hour from now)
|
||||
EXPIRATION=$(date -d '+1 hour' +%s)
|
||||
|
||||
# Generate the event using nak with WSB's private key
|
||||
EVENT_JSON=$(nak event -k 24242 -c "" \
|
||||
--sec "22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd" \
|
||||
-t "t=upload" \
|
||||
-t "x=${HASH}" \
|
||||
-t "expiration=${EXPIRATION}")
|
||||
|
||||
if [[ -z "$EVENT_JSON" ]]; then
|
||||
log_error "Failed to generate nostr event"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "Generated nostr event"
|
||||
echo "Event JSON: $EVENT_JSON"
|
||||
}
|
||||
|
||||
# Create authorization header
|
||||
create_auth_header() {
|
||||
log_info "Creating authorization header..."
|
||||
|
||||
# Base64 encode the event (without newlines)
|
||||
AUTH_B64=$(echo -n "$EVENT_JSON" | base64 -w 0)
|
||||
AUTH_HEADER="Nostr ${AUTH_B64}"
|
||||
|
||||
log_success "Created authorization header"
|
||||
echo "Auth header length: ${#AUTH_HEADER} characters"
|
||||
}
|
||||
|
||||
# Perform upload
|
||||
perform_upload() {
|
||||
log_info "Performing upload to ${UPLOAD_ENDPOINT}..."
|
||||
|
||||
# Create temporary file for response
|
||||
RESPONSE_FILE=$(mktemp)
|
||||
CLEANUP_FILES+=("${RESPONSE_FILE}")
|
||||
|
||||
# Perform the upload with verbose output
|
||||
HTTP_STATUS=$(curl -s -w "%{http_code}" \
|
||||
-X PUT \
|
||||
-H "Authorization: ${AUTH_HEADER}" \
|
||||
-H "Content-Type: text/plain" \
|
||||
-H "Content-Disposition: attachment; filename=\"${TEST_FILE}\"" \
|
||||
--data-binary "@${TEST_FILE}" \
|
||||
"${UPLOAD_ENDPOINT}" \
|
||||
-o "${RESPONSE_FILE}")
|
||||
|
||||
echo "HTTP Status: ${HTTP_STATUS}"
|
||||
echo "Response body:"
|
||||
cat "${RESPONSE_FILE}"
|
||||
echo
|
||||
|
||||
# Check response
|
||||
case "${HTTP_STATUS}" in
|
||||
200)
|
||||
log_success "Upload successful!"
|
||||
;;
|
||||
201)
|
||||
log_success "Upload successful (created)!"
|
||||
;;
|
||||
400)
|
||||
log_error "Bad request - check the event format"
|
||||
;;
|
||||
401)
|
||||
log_error "Unauthorized - authentication failed"
|
||||
;;
|
||||
405)
|
||||
log_error "Method not allowed - check nginx configuration"
|
||||
;;
|
||||
413)
|
||||
log_error "Payload too large"
|
||||
;;
|
||||
501)
|
||||
log_warning "Upload endpoint not yet implemented (expected for now)"
|
||||
;;
|
||||
*)
|
||||
log_error "Upload failed with HTTP status: ${HTTP_STATUS}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Test file retrieval
|
||||
test_retrieval() {
|
||||
log_info "Testing file retrieval..."
|
||||
|
||||
RETRIEVAL_URL="${SERVER_URL}/${HASH}"
|
||||
|
||||
if curl -s -f "${RETRIEVAL_URL}" > /dev/null 2>&1; then
|
||||
log_success "File can be retrieved at: ${RETRIEVAL_URL}"
|
||||
|
||||
# Download and verify
|
||||
DOWNLOADED_FILE=$(mktemp)
|
||||
CLEANUP_FILES+=("${DOWNLOADED_FILE}")
|
||||
curl -s "${RETRIEVAL_URL}" -o "${DOWNLOADED_FILE}"
|
||||
|
||||
DOWNLOADED_HASH=$(sha256sum "${DOWNLOADED_FILE}" | cut -d' ' -f1)
|
||||
if [[ "${DOWNLOADED_HASH}" == "${HASH}" ]]; then
|
||||
log_success "Downloaded file hash matches! Verification successful."
|
||||
else
|
||||
log_error "Hash mismatch! Expected: ${HASH}, Got: ${DOWNLOADED_HASH}"
|
||||
fi
|
||||
else
|
||||
log_warning "File not yet available for retrieval"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
echo "=== Ginxsom Blossom Production Upload Test ==="
|
||||
echo "Server: ${SERVER_URL}"
|
||||
echo "Timestamp: $(date -Iseconds)"
|
||||
echo
|
||||
|
||||
check_prerequisites
|
||||
check_server
|
||||
create_test_file
|
||||
calculate_hash
|
||||
generate_nostr_event
|
||||
create_auth_header
|
||||
perform_upload
|
||||
test_retrieval
|
||||
|
||||
echo
|
||||
log_info "Test completed!"
|
||||
echo "Summary:"
|
||||
echo " Test file: ${TEST_FILE}"
|
||||
echo " File hash: ${HASH}"
|
||||
echo " Server: ${SERVER_URL}"
|
||||
echo " Upload endpoint: ${UPLOAD_ENDPOINT}"
|
||||
echo " Retrieval URL: ${SERVER_URL}/${HASH}"
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Ginxsom Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Ginxsom HTML Upload Test</h1>
|
||||
<p>This is a small test file to verify HTML uploads on blossom.laantungir.net.</p>
|
||||
<p>Timestamp: 2026-03-11T21:55:00Z</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
BASE_URL="https://blossom.laantungir.net"
|
||||
PRIVKEY="22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd"
|
||||
FILE="/tmp/ginxsom_browser_sample.html"
|
||||
|
||||
cat > "$FILE" <<'EOF'
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Ginxsom Browser Sample</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; margin: 2rem; background: #0b1020; color: #e6e9f2; }
|
||||
.card { max-width: 720px; padding: 1.25rem 1.5rem; border: 1px solid #2a3350; border-radius: 12px; background: #121a33; }
|
||||
code { background: #1b2545; padding: 0.1rem 0.35rem; border-radius: 6px; }
|
||||
.ok { color: #69f0ae; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Ginxsom HTML test</h1>
|
||||
<p class="ok">If you can see this page, remote HTML upload and retrieval are working.</p>
|
||||
<p>Timestamp: <code id="ts"></code></p>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById("ts").textContent = new Date().toISOString();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
|
||||
SHA=$(sha256sum "$FILE" | cut -d' ' -f1)
|
||||
EXP=$(date -d '+1 hour' +%s)
|
||||
EVENT=$(nak event -k 24242 -c "" --sec "$PRIVKEY" -t "t=upload" -t "x=$SHA" -t "expiration=$EXP")
|
||||
AUTH="Nostr $(echo -n "$EVENT" | base64 -w 0)"
|
||||
RESP=$(mktemp)
|
||||
|
||||
STATUS=$(curl -s -w "%{http_code}" -o "$RESP" \
|
||||
-X PUT \
|
||||
-H "Authorization: $AUTH" \
|
||||
-H "Content-Type: text/html" \
|
||||
--data-binary "@$FILE" \
|
||||
"$BASE_URL/upload")
|
||||
|
||||
URL=$(jq -r '.url // empty' "$RESP")
|
||||
GET_CODE=0
|
||||
if [ -n "$URL" ]; then
|
||||
GET_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$URL")
|
||||
fi
|
||||
|
||||
echo "UPLOAD_STATUS=$STATUS"
|
||||
echo "SHA256=$SHA"
|
||||
echo "URL=$URL"
|
||||
echo "GET_CODE=$GET_CODE"
|
||||
Executable
+128
@@ -0,0 +1,128 @@
|
||||
#!/bin/bash
|
||||
|
||||
# upload_html_test.sh - Test script for uploading HTML to production Ginxsom Blossom server
|
||||
# Tests upload functionality on blossom.laantungir.net
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
# Configuration
|
||||
SERVER_URL="https://blossom.laantungir.net"
|
||||
UPLOAD_ENDPOINT="${SERVER_URL}/upload"
|
||||
TEST_FILE="tests/test_upload.html"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Helper functions
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
# Calculate file hash
|
||||
calculate_hash() {
|
||||
log_info "Calculating SHA-256 hash..."
|
||||
HASH=$(sha256sum "${TEST_FILE}" | cut -d' ' -f1)
|
||||
log_success "File hash: ${HASH}"
|
||||
}
|
||||
|
||||
# Generate nostr event
|
||||
generate_nostr_event() {
|
||||
log_info "Generating kind 24242 nostr event..."
|
||||
EXPIRATION=$(date -d '+1 hour' +%s)
|
||||
EVENT_JSON=$(nak event -k 24242 -c "" \
|
||||
--sec "22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd" \
|
||||
-t "t=upload" \
|
||||
-t "x=${HASH}" \
|
||||
-t "expiration=${EXPIRATION}")
|
||||
|
||||
if [[ -z "$EVENT_JSON" ]]; then
|
||||
log_error "Failed to generate nostr event"
|
||||
exit 1
|
||||
fi
|
||||
log_success "Generated nostr event"
|
||||
}
|
||||
|
||||
# Create authorization header
|
||||
create_auth_header() {
|
||||
log_info "Creating authorization header..."
|
||||
AUTH_B64=$(echo -n "$EVENT_JSON" | base64 -w 0)
|
||||
AUTH_HEADER="Nostr ${AUTH_B64}"
|
||||
log_success "Created authorization header"
|
||||
}
|
||||
|
||||
# Perform upload
|
||||
perform_upload() {
|
||||
log_info "Performing upload to ${UPLOAD_ENDPOINT}..."
|
||||
RESPONSE_FILE=$(mktemp)
|
||||
|
||||
HTTP_STATUS=$(curl -s -w "%{http_code}" \
|
||||
-X PUT \
|
||||
-H "Authorization: ${AUTH_HEADER}" \
|
||||
-H "Content-Type: text/html" \
|
||||
--data-binary "@${TEST_FILE}" \
|
||||
"${UPLOAD_ENDPOINT}" \
|
||||
-o "${RESPONSE_FILE}")
|
||||
|
||||
echo "HTTP Status: ${HTTP_STATUS}"
|
||||
echo "Response body:"
|
||||
cat "${RESPONSE_FILE}"
|
||||
echo
|
||||
rm -f "${RESPONSE_FILE}"
|
||||
|
||||
if [[ "${HTTP_STATUS}" == "200" || "${HTTP_STATUS}" == "201" ]]; then
|
||||
log_success "Upload successful!"
|
||||
else
|
||||
log_error "Upload failed with HTTP status: ${HTTP_STATUS}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test file retrieval
|
||||
test_retrieval() {
|
||||
log_info "Testing file retrieval..."
|
||||
|
||||
# Try with .html extension
|
||||
RETRIEVAL_URL_EXT="${SERVER_URL}/${HASH}.html"
|
||||
log_info "Trying retrieval with extension: ${RETRIEVAL_URL_EXT}"
|
||||
if curl -s -f -I "${RETRIEVAL_URL_EXT}" > /dev/null 2>&1; then
|
||||
log_success "File found with .html extension!"
|
||||
else
|
||||
log_warning "File NOT found with .html extension"
|
||||
fi
|
||||
|
||||
# Try without extension
|
||||
RETRIEVAL_URL="${SERVER_URL}/${HASH}"
|
||||
log_info "Trying retrieval without extension: ${RETRIEVAL_URL}"
|
||||
if curl -s -f -I "${RETRIEVAL_URL}" > /dev/null 2>&1; then
|
||||
log_success "File found without extension!"
|
||||
else
|
||||
log_warning "File NOT found without extension"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
calculate_hash
|
||||
generate_nostr_event
|
||||
create_auth_header
|
||||
perform_upload
|
||||
test_retrieval
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user