Skip to content

Commit 77df17d

Browse files
committed
add ui for streaming textfiles in realtime
1 parent fa5845f commit 77df17d

File tree

5 files changed

+160
-14
lines changed

5 files changed

+160
-14
lines changed

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ made in Norway 🇳🇴
5656
* [creating a playlist](#creating-a-playlist) - with a standalone mediaplayer or copyparty
5757
* [audio equalizer](#audio-equalizer) - and [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression)
5858
* [fix unreliable playback on android](#fix-unreliable-playback-on-android) - due to phone / app settings
59+
* [textfile viewer](#textfile-viewer) - with realtime streaming of logfiles and such
5960
* [markdown viewer](#markdown-viewer) - and there are *two* editors
6061
* [markdown vars](#markdown-vars) - dynamic docs with serverside variable expansion
6162
* [other tricks](#other-tricks)
@@ -257,7 +258,8 @@ also see [comparison to similar software](./docs/versus.md)
257258
* ☑ play video files as audio (converted on server)
258259
* ☑ create and play [m3u8 playlists](#playlists)
259260
* ☑ image gallery with webm player
260-
* ☑ textfile browser with syntax hilighting
261+
*[textfile browser](#textfile-viewer) with syntax hilighting
262+
* ☑ realtime streaming of growing files (logfiles and such)
261263
*[thumbnails](#thumbnails)
262264
* ☑ ...of images using Pillow, pyvips, or FFmpeg
263265
* ☑ ...of videos using FFmpeg
@@ -1127,6 +1129,18 @@ not available on iPhones / iPads because AudioContext currently breaks backgroun
11271129
due to phone / app settings, android phones may randomly stop playing music when the power saver kicks in, especially at the end of an album -- you can fix it by [disabling power saving](https://user-images.githubusercontent.com/241032/235262123-c328cca9-3930-4948-bd18-3949b9fd3fcf.png) in the [app settings](https://user-images.githubusercontent.com/241032/235262121-2ffc51ae-7821-4310-a322-c3b7a507890c.png) of the browser you use for music streaming (preferably a dedicated one)
11281130
11291131
1132+
## textfile viewer
1133+
1134+
with realtime streaming of logfiles and such , and terminal colors work too
1135+
1136+
(TODO: add screenshots)
1137+
1138+
click `-txt-` next to a textfile to open the viewer, which has the following toolbar buttons:
1139+
1140+
* `✏️ edit` opens the textfile editor
1141+
* `📡 follow` starts monitoring the file for changes, streaming new lines in realtime
1142+
1143+
11301144
## markdown viewer
11311145
11321146
and there are *two* editors
@@ -2425,6 +2439,9 @@ interact with copyparty using non-browser clients
24252439
* and for screenshots on macos, see [./contrib/ishare.iscu](./contrib/#ishareiscu)
24262440
* and for screenshots on linux, see [./contrib/flameshot.sh](./contrib/flameshot.sh)
24272441
2442+
* [Custom Uploader](https://f-droid.org/en/packages/com.nyx.custom_uploader/) (an Android app) as an alternative to copyparty's own [PartyUP!](#android-app)
2443+
* works if you set UploadURL to `https://your.com/foo/?want=url&pw=hunter2` and FormDataName `f`
2444+
24282445
* contextlet (web browser integration); see [contrib contextlet](contrib/#send-to-cppcontextletjson)
24292446
24302447
* [igloo irc](https://iglooirc.com/): Method: `post` Host: `https://you.com/up/?want=url&pw=hunter2` Multipart: `yes` File parameter: `f`

copyparty/httpcli.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4247,6 +4247,7 @@ def tx_tail(
42474247
except:
42484248
ofs = 0
42494249

4250+
ofs0 = ofs
42504251
f = None
42514252
try:
42524253
st = os.stat(abspath)
@@ -4276,6 +4277,13 @@ def tx_tail(
42764277
ofs = eof - remains
42774278
f.seek(ofs)
42784279

4280+
try:
4281+
st2 = os.stat(open_args[0])
4282+
if st.st_ino == st2.st_ino:
4283+
st = st2 # for filesize
4284+
except:
4285+
pass
4286+
42794287
gone = 0
42804288
t_fd = t_ka = time.time()
42814289
while True:
@@ -4296,21 +4304,26 @@ def tx_tail(
42964304
if t_fd < now - sec_fd:
42974305
try:
42984306
st2 = os.stat(open_args[0])
4299-
if st2.st_ino != st.st_ino or st2.st_size < sent:
4307+
if st2.st_ino != st.st_ino or st2.st_size < sent or st2.st_size < st.st_size:
43004308
assert f # !rm
43014309
# open new file before closing previous to avoid toctous (open may fail; cannot null f before)
43024310
f2 = open(*open_args)
43034311
f.close()
43044312
f = f2
43054313
f.seek(0, os.SEEK_END)
4306-
if f.tell() < sent:
4314+
eof = f.tell()
4315+
if eof < sent:
43074316
ofs = sent = 0 # shrunk; send from start
4317+
zb = b"\n\n*** file size decreased -- rewinding to the start of the file ***\n\n"
4318+
self.s.sendall(zb)
4319+
if ofs0 < 0 and eof > -ofs0:
4320+
ofs = eof + ofs0
43084321
else:
43094322
ofs = sent # just new fd? resume from same ofs
43104323
f.seek(ofs)
43114324
self.log("reopened at byte %d: %r" % (ofs, abspath), 6)
43124325
gone = 0
4313-
st = st2
4326+
st = st2
43144327
except:
43154328
gone += 1
43164329
if gone > 3:

copyparty/web/browser.css

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1825,10 +1825,11 @@ html.y #tree.nowrap .ntree a+a:hover {
18251825
line-height: 2.3em;
18261826
margin-bottom: 1.5em;
18271827
}
1828+
#hdoc,
18281829
#ghead {
18291830
position: sticky;
18301831
top: -.3em;
1831-
z-index: 1;
1832+
z-index: 2;
18321833
}
18331834
.ghead .btn {
18341835
position: relative;
@@ -1838,6 +1839,17 @@ html.y #tree.nowrap .ntree a+a:hover {
18381839
white-space: pre;
18391840
padding-left: .3em;
18401841
}
1842+
#tail2end,
1843+
#tailansi,
1844+
#tailnb {
1845+
display: none;
1846+
}
1847+
#taildoc.on+#tail2end,
1848+
#taildoc.on+#tail2end+#tailansi,
1849+
#taildoc.on+#tail2end+#tailansi+#tailnb {
1850+
display: inherit;
1851+
display: unset;
1852+
}
18411853
#op_unpost {
18421854
padding: 1em;
18431855
}

copyparty/web/browser.js

Lines changed: 110 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ var Ls = {
337337
"f_empty": 'this folder is empty',
338338
"f_chide": 'this will hide the column «{0}»\n\nyou can unhide columns in the settings tab',
339339
"f_bigtxt": "this file is {0} MiB large -- really view as text?",
340+
"f_bigtxt2": "view just the end of the file instead? this will also enable following/tailing, showing newly added lines of text in real time",
340341
"fbd_more": '<div id="blazy">showing <code>{0}</code> of <code>{1}</code> files; <a href="#" id="bd_more">show {2}</a> or <a href="#" id="bd_all">show all</a></div>',
341342
"fbd_all": '<div id="blazy">showing <code>{0}</code> of <code>{1}</code> files; <a href="#" id="bd_all">show all</a></div>',
342343
"f_anota": "only {0} of the {1} items were selected;\nto select the full folder, first scroll to the bottom",
@@ -441,6 +442,10 @@ var Ls = {
441442
"tvt_next": "show next document$NHotkey: K\">⬇ next",
442443
"tvt_sel": "select file &nbsp; ( for cut / copy / delete / ... )$NHotkey: S\">sel",
443444
"tvt_edit": "open file in text editor$NHotkey: E\">✏️ edit",
445+
"tvt_tail": "monitor file for changes; show new lines in real time\">📡 follow",
446+
"tvt_atail": "lock scroll to bottom of page\">⚓",
447+
"tvt_ctail": "decode terminal colors (ansi escape codes)\">🌈",
448+
"tvt_ntail": "scrollback limit (how many bytes of text to keep loaded)",
444449

445450
"m3u_add1": "song added to m3u playlist",
446451
"m3u_addn": "{0} songs added to m3u playlist",
@@ -540,6 +545,7 @@ var Ls = {
540545
"u_https3": "for better performance",
541546
"u_ancient": 'your browser is impressively ancient -- maybe you should <a href="#" onclick="goto(\'bup\')">use bup instead</a>',
542547
"u_nowork": "need firefox 53+ or chrome 57+ or iOS 11+",
548+
"tail_2old": "need firefox 105+ or chrome 71+ or iOS 14.5+",
543549
"u_nodrop": 'your browser is too old for drag-and-drop uploading',
544550
"u_notdir": "that's not a folder!\n\nyour browser is too old,\nplease try dragdrop instead",
545551
"u_uri": "to dragdrop images from other browser windows,\nplease drop it onto the big upload button",
@@ -954,6 +960,7 @@ var Ls = {
954960
"f_empty": 'denne mappen er tom',
955961
"f_chide": 'dette vil skjule kolonnen «{0}»\n\nfanen for "andre innstillinger" lar deg vise kolonnen igjen',
956962
"f_bigtxt": "denne filen er hele {0} MiB -- vis som tekst?",
963+
"f_bigtxt2": "vil du se bunnen av filen istedenfor? du vil da også se nye linjer som blir lagt til på slutten av filen i sanntid",
957964
"fbd_more": '<div id="blazy">viser <code>{0}</code> av <code>{1}</code> filer; <a href="#" id="bd_more">vis {2}</a> eller <a href="#" id="bd_all">vis alle</a></div>',
958965
"fbd_all": '<div id="blazy">viser <code>{0}</code> av <code>{1}</code> filer; <a href="#" id="bd_all">vis alle</a></div>',
959966
"f_anota": "kun {0} av totalt {1} elementer ble markert;\nfor å velge alt må du bla til bunnen av mappen først",
@@ -1058,6 +1065,10 @@ var Ls = {
10581065
"tvt_next": "vis neste dokument$NSnarvei: K\">⬇ neste",
10591066
"tvt_sel": "markér filen &nbsp; ( for utklipp / sletting / ... )$NSnarvei: S\">merk",
10601067
"tvt_edit": "redigér filen$NSnarvei: E\">✏️ endre",
1068+
"tvt_tail": "overvåk filen for endringer og vis nye linjer i sanntid\">📡 følg",
1069+
"tvt_atail": "hold de nyeste linjene synlig (lås til bunnen av siden)\">⚓",
1070+
"tvt_ctail": "forstå og vis terminalfarger (ansi-sekvenser)\">🌈",
1071+
"tvt_ntail": "maks-grense for antall bokstaver som skal vises i vinduet",
10611072

10621073
"m3u_add1": "sangen ble lagt til i m3u-spillelisten",
10631074
"m3u_addn": "{0} sanger ble lagt til i m3u-spillelisten",
@@ -1157,6 +1168,7 @@ var Ls = {
11571168
"u_https3": "for høyere hastighet",
11581169
"u_ancient": 'nettleseren din er prehistorisk -- mulig du burde <a href="#" onclick="goto(\'bup\')">bruke bup istedenfor</a>',
11591170
"u_nowork": "krever firefox 53+, chrome 57+, eller iOS 11+",
1171+
"tail_2old": "krever firefox 105+, chrome 71+, eller iOS 14.5+",
11601172
"u_nodrop": 'nettleseren din er for gammel til å laste opp filer ved å dra dem inn i vinduet',
11611173
"u_notdir": "mottok ikke mappen!\n\nnettleseren din er for gammel,\nprøv å dra mappen inn i vinduet istedenfor",
11621174
"u_uri": "for å laste opp bilder ifra andre nettleservinduer,\nslipp bildet rett på den store last-opp-knappen",
@@ -1571,6 +1583,7 @@ var Ls = {
15711583
"f_empty": '该文件夹为空',
15721584
"f_chide": '隐藏列 «{0}»\n\n你可以在设置选项卡中重新显示列',
15731585
"f_bigtxt": "这个文件大小为 {0} MiB -- 真的以文本形式查看?",
1586+
"f_bigtxt2": " 你想查看文件的结尾部分吗?这也将启用实时跟踪功能,能够实时显示新添加的文本行。", //m
15741587
"fbd_more": '<div id="blazy">显示 <code>{0}</code> 个文件中的 <code>{1}</code> 个;<a href="#" id="bd_more">显示 {2}</a> 或 <a href="#" id="bd_all">显示全部</a></div>',
15751588
"fbd_all": '<div id="blazy">显示 <code>{0}</code> 个文件中的 <code>{1}</code> 个;<a href="#" id="bd_all">显示全部</a></div>',
15761589
"f_anota": "仅选择了 {0} 个项目,共 {1} 个;\n要选择整个文件夹,请先滚动到底部", //m
@@ -1675,6 +1688,10 @@ var Ls = {
16751688
"tvt_next": "显示下一个文档$N快捷键: K\">⬇ 下一个",
16761689
"tvt_sel": "选择文件&nbsp;(用于剪切/删除/...)$N快捷键: S\">选择",
16771690
"tvt_edit": "在文本编辑器中打开文件$N快捷键: E\">✏️ 编辑",
1691+
"tvt_tail": "监视文件更改,并实时显示新增的行\">📡 跟踪", //m
1692+
"tvt_atail": "锁定到底部,显示最新内容\">⚓", //m
1693+
"tvt_ctail": "解析终端颜色(ANSI 转义码)\">🌈", //m
1694+
"tvt_ntail": "滚动历史上限(保留多少字节的文本)", //m
16781695

16791696
"m3u_add1": "歌曲已添加到 m3u 播放列表", //m
16801697
"m3u_addn": "已添加 {0} 首歌曲到 m3u 播放列表", //m
@@ -1774,6 +1791,7 @@ var Ls = {
17741791
"u_https3": "以获得更好的性能",
17751792
"u_ancient": '你的浏览器非常古老 -- 也许你应该 <a href="#" onclick="goto(\'bup\')">改用 bup</a>',
17761793
"u_nowork": "需要 Firefox 53+ 或 Chrome 57+ 或 iOS 11+",
1794+
"tail_2old": "需要 Firefox 105+ 或 Chrome 71+ 或 iOS 14.5+",
17771795
"u_nodrop": '浏览器版本低,不支持通过拖动文件到窗口来上传文件',
17781796
"u_notdir": "不是文件夹!\n\n您的浏览器太旧;\n请尝试将文件夹拖入窗口",
17791797
"u_uri": "要从其他浏览器窗口拖放图片,\n请将其拖放到大的上传按钮上",
@@ -5912,16 +5930,73 @@ var showfile = (function () {
59125930
}
59135931
r.mktree();
59145932
if (em) {
5915-
render(em);
5933+
if (r.taildoc)
5934+
r.show(em[0], true);
5935+
else
5936+
render(em);
59165937
em = null;
59175938
}
59185939
};
59195940

5941+
r.tail = function (url, no_push) {
5942+
r.abrt = new AbortController();
5943+
render([url, '', ''], no_push);
5944+
var me = r.tail_id = Date.now(),
5945+
wfp = ebi('wfp'),
5946+
edoc = ebi('doc'),
5947+
txt = '';
5948+
5949+
url = addq(url, 'tail=-' + r.tailnb);
5950+
fetch(url, {'signal': r.abrt.signal}).then(function(rsp) {
5951+
var ro = rsp.body.pipeThrough(
5952+
new TextDecoderStream('utf-8', {'fatal': false}),
5953+
{'signal': r.abrt.signal}).getReader();
5954+
5955+
var rf = function() {
5956+
ro.read().then(function(v) {
5957+
if (r.tail_id != me)
5958+
return;
5959+
v = v.value;
5960+
if (v == '\x00')
5961+
return rf();
5962+
txt += v;
5963+
var ofs = txt.length - r.tailnb;
5964+
if (ofs > 0) {
5965+
var ofs2 = txt.indexOf('\n', ofs);
5966+
if (ofs2 >= ofs && ofs - ofs2 < 512)
5967+
ofs = ofs2;
5968+
txt = txt.slice(ofs);
5969+
}
5970+
var html = esc(txt);
5971+
if (r.tailansi)
5972+
html = r.ansify(html);
5973+
edoc.innerHTML = html;
5974+
if (r.tail2end)
5975+
window.scrollTo(0, wfp.offsetTop - window.innerHeight);
5976+
rf();
5977+
});
5978+
};
5979+
if (r.tail_id == me)
5980+
rf();
5981+
});
5982+
};
5983+
5984+
r.untail = function () {
5985+
if (!r.abrt)
5986+
return;
5987+
r.abrt.abort();
5988+
r.tail_id = -1;
5989+
};
5990+
59205991
r.show = function (url, no_push) {
5992+
r.untail();
59215993
var xhr = new XHR(),
59225994
m = /[?&](k=[^&#]+)/.exec(url);
59235995

59245996
url = url.split('?')[0] + (m ? '?' + m[1] : '');
5997+
if (r.taildoc)
5998+
return r.tail(url, no_push);
5999+
59256000
xhr.url = url;
59266001
xhr.fname = uricom_dec(url.split('/').pop());
59276002
xhr.no_push = no_push;
@@ -5961,7 +6036,7 @@ var showfile = (function () {
59616036

59626037
function render(doc, no_push) {
59636038
r.q = null;
5964-
var url = doc[0],
6039+
var url = r.url = doc[0],
59656040
lnh = doc[1],
59666041
txt = doc[2],
59676042
name = url.split('?')[0].split('/').pop(),
@@ -5985,12 +6060,12 @@ var showfile = (function () {
59856060
el = el || QS('#doc>code');
59866061
Prism.highlightElement(el);
59876062
if (el.className == 'language-ans' || (!lang && /\x1b\[[0-9;]{0,16}m/.exec(txt.slice(0, 4096))))
5988-
r.ansify(el);
6063+
el.innerHTML = r.ansify(el.innerHTML);
59896064
}
59906065
catch (ex) { }
59916066
}
59926067

5993-
if (txt.length > 1024 * 256)
6068+
if (!txt || txt.length > 1024 * 256)
59946069
fun = function (el) { };
59956070

59966071
qsr('#doc');
@@ -6034,11 +6109,11 @@ var showfile = (function () {
60346109
tree_scrollto();
60356110
}
60366111

6037-
r.ansify = function (el) {
6112+
r.ansify = function (html) {
60386113
var ctab = (light ?
60396114
'bfbfbf d30253 497600 b96900 006fbb a50097 288276 2d2d2d 9f9f9f 943b55 3a5600 7f4f00 00507d 683794 004343 000000' :
60406115
'404040 f03669 b8e346 ffa402 02a2ff f65be3 3da698 d2d2d2 606060 c75b79 c8e37e ffbe4a 71cbff b67fe3 9cf0ed ffffff').split(/ /g),
6041-
src = el.innerHTML.split(/\x1b\[/g),
6116+
src = html.split(/\x1b\[/g),
60426117
out = ['<span>'], fg = 7, bg = null, bfg = 0, bbg = 0, inv = 0, bold = 0;
60436118

60446119
for (var a = 0; a < src.length; a++) {
@@ -6091,7 +6166,7 @@ var showfile = (function () {
60916166

60926167
out.push(s + '">' + txt);
60936168
}
6094-
el.innerHTML = out.join('');
6169+
return out.join('');
60956170
};
60966171

60976172
r.mktree = function () {
@@ -6138,6 +6213,14 @@ var showfile = (function () {
61386213
msel.selui();
61396214
};
61406215

6216+
r.tgltail = function () {
6217+
if (!window.TextDecoderStream) {
6218+
bcfg_set('taildoc', r.taildoc = false);
6219+
return toast.err(10, L.tail_2old);
6220+
}
6221+
r.show(r.url, true);
6222+
};
6223+
61416224
var bdoc = ebi('bdoc');
61426225
bdoc.className = 'line-numbers';
61436226
bdoc.innerHTML = (
@@ -6148,15 +6231,28 @@ var showfile = (function () {
61486231
'<a href="#" class="btn" id="nextdoc" tt="' + L.tvt_next + '</a>\n' +
61496232
'<a href="#" class="btn" id="seldoc" tt="' + L.tvt_sel + '</a>\n' +
61506233
'<a href="#" class="btn" id="editdoc" tt="' + L.tvt_edit + '</a>\n' +
6234+
'<a href="#" class="btn tgl" id="taildoc" tt="' + L.tvt_tail + '</a>\n' +
6235+
'<a href="#" class="btn tgl" id="tail2end" tt="' + L.tvt_atail + '</a>\n' +
6236+
'<a href="#" class="btn tgl" id="tailansi" tt="' + L.tvt_ctail + '</a>\n' +
6237+
'<input type="text" id="tailnb" value="" ' + NOAC + ' style="width:4em" tt="' + L.tvt_ntail + '" />' +
61516238
'</div>'
61526239
);
61536240
ebi('xdoc').onclick = function () {
6241+
r.untail();
61546242
thegrid.setvis(true);
61556243
};
61566244
ebi('dldoc').setAttribute('download', '');
61576245
ebi('prevdoc').onclick = function () { tree_neigh(-1); };
61586246
ebi('nextdoc').onclick = function () { tree_neigh(1); };
61596247
ebi('seldoc').onclick = r.tglsel;
6248+
bcfg_bind(r, 'taildoc', 'taildoc', false, r.tgltail);
6249+
bcfg_bind(r, 'tail2end', 'tail2end', true);
6250+
bcfg_bind(r, 'tailansi', 'tailansi', false, r.tgltail);
6251+
6252+
r.tailnb = ebi('tailnb').value = icfg_get('tailnb', 131072);
6253+
ebi('tailnb').oninput = function (e) {
6254+
swrite('tailnb', r.tailnb = this.value);
6255+
};
61606256

61616257
return r;
61626258
})();
@@ -10111,13 +10207,18 @@ ebi('files').onclick = ebi('docul').onclick = function (e) {
1011110207
fun = function () {
1011210208
showfile.show(href, tgt.getAttribute('lang'));
1011310209
},
10210+
tfun = function () {
10211+
bcfg_set('taildoc', showfile.taildoc = true);
10212+
fun();
10213+
},
1011410214
szs = ft2dict(a.closest('tr'))[0].sz,
1011510215
sz = parseInt(szs.replace(/[, ]/g, ''));
1011610216

10117-
if (sz < 1024 * 1024)
10217+
if (sz < 1024 * 1024 || showfile.taildoc)
1011810218
fun();
1011910219
else
10120-
modal.confirm(L.f_bigtxt.format(f2f(sz / 1024 / 1024, 1)), fun, null);
10220+
modal.confirm(L.f_bigtxt.format(f2f(sz / 1024 / 1024, 1)), fun, function() {
10221+
modal.confirm(L.f_bigtxt2, tfun, null)});
1012110222

1012210223
return ev(e);
1012310224
}

0 commit comments

Comments
 (0)