textfile-streaming fixes;

* add optional max duration, default-infinite
* add optional wordwrap, default-enabled
* url-param `...&tail` enables tailing in textviewer too
* hide bottom tray while tailing
This commit is contained in:
ed 2025-06-21 23:36:19 +00:00
parent 8cae7a715b
commit 6ecf4fdceb
8 changed files with 55 additions and 18 deletions

View file

@ -56,7 +56,7 @@ made in Norway 🇳🇴
* [creating a playlist](#creating-a-playlist) - with a standalone mediaplayer or copyparty
* [audio equalizer](#audio-equalizer) - and [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression)
* [fix unreliable playback on android](#fix-unreliable-playback-on-android) - due to phone / app settings
* [textfile viewer](#textfile-viewer) - with realtime streaming of logfiles and such
* [textfile viewer](#textfile-viewer) - with realtime streaming of logfiles and such ([demo](https://a.ocv.me/pub/demo/logtail/?doc=lipsum.txt&tail))
* [markdown viewer](#markdown-viewer) - and there are *two* editors
* [markdown vars](#markdown-vars) - dynamic docs with serverside variable expansion
* [other tricks](#other-tricks)
@ -1131,14 +1131,13 @@ due to phone / app settings, android phones may randomly stop playing music whe
## textfile viewer
with realtime streaming of logfiles and such , and terminal colors work too
(TODO: add screenshots)
with realtime streaming of logfiles and such ([demo](https://a.ocv.me/pub/demo/logtail/?doc=lipsum.txt&tail)) , and terminal colors work too
click `-txt-` next to a textfile to open the viewer, which has the following toolbar buttons:
* `✏️ edit` opens the textfile editor
* `📡 follow` starts monitoring the file for changes, streaming new lines in realtime
* similar to `tail -f`
## markdown viewer

View file

@ -1405,6 +1405,7 @@ def add_tail(ap):
ap2 = ap.add_argument_group('tailing options (realtime streaming of a growing file)')
ap2.add_argument("--tail-who", metavar="LVL", type=int, default=2, help="who can tail? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=authenticated-with-read-access, [\033[32m3\033[0m]=everyone-with-read-access (volflag=tail_who)")
ap2.add_argument("--tail-cmax", metavar="N", type=int, default=64, help="do not allow starting a new tail if more than \033[33mN\033[0m active downloads")
ap2.add_argument("--tail-tmax", metavar="SEC", type=float, default=0, help="terminate connection after \033[33mSEC\033[0m seconds; [\033[32m0\033[0m]=never (volflag=tail_tmax)")
ap2.add_argument("--tail-rate", metavar="SEC", type=float, default=0.2, help="check for new data every \033[33mSEC\033[0m seconds (volflag=tail_rate)")
ap2.add_argument("--tail-ka", metavar="SEC", type=float, default=3.0, help="send a zerobyte if connection is idle for \033[33mSEC\033[0m seconds to prevent disconnect")
ap2.add_argument("--tail-fd", metavar="SEC", type=float, default=1.0, help="check if file was replaced (new fd) if idle for \033[33mSEC\033[0m seconds (volflag=tail_fd)")

View file

@ -2075,12 +2075,13 @@ class AuthSrv(object):
if vf not in vol.flags:
vol.flags[vf] = getattr(self.args, ga)
zs = "forget_ip nrand u2abort u2ow ups_who zip_who"
zs = "forget_ip nrand tail_who u2abort u2ow ups_who zip_who"
for k in zs.split():
if k in vol.flags:
vol.flags[k] = int(vol.flags[k])
for k in ("convt",):
zs = "convt tail_fd tail_rate tail_tmax"
for k in zs.split():
if k in vol.flags:
vol.flags[k] = float(vol.flags[k])

View file

@ -105,6 +105,7 @@ def vf_vmap() -> dict[str, str]:
"sort",
"tail_fd",
"tail_rate",
"tail_tmax",
"tail_who",
"tcolor",
"unlist",
@ -311,8 +312,9 @@ flagcats = {
},
"tailing": {
"notail": "disable ?tail (download a growing file continuously)",
"tail_fd=1": "interval for checking if file was replaced (new fd)",
"tail_rate=0.2": "interval for checking for new data",
"tail_fd=1": "check if file was replaced (new fd) every 1 sec",
"tail_rate=0.2": "check for new data every 0.2 sec",
"tail_tmax=30": "kill connection after 30 sec",
"tail_who=2": "restrict ?tail access (1=admins,2=authed,3=everyone)",
},
"others": {

View file

@ -4246,11 +4246,13 @@ class HttpCli(object):
status: int,
mime: str,
) -> None:
vf = self.vn.flags
self.send_headers(length=None, status=status, mime=mime)
abspath: bytes = open_args[0]
sec_rate = self.args.tail_rate
sec_rate = vf["tail_rate"]
sec_max = vf["tail_tmax"]
sec_fd = vf["tail_fd"]
sec_ka = self.args.tail_ka
sec_fd = self.args.tail_fd
wr_slp = self.args.s_wr_slp
wr_sz = self.args.s_wr_sz
dls = self.conn.hsrv.dls
@ -4264,6 +4266,7 @@ class HttpCli(object):
except:
ofs = 0
t0 = time.time()
ofs0 = ofs
f = None
try:
@ -4307,6 +4310,13 @@ class HttpCli(object):
assert f # !rm
buf = f.read(4096)
now = time.time()
if sec_max and now - t0 >= sec_max:
self.log("max duration exceeded; kicking client", 6)
zb = b"\n\n*** max duration exceeded; disconnecting ***\n"
self.s.sendall(zb)
break
if buf:
t_fd = t_ka = now
self.s.sendall(buf)

View file

@ -1839,14 +1839,10 @@ html.y #tree.nowrap .ntree a+a:hover {
white-space: pre;
padding-left: .3em;
}
#tail2end,
#tailansi,
#tailnb {
#tailbtns {
display: none;
}
#taildoc.on+#tail2end,
#taildoc.on+#tail2end+#tailansi,
#taildoc.on+#tail2end+#tailansi+#tailnb {
#taildoc.on+#tailbtns {
display: inherit;
display: unset;
}
@ -1946,6 +1942,9 @@ html.y #tree.nowrap .ntree a+a:hover {
padding: 1em 0 1em 0;
border-radius: .3em;
}
#doc.wrap {
white-space: pre-wrap;
}
html.y #doc {
box-shadow: 0 0 .3em var(--bg-u5);
background: #f7f7f7;

View file

@ -443,6 +443,7 @@ var Ls = {
"tvt_sel": "select file   ( for cut / copy / delete / ... )$NHotkey: S\">sel",
"tvt_edit": "open file in text editor$NHotkey: E\">✏️ edit",
"tvt_tail": "monitor file for changes; show new lines in real time\">📡 follow",
"tvt_wrap": "word-wrap\">↵",
"tvt_atail": "lock scroll to bottom of page\">⚓",
"tvt_ctail": "decode terminal colors (ansi escape codes)\">🌈",
"tvt_ntail": "scrollback limit (how many bytes of text to keep loaded)",
@ -1066,6 +1067,7 @@ var Ls = {
"tvt_sel": "markér filen   ( for utklipp / sletting / ... )$NSnarvei: S\">merk",
"tvt_edit": "redigér filen$NSnarvei: E\">✏️ endre",
"tvt_tail": "overvåk filen for endringer og vis nye linjer i sanntid\">📡 følg",
"tvt_wrap": "tekstbryting\">↵",
"tvt_atail": "hold de nyeste linjene synlig (lås til bunnen av siden)\">⚓",
"tvt_ctail": "forstå og vis terminalfarger (ansi-sekvenser)\">🌈",
"tvt_ntail": "maks-grense for antall bokstaver som skal vises i vinduet",
@ -1689,6 +1691,7 @@ var Ls = {
"tvt_sel": "选择文件 (用于剪切/删除/...$N快捷键: S\">选择",
"tvt_edit": "在文本编辑器中打开文件$N快捷键: E\">✏️ 编辑",
"tvt_tail": "监视文件更改,并实时显示新增的行\">📡 跟踪", //m
"tvt_wrap": "自动换行\">↵", //m
"tvt_atail": "锁定到底部,显示最新内容\">⚓", //m
"tvt_ctail": "解析终端颜色ANSI 转义码)\">🌈", //m
"tvt_ntail": "滚动历史上限(保留多少字节的文本)", //m
@ -2984,6 +2987,9 @@ var widget = (function () {
ebi('bplay').innerHTML = paused ? '▶' : '⏸';
}
};
r.setvis = function () {
widget.style.display = !has(perms, "read") || showfile.abrt ? 'none' : '';
};
wtico.onclick = function (e) {
if (!touchmode)
r.toggle(e);
@ -5942,6 +5948,7 @@ var showfile = (function () {
r.tail = function (url, no_push) {
r.abrt = new AbortController();
widget.setvis();
render([url, '', ''], no_push);
var me = r.tail_id = Date.now(),
wfp = ebi('wfp'),
@ -5988,7 +5995,9 @@ var showfile = (function () {
if (!r.abrt)
return;
r.abrt.abort();
r.abrt = null;
r.tail_id = -1;
widget.setvis();
};
r.show = function (url, no_push) {
@ -6099,6 +6108,8 @@ var showfile = (function () {
else
import_js(SR + '/.cpr/deps/prism.js', function () { fun(); });
}
if (!txt && r.wrap)
el.className = 'wrap';
}
wr.appendChild(el);
@ -6232,6 +6243,10 @@ var showfile = (function () {
r.show(r.url, true);
};
r.tglwrap = function () {
r.show(r.url, true);
};
var bdoc = ebi('bdoc');
bdoc.className = 'line-numbers';
bdoc.innerHTML = (
@ -6243,19 +6258,24 @@ var showfile = (function () {
'<a href="#" class="btn" id="seldoc" tt="' + L.tvt_sel + '</a>\n' +
'<a href="#" class="btn" id="editdoc" tt="' + L.tvt_edit + '</a>\n' +
'<a href="#" class="btn tgl" id="taildoc" tt="' + L.tvt_tail + '</a>\n' +
'<div id="tailbtns">\n' +
'<a href="#" class="btn tgl" id="wrapdoc" tt="' + L.tvt_wrap + '</a>\n' +
'<a href="#" class="btn tgl" id="tail2end" tt="' + L.tvt_atail + '</a>\n' +
'<a href="#" class="btn tgl" id="tailansi" tt="' + L.tvt_ctail + '</a>\n' +
'<input type="text" id="tailnb" value="" ' + NOAC + ' style="width:4em" tt="' + L.tvt_ntail + '" />' +
'</div>\n' +
'</div>'
);
ebi('xdoc').onclick = function () {
r.untail();
thegrid.setvis(true);
bcfg_bind(r, 'taildoc', 'taildoc', false, r.tgltail);
};
ebi('dldoc').setAttribute('download', '');
ebi('prevdoc').onclick = function () { tree_neigh(-1); };
ebi('nextdoc').onclick = function () { tree_neigh(1); };
ebi('seldoc').onclick = r.tglsel;
bcfg_bind(r, 'wrap', 'wrapdoc', true, r.tglwrap);
bcfg_bind(r, 'taildoc', 'taildoc', false, r.tgltail);
bcfg_bind(r, 'tail2end', 'tail2end', true);
bcfg_bind(r, 'tailansi', 'tailansi', false, r.tgltail);
@ -6265,6 +6285,11 @@ var showfile = (function () {
swrite('tailnb', r.tailnb = this.value);
};
if (/[?&]tail\b/.exec(sloc0)) {
clmod(ebi('taildoc'), 'on', 1);
r.taildoc = true;
}
return r;
})();
@ -8690,7 +8715,7 @@ function apply_perms(res) {
if (up2k)
up2k.set_fsearch();
ebi('widget').style.display = have_read ? '' : 'none';
widget.setvis();
thegrid.setvis();
if (!have_read && have_write)
goto('up2k');

View file

@ -155,7 +155,7 @@ class Cfg(Namespace):
ex = "hash_mt hsortn safe_dedup srch_time tail_fd tail_rate u2abort u2j u2sz"
ka.update(**{k: 1 for k in ex.split()})
ex = "au_vol dl_list mtab_age reg_cap s_thead s_tbody tail_who th_convt ups_who zip_who"
ex = "au_vol dl_list mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt ups_who zip_who"
ka.update(**{k: 9 for k in ex.split()})
ex = "db_act forget_ip k304 loris no304 nosubtle re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs"