viewer: stream artifacts, skip unchanged live refresh, debounce search
This commit is contained in:
1 parent
ac2d32db59
commit
d74bd7ce56
1 file changed
+32
-7
@@ -7,6 +7,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
@@ -102,13 +103,19 @@ function select(i){selected=i;
|
|||||||
jsonBox.textContent=JSON.stringify(e,null,2);
|
jsonBox.textContent=JSON.stringify(e,null,2);
|
||||||
if(e.evidence&&e.evidence.sha256)loadPayload(e.evidence.sha256);
|
if(e.evidence&&e.evidence.sha256)loadPayload(e.evidence.sha256);
|
||||||
else{payloadBytes=null;art.innerHTML='<h2>Payload</h2><pre>no payload in this event</pre>';}}
|
else{payloadBytes=null;art.innerHTML='<h2>Payload</h2><pre>no payload in this event</pre>';}}
|
||||||
|
let lastBytes=-1,lastKey='';
|
||||||
async function load(){
|
async function load(){
|
||||||
const p=new URLSearchParams({q:document.getElementById('q').value,event:document.getElementById('event').value,
|
const p=new URLSearchParams({q:document.getElementById('q').value,event:document.getElementById('event').value,
|
||||||
peer:document.getElementById('peer').value,payload:document.getElementById('payload').checked?'1':'0',
|
peer:document.getElementById('peer').value,payload:document.getElementById('payload').checked?'1':'0',
|
||||||
hide:document.getElementById('hide').checked?'1':'0',
|
hide:document.getElementById('hide').checked?'1':'0',
|
||||||
size:document.getElementById('size').value,page:page});
|
size:document.getElementById('size').value,page:page});
|
||||||
|
const key=p.toString();
|
||||||
const r=await fetch('/api/events?'+p);const d=await r.json();
|
const r=await fetch('/api/events?'+p);const d=await r.json();
|
||||||
current=d.events;lastScanned=d.scanned;lastHidden=d.hidden;hasMore=d.has_more;render(current);
|
lastScanned=d.scanned;lastHidden=d.hidden;hasMore=d.has_more;
|
||||||
|
// Skip re-render when nothing changed on the live page (cheap 3s refresh).
|
||||||
|
if(page===0 && d.journal_bytes===lastBytes && key===lastKey){return;}
|
||||||
|
lastBytes=d.journal_bytes;lastKey=key;
|
||||||
|
current=d.events;render(current);
|
||||||
document.getElementById('page').textContent='page '+(page+1);
|
document.getElementById('page').textContent='page '+(page+1);
|
||||||
document.getElementById('prev').disabled=page<=0;
|
document.getElementById('prev').disabled=page<=0;
|
||||||
document.getElementById('next').disabled=!hasMore;}
|
document.getElementById('next').disabled=!hasMore;}
|
||||||
@@ -117,6 +124,7 @@ document.getElementById('reload').onclick=reset;
|
|||||||
document.getElementById('prev').onclick=()=>{if(page>0){page--;load();}};
|
document.getElementById('prev').onclick=()=>{if(page>0){page--;load();}};
|
||||||
document.getElementById('next').onclick=()=>{if(hasMore){page++;load();}};
|
document.getElementById('next').onclick=()=>{if(hasMore){page++;load();}};
|
||||||
document.getElementById('q').addEventListener('keydown',e=>{if(e.key==='Enter')reset();});
|
document.getElementById('q').addEventListener('keydown',e=>{if(e.key==='Enter')reset();});
|
||||||
|
let qTimer;document.getElementById('q').addEventListener('input',()=>{clearTimeout(qTimer);qTimer=setTimeout(reset,300);});
|
||||||
document.getElementById('peer').addEventListener('keydown',e=>{if(e.key==='Enter')reset();});
|
document.getElementById('peer').addEventListener('keydown',e=>{if(e.key==='Enter')reset();});
|
||||||
document.getElementById('event').onchange=reset;
|
document.getElementById('event').onchange=reset;
|
||||||
document.getElementById('payload').onchange=reset;
|
document.getElementById('payload').onchange=reset;
|
||||||
@@ -236,20 +244,37 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
if len(rows) >= need:
|
if len(rows) >= need:
|
||||||
break
|
break
|
||||||
page_rows = rows[page * size:(page + 1) * size]
|
page_rows = rows[page * size:(page + 1) * size]
|
||||||
|
try:
|
||||||
|
stat = self.server.journal.stat()
|
||||||
|
journal_bytes, journal_mtime = stat.st_size, int(stat.st_mtime)
|
||||||
|
except OSError:
|
||||||
|
journal_bytes, journal_mtime = 0, 0
|
||||||
body = json.dumps({'events': page_rows, 'scanned': scanned, 'page': page, 'size': size,
|
body = json.dumps({'events': page_rows, 'scanned': scanned, 'page': page, 'size': size,
|
||||||
'has_more': len(rows) >= need, 'hidden': len(hidden),
|
'has_more': len(rows) >= need, 'hidden': len(hidden),
|
||||||
'hidden_ips': sorted(hidden)}, ensure_ascii=True).encode()
|
'hidden_ips': sorted(hidden), 'journal_bytes': journal_bytes,
|
||||||
|
'journal_mtime': journal_mtime}, ensure_ascii=True).encode()
|
||||||
self.send(200, body, 'application/json', (('Content-Disposition', 'inline'),))
|
self.send(200, body, 'application/json', (('Content-Disposition', 'inline'),))
|
||||||
|
|
||||||
|
def send_stream(self, path, download_name=None):
|
||||||
|
# Stream large payloads without loading them into memory.
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-Type', 'application/octet-stream')
|
||||||
|
self.send_header('Content-Length', str(path.stat().st_size))
|
||||||
|
self.send_header('X-Content-Type-Options', 'nosniff')
|
||||||
|
self.send_header('Cache-Control', 'no-store')
|
||||||
|
if download_name:
|
||||||
|
self.send_header('Content-Disposition', f'attachment; filename="{download_name}"')
|
||||||
|
self.end_headers()
|
||||||
|
if self.command != 'HEAD':
|
||||||
|
with path.open('rb') as stream:
|
||||||
|
shutil.copyfileobj(stream, self.wfile, length=1 << 20)
|
||||||
|
|
||||||
def artifact(self, sha, query):
|
def artifact(self, sha, query):
|
||||||
path = artifact_path(self.server.artifacts, sha)
|
path = artifact_path(self.server.artifacts, sha)
|
||||||
if path is None:
|
if path is None:
|
||||||
return self.send(404, b'not found', 'text/plain; charset=utf-8')
|
return self.send(404, b'not found', 'text/plain; charset=utf-8')
|
||||||
data = path.read_bytes()
|
download = None if self.one(query, 'inline', '0') == '1' else f'{sha}.bin'
|
||||||
if self.one(query, 'inline', '0') == '1':
|
self.send_stream(path, download)
|
||||||
return self.send(200, data, 'application/octet-stream')
|
|
||||||
self.send(200, data, 'application/octet-stream',
|
|
||||||
(('Content-Disposition', f'attachment; filename="{sha}.bin"'),))
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
Reference in new issue
Block a user