Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1ea256076 | ||
|
|
09654fa557 | ||
|
|
469d2180c1 | ||
|
|
fa5f5e60cb | ||
|
|
015094949e | ||
|
|
9491f5c373 | ||
|
|
485e351998 | ||
|
|
66ac92b4a0 | ||
|
|
ba8b51d1c6 | ||
|
|
b9e9a08e9e | ||
|
|
d400744f68 | ||
|
|
4b12cb19dc | ||
|
|
e5b1556a80 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -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.
|
||||
@@ -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 && \
|
||||
|
||||
0
Trash/[builder
Normal file
0
Trash/[builder
Normal file
155
api/index.css
155
api/index.css
@@ -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
api/index.js
473
api/index.js
@@ -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.
BIN
build/bud04.o
BIN
build/bud04.o
Binary file not shown.
BIN
build/bud06.o
BIN
build/bud06.o
Binary file not shown.
BIN
build/bud08.o
BIN
build/bud08.o
Binary file not shown.
BIN
build/bud09.o
BIN
build/bud09.o
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
build/ginxsom-fcgi_static_arm64
Executable file
BIN
build/ginxsom-fcgi_static_arm64
Executable file
Binary file not shown.
BIN
build/ginxsom-fcgi_static_x86_64
Executable file
BIN
build/ginxsom-fcgi_static_x86_64
Executable file
Binary file not shown.
BIN
build/main.o
BIN
build/main.o
Binary file not shown.
Binary file not shown.
Binary file not shown.
110
build_static.sh
110
build_static.sh
@@ -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 ""
|
||||
|
||||
@@ -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.
@@ -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/"
|
||||
|
||||
103
deploy_lt.sh
103
deploy_lt.sh
@@ -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 ""
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
149
plans/fix-ginxsom-cpu-spin.md
Normal file
149
plans/fix-ginxsom-cpu-spin.md
Normal file
@@ -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
|
||||
142
plans/unified-logging-migration.md
Normal file
142
plans/unified-logging-migration.md
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
175
src/admin_api.c
175
src/admin_api.c
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
24
src/app_log.h
Normal file
24
src/app_log.h
Normal file
@@ -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
|
||||
71
src/bud08.c
71
src/bud08.c
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
896
src/main.c
896
src/main.c
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
5
tests/local_webtype_test/test_html.html
Normal file
5
tests/local_webtype_test/test_html.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>test_html</title>
|
||||
/* payload type: text/html */
|
||||
console.log("test_html");
|
||||
266
tests/production_file_put_html_tmp.sh
Executable file
266
tests/production_file_put_html_tmp.sh
Executable file
@@ -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 "$@"
|
||||
11
tests/test_upload.html
Normal file
11
tests/test_upload.html
Normal file
@@ -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>
|
||||
57
tests/upload_browser_sample_html.sh
Normal file
57
tests/upload_browser_sample_html.sh
Normal file
@@ -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"
|
||||
128
tests/upload_html_test.sh
Executable file
128
tests/upload_html_test.sh
Executable file
@@ -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