diff --git a/README.md b/README.md index e07ad4b3..36457872 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 4b050fa7..7cfee783 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -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)") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index d25714c2..5e42f07a 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -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]) diff --git a/copyparty/cfg.py b/copyparty/cfg.py index c8fd0cde..01ab85c8 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -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": { diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index f7f8157d..589e2bcf 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -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) diff --git a/copyparty/web/browser.css b/copyparty/web/browser.css index 1fec318f..eb8331fc 100644 --- a/copyparty/web/browser.css +++ b/copyparty/web/browser.css @@ -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; diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 94555c3c..e91facc4 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -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 () { '\n' + + '' + + '\n' + '' ); 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'); diff --git a/tests/util.py b/tests/util.py index 2492b91e..cdc6b6c4 100644 --- a/tests/util.py +++ b/tests/util.py @@ -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"