diff --git a/README.md b/README.md
index 6f7951a1..4ba6b60b 100644
--- a/README.md
+++ b/README.md
@@ -104,7 +104,7 @@ turn almost any device into a file server with resumable uploads/downloads using
* [sfx](#sfx) - the self-contained "binary"
* [copyparty.exe](#copypartyexe) - download [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) (win8+) or [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) (win7+)
* [install on android](#install-on-android)
-* [reporting bugs](#reporting-bugs) - ideas for context to include in bug reports
+* [reporting bugs](#reporting-bugs) - ideas for context to include, and where to submit them
* [devnotes](#devnotes) - for build instructions etc, see [./docs/devnotes.md](./docs/devnotes.md)
@@ -286,6 +286,9 @@ roughly sorted by chance of encounter
* cannot index non-ascii filenames with `-e2d`
* cannot handle filenames with mojibake
+if you have a new exciting bug to share, see [reporting bugs](#reporting-bugs)
+
+
## not my bugs
same order here too
@@ -341,9 +344,18 @@ upgrade notes
* yes, using the [`g` permission](#accounts-and-volumes), see the examples there
* you can also do this with linux filesystem permissions; `chmod 111 music` will make it possible to access files and folders inside the `music` folder but not list the immediate contents -- also works with other software, not just copyparty
+* can I link someone to a password-protected volume/file by including the password in the URL?
+ * yes, by adding `?pw=hunter2` to the end; replace `?` with `&` if there are parameters in the URL already, meaning it contains a `?` near the end
+
+* how do I stop `.hist` folders from appearing everywhere on my HDD?
+ * by default, a `.hist` folder is created inside each volume for the filesystem index, thumbnails, audio transcodes, and markdown document history. Use the `--hist` global-option or the `hist` volflag to move it somewhere else; see [database location](#database-location)
+
* can I make copyparty download a file to my server if I give it a URL?
* yes, using [hooks](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/wget.py)
+* firefox refuses to connect over https, saying "Secure Connection Failed" or "SEC_ERROR_BAD_SIGNATURE", but the usual button to "Accept the Risk and Continue" is not shown
+ * firefox has corrupted its certstore; fix this by exiting firefox, then find and delete the file named `cert9.db` somewhere in your firefox profile folder
+
* i want to learn python and/or programming and am considering looking at the copyparty source code in that occasion
* ```bash
_| _ __ _ _|_
@@ -1292,6 +1304,8 @@ the classname of the HTML tag is set according to the selected theme, which is u
see the top of [./copyparty/web/browser.css](./copyparty/web/browser.css) where the color variables are set, and there's layout-specific stuff near the bottom
+if you want to change the fonts, see [./docs/rice/](./docs/rice/)
+
## complete examples
@@ -1943,7 +1957,12 @@ if you want thumbnails (photos+videos) and you're okay with spending another 132
# reporting bugs
-ideas for context to include in bug reports
+ideas for context to include, and where to submit them
+
+please get in touch using any of the following URLs:
+* https://github.com/9001/copyparty/ **(primary)**
+* https://gitlab.com/9001/copyparty/ *(mirror)*
+* https://codeberg.org/9001/copyparty *(mirror)*
in general, commandline arguments (and config file if any)
@@ -1958,3 +1977,6 @@ if there's a wall of base64 in the log (thread stacks) then please include that,
# devnotes
for build instructions etc, see [./docs/devnotes.md](./docs/devnotes.md)
+
+see [./docs/TODO.md](./docs/TODO.md) for planned features / fixes / changes
+
diff --git a/contrib/README.md b/contrib/README.md
index 1dab4824..01a175f2 100644
--- a/contrib/README.md
+++ b/contrib/README.md
@@ -16,6 +16,8 @@
* sharex config file to upload screenshots and grab the URL
* `RequestURL`: full URL to the target folder
* `pw`: password (remove the `pw` line if anon-write)
+* the `act:bput` thing is optional since copyparty v1.9.29
+* using an older sharex version, maybe sharex v12.1.1 for example? dw fam i got your back đđđ [`sharex12.sxcu`](sharex12.sxcu)
### [`send-to-cpp.contextlet.json`](send-to-cpp.contextlet.json)
* browser integration, kind of? custom rightclick actions and stuff
diff --git a/contrib/sharex12.sxcu b/contrib/sharex12.sxcu
new file mode 100644
index 00000000..297eed50
--- /dev/null
+++ b/contrib/sharex12.sxcu
@@ -0,0 +1,13 @@
+{
+ "Name": "copyparty",
+ "DestinationType": "ImageUploader, TextUploader, FileUploader",
+ "RequestURL": "http://127.0.0.1:3923/sharex",
+ "FileFormName": "f",
+ "Arguments": {
+ "act": "bput"
+ },
+ "Headers": {
+ "accept": "url",
+ "pw": "PUT_YOUR_PASSWORD_HERE_MY_DUDE"
+ }
+}
diff --git a/copyparty/__main__.py b/copyparty/__main__.py
index f11672c1..64a36145 100755
--- a/copyparty/__main__.py
+++ b/copyparty/__main__.py
@@ -395,7 +395,7 @@ def configure_ssl_ciphers(al: argparse.Namespace) -> None:
def args_from_cfg(cfg_path: str) -> list[str]:
lines: list[str] = []
- expand_config_file(lines, cfg_path, "")
+ expand_config_file(None, lines, cfg_path, "")
lines = upgrade_cfg_fmt(None, argparse.Namespace(vc=False), lines, "")
ret: list[str] = []
@@ -876,6 +876,7 @@ def add_upload(ap):
ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless \033[33m-ed\033[0m")
ap2.add_argument("--plain-ip", action="store_true", help="when avoiding filename collisions by appending the uploader's ip to the filename: append the plaintext ip instead of salting and hashing the ip")
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, default=12h")
+ ap2.add_argument("--u2abort", metavar="NUM", type=int, default=1, help="clients can abort incomplete uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=u2abort)")
ap2.add_argument("--blank-wt", metavar="SEC", type=int, default=300, help="file write grace period (any client can write to a blank file last-modified more recently than \033[33mSEC\033[0m seconds ago)")
ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without \033[33m-e2d\033[0m; 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 (bad idea to enable this on windows and/or cow filesystems)")
@@ -1274,6 +1275,7 @@ def add_ui(ap, retry):
ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)")
ap2.add_argument("--pb-url", metavar="URL", type=u, default="https://github.com/9001/copyparty", help="powered-by link; disable with \033[33m-np\033[0m")
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m)")
+ ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
ap2.add_argument("--md-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to ALLOW for README.md docs (volflag=md_sbf); see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox")
ap2.add_argument("--lg-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to ALLOW for prologue/epilogue docs (volflag=lg_sbf)")
ap2.add_argument("--no-sb-md", action="store_true", help="don't sandbox README.md documents (volflags: no_sb_md | sb_md)")
diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py
index eba072f4..612d661c 100644
--- a/copyparty/authsrv.py
+++ b/copyparty/authsrv.py
@@ -991,7 +991,7 @@ class AuthSrv(object):
) -> None:
self.line_ctr = 0
- expand_config_file(cfg_lines, fp, "")
+ expand_config_file(self.log, cfg_lines, fp, "")
if self.args.vc:
lns = ["{:4}: {}".format(n, s) for n, s in enumerate(cfg_lines, 1)]
self.log("expanded config file (unprocessed):\n" + "\n".join(lns))
@@ -1745,7 +1745,7 @@ class AuthSrv(object):
if k not in vol.flags:
vol.flags[k] = getattr(self.args, k)
- for k in ("nrand",):
+ for k in ("nrand", "u2abort"):
if k in vol.flags:
vol.flags[k] = int(vol.flags[k])
@@ -2367,27 +2367,50 @@ def split_cfg_ln(ln: str) -> dict[str, Any]:
return ret
-def expand_config_file(ret: list[str], fp: str, ipath: str) -> None:
+def expand_config_file(
+ log: Optional["NamedLogger"], ret: list[str], fp: str, ipath: str
+) -> None:
"""expand all % file includes"""
fp = absreal(fp)
if len(ipath.split(" -> ")) > 64:
raise Exception("hit max depth of 64 includes")
if os.path.isdir(fp):
- names = os.listdir(fp)
- crumb = "#\033[36m cfg files in {} => {}\033[0m".format(fp, names)
- ret.append(crumb)
- for fn in sorted(names):
+ names = list(sorted(os.listdir(fp)))
+ cnames = [x for x in names if x.lower().endswith(".conf")]
+ if not cnames:
+ t = "warning: tried to read config-files from folder '%s' but it does not contain any "
+ if names:
+ t += ".conf files; the following files were ignored: %s"
+ t = t % (fp, ", ".join(names[:8]))
+ else:
+ t += "files at all"
+ t = t % (fp,)
+
+ if log:
+ log(t, 3)
+
+ ret.append("#\033[33m %s\033[0m" % (t,))
+ else:
+ zs = "#\033[36m cfg files in %s => %s\033[0m" % (fp, cnames)
+ ret.append(zs)
+
+ for fn in cnames:
fp2 = os.path.join(fp, fn)
- if not fp2.endswith(".conf") or fp2 in ipath:
+ if fp2 in ipath:
continue
- expand_config_file(ret, fp2, ipath)
+ expand_config_file(log, ret, fp2, ipath)
- if ret[-1] == crumb:
- # no config files below; remove breadcrumb
- ret.pop()
+ return
+ if not os.path.exists(fp):
+ t = "warning: tried to read config from '%s' but the file/folder does not exist"
+ t = t % (fp,)
+ if log:
+ log(t, 3)
+
+ ret.append("#\033[31m %s\033[0m" % (t,))
return
ipath += " -> " + fp
@@ -2401,7 +2424,7 @@ def expand_config_file(ret: list[str], fp: str, ipath: str) -> None:
fp2 = ln[1:].strip()
fp2 = os.path.join(os.path.dirname(fp), fp2)
ofs = len(ret)
- expand_config_file(ret, fp2, ipath)
+ expand_config_file(log, ret, fp2, ipath)
for n in range(ofs, len(ret)):
ret[n] = pad + ret[n]
continue
diff --git a/copyparty/cfg.py b/copyparty/cfg.py
index 10e921e8..9781979f 100644
--- a/copyparty/cfg.py
+++ b/copyparty/cfg.py
@@ -66,6 +66,7 @@ def vf_vmap() -> dict[str, str]:
"rm_retry",
"sort",
"unlist",
+ "u2abort",
"u2ts",
):
ret[k] = k
@@ -116,6 +117,7 @@ flagcats = {
"hardlink": "does dedup with hardlinks instead of symlinks",
"neversymlink": "disables symlink fallback; full copy instead",
"copydupes": "disables dedup, always saves full copies of dupes",
+ "sparse": "force use of sparse files, mainly for s3-backed storage",
"daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files",
"nosub": "forces all uploads into the top folder of the vfs",
"magic": "enables filetype detection for nameless uploads",
@@ -130,6 +132,7 @@ flagcats = {
"rand": "force randomized filenames, 9 chars long by default",
"nrand=N": "randomized filenames are N chars long",
"u2ts=fc": "[f]orce [c]lient-last-modified or [u]pload-time",
+ "u2abort=1": "allow aborting unfinished uploads? 0=no 1=strict 2=ip-chk 3=acct-chk",
"sz=1k-3m": "allow filesizes between 1 KiB and 3MiB",
"df=1g": "ensure 1 GiB free disk space",
},
diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py
index 4d72c4b1..4d9c4879 100644
--- a/copyparty/ftpd.py
+++ b/copyparty/ftpd.py
@@ -300,7 +300,7 @@ class FtpFs(AbstractedFS):
vp = join(self.cwd, path).lstrip("/")
try:
- self.hub.up2k.handle_rm(self.uname, self.h.cli_ip, [vp], [], False)
+ self.hub.up2k.handle_rm(self.uname, self.h.cli_ip, [vp], [], False, False)
except Exception as ex:
raise FSE(str(ex))
diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py
index bc18a3db..0c0c3a0a 100644
--- a/copyparty/httpcli.py
+++ b/copyparty/httpcli.py
@@ -319,7 +319,9 @@ class HttpCli(object):
if self.args.xff_re and not self.args.xff_re.match(pip):
t = 'got header "%s" from untrusted source "%s" claiming the true client ip is "%s" (raw value: "%s"); if you trust this, you must allowlist this proxy with "--xff-src=%s"'
if self.headers.get("cf-connecting-ip"):
- t += " Alternatively, if you are behind cloudflare, it is better to specify these two instead: --xff-hdr=cf-connecting-ip --xff-src=any"
+ t += ' Note: if you are behind cloudflare, then this default header is not a good choice; please first make sure your local reverse-proxy (if any) does not allow non-cloudflare IPs from providing cf-* headers, and then add this additional global setting: "--xff-hdr=cf-connecting-ip"'
+ else:
+ t += ' Note: depending on your reverse-proxy, and/or WAF, and/or other intermediates, you may want to read the true client IP from another header by also specifying "--xff-hdr=SomeOtherHeader"'
zs = (
".".join(pip.split(".")[:2]) + "."
if "." in pip
@@ -529,9 +531,13 @@ class HttpCli(object):
return self.handle_options() and self.keepalive
if not cors_k:
+ host = self.headers.get("host", ">")
origin = self.headers.get("origin", ">")
- self.log("cors-reject {} from {}".format(self.mode, origin), 3)
- raise Pebkac(403, "no surfing")
+ proto = "https://" if self.is_https else "http://"
+ guess = "modifying" if (origin and host) else "stripping"
+ t = "cors-reject %s because request-header Origin='%s' does not match request-protocol '%s' and host '%s' based on request-header Host='%s' (note: if this request is not malicious, check if your reverse-proxy is accidentally %s request headers, in particular 'Origin', for example by running copyparty with --ihead='*' to show all request headers)"
+ self.log(t % (self.mode, origin, proto, self.host, host, guess), 3)
+ raise Pebkac(403, "rejected by cors-check")
# getattr(self.mode) is not yet faster than this
if self.mode == "POST":
@@ -662,7 +668,11 @@ class HttpCli(object):
def k304(self) -> bool:
k304 = self.cookies.get("k304")
- return k304 == "y" or ("; Trident/" in self.ua and not k304)
+ return (
+ k304 == "y"
+ or (self.args.k304 == 2 and k304 != "n")
+ or ("; Trident/" in self.ua and not k304)
+ )
def send_headers(
self,
@@ -2838,11 +2848,11 @@ class HttpCli(object):
logtail = ""
#
- # if request is for foo.js, check if we have foo.js.{gz,br}
+ # if request is for foo.js, check if we have foo.js.gz
file_ts = 0.0
editions: dict[str, tuple[str, int]] = {}
- for ext in ["", ".gz", ".br"]:
+ for ext in ("", ".gz"):
try:
fs_path = req_path + ext
st = bos.stat(fs_path)
@@ -2887,12 +2897,7 @@ class HttpCli(object):
x.strip()
for x in self.headers.get("accept-encoding", "").lower().split(",")
]
- if ".br" in editions and "br" in supported_editions:
- is_compressed = True
- selected_edition = ".br"
- fs_path, file_sz = editions[".br"]
- self.out_headers["Content-Encoding"] = "br"
- elif ".gz" in editions:
+ if ".gz" in editions:
is_compressed = True
selected_edition = ".gz"
fs_path, file_sz = editions[".gz"]
@@ -2908,13 +2913,8 @@ class HttpCli(object):
is_compressed = False
selected_edition = "plain"
- try:
- fs_path, file_sz = editions[selected_edition]
- logmsg += "{} ".format(selected_edition.lstrip("."))
- except:
- # client is old and we only have .br
- # (could make brotli a dep to fix this but it's not worth)
- raise Pebkac(404)
+ fs_path, file_sz = editions[selected_edition]
+ logmsg += "{} ".format(selected_edition.lstrip("."))
#
# partial
@@ -3369,6 +3369,7 @@ class HttpCli(object):
dbwt=vs["dbwt"],
url_suf=suf,
k304=self.k304(),
+ k304vis=self.args.k304 > 0,
ver=S_VERSION if self.args.ver else "",
ahttps="" if self.is_https else "https://" + self.host + self.req,
)
@@ -3377,7 +3378,7 @@ class HttpCli(object):
def set_k304(self) -> bool:
v = self.uparam["k304"].lower()
- if v == "y":
+ if v in "yn":
dur = 86400 * 299
else:
dur = 0
@@ -3560,8 +3561,7 @@ class HttpCli(object):
return ret
def tx_ups(self) -> bool:
- if not self.args.unpost:
- raise Pebkac(403, "the unpost feature is disabled in server config")
+ have_unpost = self.args.unpost and "e2d" in self.vn.flags
idx = self.conn.get_u2idx()
if not idx or not hasattr(idx, "p_end"):
@@ -3580,7 +3580,14 @@ class HttpCli(object):
if "fk" in vol.flags
and (self.uname in vol.axs.uread or self.uname in vol.axs.upget)
}
- for vol in self.asrv.vfs.all_vols.values():
+
+ x = self.conn.hsrv.broker.ask(
+ "up2k.get_unfinished_by_user", self.uname, self.ip
+ )
+ uret = x.get()
+
+ allvols = self.asrv.vfs.all_vols if have_unpost else {}
+ for vol in allvols.values():
cur = idx.get_cur(vol.realpath)
if not cur:
continue
@@ -3632,9 +3639,13 @@ class HttpCli(object):
for v in ret:
v["vp"] = self.args.SR + v["vp"]
- jtxt = json.dumps(ret, indent=2, sort_keys=True).encode("utf-8", "replace")
- self.log("{} #{} {:.2f}sec".format(lm, len(ret), time.time() - t0))
- self.reply(jtxt, mime="application/json")
+ if not have_unpost:
+ ret = [{"kinshi": 1}]
+
+ jtxt = '{"u":%s,"c":%s}' % (uret, json.dumps(ret, indent=0))
+ zi = len(uret.split('\n"pd":')) - 1
+ self.log("%s #%d+%d %.2fsec" % (lm, zi, len(ret), time.time() - t0))
+ self.reply(jtxt.encode("utf-8", "replace"), mime="application/json")
return True
def handle_rm(self, req: list[str]) -> bool:
@@ -3649,11 +3660,12 @@ class HttpCli(object):
elif self.is_vproxied:
req = [x[len(self.args.SR) :] for x in req]
+ unpost = "unpost" in self.uparam
nlim = int(self.uparam.get("lim") or 0)
lim = [nlim, nlim] if nlim else []
x = self.conn.hsrv.broker.ask(
- "up2k.handle_rm", self.uname, self.ip, req, lim, False
+ "up2k.handle_rm", self.uname, self.ip, req, lim, False, unpost
)
self.loud_reply(x.get())
return True
diff --git a/copyparty/httpsrv.py b/copyparty/httpsrv.py
index 40bd108f..cade2df7 100644
--- a/copyparty/httpsrv.py
+++ b/copyparty/httpsrv.py
@@ -191,7 +191,7 @@ class HttpSrv(object):
for fn in df:
ap = absreal(os.path.join(dp, fn))
self.statics.add(ap)
- if ap.endswith(".gz") or ap.endswith(".br"):
+ if ap.endswith(".gz"):
self.statics.add(ap[:-3])
def set_netdevs(self, netdevs: dict[str, Netdev]) -> None:
diff --git a/copyparty/metrics.py b/copyparty/metrics.py
index 72e86fdb..3af8be9d 100644
--- a/copyparty/metrics.py
+++ b/copyparty/metrics.py
@@ -206,6 +206,9 @@ class Metrics(object):
try:
x = self.hsrv.broker.ask("up2k.get_unfinished")
xs = x.get()
+ if not xs:
+ raise Exception("up2k mutex acquisition timed out")
+
xj = json.loads(xs)
for ptop, (nbytes, nfiles) in xj.items():
tnbytes += nbytes
diff --git a/copyparty/smbd.py b/copyparty/smbd.py
index 1e97386d..979c11df 100644
--- a/copyparty/smbd.py
+++ b/copyparty/smbd.py
@@ -340,7 +340,7 @@ class SMB(object):
yeet("blocked delete (no-del-acc): " + vpath)
vpath = vpath.replace("\\", "/").lstrip("/")
- self.hub.up2k.handle_rm(uname, "1.7.6.2", [vpath], [], False)
+ self.hub.up2k.handle_rm(uname, "1.7.6.2", [vpath], [], False, False)
def _utime(self, vpath: str, times: tuple[float, float]) -> None:
if not self.args.smbw:
diff --git a/copyparty/svchub.py b/copyparty/svchub.py
index 08335ad2..80ead14c 100644
--- a/copyparty/svchub.py
+++ b/copyparty/svchub.py
@@ -28,7 +28,7 @@ if True: # pylint: disable=using-constant-test
import typing
from typing import Any, Optional, Union
-from .__init__ import ANYWIN, EXE, MACOS, TYPE_CHECKING, EnvParams, unicode
+from .__init__ import ANYWIN, E, EXE, MACOS, TYPE_CHECKING, EnvParams, unicode
from .authsrv import BAD_CFG, AuthSrv
from .cert import ensure_cert
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE
@@ -94,7 +94,7 @@ class SvcHub(object):
self.stopping = False
self.stopped = False
self.reload_req = False
- self.reloading = False
+ self.reloading = 0
self.stop_cond = threading.Condition()
self.nsigs = 3
self.retcode = 0
@@ -154,6 +154,8 @@ class SvcHub(object):
lg.handlers = [lh]
lg.setLevel(logging.DEBUG)
+ self._check_env()
+
if args.stackmon:
start_stackmon(args.stackmon, 0)
@@ -385,6 +387,17 @@ class SvcHub(object):
Daemon(self.sd_notify, "sd-notify")
+ def _check_env(self) -> None:
+ try:
+ files = os.listdir(E.cfg)
+ except:
+ files = []
+
+ hits = [x for x in files if x.lower().endswith(".conf")]
+ if hits:
+ t = "WARNING: found config files in [%s]: %s\n config files are not expected here, and will NOT be loaded (unless your setup is intentionally hella funky)"
+ self.log("root", t % (E.cfg, ", ".join(hits)), 3)
+
def _process_config(self) -> bool:
al = self.args
@@ -674,23 +687,24 @@ class SvcHub(object):
self.log("root", "ssdp startup failed;\n" + min_ex(), 3)
def reload(self) -> str:
- if self.reloading:
- return "cannot reload; already in progress"
+ with self.up2k.mutex:
+ if self.reloading:
+ return "cannot reload; already in progress"
+ self.reloading = 1
- self.reloading = True
Daemon(self._reload, "reloading")
return "reload initiated"
def _reload(self, rescan_all_vols: bool = True) -> None:
- self.reloading = True
- self.log("root", "reload scheduled")
with self.up2k.mutex:
- self.reloading = True
+ if self.reloading != 1:
+ return
+ self.reloading = 2
+ self.log("root", "reloading config")
self.asrv.reload()
self.up2k.reload(rescan_all_vols)
self.broker.reload()
-
- self.reloading = False
+ self.reloading = 0
def stop_thr(self) -> None:
while not self.stop_req:
diff --git a/copyparty/tftpd.py b/copyparty/tftpd.py
index 0020e96a..7b09533a 100644
--- a/copyparty/tftpd.py
+++ b/copyparty/tftpd.py
@@ -360,7 +360,7 @@ class Tftpd(object):
yeet("attempted delete of non-empty file")
vpath = vpath.replace("\\", "/").lstrip("/")
- self.hub.up2k.handle_rm("*", "8.3.8.7", [vpath], [], False)
+ self.hub.up2k.handle_rm("*", "8.3.8.7", [vpath], [], False, False)
def _access(self, *a: Any) -> bool:
return True
diff --git a/copyparty/up2k.py b/copyparty/up2k.py
index c154431c..1418e36d 100644
--- a/copyparty/up2k.py
+++ b/copyparty/up2k.py
@@ -200,15 +200,15 @@ class Up2k(object):
Daemon(self.deferred_init, "up2k-deferred-init")
def reload(self, rescan_all_vols: bool) -> None:
- self.gid += 1
- self.log("reload #{} initiated".format(self.gid))
+ """mutex me"""
+ self.log("reload #{} scheduled".format(self.gid + 1))
all_vols = self.asrv.vfs.all_vols
scan_vols = [k for k, v in all_vols.items() if v.realpath not in self.registry]
if rescan_all_vols:
scan_vols = list(all_vols.keys())
- self.rescan(all_vols, scan_vols, True, False)
+ self._rescan(all_vols, scan_vols, True, False)
def deferred_init(self) -> None:
all_vols = self.asrv.vfs.all_vols
@@ -237,7 +237,7 @@ class Up2k(object):
for n in range(max(1, self.args.mtag_mt)):
Daemon(self._tagger, "tagger-{}".format(n))
- Daemon(self._run_all_mtp, "up2k-mtp-init")
+ Daemon(self._run_all_mtp, "up2k-mtp-init", (self.gid,))
def log(self, msg: str, c: Union[int, str] = 0) -> None:
if self.pp:
@@ -287,9 +287,48 @@ class Up2k(object):
}
return json.dumps(ret, indent=4)
+ def get_unfinished_by_user(self, uname, ip) -> str:
+ if PY2 or not self.mutex.acquire(timeout=2):
+ return '[{"timeout":1}]'
+
+ ret: list[tuple[int, str, int, int, int]] = []
+ try:
+ for ptop, tab2 in self.registry.items():
+ cfg = self.flags.get(ptop, {}).get("u2abort", 1)
+ if not cfg:
+ continue
+ addr = (ip or "\n") if cfg in (1, 2) else ""
+ user = (uname or "\n") if cfg in (1, 3) else ""
+ drp = self.droppable.get(ptop, {})
+ for wark, job in tab2.items():
+ if (
+ wark in drp
+ or (user and user != job["user"])
+ or (addr and addr != job["addr"])
+ ):
+ continue
+
+ zt5 = (
+ int(job["t0"]),
+ djoin(job["vtop"], job["prel"], job["name"]),
+ job["size"],
+ len(job["need"]),
+ len(job["hash"]),
+ )
+ ret.append(zt5)
+ finally:
+ self.mutex.release()
+
+ ret.sort(reverse=True)
+ ret2 = [
+ {"at": at, "vp": "/" + vp, "pd": 100 - ((nn * 100) // (nh or 1)), "sz": sz}
+ for (at, vp, sz, nn, nh) in ret
+ ]
+ return json.dumps(ret2, indent=0)
+
def get_unfinished(self) -> str:
if PY2 or not self.mutex.acquire(timeout=0.5):
- return "{}"
+ return ""
ret: dict[str, tuple[int, int]] = {}
try:
@@ -342,14 +381,21 @@ class Up2k(object):
def rescan(
self, all_vols: dict[str, VFS], scan_vols: list[str], wait: bool, fscan: bool
) -> str:
+ with self.mutex:
+ return self._rescan(all_vols, scan_vols, wait, fscan)
+
+ def _rescan(
+ self, all_vols: dict[str, VFS], scan_vols: list[str], wait: bool, fscan: bool
+ ) -> str:
+ """mutex me"""
if not wait and self.pp:
return "cannot initiate; scan is already in progress"
- args = (all_vols, scan_vols, fscan)
+ self.gid += 1
Daemon(
self.init_indexes,
"up2k-rescan-{}".format(scan_vols[0] if scan_vols else "all"),
- args,
+ (all_vols, scan_vols, fscan, self.gid),
)
return ""
@@ -461,7 +507,7 @@ class Up2k(object):
if vp:
fvp = "%s/%s" % (vp, fvp)
- self._handle_rm(LEELOO_DALLAS, "", fvp, [], True)
+ self._handle_rm(LEELOO_DALLAS, "", fvp, [], True, False)
nrm += 1
if nrm:
@@ -580,19 +626,32 @@ class Up2k(object):
return True, ret
def init_indexes(
- self, all_vols: dict[str, VFS], scan_vols: list[str], fscan: bool
+ self, all_vols: dict[str, VFS], scan_vols: list[str], fscan: bool, gid: int = 0
) -> bool:
- gid = self.gid
- while self.pp and gid == self.gid:
- time.sleep(0.1)
+ if not gid:
+ with self.mutex:
+ gid = self.gid
- if gid != self.gid:
- return False
+ nspin = 0
+ while True:
+ nspin += 1
+ if nspin > 1:
+ time.sleep(0.1)
+
+ with self.mutex:
+ if gid != self.gid:
+ return False
+
+ if self.pp:
+ continue
+
+ self.pp = ProgressPrinter(self.log, self.args)
+
+ break
if gid:
- self.log("reload #{} running".format(self.gid))
+ self.log("reload #%d running" % (gid,))
- self.pp = ProgressPrinter(self.log, self.args)
vols = list(all_vols.values())
t0 = time.time()
have_e2d = False
@@ -780,7 +839,7 @@ class Up2k(object):
if self.mtag:
t = "online (running mtp)"
if scan_vols:
- thr = Daemon(self._run_all_mtp, "up2k-mtp-scan", r=False)
+ thr = Daemon(self._run_all_mtp, "up2k-mtp-scan", (gid,), r=False)
else:
self.pp = None
t = "online, idle"
@@ -1814,8 +1873,7 @@ class Up2k(object):
self.pending_tags = []
return ret
- def _run_all_mtp(self) -> None:
- gid = self.gid
+ def _run_all_mtp(self, gid: int) -> None:
t0 = time.time()
for ptop, flags in self.flags.items():
if "mtp" in flags:
@@ -2676,6 +2734,9 @@ class Up2k(object):
a = [job[x] for x in zs.split()]
self.db_add(cur, vfs.flags, *a)
cur.connection.commit()
+ elif wark in reg:
+ # checks out, but client may have hopped IPs
+ job["addr"] = cj["addr"]
if not job:
ap1 = djoin(cj["ptop"], cj["prel"])
@@ -3212,7 +3273,13 @@ class Up2k(object):
pass
def handle_rm(
- self, uname: str, ip: str, vpaths: list[str], lim: list[int], rm_up: bool
+ self,
+ uname: str,
+ ip: str,
+ vpaths: list[str],
+ lim: list[int],
+ rm_up: bool,
+ unpost: bool,
) -> str:
n_files = 0
ok = {}
@@ -3222,7 +3289,7 @@ class Up2k(object):
self.log("hit delete limit of {} files".format(lim[1]), 3)
break
- a, b, c = self._handle_rm(uname, ip, vp, lim, rm_up)
+ a, b, c = self._handle_rm(uname, ip, vp, lim, rm_up, unpost)
n_files += a
for k in b:
ok[k] = 1
@@ -3236,25 +3303,43 @@ class Up2k(object):
return "deleted {} files (and {}/{} folders)".format(n_files, iok, iok + ing)
def _handle_rm(
- self, uname: str, ip: str, vpath: str, lim: list[int], rm_up: bool
+ self, uname: str, ip: str, vpath: str, lim: list[int], rm_up: bool, unpost: bool
) -> tuple[int, list[str], list[str]]:
self.db_act = time.time()
- try:
+ partial = ""
+ if not unpost:
permsets = [[True, False, False, True]]
vn, rem = self.asrv.vfs.get(vpath, uname, *permsets[0])
vn, rem = vn.get_dbv(rem)
- unpost = False
- except:
+ else:
# unpost with missing permissions? verify with db
- if not self.args.unpost:
- raise Pebkac(400, "the unpost feature is disabled in server config")
-
- unpost = True
permsets = [[False, True]]
vn, rem = self.asrv.vfs.get(vpath, uname, *permsets[0])
vn, rem = vn.get_dbv(rem)
+ ptop = vn.realpath
with self.mutex:
- _, _, _, _, dip, dat = self._find_from_vpath(vn.realpath, rem)
+ abrt_cfg = self.flags.get(ptop, {}).get("u2abort", 1)
+ addr = (ip or "\n") if abrt_cfg in (1, 2) else ""
+ user = (uname or "\n") if abrt_cfg in (1, 3) else ""
+ reg = self.registry.get(ptop, {}) if abrt_cfg else {}
+ for wark, job in reg.items():
+ if (user and user != job["user"]) or (addr and addr != job["addr"]):
+ continue
+ if djoin(job["prel"], job["name"]) == rem:
+ if job["ptop"] != ptop:
+ t = "job.ptop [%s] != vol.ptop [%s] ??"
+ raise Exception(t % (job["ptop"] != ptop))
+ partial = vn.canonical(vjoin(job["prel"], job["tnam"]))
+ break
+ if partial:
+ dip = ip
+ dat = time.time()
+ else:
+ if not self.args.unpost:
+ t = "the unpost feature is disabled in server config"
+ raise Pebkac(400, t)
+
+ _, _, _, _, dip, dat = self._find_from_vpath(ptop, rem)
t = "you cannot delete this: "
if not dip:
@@ -3347,6 +3432,9 @@ class Up2k(object):
cur.connection.commit()
wunlink(self.log, abspath, dbv.flags)
+ if partial:
+ wunlink(self.log, partial, dbv.flags)
+ partial = ""
if xad:
runhook(
self.log,
@@ -3942,7 +4030,13 @@ class Up2k(object):
if not ANYWIN and sprs and sz > 1024 * 1024:
fs = self.fstab.get(pdir)
- if fs != "ok":
+ if fs == "ok":
+ pass
+ elif "sparse" in self.flags[job["ptop"]]:
+ t = "volflag 'sparse' is forcing use of sparse files for uploads to [%s]"
+ self.log(t % (job["ptop"],))
+ relabel = True
+ else:
relabel = True
f.seek(1024 * 1024 - 1)
f.write(b"e")
diff --git a/copyparty/web/browser.css b/copyparty/web/browser.css
index ca31c7a3..c1e8595f 100644
--- a/copyparty/web/browser.css
+++ b/copyparty/web/browser.css
@@ -494,6 +494,7 @@ html.dz {
text-shadow: none;
font-family: 'scp', monospace, monospace;
+ font-family: var(--font-mono), 'scp', monospace, monospace;
}
html.dy {
--fg: #000;
@@ -603,6 +604,7 @@ html {
color: var(--fg);
background: var(--bgg);
font-family: sans-serif;
+ font-family: var(--font-main), sans-serif;
text-shadow: 1px 1px 0px var(--bg-max);
}
html, body {
@@ -611,6 +613,7 @@ html, body {
}
pre, code, tt, #doc, #doc>code {
font-family: 'scp', monospace, monospace;
+ font-family: var(--font-mono), 'scp', monospace, monospace;
}
.ayjump {
position: fixed;
@@ -759,6 +762,7 @@ html #files.hhpick thead th {
}
#files tbody td:nth-child(3) {
font-family: 'scp', monospace, monospace;
+ font-family: var(--font-mono), 'scp', monospace, monospace;
text-align: right;
padding-right: 1em;
white-space: nowrap;
@@ -821,6 +825,7 @@ html.y #path a:hover {
.logue.raw {
white-space: pre;
font-family: 'scp', 'consolas', monospace;
+ font-family: var(--font-mono), 'scp', 'consolas', monospace;
}
#doc>iframe,
.logue>iframe {
@@ -1417,6 +1422,7 @@ input[type="checkbox"]:checked+label {
}
html.dz input {
font-family: 'scp', monospace, monospace;
+ font-family: var(--font-mono), 'scp', monospace, monospace;
}
.opwide div>span>input+label {
padding: .3em 0 .3em .3em;
@@ -1702,6 +1708,7 @@ html.y #tree.nowrap .ntree a+a:hover {
}
.ntree a:first-child {
font-family: 'scp', monospace, monospace;
+ font-family: var(--font-mono), 'scp', monospace, monospace;
font-size: 1.2em;
line-height: 0;
}
@@ -1832,6 +1839,10 @@ html.y #tree.nowrap .ntree a+a:hover {
margin: 0;
padding: 0;
}
+#unpost td:nth-child(3),
+#unpost td:nth-child(4) {
+ text-align: right;
+}
#rui {
background: #fff;
background: var(--bg);
@@ -1859,6 +1870,7 @@ html.y #tree.nowrap .ntree a+a:hover {
}
#rn_vadv input {
font-family: 'scp', monospace, monospace;
+ font-family: var(--font-mono), 'scp', monospace, monospace;
}
#rui td+td,
#rui td input[type="text"] {
@@ -1922,6 +1934,7 @@ html.y #doc {
#doc.mdo {
white-space: normal;
font-family: sans-serif;
+ font-family: var(--font-main), sans-serif;
}
#doc.prism * {
line-height: 1.5em;
@@ -1981,6 +1994,7 @@ a.btn,
}
#hkhelp td:first-child {
font-family: 'scp', monospace, monospace;
+ font-family: var(--font-mono), 'scp', monospace, monospace;
}
html.noscroll,
html.noscroll .sbar {
@@ -2490,6 +2504,7 @@ html.y #bbox-overlay figcaption a {
}
#op_up2k.srch td.prog {
font-family: sans-serif;
+ font-family: var(--font-main), sans-serif;
font-size: 1em;
width: auto;
}
@@ -2504,6 +2519,7 @@ html.y #bbox-overlay figcaption a {
white-space: nowrap;
display: inline-block;
font-family: 'scp', monospace, monospace;
+ font-family: var(--font-mono), 'scp', monospace, monospace;
}
#u2etas.o {
width: 20em;
@@ -2573,6 +2589,7 @@ html.y #bbox-overlay figcaption a {
#u2cards span {
color: var(--fg-max);
font-family: 'scp', monospace;
+ font-family: var(--font-mono), 'scp', monospace;
}
#u2cards > a:nth-child(4) > span {
display: inline-block;
@@ -2738,6 +2755,7 @@ html.b #u2conf a.b:hover {
}
.prog {
font-family: 'scp', monospace, monospace;
+ font-family: var(--font-mono), 'scp', monospace, monospace;
}
#u2tab span.inf,
#u2tab span.ok,
diff --git a/copyparty/web/browser.html b/copyparty/web/browser.html
index 0f48b211..76f6fcca 100644
--- a/copyparty/web/browser.html
+++ b/copyparty/web/browser.html
@@ -7,9 +7,9 @@
-{{ html_head }}
+{{ html_head }}
{%- if css %}
{%- endif %}
diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js
index 03fb2339..5c9ec3bc 100644
--- a/copyparty/web/browser.js
+++ b/copyparty/web/browser.js
@@ -102,7 +102,7 @@ var Ls = {
"access": " access",
"ot_close": "close submenu",
"ot_search": "search for files by attributes, path / name, music tags, or any combination of those$N$N<code>foo bar</code> = must contain both «foo» and «bar»,$N<code>foo -bar</code> = must contain «foo» but not «bar»,$N<code>^yana .opus$</code> = start with «yana» and be an «opus» file$N<code>"try unite"</code> = contain exactly «try unite»$N$Nthe date format is iso-8601, like$N<code>2009-12-31</code> or <code>2020-09-12 23:30:00</code>",
- "ot_unpost": "unpost: delete your recent uploads",
+ "ot_unpost": "unpost: delete your recent uploads, or abort unfinished ones",
"ot_bup": "bup: basic uploader, even supports netscape 4.0",
"ot_mkdir": "mkdir: create a new directory",
"ot_md": "new-md: create a new markdown document",
@@ -240,13 +240,14 @@ var Ls = {
"ml_drc": "dynamic range compressor",
"mt_shuf": "shuffle the songs in each folder\">đ",
+ "mt_aplay": "autoplay if there is a song-ID in the link you clicked to access the server$N$Ndisabling this will also stop the page URL from being updated with song-IDs when playing music, to prevent autoplay if these settings are lost but the URL remains\">aâ¶",
"mt_preload": "start loading the next song near the end for gapless playback\">preload",
"mt_prescan": "go to the next folder before the last song$Nends, keeping the webbrowser happy$Nso it doesn't stop the playback\">nav",
"mt_fullpre": "try to preload the entire song;$Nâ
enable on unreliable connections,$Nâ disable on slow connections probably\">full",
"mt_waves": "waveform seekbar:$Nshow audio amplitude in the scrubber\">~s",
"mt_npclip": "show buttons for clipboarding the currently playing song\">/np",
"mt_octl": "os integration (media hotkeys / osd)\">os-ctl",
- "mt_oseek": "allow seeking through os integration\">seek",
+ "mt_oseek": "allow seeking through os integration$N$Nnote: on some devices (iPhones),$Nthis replaces the next-song button\">seek",
"mt_oscv": "show album cover in osd\">art",
"mt_follow": "keep the playing track scrolled into view\">đŻ",
"mt_compact": "compact controls\">â",
@@ -388,6 +389,8 @@ var Ls = {
"md_eshow": "cannot render ",
"md_off": "[đreadme] disabled in [âïž] -- document hidden",
+ "badreply": "Failed to parse reply from server",
+
"xhr403": "403: Access denied\n\ntry pressing F5, maybe you got logged out",
"cf_ok": "sorry about that -- DD" + wah + "oS protection kicked in\n\nthings should resume in about 30 sec\n\nif nothing happens, hit F5 to reload the page",
"tl_xe1": "could not list subfolders:\n\nerror ",
@@ -409,7 +412,7 @@ var Ls = {
"fz_zipd": "zip with traditional cp437 filenames, for really old software",
"fz_zipc": "cp437 with crc32 computed early,$Nfor MS-DOS PKZIP v2.04g (october 1993)$N(takes longer to process before download can start)",
- "un_m1": "you can delete your recent uploads below",
+ "un_m1": "you can delete your recent uploads (or abort unfinished ones) below",
"un_upd": "refresh",
"un_m4": "or share the files visible below:",
"un_ulist": "show",
@@ -418,12 +421,15 @@ var Ls = {
"un_fclr": "clear filter",
"un_derr": 'unpost-delete failed:\n',
"un_f5": 'something broke, please try a refresh or hit F5',
+ "un_nou": 'warning: server too busy to show unfinished uploads; click the "refresh" link in a bit',
+ "un_noc": 'warning: unpost of fully uploaded files is not enabled/permitted in server config',
"un_max": "showing first 2000 files (use the filter)",
- "un_avail": "{0} uploads can be deleted",
- "un_m2": "sorted by upload time – most recent first:",
+ "un_avail": "{0} recent uploads can be deleted
{1} unfinished ones can be aborted",
+ "un_m2": "sorted by upload time; most recent first:",
"un_no1": "sike! no uploads are sufficiently recent",
"un_no2": "sike! no uploads matching that filter are sufficiently recent",
"un_next": "delete the next {0} files below",
+ "un_abrt": "abort",
"un_del": "delete",
"un_m3": "loading your recent uploads...",
"un_busy": "deleting {0} files...",
@@ -737,13 +743,14 @@ var Ls = {
"ml_drc": "compressor (volum-utjevning)",
"mt_shuf": "sangene i hver mappe$Nspilles i tilfeldig rekkefĂžlge\">đ",
+ "mt_aplay": "forsĂžk Ă„ starte avspilling hvis linken du klikket pĂ„ for Ă„ Ă„pne nettsiden inneholder en sang-ID$N$Nhvis denne deaktiveres sĂ„ vil heller ikke nettside-URLen bli oppdatert med sang-ID'er nĂ„r musikk spilles, i tilfelle innstillingene skulle gĂ„ tapt og nettsiden lastes pĂ„ ny\">aâ¶",
"mt_preload": "hent ned litt av neste sang i forkant,$Nslik at pausen i overgangen blir mindre\">forles",
"mt_prescan": "ved behov, bla til neste mappe$Nslik at nettleseren lar oss$Nfortsette Ă„ spille musikk\">bla",
"mt_fullpre": "hent ned hele neste sang, ikke bare litt:$Nâ
skru pĂ„ hvis nettet ditt er ustabilt,$Nâ skru av hvis nettet ditt er tregt\">full",
"mt_waves": "waveform seekbar:$Nvis volumkurve i avspillingsfeltet\">~s",
"mt_npclip": "vis knapper for Ä kopiere info om sangen du hÞrer pÄ\">/np",
"mt_octl": "integrering med operativsystemet (fjernkontroll, info-skjerm)\">os-ctl",
- "mt_oseek": "tillat spoling med fjernkontroll\">spoling",
+ "mt_oseek": "tillat spoling med fjernkontroll$N$Nmerk: pÄ noen enheter (iPhones) sÄ vil$Ndette erstatte knappen for neste sang\">spoling",
"mt_oscv": "vis album-cover pÄ infoskjermen\">bilde",
"mt_follow": "bla slik at sangen som spilles alltid er synlig\">đŻ",
"mt_compact": "tettpakket avspillerpanel\">â",
@@ -885,6 +892,8 @@ var Ls = {
"md_eshow": "viser forenklet ",
"md_off": "[đreadme] er avskrudd i [âïž] -- dokument skjult",
+ "badreply": "Ugyldig svar ifra serveren",
+
"xhr403": "403: Tilgang nektet\n\nkanskje du ble logget ut? prĂžv Ă„ trykk F5",
"cf_ok": "beklager -- liten tilfeldig kontroll, alt OK\n\nting skal fortsette om ca. 30 sekunder\n\nhvis ikkeno skjer, trykk F5 for Ä laste siden pÄ nytt",
"tl_xe1": "kunne ikke hente undermapper:\n\nfeil ",
@@ -906,7 +915,7 @@ var Ls = {
"fz_zipd": "zip med filnavn i cp437, for hĂžggamle maskiner",
"fz_zipc": "cp437 med tidlig crc32,$Nfor MS-DOS PKZIP v2.04g (oktober 1993)$N(Þker behandlingstid pÄ server)",
- "un_m1": "nedenfor kan du angre / slette filer som du nylig har lastet opp",
+ "un_m1": "nedenfor kan du angre / slette filer som du nylig har lastet opp, eller avbryte ufullstendige opplastninger",
"un_upd": "oppdater",
"un_m4": "eller hvis du vil dele nedlastnings-lenkene:",
"un_ulist": "vis",
@@ -915,12 +924,15 @@ var Ls = {
"un_fclr": "nullstill filter",
"un_derr": 'unpost-sletting feilet:\n',
"un_f5": 'noe gikk galt, prĂžv Ă„ oppdatere listen eller trykk F5',
+ "un_nou": 'advarsel: kan ikke vise ufullstendige opplastninger akkurat nÄ; klikk pÄ oppdater-linken om litt',
+ "un_noc": 'advarsel: angring av fullfĂžrte opplastninger er deaktivert i serverkonfigurasjonen',
"un_max": "viser de fĂžrste 2000 filene (bruk filteret for Ă„ innsnevre)",
- "un_avail": "{0} filer kan slettes",
- "un_m2": "sortert etter opplastningstid – nyeste fĂžrst:",
+ "un_avail": "{0} nylig opplastede filer kan slettes
{1} ufullstendige opplastninger kan avbrytes",
+ "un_m2": "sortert etter opplastningstid; nyeste fĂžrst:",
"un_no1": "men nei, her var det jaggu ikkeno som slettes kan",
"un_no2": "men nei, her var det jaggu ingenting som passet overens med filteret",
"un_next": "slett de neste {0} filene nedenfor",
+ "un_abrt": "avbryt",
"un_del": "slett",
"un_m3": "henter listen med nylig opplastede filer...",
"un_busy": "sletter {0} filer...",
@@ -967,7 +979,7 @@ var Ls = {
"u_emtleakf": 'prĂžver fĂžlgende:\n
đ„
("enkelt UI") i opplasterenconnections-per-server
in about:config
",
+ "u_maxconn": "de fleste nettlesere tillater ikke mer enn 6, men firefox lar deg Ăžke grensen med connections-per-server
i about:config
",
"u_tu": 'ADVARSEL: turbo er pÄ, avbrutte opplastninger vil muligens ikke oppdages og gjenopptas; hold musepekeren over turbo-knappen for mer info
', "u_ts": 'ADVARSEL: turbo er pÄ, sÞkeresultater kan vÊre feil; hold musepekeren over turbo-knappen for mer info
', "u_turbo_c": "turbo er deaktivert i serverkonfigurasjonen", @@ -1024,7 +1036,7 @@ modal.load(); ebi('ops').innerHTML = ( '--' + 'đ' + - (have_del && have_unpost ? 'đ§Ż' : '') + + (have_del ? 'đ§Ż' : '') + 'đ' + 'đ' + 'đ' + @@ -1401,6 +1413,7 @@ var mpl = (function () { ebi('op_player').innerHTML = ( '' + L.badreply + ':
' + unpre(this.responseText); + } + + if (ores.u.length == 1 && ores.u[0].timeout) { + html.push('' + L.un_nou + '
'); + ores.u = []; + } + + if (ores.c.length == 1 && ores.c[0].kinshi) { + html.push('' + L.un_noc + '
'); + ores.c = []; + } + + for (var a = 0; a < ores.u.length; a++) + ores.u[a].k = 'u'; + + for (var a = 0; a < ores.c.length; a++) + ores.c[a].k = 'c'; + + var res = ores.u.concat(ores.c); + if (res.length) { if (res.length == 2000) html.push("" + L.un_max); else - html.push("
" + L.un_avail.format(res.length)); + html.push("
" + L.un_avail.format(ores.c.length, ores.u.length)); - html.push(" – " + L.un_m2 + "
"); - html.push("time | size | file |
time | size | done | file | |||
' + '' + L.un_next.format(Math.min(mods[b], res.length - a)) + ' | ||||||
' + L.un_del + ' | ' + + '||||||
' + (done ? L.un_del : L.un_abrt) + ' | ' + '' + unix2iso(res[a].at) + ' | ' + - '' + res[a].sz + ' | ' + + '' + ('' + res[a].sz).replace(/\B(?=(\d{3})+(?!\d))/g, " ") + ' | ' + + (done ? '100% | ' : '' + res[a].pd + '% | ') + '' + linksplit(res[a].vp).join(' / ') + ' |