v0.1.37 - Use static-only build flow in increment_and_push and include blob browser sorting/delete deployment fix
This commit is contained in:
@@ -31,6 +31,7 @@ RUN apk add --no-cache \
|
|||||||
linux-headers \
|
linux-headers \
|
||||||
wget \
|
wget \
|
||||||
bash \
|
bash \
|
||||||
|
openssh-client \
|
||||||
nghttp2-dev \
|
nghttp2-dev \
|
||||||
nghttp2-static \
|
nghttp2-static \
|
||||||
c-ares-dev \
|
c-ares-dev \
|
||||||
@@ -58,17 +59,14 @@ RUN cd /tmp && \
|
|||||||
make install && \
|
make install && \
|
||||||
rm -rf /tmp/secp256k1
|
rm -rf /tmp/secp256k1
|
||||||
|
|
||||||
# Copy only submodule configuration and git directory
|
# Fetch nostr_core_lib source directly over HTTPS.
|
||||||
COPY .gitmodules /build/.gitmodules
|
# This avoids relying on local git metadata/submodule state in the Docker build context.
|
||||||
COPY .git /build/.git
|
ARG NOSTR_CORE_LIB_REF=2e7eacc02eef0ccf261f655330dc6a43d6cebdfb
|
||||||
|
RUN git clone https://git.laantungir.net/laantungir/nostr_core_lib.git /build/nostr_core_lib && \
|
||||||
|
cd /build/nostr_core_lib && \
|
||||||
|
git checkout "$NOSTR_CORE_LIB_REF"
|
||||||
|
|
||||||
# Initialize submodules (cached unless .gitmodules changes)
|
# Build nostr_core_lib with required NIPs (cached unless submodule changes)
|
||||||
RUN git submodule update --init --recursive
|
|
||||||
|
|
||||||
# Copy nostr_core_lib source files (cached unless nostr_core_lib changes)
|
|
||||||
COPY nostr_core_lib /build/nostr_core_lib/
|
|
||||||
|
|
||||||
# Build nostr_core_lib with required NIPs (cached unless nostr_core_lib changes)
|
|
||||||
# Disable fortification in build.sh to prevent __*_chk symbol issues
|
# 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)
|
# NIPs: 001(Basic), 006(Keys), 013(PoW), 017(DMs), 019(Bech32), 042(Auth), 044(Encryption), 059(Gift Wrap)
|
||||||
RUN cd nostr_core_lib && \
|
RUN cd nostr_core_lib && \
|
||||||
@@ -82,17 +80,14 @@ RUN cd nostr_core_lib && \
|
|||||||
COPY api/ /build/api/
|
COPY api/ /build/api/
|
||||||
COPY scripts/embed_web_files.sh /build/scripts/
|
COPY scripts/embed_web_files.sh /build/scripts/
|
||||||
|
|
||||||
# Create src directory and embed web files into C headers
|
# Copy Ginxsom source files
|
||||||
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 src/ /build/src/
|
COPY src/ /build/src/
|
||||||
COPY include/ /build/include/
|
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)
|
# Build Ginxsom with full static linking (only rebuilds when src/ changes)
|
||||||
# Disable fortification to avoid __*_chk symbols that don't exist in MUSL
|
# Disable fortification to avoid __*_chk symbols that don't exist in MUSL
|
||||||
# Use conditional compilation flags based on DEBUG_BUILD argument
|
# Use conditional compilation flags based on DEBUG_BUILD argument
|
||||||
|
|||||||
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;
|
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">
|
<nav class="side-nav" id="side-nav">
|
||||||
<ul class="nav-menu">
|
<ul class="nav-menu">
|
||||||
<li><button class="nav-item" data-page="statistics">Statistics</button></li>
|
<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="configuration">Configuration</button></li>
|
||||||
<li><button class="nav-item" data-page="authorization">Authorization</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>
|
<li><button class="nav-item" data-page="relay-events">Blossom Events</button></li>
|
||||||
@@ -372,6 +373,57 @@ AUTH RULES MANAGEMENT
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- SQL QUERY Section -->
|
||||||
<div class="section" id="sqlQuerySection" style="display: none;">
|
<div class="section" id="sqlQuerySection" style="display: none;">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
|
|||||||
473
api/index.js
473
api/index.js
@@ -39,6 +39,20 @@ let pendingSqlQueries = new Map();
|
|||||||
let eventRateChart = null;
|
let eventRateChart = null;
|
||||||
let previousTotalBlobs = 0; // Track previous total for rate calculation
|
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
|
// Relay Events state - now handled by main subscription
|
||||||
|
|
||||||
// DOM elements
|
// DOM elements
|
||||||
@@ -3405,6 +3419,457 @@ function formatTimestamp(timestamp) {
|
|||||||
return date.toLocaleString();
|
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
|
// Update statistics cell with flash animation if value changed
|
||||||
function updateStatsCell(cellId, newValue) {
|
function updateStatsCell(cellId, newValue) {
|
||||||
const cell = document.getElementById(cellId);
|
const cell = document.getElementById(cellId);
|
||||||
@@ -3698,6 +4163,7 @@ function switchPage(pageName) {
|
|||||||
// Hide all sections
|
// Hide all sections
|
||||||
const sections = [
|
const sections = [
|
||||||
'databaseStatisticsSection',
|
'databaseStatisticsSection',
|
||||||
|
'blobBrowserSection',
|
||||||
'subscriptionDetailsSection',
|
'subscriptionDetailsSection',
|
||||||
'div_config',
|
'div_config',
|
||||||
'authRulesSection',
|
'authRulesSection',
|
||||||
@@ -3716,6 +4182,7 @@ function switchPage(pageName) {
|
|||||||
// Show selected section
|
// Show selected section
|
||||||
const pageMap = {
|
const pageMap = {
|
||||||
'statistics': 'databaseStatisticsSection',
|
'statistics': 'databaseStatisticsSection',
|
||||||
|
'blob-browser': 'blobBrowserSection',
|
||||||
'subscriptions': 'subscriptionDetailsSection',
|
'subscriptions': 'subscriptionDetailsSection',
|
||||||
'configuration': 'div_config',
|
'configuration': 'div_config',
|
||||||
'authorization': 'authRulesSection',
|
'authorization': 'authRulesSection',
|
||||||
@@ -3744,6 +4211,12 @@ function switchPage(pageName) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Blob browser page setup
|
||||||
|
if (pageName === 'blob-browser') {
|
||||||
|
initializeBlobBrowserControls();
|
||||||
|
fetchBlobBrowserData(false);
|
||||||
|
}
|
||||||
|
|
||||||
// Close side navigation
|
// Close side navigation
|
||||||
closeSideNav();
|
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_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.
103
deploy_lt.sh
103
deploy_lt.sh
@@ -199,56 +199,43 @@ else
|
|||||||
fi
|
fi
|
||||||
echo ""
|
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..."
|
print_status "Step 8: Starting ginxsom service..."
|
||||||
ssh $REMOTE_USER@$REMOTE_HOST << EOF
|
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
echo "Starting ginxsom FastCGI with configuration:"
|
echo "Restarting systemd service..."
|
||||||
echo " Binary: $REMOTE_BINARY_PATH"
|
sudo systemctl restart ginxsom.service
|
||||||
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
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
# Verify process is running
|
if sudo systemctl is-active --quiet ginxsom.service; then
|
||||||
if [ -S $REMOTE_SOCKET ]; then
|
echo "Service is active"
|
||||||
echo "FastCGI socket created successfully"
|
|
||||||
ls -la $REMOTE_SOCKET
|
|
||||||
else
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if process is running
|
# Verify socket is listening
|
||||||
if pgrep -f ginxsom-fcgi > /dev/null; then
|
if [ -S /tmp/ginxsom-fcgi.sock ]; then
|
||||||
echo "Process is running (PID: \$(pgrep -f ginxsom-fcgi))"
|
echo "FastCGI socket created successfully"
|
||||||
|
ls -la /tmp/ginxsom-fcgi.sock
|
||||||
else
|
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
|
fi
|
||||||
|
|
||||||
|
# Show process list for diagnostics
|
||||||
|
pgrep -af ginxsom-fcgi || true
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
print_success "FastCGI process started"
|
print_success "FastCGI service started"
|
||||||
else
|
else
|
||||||
print_error "Failed to start FastCGI process"
|
print_error "Failed to start FastCGI service"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
@@ -282,13 +269,42 @@ echo ""
|
|||||||
# Wait a moment for service to fully start
|
# Wait a moment for service to fully start
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
|
VERIFY_FAILED=false
|
||||||
|
|
||||||
# Test health endpoint
|
# Test health endpoint
|
||||||
echo "Testing health endpoint..."
|
echo "Testing health endpoint..."
|
||||||
if curl -k -s --max-time 10 "https://blossom.laantungir.net/health" | grep -q "OK"; then
|
if curl -k -s --max-time 10 "https://blossom.laantungir.net/health" | grep -q "OK"; then
|
||||||
print_success "✓ Health check passed"
|
print_success "✓ Health check passed"
|
||||||
else
|
else
|
||||||
print_warning "✗ Health check failed - checking response..."
|
print_error "✗ Health check failed"
|
||||||
curl -k -v --max-time 10 "https://blossom.laantungir.net/health" 2>&1 | head -10
|
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
|
fi
|
||||||
|
|
||||||
# Test root endpoint
|
# Test root endpoint
|
||||||
@@ -300,6 +316,11 @@ else
|
|||||||
print_warning "✗ Root endpoint not responding as expected"
|
print_warning "✗ Root endpoint not responding as expected"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$VERIFY_FAILED" = true ]; then
|
||||||
|
print_error "Deployment verification failed - one or more critical endpoints are unhealthy"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
print_status "=========================================="
|
print_status "=========================================="
|
||||||
print_success "Deployment completed!"
|
print_success "Deployment completed!"
|
||||||
@@ -314,6 +335,8 @@ echo " Socket: $REMOTE_SOCKET"
|
|||||||
echo ""
|
echo ""
|
||||||
print_status "Test Commands:"
|
print_status "Test Commands:"
|
||||||
echo " Health: curl -k https://blossom.laantungir.net/health"
|
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 " Info: curl -k https://blossom.laantungir.net/"
|
||||||
echo " Upload: ./tests/file_put_bud02.sh"
|
echo " Upload: ./tests/file_put_bud02.sh"
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -158,51 +158,43 @@ update_version_in_header() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to compile the Ginxsom project
|
# Function to build the static Ginxsom binary
|
||||||
compile_project() {
|
compile_project() {
|
||||||
print_status "Compiling Ginxsom FastCGI server..."
|
print_status "Building static Ginxsom FastCGI binary via build_static.sh..."
|
||||||
|
|
||||||
# Clean previous build
|
if [[ ! -x "./build_static.sh" ]]; then
|
||||||
if make clean > /dev/null 2>&1; then
|
print_error "build_static.sh not found or not executable"
|
||||||
print_success "Cleaned previous build"
|
exit 1
|
||||||
else
|
|
||||||
print_warning "Clean failed or no Makefile found"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Compile the project
|
if ./build_static.sh > /dev/null 2>&1; then
|
||||||
if make > /dev/null 2>&1; then
|
print_success "Static build completed successfully"
|
||||||
print_success "Ginxsom compiled successfully"
|
|
||||||
|
|
||||||
# Verify the binary was created
|
# Verify the static binary was created
|
||||||
if [[ -f "build/ginxsom-fcgi" ]]; then
|
if [[ -f "build/ginxsom-fcgi_static_x86_64" || -f "build/ginxsom-fcgi_static_arm64" ]]; then
|
||||||
print_success "Binary created: build/ginxsom-fcgi"
|
print_success "Static binary created in build/"
|
||||||
else
|
else
|
||||||
print_error "Binary not found after compilation"
|
print_error "Static binary not found after build_static.sh"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
print_error "Compilation failed"
|
print_error "Static build failed"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to build release binary
|
# Function to build release binary
|
||||||
build_release_binary() {
|
build_release_binary() {
|
||||||
print_status "Building release binary..."
|
print_status "Preparing release binary from static build output..."
|
||||||
|
|
||||||
# Build the FastCGI server
|
if [[ -f "build/ginxsom-fcgi_static_x86_64" ]]; then
|
||||||
print_status "Building Ginxsom FastCGI server..."
|
cp build/ginxsom-fcgi_static_x86_64 ginxsom-fcgi-linux-x86_64
|
||||||
make clean > /dev/null 2>&1
|
print_success "Release binary created: ginxsom-fcgi-linux-x86_64"
|
||||||
if make > /dev/null 2>&1; then
|
elif [[ -f "build/ginxsom-fcgi_static_arm64" ]]; then
|
||||||
if [[ -f "build/ginxsom-fcgi" ]]; then
|
cp build/ginxsom-fcgi_static_arm64 ginxsom-fcgi-linux-arm64
|
||||||
cp build/ginxsom-fcgi ginxsom-fcgi-linux-x86_64
|
print_success "Release binary created: ginxsom-fcgi-linux-arm64"
|
||||||
print_success "Release binary created: ginxsom-fcgi-linux-x86_64"
|
|
||||||
else
|
|
||||||
print_error "Binary not found after compilation"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
print_error "Build failed"
|
print_error "No static build artifact found in build/"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -385,6 +377,17 @@ upload_release_binary() {
|
|||||||
print_warning "Failed to upload FastCGI binary"
|
print_warning "Failed to upload FastCGI binary"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -f "ginxsom-fcgi-linux-arm64" ]]; then
|
||||||
|
print_status "Uploading Ginxsom ARM64 FastCGI binary..."
|
||||||
|
if curl -s -X POST "$GITEA_REPO_URL/releases/$release_id/assets" \
|
||||||
|
-H "Authorization: token $token" \
|
||||||
|
-F "attachment=@ginxsom-fcgi-linux-arm64;filename=ginxsom-fcgi-${NEW_VERSION}-linux-arm64" > /dev/null; then
|
||||||
|
print_success "Uploaded ARM64 FastCGI binary"
|
||||||
|
else
|
||||||
|
print_warning "Failed to upload ARM64 FastCGI binary"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to clean up release binary
|
# Function to clean up release binary
|
||||||
@@ -393,6 +396,11 @@ cleanup_release_binary() {
|
|||||||
rm -f ginxsom-fcgi-linux-x86_64
|
rm -f ginxsom-fcgi-linux-x86_64
|
||||||
print_status "Cleaned up release binary"
|
print_status "Cleaned up release binary"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -f "ginxsom-fcgi-linux-arm64" ]]; then
|
||||||
|
rm -f ginxsom-fcgi-linux-arm64
|
||||||
|
print_status "Cleaned up ARM64 release binary"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Main execution
|
# Main execution
|
||||||
|
|||||||
@@ -63,9 +63,15 @@ for file in "${FILES[@]}"; do
|
|||||||
# Convert file to C byte array
|
# Convert file to C byte array
|
||||||
echo "static const unsigned char embedded_${varname}[] = {" >> "${OUTPUT_FILE}"
|
echo "static const unsigned char embedded_${varname}[] = {" >> "${OUTPUT_FILE}"
|
||||||
|
|
||||||
# Use xxd to convert to hex, then format as C array
|
# Use xxd when available, otherwise fall back to od
|
||||||
xxd -i < "${filepath}" >> "${OUTPUT_FILE}"
|
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 "};" >> "${OUTPUT_FILE}"
|
||||||
echo "static const size_t embedded_${varname}_size = sizeof(embedded_${varname});" >> "${OUTPUT_FILE}"
|
echo "static const size_t embedded_${varname}_size = sizeof(embedded_${varname});" >> "${OUTPUT_FILE}"
|
||||||
done
|
done
|
||||||
|
|||||||
151
src/admin_api.c
151
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 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 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 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)
|
// Local utility functions (from main.c but implemented here for admin API)
|
||||||
static int admin_nip94_get_origin(char* out, size_t out_size) {
|
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;
|
int rc;
|
||||||
|
|
||||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
|
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
|
||||||
if (rc) {
|
if (rc == SQLITE_OK) {
|
||||||
// Default on DB error
|
const char* sql = "SELECT value FROM config WHERE key = 'cdn_origin'";
|
||||||
strncpy(out, "http://localhost:9001", out_size - 1);
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||||
out[out_size - 1] = '\0';
|
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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* sql = "SELECT value FROM config WHERE key = 'cdn_origin'";
|
// Final fallback
|
||||||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
strncpy(out, "https://blossom.laantungir.net", out_size - 1);
|
||||||
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);
|
|
||||||
out[out_size - 1] = '\0';
|
out[out_size - 1] = '\0';
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -93,6 +105,22 @@ static const char* admin_mime_to_extension(const char* mime_type) {
|
|||||||
return mime_to_extension(mime_type);
|
return mime_to_extension(mime_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int validate_hex64_format(const char* hash) {
|
||||||
|
if (!hash || strlen(hash) != 64) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < 64; i++) {
|
||||||
|
if (!((hash[i] >= '0' && hash[i] <= '9') ||
|
||||||
|
(hash[i] >= 'a' && hash[i] <= 'f') ||
|
||||||
|
(hash[i] >= 'A' && hash[i] <= 'F'))) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Main API request handler
|
// Main API request handler
|
||||||
void handle_admin_api_request(const char* method, const char* uri, const char* validated_pubkey, int is_authenticated) {
|
void handle_admin_api_request(const char* method, const char* uri, const char* validated_pubkey, int is_authenticated) {
|
||||||
const char* path = uri + 4; // Skip "/api"
|
const char* path = uri + 4; // Skip "/api"
|
||||||
@@ -107,7 +135,8 @@ void handle_admin_api_request(const char* method, const char* uri, const char* v
|
|||||||
// Health endpoint and POST /admin (Kind 23456 events) are exempt from authentication requirement
|
// Health endpoint and POST /admin (Kind 23456 events) are exempt from authentication requirement
|
||||||
// Kind 23456 events authenticate themselves via signed event validation
|
// Kind 23456 events authenticate themselves via signed event validation
|
||||||
int skip_auth = (strcmp(path, "/health") == 0) ||
|
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 (!skip_auth) {
|
||||||
if (!is_authenticated || !validated_pubkey) {
|
if (!is_authenticated || !validated_pubkey) {
|
||||||
@@ -577,14 +606,15 @@ void handle_config_key_put_api(const char* key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void handle_files_api(void) {
|
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");
|
const char* query_string = getenv("QUERY_STRING");
|
||||||
int limit = 50;
|
int limit = 50;
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
|
char pubkey_filter[65] = {0};
|
||||||
|
|
||||||
if (query_string) {
|
if (query_string) {
|
||||||
char params[10][256];
|
char params[12][256];
|
||||||
int param_count = parse_query_params(query_string, params, 10);
|
int param_count = parse_query_params(query_string, params, 12);
|
||||||
|
|
||||||
for (int i = 0; i < param_count; i++) {
|
for (int i = 0; i < param_count; i++) {
|
||||||
char* key = params[i];
|
char* key = params[i];
|
||||||
@@ -597,6 +627,12 @@ void handle_files_api(void) {
|
|||||||
} else if (strcmp(key, "offset") == 0) {
|
} else if (strcmp(key, "offset") == 0) {
|
||||||
offset = atoi(value);
|
offset = atoi(value);
|
||||||
if (offset < 0) offset = 0;
|
if (offset < 0) offset = 0;
|
||||||
|
} else if (strcmp(key, "pubkey") == 0) {
|
||||||
|
strncpy(pubkey_filter, value, sizeof(pubkey_filter) - 1);
|
||||||
|
pubkey_filter[sizeof(pubkey_filter) - 1] = '\0';
|
||||||
|
if (!validate_hex64_format(pubkey_filter)) {
|
||||||
|
pubkey_filter[0] = '\0';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -612,9 +648,12 @@ void handle_files_api(void) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query recent files with pagination
|
// Query recent files with pagination and optional uploader filter
|
||||||
const char* sql = "SELECT sha256, size, type, uploaded_at, uploader_pubkey, filename "
|
const char* sql = (pubkey_filter[0] != '\0')
|
||||||
"FROM blobs ORDER BY uploaded_at DESC LIMIT ? OFFSET ?";
|
? "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);
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||||
if (rc != SQLITE_OK) {
|
if (rc != SQLITE_OK) {
|
||||||
@@ -623,23 +662,38 @@ void handle_files_api(void) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlite3_bind_int(stmt, 1, limit);
|
int bind_index = 1;
|
||||||
sqlite3_bind_int(stmt, 2, offset);
|
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* response = cJSON_CreateObject();
|
||||||
cJSON* data = cJSON_CreateObject();
|
cJSON* data = cJSON_CreateObject();
|
||||||
cJSON* files_array = cJSON_CreateArray();
|
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_AddStringToObject(response, "status", "success");
|
||||||
cJSON_AddItemToObject(response, "data", data);
|
cJSON_AddItemToObject(response, "data", data);
|
||||||
cJSON_AddItemToObject(data, "files", files_array);
|
cJSON_AddItemToObject(data, "files", files_array);
|
||||||
cJSON_AddNumberToObject(data, "limit", limit);
|
cJSON_AddNumberToObject(data, "limit", limit);
|
||||||
cJSON_AddNumberToObject(data, "offset", offset);
|
cJSON_AddNumberToObject(data, "offset", offset);
|
||||||
|
|
||||||
int total_count = 0;
|
|
||||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||||
total_count++;
|
|
||||||
|
|
||||||
cJSON* file_obj = cJSON_CreateObject();
|
cJSON* file_obj = cJSON_CreateObject();
|
||||||
|
if (!file_obj) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
cJSON_AddItemToArray(files_array, file_obj);
|
cJSON_AddItemToArray(files_array, file_obj);
|
||||||
|
|
||||||
const char* sha256 = (const char*)sqlite3_column_text(stmt, 0);
|
const char* sha256 = (const char*)sqlite3_column_text(stmt, 0);
|
||||||
@@ -669,12 +723,18 @@ void handle_files_api(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get total count for pagination info
|
// Get total count for pagination info (respecting optional pubkey filter)
|
||||||
const char* count_sql = "SELECT COUNT(*) FROM blobs";
|
const char* count_sql = (pubkey_filter[0] != '\0')
|
||||||
|
? "SELECT COUNT(*) FROM blobs WHERE uploader_pubkey = ?"
|
||||||
|
: "SELECT COUNT(*) FROM blobs";
|
||||||
sqlite3_stmt* count_stmt;
|
sqlite3_stmt* count_stmt;
|
||||||
|
|
||||||
rc = sqlite3_prepare_v2(db, count_sql, -1, &count_stmt, NULL);
|
rc = sqlite3_prepare_v2(db, count_sql, -1, &count_stmt, NULL);
|
||||||
if (rc == SQLITE_OK) {
|
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);
|
rc = sqlite3_step(count_stmt);
|
||||||
if (rc == SQLITE_ROW) {
|
if (rc == SQLITE_ROW) {
|
||||||
int total = sqlite3_column_int(count_stmt, 0);
|
int total = sqlite3_column_int(count_stmt, 0);
|
||||||
@@ -683,6 +743,10 @@ void handle_files_api(void) {
|
|||||||
sqlite3_finalize(count_stmt);
|
sqlite3_finalize(count_stmt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pubkey_filter[0] != '\0') {
|
||||||
|
cJSON_AddStringToObject(data, "pubkey_filter", pubkey_filter);
|
||||||
|
}
|
||||||
|
|
||||||
sqlite3_finalize(stmt);
|
sqlite3_finalize(stmt);
|
||||||
sqlite3_close(db);
|
sqlite3_close(db);
|
||||||
|
|
||||||
@@ -785,15 +849,14 @@ int parse_query_params(const char* query_string, char params[][256], int max_par
|
|||||||
memcpy(query_copy, query_string, query_len + 1);
|
memcpy(query_copy, query_string, query_len + 1);
|
||||||
|
|
||||||
int count = 0;
|
int count = 0;
|
||||||
char* token = strtok(query_copy, "&");
|
char* saveptr = NULL;
|
||||||
|
char* token = strtok_r(query_copy, "&", &saveptr);
|
||||||
|
|
||||||
while (token && count < max_params) {
|
while (token && count < max_params) {
|
||||||
if (strlen(token) >= sizeof(params[0])) {
|
strncpy(params[count], token, sizeof(params[0]) - 1);
|
||||||
token[sizeof(params[0]) - 1] = '\0';
|
params[count][sizeof(params[0]) - 1] = '\0';
|
||||||
}
|
|
||||||
strcpy(params[count], token);
|
|
||||||
count++;
|
count++;
|
||||||
token = strtok(NULL, "&");
|
token = strtok_r(NULL, "&", &saveptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
free(query_copy);
|
free(query_copy);
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include "ginxsom.h"
|
||||||
|
|
||||||
// Global state
|
// Global state
|
||||||
static struct {
|
static struct {
|
||||||
@@ -128,6 +131,9 @@ cJSON* admin_commands_process(cJSON* command_array, const char* request_event_id
|
|||||||
else if (strcmp(command, "blob_list") == 0) {
|
else if (strcmp(command, "blob_list") == 0) {
|
||||||
return admin_cmd_blob_list(command_array);
|
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) {
|
else if (strcmp(command, "storage_stats") == 0) {
|
||||||
return admin_cmd_storage_stats(command_array);
|
return admin_cmd_storage_stats(command_array);
|
||||||
}
|
}
|
||||||
@@ -585,6 +591,142 @@ cJSON* admin_cmd_blob_list(cJSON* args) {
|
|||||||
return response;
|
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) {
|
cJSON* admin_cmd_storage_stats(cJSON* args) {
|
||||||
(void)args;
|
(void)args;
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ cJSON* admin_cmd_config_update(cJSON* args);
|
|||||||
cJSON* admin_cmd_stats_query(cJSON* args);
|
cJSON* admin_cmd_stats_query(cJSON* args);
|
||||||
cJSON* admin_cmd_system_status(cJSON* args);
|
cJSON* admin_cmd_system_status(cJSON* args);
|
||||||
cJSON* admin_cmd_blob_list(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_storage_stats(cJSON* args);
|
||||||
cJSON* admin_cmd_sql_query(cJSON* args);
|
cJSON* admin_cmd_sql_query(cJSON* args);
|
||||||
cJSON* admin_cmd_query_view(cJSON* args);
|
cJSON* admin_cmd_query_view(cJSON* args);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -10,8 +10,8 @@
|
|||||||
// Version information (auto-updated by build system)
|
// Version information (auto-updated by build system)
|
||||||
#define VERSION_MAJOR 0
|
#define VERSION_MAJOR 0
|
||||||
#define VERSION_MINOR 1
|
#define VERSION_MINOR 1
|
||||||
#define VERSION_PATCH 33
|
#define VERSION_PATCH 37
|
||||||
#define VERSION "v0.1.33"
|
#define VERSION "v0.1.37"
|
||||||
|
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ int initialize_database(const char *db_path) {
|
|||||||
" ('nip42_require_auth', 'false', 'Enable NIP-42 challenge/response authentication'),"
|
" ('nip42_require_auth', 'false', 'Enable NIP-42 challenge/response authentication'),"
|
||||||
" ('nip42_challenge_timeout', '600', 'NIP-42 challenge timeout in seconds'),"
|
" ('nip42_challenge_timeout', '600', 'NIP-42 challenge timeout in seconds'),"
|
||||||
" ('nip42_time_tolerance', '300', 'NIP-42 timestamp tolerance in seconds'),"
|
" ('nip42_time_tolerance', '300', 'NIP-42 timestamp tolerance in seconds'),"
|
||||||
" ('enable_relay_connect', 'true', 'Enable connection to Nostr relays'),"
|
" ('enable_relay_connect', 'false', 'Enable connection to Nostr relays'),"
|
||||||
" ('kind_0_content', '{\"name\":\"Ginxsom Blossom Server\",\"about\":\"A Nostr-enabled Blossom media server\",\"picture\":\"\"}', 'JSON content for Kind 0 profile event'),"
|
" ('kind_0_content', '{\"name\":\"Ginxsom Blossom Server\",\"about\":\"A Nostr-enabled Blossom media server\",\"picture\":\"\"}', 'JSON content for Kind 0 profile event'),"
|
||||||
" ('kind_10002_tags', '[\"wss://relay.laantungir.net\"]', 'JSON array of relay URLs for Kind 10002');";
|
" ('kind_10002_tags', '[\"wss://relay.laantungir.net\"]', 'JSON array of relay URLs for Kind 10002');";
|
||||||
|
|
||||||
|
|||||||
@@ -263,14 +263,30 @@ int relay_client_start(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app_log(LOG_INFO, "Starting relay client...");
|
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
|
// Start management thread
|
||||||
g_relay_state.running = 1;
|
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");
|
app_log(LOG_ERROR, "Failed to create relay management thread");
|
||||||
g_relay_state.running = 0;
|
g_relay_state.running = 0;
|
||||||
|
pthread_attr_destroy(&thread_attr);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pthread_attr_destroy(&thread_attr);
|
||||||
|
|
||||||
app_log(LOG_INFO, "Relay client started successfully");
|
app_log(LOG_INFO, "Relay client started successfully");
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user