Compare commits

...

13 Commits

Author SHA1 Message Date
ed
cea5aecbf2 v1.3.2 2022-06-20 01:31:29 +02:00
ed
0e61e70670 audioplayer continues to next folder by default 2022-06-20 00:20:13 +02:00
ed
1e333c0939 fix doc traversal 2022-06-19 23:32:36 +02:00
ed
917b6ec03c naming 2022-06-19 22:58:20 +02:00
ed
fe67c52ead configurable list of sparse-supporting filesystems +
close nonsparse files after each write to force flush
2022-06-19 22:38:52 +02:00
ed
909c7bee3e ignore md plugin errors 2022-06-19 20:28:45 +02:00
ed
27ca54d138 md: ol appeared as ul 2022-06-19 19:05:41 +02:00
ed
2147c3a646 run markdown plugins in directory listings 2022-06-19 18:17:22 +02:00
ed
a99120116f ux: breadcrumb ctrl-click 2022-06-19 17:51:03 +02:00
ed
802efeaff2 dont let tags imply subdirectories when renaming 2022-06-19 16:06:39 +02:00
ed
9ad3af1ef6 misc tweaks 2022-06-19 16:05:48 +02:00
ed
715727b811 add changelog 2022-06-17 15:33:57 +02:00
ed
c6eaa7b836 aight good to know 2022-06-17 00:37:56 +02:00
19 changed files with 2751 additions and 106 deletions

View File

@@ -9,11 +9,13 @@
turn your phone or raspi into a portable file server with resumable uploads/downloads using *any* web browser
* server only needs `py2.7` or `py3.3+`, all dependencies optional
* browse/upload with IE4 / netscape4.0 on win3.11 (heh)
* browse/upload with [IE4](#browser-support) / netscape4.0 on win3.11 (heh)
* *resumable* uploads need `firefox 34+` / `chrome 41+` / `safari 7+` for full speed
* code standard: `black`
📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [unpost](#unpost) // [thumbnails](#thumbnails) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [md-viewer](#markdown-viewer) // [ie4](#browser-support)
try the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running from a basement in finland
📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [unpost](#unpost) // [thumbnails](#thumbnails) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [md-viewer](#markdown-viewer)
## get the app
@@ -358,11 +360,13 @@ the browser has the following hotkeys (always qwerty)
* `U/O` skip 10sec back/forward
* `0..9` jump to 0%..90%
* `P` play/pause (also starts playing the folder)
* `Y` download file
* when viewing images / playing videos:
* `J/L, Left/Right` prev/next file
* `Home/End` first/last file
* `S` toggle selection
* `R` rotate clockwise (shift=ccw)
* `Y` download file
* `Esc` close viewer
* videos:
* `U/O` skip 10sec back/forward
@@ -882,6 +886,7 @@ quick summary of more eccentric web-browsers trying to view a directory index:
| **netsurf** (3.10/arch) | is basically ie6 with much better css (javascript has almost no effect) |
| **opera** (11.60/winxp) | OK: thumbnails, image-viewer, zip-selection, rename/cut/paste. NG: up2k, navpane, markdown, audio |
| **ie4** and **netscape** 4.0 | can browse, upload with `?b=u`, auth with `&pw=wark` |
| **ncsa mosaic** 2.7 | does not get a pass, [pic1](https://user-images.githubusercontent.com/241032/174189227-ae816026-cf6f-4be5-a26e-1b3b072c1b2f.png) - [pic2](https://user-images.githubusercontent.com/241032/174189225-5651c059-5152-46e9-ac26-7e98e497901b.png) |
| **SerenityOS** (7e98457) | hits a page fault, works with `?b=u`, file upload not-impl |
@@ -988,13 +993,25 @@ when uploading files,
some notes on hardening
on public copyparty instances with anonymous upload enabled:
* option `-s` is a shortcut to set the following options:
* `--no-thumb` disables thumbnails and audio transcoding to stop copyparty from running `FFmpeg`/`Pillow`/`VIPS` on uploaded files, which is a [good idea](https://www.cvedetails.com/vulnerability-list.php?vendor_id=3611) if anonymous upload is enabled
* `--no-mtag-ff` uses `mutagen` to grab music tags instead of `FFmpeg`, which is safer and faster but less accurate
* `--dotpart` hides uploads from directory listings while they're still incoming
* `--no-robots` and `--force-js` makes life harder for crawlers, see [hiding from google](#hiding-from-google)
* users can upload html/css/js which will evaluate for other visitors in a few ways,
* unless `--no-readme` is set: by uploading/modifying a file named `readme.md`
* if `move` access is granted AND none of `--no-logues`, `--no-dot-mv`, `--no-dot-ren` is set: by uploading some .html file and renaming it to `.epilogue.html` (uploading it directly is blocked)
* option `-ss` is a shortcut for the above plus:
* `--no-logues` and `--no-readme` disables support for readme's and prologues / epilogues in directory listings, which otherwise lets people upload arbitrary `<script>` tags
* `--unpost 0`, `--no-del`, `--no-mv` disables all move/delete support
* `--hardlink` creates hardlinks instead of symlinks when deduplicating uploads, which is less maintenance
* however note if you edit one file it will also affect the other copies
* `--vague-403` returns a "404 not found" instead of "403 forbidden" which is a common enterprise meme
* `--nih` removes the server hostname from directory listings
other misc:
* option `-sss` is a shortcut for the above plus:
* `-lo cpp-%Y-%m%d-%H%M%S.txt.xz` enables logging to disk
* `-ls **,*,ln,p,r` does a scan on startup for any dangerous symlinks
other misc notes:
* you can disable directory listings by giving permission `g` instead of `r`, only accepting direct URLs to files
* combine this with volume-flag `c,fk` to generate per-file accesskeys; users which have full read-access will then see URLs with `?k=...` appended to the end, and `g` users must provide that URL including the correct key to avoid a 404

View File

@@ -63,7 +63,7 @@ set -e
EOF
chmod 755 /usr/local/bin/chromium-browser
# start the server (note: replace `-v.::rw:` with `-v.::r:` to disallow retrieving uploaded stuff)
# start the server (note: replace `-v.::rw:` with `-v.::w:` to disallow retrieving uploaded stuff)
cd ~/Downloads; python3 copyparty-sfx.py --urlform save,get -v.::rw:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,very-bad-idea.py
"""

View File

@@ -474,14 +474,15 @@ def run_argparse(argv: list[str], formatter: Any) -> argparse.Namespace:
ap2 = ap.add_argument_group('upload options')
ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless -ed")
ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files")
ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled")
ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without -e2d; roughly 1 MiB RAM per 600")
ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload")
ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even if copyparty thinks you're better off without -- probably useful on nfs and cow filesystems (zfs, btrfs)")
ap2.add_argument("--hardlink", action="store_true", help="prefer hardlinks instead of symlinks when possible (within same filesystem)")
ap2.add_argument("--never-symlink", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made")
ap2.add_argument("--no-dedup", action="store_true", help="disable symlink/hardlink creation; copy file contents instead")
ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without -e2d; roughly 1 MiB RAM per 600")
ap2.add_argument("--thickfs", metavar="REGEX", type=u, default="fat|vfat|ex.?fat|hpfs|fuse", help="filesystems which dont support sparse files")
ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files")
ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; 0 = off and warn if enabled, 1 = off, 2 = on, 3 = on and disable datecheck")
ap2 = ap.add_argument_group('network options')

View File

@@ -1,8 +1,8 @@
# coding: utf-8
VERSION = (1, 3, 1)
VERSION = (1, 3, 2)
CODENAME = "god dag"
BUILD_DT = (2022, 6, 16)
BUILD_DT = (2022, 6, 20)
S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View File

@@ -21,25 +21,6 @@ class Fstab(object):
def __init__(self, log: RootLogger):
self.log_func = log
self.no_sparse = set(
[
"fuse", # termux-sdcard
"vfat", # linux-efi
"fat32",
"fat16",
"fat12",
"fat-32",
"fat-16",
"fat-12",
"fat 32",
"fat 16",
"fat 12",
"exfat",
"ex-fat",
"ex fat",
"hpfs", # macos
]
)
self.tab: Optional[VFS] = None
self.cache: dict[str, str] = {}
self.age = 0.0

View File

@@ -1048,7 +1048,7 @@ class HttpCli(object):
reader = read_socket(self.sr, remains)
f = None
fpool = not self.args.no_fpool and (not ANYWIN or sprs)
fpool = not self.args.no_fpool and sprs
if fpool:
with self.mutex:
try:
@@ -1855,7 +1855,7 @@ class HttpCli(object):
"edit": "edit" in self.uparam,
"title": html_escape(self.vpath, crlf=True),
"lastmod": int(ts_md * 1000),
"md_plug": "true" if self.args.emp else "false",
"have_emp": self.args.emp,
"md_chk_rate": self.args.mcr,
"md": boundary,
"arg_base": arg_base,
@@ -2384,6 +2384,7 @@ class HttpCli(object):
"perms": json.dumps(perms),
"taglist": [],
"def_hcols": [],
"have_emp": self.args.emp,
"have_up2k_idx": ("e2d" in vn.flags),
"have_tags_idx": ("e2t" in vn.flags),
"have_acode": (not self.args.no_acode),

View File

@@ -64,8 +64,8 @@ class SvcHub(object):
if args.ss or args.s >= 2:
args.s = True
args.no_dot_mv = True
args.no_dot_ren = True
args.no_logues = True
args.no_readme = True
args.unpost = 0
args.no_del = True
args.no_mv = True

View File

@@ -132,6 +132,7 @@ class Up2k(object):
thr.start()
self.fstab = Fstab(self.log_func)
self.no_sparse = re.compile(self.args.thickfs)
if self.args.no_fastboot:
self.deferred_init()
@@ -1326,7 +1327,7 @@ class Up2k(object):
# check if filesystem supports sparse files;
# refuse out-of-order / multithreaded uploading if sprs False
sprs = self.fstab.get(pdir) not in self.fstab.no_sparse
sprs = not self.no_sparse.match(self.fstab.get(pdir))
with self.mutex:
cur = self.cur.get(cj["ptop"])
@@ -1359,7 +1360,7 @@ class Up2k(object):
"prel": dp_dir,
"vtop": cj["vtop"],
"ptop": cj["ptop"],
"sprs": sprs,
"sprs": sprs, # dontcare; finished anyways
"size": dsize,
"lmod": dtime,
"addr": ip,
@@ -1494,7 +1495,7 @@ class Up2k(object):
"purl": purl,
"size": job["size"],
"lmod": job["lmod"],
"sprs": sprs,
"sprs": job.get("sprs", sprs),
"hash": job["need"],
"wark": wark,
}
@@ -2213,6 +2214,7 @@ class Up2k(object):
dip = job["addr"].replace(":", ".")
suffix = "-{:.6f}-{}".format(job["t0"], dip)
t0 = time.time()
with ren_open(tnam, "wb", fdir=pdir, suffix=suffix) as zfw:
f, job["tnam"] = zfw["orz"]
if (
@@ -2231,6 +2233,12 @@ class Up2k(object):
f.seek(job["size"] - 1)
f.write(b"e")
td = time.time() - t0
if td > 3 and not ANYWIN:
t = "WARNING: filesystem [{}] at [{}] probably does support sparse files; adjust the list in --thickfs and maybe create a github issue (please mention the filesystem + any related info about your setup if you do)"
fs = self.fstab.get(pdir)
self.log(t.format(fs, pdir), 1)
if not job["hash"]:
self._finish_upload(job["ptop"], job["wark"])

View File

@@ -371,7 +371,7 @@ html.dz {
--bg: #010;
--bgg: var(--bg);
--bg-d1: #000;
--bg-d2: #000;
--bg-d2: #020;
--bg-d3: #000;
--bg-max: #000;
@@ -381,8 +381,8 @@ html.dz {
--scroll: #0f0;
--a: #9f9;
--a-b: #fff;
--a-hil: #fff;
--a-b: #cfc;
--a-hil: #cfc;
--a-dark: #afa;
--a-gray: #2a2;
@@ -458,7 +458,7 @@ html.dz {
--f-sh1: 0.33;
--f-sh2: 0.02;
--f-sh3: 0.2;
--f-h-b1: rgba(128,128,128,0.7);
--f-h-b1: #3b3;
--f-play-bg: #fc5;
--f-play-fg: #000;
@@ -713,6 +713,7 @@ html.y #files thead th {
}
#files td:first-child {
border-radius: .25em 0 0 .25em;
white-space: nowrap;
}
#files td:last-child {
border-radius: 0 .25em .25em 0;
@@ -1841,6 +1842,7 @@ a.btn,
.full-image img,
.full-image video {
display: inline-block;
outline: none;
width: auto;
height: auto;
max-width: 100%;
@@ -2778,6 +2780,17 @@ html.cy #files tbody div a:last-child {
html.dz * {
border-radius: 0 !important;
}
html.d #treepar {
border-bottom: .2em solid var(--f-h-b1);
}
@media (min-width: 70em) {
#barpos,
#barbuf {

View File

@@ -148,6 +148,7 @@
have_unpost = {{ have_unpost|tojson }},
have_zip = {{ have_zip|tojson }},
turbolvl = {{ turbolvl|tojson }},
have_emp = {{ have_emp|tojson }},
txt_ext = "{{ txt_ext }}",
{% if no_prism %}no_prism = 1,{% endif %}
readme = {{ readme|tojson }},

View File

@@ -1008,7 +1008,7 @@ var mpl = (function () {
'<div><h3>' + L.ml_eq + '</h3><div id="audio_eq"></div></div>');
var r = {
"pb_mode": (sread('pb_mode') || 'loop').split('-')[0],
"pb_mode": (sread('pb_mode') || 'next').split('-')[0],
"os_ctl": bcfg_get('au_os_ctl', have_mctl) && have_mctl,
};
bcfg_bind(r, 'preload', 'au_preload', true);
@@ -2644,6 +2644,8 @@ var fileman = (function () {
if (!md.hasOwnProperty(k))
continue;
md[k] = (md[k] + '').replace(/[\/\\]/g, '-');
if (k.startsWith('.'))
md[k.slice(1)] = md[k];
}
@@ -5791,13 +5793,34 @@ function show_md(md, name, div, url, depth) {
});
}
md_plug = {}
md = load_md_plug(md, 'pre');
md = load_md_plug(md, 'post');
var marked_opts = {
headerPrefix: 'md-',
breaks: true,
gfm: true
};
var ext = md_plug.pre;
if (ext)
Object.assign(marked_opts, ext[0]);
try {
clmod(div, 'mdo', 1);
div.innerHTML = marked.parse(md, {
headerPrefix: 'md-',
breaks: true,
gfm: true
});
div.innerHTML = marked.parse(md, marked_opts);
ext = md_plug.post;
ext = ext ? [ext[0].render, ext[0].render2] : [];
for (var a = 0; a < ext.length; a++)
if (ext[a])
try {
ext[a](div);
}
catch (ex) {
console.log(ex);
}
var els = QSA('#epi a');
for (var a = 0, aa = els.length; a < aa; a++) {
var href = els[a].getAttribute('href');
@@ -6011,6 +6034,9 @@ function wintitle(txt) {
ebi('path').onclick = function (e) {
if (ctrl(e))
return true;
var a = e.target.closest('a[href]');
if (!a || !(a = a.getAttribute('href') + '') || !a.endsWith('/'))
return;
@@ -6038,9 +6064,11 @@ ebi('files').onclick = ebi('docul').onclick = function (e) {
tgt = e.target.closest('a[hl]');
if (tgt) {
var fun = function () {
showfile.show(noq_href(ebi(tgt.getAttribute('hl'))), tgt.getAttribute('lang'));
}, szs = ft2dict(tgt.closest('tr'))[0].sz,
var a = ebi(tgt.getAttribute('hl')),
fun = function () {
showfile.show(noq_href(a), tgt.getAttribute('lang'));
},
szs = ft2dict(a.closest('tr'))[0].sz,
sz = parseInt(szs.replace(/[, ]/g, ''));
if (sz < 1024 * 1024)

View File

@@ -127,10 +127,11 @@ write markdown (most html is 🙆 too)
<script>
var last_modified = {{ lastmod }};
var last_modified = {{ lastmod }},
have_emp = {{ have_emp|tojson }};
var md_opt = {
link_md_as_html: false,
allow_plugins: {{ md_plug }},
modpoll_freq: {{ md_chk_rate }}
};

View File

@@ -20,10 +20,6 @@ var dbg = function () { };
// dbg = console.log
// plugins
var md_plug = {};
// dodge browser issues
(function () {
var ua = navigator.userAgent;
@@ -160,7 +156,7 @@ function copydom(src, dst, lv) {
}
function md_plug_err(ex, js) {
md_plug_err = function (ex, js) {
qsr('#md_errbox');
if (!ex)
return;
@@ -197,50 +193,12 @@ function md_plug_err(ex, js) {
}
function load_plug(md_text, plug_type) {
if (!md_opt.allow_plugins)
return md_text;
var find = '\n```copyparty_' + plug_type + '\n';
var ofs = md_text.indexOf(find);
if (ofs === -1)
return md_text;
var ofs2 = md_text.indexOf('\n```', ofs + 1);
if (ofs2 == -1)
return md_text;
var js = md_text.slice(ofs + find.length, ofs2 + 1);
var md = md_text.slice(0, ofs + 1) + md_text.slice(ofs2 + 4);
var old_plug = md_plug[plug_type];
if (!old_plug || old_plug[1] != js) {
js = 'const x = { ' + js + ' }; x;';
try {
var x = eval(js);
}
catch (ex) {
md_plug[plug_type] = null;
md_plug_err(ex, js);
return md;
}
if (x['ctor']) {
x['ctor']();
delete x['ctor'];
}
md_plug[plug_type] = [x, js];
}
return md;
}
function convert_markdown(md_text, dest_dom) {
md_text = md_text.replace(/\r/g, '');
md_plug_err(null);
md_text = load_plug(md_text, 'pre');
md_text = load_plug(md_text, 'post');
md_text = load_md_plug(md_text, 'pre');
md_text = load_md_plug(md_text, 'post');
var marked_opts = {
//headerPrefix: 'h-',
@@ -248,7 +206,7 @@ function convert_markdown(md_text, dest_dom) {
gfm: true
};
var ext = md_plug['pre'];
var ext = md_plug.pre;
if (ext)
Object.assign(marked_opts, ext[0]);
@@ -349,7 +307,7 @@ function convert_markdown(md_text, dest_dom) {
el.innerHTML = '<a href="#' + id + '">' + el.innerHTML + '</a>';
}
ext = md_plug['post'];
ext = md_plug.post;
if (ext && ext[0].render)
try {
ext[0].render(md_dom);

View File

@@ -25,10 +25,11 @@
<a href="#" id="repl">π</a>
<script>
var last_modified = {{ lastmod }};
var last_modified = {{ lastmod }},
have_emp = {{ have_emp|tojson }};
var md_opt = {
link_md_as_html: false,
allow_plugins: {{ md_plug }},
modpoll_freq: {{ md_chk_rate }}
};

View File

@@ -380,11 +380,13 @@ html.y textarea:focus {
padding-left: 2em;
border-left: .3em solid #ddd;
}
.mdo ul>li,
.mdo ol>li {
.mdo ul>li {
margin: .7em 0;
list-style-type: disc;
}
.mdo ol>li {
margin: .7em 0 .7em 2em;
}
.mdo strong {
color: #000;
}

View File

@@ -85,15 +85,18 @@ catch (ex) {
}
var crashed = false, ignexd = {};
function vis_exh(msg, url, lineNo, columnNo, error) {
if ((msg + '').indexOf('ResizeObserver') !== -1)
if ((msg + '').indexOf('ResizeObserver') + 1)
return; // chrome issue 809574 (benign, from <video>)
if ((msg + '').indexOf('l2d.js') !== -1)
if ((msg + '').indexOf('l2d.js') + 1)
return; // `t` undefined in tapEvent -> hitTestSimpleCustom
if (!/\.js($|\?)/.exec('' + url))
return; // chrome debugger
if ((url + '').indexOf(' > eval') + 1)
return; // md timer
var ekey = url + '\n' + lineNo + '\n' + msg;
if (ignexd[ekey] || crashed)
return;
@@ -1355,6 +1358,49 @@ if (ebi('repl'))
ebi('repl').onclick = repl;
var md_plug = {};
var md_plug_err = function (ex, js) {
if (ex)
console.log(ex, js);
};
function load_md_plug(md_text, plug_type) {
if (!have_emp)
return md_text;
var find = '\n```copyparty_' + plug_type + '\n';
var ofs = md_text.indexOf(find);
if (ofs === -1)
return md_text;
var ofs2 = md_text.indexOf('\n```', ofs + 1);
if (ofs2 == -1)
return md_text;
var js = md_text.slice(ofs + find.length, ofs2 + 1);
var md = md_text.slice(0, ofs + 1) + md_text.slice(ofs2 + 4);
var old_plug = md_plug[plug_type];
if (!old_plug || old_plug[1] != js) {
js = 'const x = { ' + js + ' }; x;';
try {
var x = eval(js);
if (x['ctor']) {
x['ctor']();
delete x['ctor'];
}
}
catch (ex) {
md_plug[plug_type] = null;
md_plug_err(ex, js);
return md;
}
md_plug[plug_type] = [x, js];
}
return md;
}
var svg_decl = '<?xml version="1.0" encoding="UTF-8"?>\n';

View File

@@ -13,6 +13,9 @@
# other stuff
## [`changelog.md`](changelog.md)
* occasionally grabbed from github release notes
## [`rclone.md`](rclone.md)
* notes on using rclone as a fuse client/server

2581
docs/changelog.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -200,6 +200,9 @@ git pull; git reset --hard origin/HEAD && git log --format=format:"%H %ai %d" --
# download all sfx versions
curl https://api.github.com/repos/9001/copyparty/releases?per_page=100 | jq -r '.[] | .tag_name + " " + .name' | tr -d '\r' | while read v t; do fn="$(printf '%s\n' "copyparty $v $t.py" | tr / -)"; [ -e "$fn" ] || curl https://github.com/9001/copyparty/releases/download/$v/copyparty-sfx.py -Lo "$fn"; done
# convert releasenotes to changelog
curl https://api.github.com/repos/9001/copyparty/releases?per_page=100 | jq -r '.[] | "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ \n# \(.created_at) `\(.tag_name)` \(.name)\n\n\(.body)\n\n\n"' | sed -r 's/^# ([0-9]{4}-)([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})Z /# \1\2\3-\4\5 /' > changelog.md
# push to multiple git remotes
git config -l | grep '^remote'
git remote add all git@github.com:9001/copyparty.git