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"