copyparty 1.13.0__py3-none-any.whl → 1.13.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
copyparty/__main__.py CHANGED
@@ -854,6 +854,7 @@ def add_fs(ap):
854
854
  ap2.add_argument("--rm-retry", metavar="T/R", type=u, default=rm_re_def, help="if a file cannot be deleted because it is busy, continue trying for \033[33mT\033[0m seconds, retry every \033[33mR\033[0m seconds; disable with 0/0 (volflag=rm_retry)")
855
855
  ap2.add_argument("--mv-retry", metavar="T/R", type=u, default=rm_re_def, help="if a file cannot be renamed because it is busy, continue trying for \033[33mT\033[0m seconds, retry every \033[33mR\033[0m seconds; disable with 0/0 (volflag=mv_retry)")
856
856
  ap2.add_argument("--iobuf", metavar="BYTES", type=int, default=256*1024, help="file I/O buffer-size; if your volumes are on a network drive, try increasing to \033[32m524288\033[0m or even \033[32m4194304\033[0m (and let me know if that improves your performance)")
857
+ ap2.add_argument("--mtab-age", metavar="SEC", type=int, default=60, help="rebuild mountpoint cache every \033[33mSEC\033[0m to keep track of sparse-files support; keep low on servers with removable media")
857
858
 
858
859
 
859
860
  def add_upload(ap):
@@ -1250,19 +1251,38 @@ def add_txt(ap):
1250
1251
  ap2.add_argument("--exp-lg", metavar="V,V,V", type=u, default=DEF_EXP, help="comma/space-separated list of placeholders to expand in prologue/epilogue files (volflag=exp_lg)")
1251
1252
 
1252
1253
 
1254
+ def add_og(ap):
1255
+ ap2 = ap.add_argument_group('og / open graph / discord-embed options')
1256
+ ap2.add_argument("--og", action="store_true", help="disable hotlinking and return an html document instead; this is required by open-graph, but can also be useful on its own (volflag=og)")
1257
+ ap2.add_argument("--og-ua", metavar="RE", type=u, default="", help="only disable hotlinking / engage OG behavior if the useragent matches regex \033[33mRE\033[0m (volflag=og_ua)")
1258
+ ap2.add_argument("--og-tpl", metavar="PATH", type=u, default="", help="do not return the regular copyparty html, but instead load the jinja2 template at \033[33mPATH\033[0m (if path contains 'EXT' then EXT will be replaced with the requested file's extension) (volflag=og_tpl)")
1259
+ ap2.add_argument("--og-no-head", action="store_true", help="do not automatically add OG entries into <head> (useful if you're doing this yourself in a template or such) (volflag=og_no_head)")
1260
+ ap2.add_argument("--og-th", metavar="FMT", type=u, default="jf3", help="thumbnail format; j=jpeg, jf=jpeg-uncropped, jf3=jpeg-uncropped-large, w=webm, ... (volflag=og_th)")
1261
+ ap2.add_argument("--og-title", metavar="TXT", type=u, default="", help="fallback title if there is nothing in the \033[33m-e2t\033[0m database (volflag=og_title)")
1262
+ ap2.add_argument("--og-title-a", metavar="T", type=u, default="🎵 {{ artist }} - {{ title }}", help="audio title format; takes any metadata key (volflag=og_title_a)")
1263
+ ap2.add_argument("--og-title-v", metavar="T", type=u, default="{{ title }}", help="video title format; takes any metadata key (volflag=og_title_v)")
1264
+ ap2.add_argument("--og-title-i", metavar="T", type=u, default="{{ title }}", help="image title format; takes any metadata key (volflag=og_title_i)")
1265
+ ap2.add_argument("--og-s-title", action="store_true", help="force default title; do not read from tags (volflag=og_s_title)")
1266
+ ap2.add_argument("--og-desc", metavar="TXT", type=u, default="", help="description text; same for all files, disable with [\033[32m-\033[0m] (volflag=og_desc)")
1267
+ ap2.add_argument("--og-site", metavar="TXT", type=u, default="", help="sitename; defaults to \033[33m--name\033[0m, disable with [\033[32m-\033[0m] (volflag=og_site)")
1268
+ ap2.add_argument("--tcolor", metavar="RGB", type=u, default="333", help="accent color (3 or 6 hex digits); may also affect safari and/or android-chrome (volflag=tcolor)")
1269
+ ap2.add_argument("--uqe", action="store_true", help="query-string parceling; translate a request for \033[33m/foo/.uqe/BASE64\033[0m into \033[33m/foo?TEXT\033[0m, or \033[33m/foo/?TEXT\033[0m if the first character in \033[33mTEXT\033[0m is a slash. Automatically enabled for \033[33m--og\033[0m")
1270
+
1271
+
1253
1272
  def add_ui(ap, retry):
1254
1273
  ap2 = ap.add_argument_group('ui options')
1255
1274
  ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
1256
1275
  ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language; one of the following: \033[32meng nor\033[0m")
1257
1276
  ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..7)")
1258
1277
  ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed")
1278
+ ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent")
1259
1279
  ap2.add_argument("--sort", metavar="C,C,C", type=u, default="href", help="default sort order, comma-separated column IDs (see header tooltips), prefix with '-' for descending. Examples: \033[32mhref -href ext sz ts tags/Album tags/.tn\033[0m (volflag=sort)")
1260
1280
  ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files matching \033[33mREGEX\033[0m in file list. Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\\.(js|css)$\033[0m] (volflag=unlist)")
1261
1281
  ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable")
1262
1282
  ap2.add_argument("--mpmc", metavar="URL", type=u, default="", help="change the mediaplayer-toggle mouse cursor; URL to a folder with {2..5}.png inside (or disable with [\033[32m.\033[0m])")
1263
1283
  ap2.add_argument("--js-browser", metavar="L", type=u, help="URL to additional JS to include")
1264
1284
  ap2.add_argument("--css-browser", metavar="L", type=u, help="URL to additional CSS to include")
1265
- ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages")
1285
+ ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages; can be @PATH to send the contents of a file at PATH, and/or begin with %% to render as jinja2 template (volflag=html_head)")
1266
1286
  ap2.add_argument("--ih", action="store_true", help="if a folder contains index.html, show that instead of the directory listing by default (can be changed in the client settings UI, or add ?v to URL for override)")
1267
1287
  ap2.add_argument("--textfiles", metavar="CSV", type=u, default="txt,nfo,diz,cue,readme", help="file extensions to present as plaintext")
1268
1288
  ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)")
@@ -1350,6 +1370,7 @@ def run_argparse(
1350
1370
  add_hooks(ap)
1351
1371
  add_stats(ap)
1352
1372
  add_txt(ap)
1373
+ add_og(ap)
1353
1374
  add_ui(ap, retry)
1354
1375
  add_admin(ap)
1355
1376
  add_logging(ap)
@@ -1384,12 +1405,16 @@ def run_argparse(
1384
1405
  return ret
1385
1406
 
1386
1407
 
1387
- def main(argv = None) :
1408
+ def main(argv = None, rsrc = None) :
1388
1409
  time.strptime("19970815", "%Y%m%d") # python#7980
1389
1410
  if WINDOWS:
1390
1411
  os.system("rem") # enables colors
1391
1412
 
1392
1413
  init_E(E)
1414
+
1415
+ if rsrc: # pyz
1416
+ E.mod = rsrc
1417
+
1393
1418
  if argv is None:
1394
1419
  argv = sys.argv
1395
1420
 
copyparty/__version__.py CHANGED
@@ -1,8 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
- VERSION = (1, 13, 0)
3
+ VERSION = (1, 13, 1)
4
4
  CODENAME = "race the beam"
5
- BUILD_DT = (2024, 4, 20)
5
+ BUILD_DT = (2024, 5, 6)
6
6
 
7
7
  S_VERSION = ".".join(map(str, VERSION))
8
8
  S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
copyparty/authsrv.py CHANGED
@@ -1435,6 +1435,7 @@ class AuthSrv(object):
1435
1435
  elif "" not in mount:
1436
1436
  # there's volumes but no root; make root inaccessible
1437
1437
  vfs = VFS(self.log_func, "", "", AXS(), {})
1438
+ vfs.flags["tcolor"] = self.args.tcolor
1438
1439
  vfs.flags["d2d"] = True
1439
1440
 
1440
1441
  maxdepth = 0
@@ -1720,7 +1721,11 @@ class AuthSrv(object):
1720
1721
  if self.args.e2d or "e2ds" in vol.flags:
1721
1722
  vol.flags["e2d"] = True
1722
1723
 
1723
- for ga, vf in [["no_hash", "nohash"], ["no_idx", "noidx"]]:
1724
+ for ga, vf in [
1725
+ ["no_hash", "nohash"],
1726
+ ["no_idx", "noidx"],
1727
+ ["og_ua", "og_ua"],
1728
+ ]:
1724
1729
  if vf in vol.flags:
1725
1730
  ptn = re.compile(vol.flags.pop(vf))
1726
1731
  else:
@@ -1766,6 +1771,13 @@ class AuthSrv(object):
1766
1771
  t = 'volume "/%s" has invalid %stry [%s]'
1767
1772
  raise Exception(t % (vol.vpath, k, vol.flags.get(k + "try")))
1768
1773
 
1774
+ if vol.flags.get("og"):
1775
+ self.args.uqe = True
1776
+
1777
+ zs = str(vol.flags.get("tcolor", "")).lstrip("#")
1778
+ if len(zs) == 3: # fc5 => ffcc55
1779
+ vol.flags["tcolor"] = "".join([x * 2 for x in zs])
1780
+
1769
1781
  for k1, k2 in IMPLICATIONS:
1770
1782
  if k1 in vol.flags:
1771
1783
  vol.flags[k2] = True
@@ -2403,7 +2415,7 @@ def expand_config_file(
2403
2415
  if not cnames:
2404
2416
  t = "warning: tried to read config-files from folder '%s' but it does not contain any "
2405
2417
  if names:
2406
- t += ".conf files; the following files were ignored: %s"
2418
+ t += ".conf files; the following files/subfolders were ignored: %s"
2407
2419
  t = t % (fp, ", ".join(names[:8]))
2408
2420
  else:
2409
2421
  t += "files at all"
copyparty/cfg.py CHANGED
@@ -39,6 +39,9 @@ def vf_bmap() :
39
39
  "magic",
40
40
  "no_sb_md",
41
41
  "no_sb_lg",
42
+ "og",
43
+ "og_no_head",
44
+ "og_s_title",
42
45
  "rand",
43
46
  "xdev",
44
47
  "xlink",
@@ -61,12 +64,23 @@ def vf_vmap() :
61
64
  }
62
65
  for k in (
63
66
  "dbd",
67
+ "html_head",
64
68
  "lg_sbf",
65
69
  "md_sbf",
66
70
  "nrand",
71
+ "og_desc",
72
+ "og_site",
73
+ "og_th",
74
+ "og_title",
75
+ "og_title_a",
76
+ "og_title_v",
77
+ "og_title_i",
78
+ "og_tpl",
79
+ "og_ua",
67
80
  "mv_retry",
68
81
  "rm_retry",
69
82
  "sort",
83
+ "tcolor",
70
84
  "unlist",
71
85
  "u2abort",
72
86
  "u2ts",
@@ -81,7 +95,6 @@ def vf_cmap() :
81
95
  for k in (
82
96
  "exp_lg",
83
97
  "exp_md",
84
- "html_head",
85
98
  "mte",
86
99
  "mth",
87
100
  "mtp",
@@ -201,7 +214,7 @@ flagcats = {
201
214
  "grid": "show grid/thumbnails by default",
202
215
  "sort": "default sort order",
203
216
  "unlist": "dont list files matching REGEX",
204
- "html_head=TXT": "includes TXT in the <head>",
217
+ "html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH",
205
218
  "robots": "allows indexing by search engines (default)",
206
219
  "norobots": "kindly asks search engines to leave",
207
220
  "no_sb_md": "disable js sandbox for markdown files",
copyparty/fsutil.py CHANGED
@@ -1,6 +1,7 @@
1
1
  # coding: utf-8
2
2
  from __future__ import print_function, unicode_literals
3
3
 
4
+ import argparse
4
5
  import os
5
6
  import re
6
7
  import time
@@ -11,20 +12,26 @@ from .bos import bos
11
12
  from .util import chkcmd, min_ex
12
13
 
13
14
  class Fstab(object):
14
- def __init__(self, log ):
15
+ def __init__(self, log , args ):
15
16
  self.log_func = log
16
17
 
18
+ self.warned = False
17
19
  self.trusted = False
18
20
  self.tab = None
21
+ self.oldtab = None
22
+ self.srctab = "a"
19
23
  self.cache = {}
20
24
  self.age = 0.0
25
+ self.maxage = args.mtab_age
21
26
 
22
27
  def log(self, msg , c = 0) :
23
28
  self.log_func("fstab", msg, c)
24
29
 
25
30
  def get(self, path ) :
26
- if len(self.cache) > 9000:
27
- self.age = time.time()
31
+ now = time.time()
32
+ if now - self.age > self.maxage or len(self.cache) > 9000:
33
+ self.age = now
34
+ self.oldtab = self.tab or self.oldtab
28
35
  self.tab = None
29
36
  self.cache = {}
30
37
 
@@ -69,7 +76,7 @@ class Fstab(object):
69
76
  self.trusted = False
70
77
 
71
78
  def build_tab(self) :
72
- self.log("building tab")
79
+ self.log("inspecting mtab for changes")
73
80
 
74
81
  sptn = r"^.*? on (.*) type ([^ ]+) \(.*"
75
82
  if MACOS:
@@ -78,6 +85,7 @@ class Fstab(object):
78
85
  ptn = re.compile(sptn)
79
86
  so, _ = chkcmd(["mount"])
80
87
  tab1 = []
88
+ atab = []
81
89
  for ln in so.split("\n"):
82
90
  m = ptn.match(ln)
83
91
  if not m:
@@ -85,6 +93,15 @@ class Fstab(object):
85
93
 
86
94
  zs1, zs2 = m.groups()
87
95
  tab1.append((str(zs1), str(zs2)))
96
+ atab.append(ln)
97
+
98
+ # keep empirically-correct values if mounttab unchanged
99
+ srctab = "\n".join(sorted(atab))
100
+ if srctab == self.srctab:
101
+ self.tab = self.oldtab
102
+ return
103
+
104
+ self.log("mtab has changed; reevaluating support for sparse files")
88
105
 
89
106
  tab1.sort(key=lambda x: (len(x[0]), x[0]))
90
107
  path1, fs1 = tab1[0]
@@ -93,6 +110,7 @@ class Fstab(object):
93
110
  tab.add(fs, path.lstrip("/"))
94
111
 
95
112
  self.tab = tab
113
+ self.srctab = srctab
96
114
 
97
115
  def relabel(self, path , nval ) :
98
116
  assert self.tab
@@ -127,7 +145,9 @@ class Fstab(object):
127
145
  self.trusted = True
128
146
  except:
129
147
  # prisonparty or other restrictive environment
130
- self.log("failed to build tab:\n{}".format(min_ex()), 3)
148
+ if not self.warned:
149
+ self.warned = True
150
+ self.log("failed to build tab:\n{}".format(min_ex()), 3)
131
151
  self.build_fallback()
132
152
 
133
153
  assert self.tab
copyparty/httpcli.py CHANGED
@@ -84,6 +84,9 @@ from .util import (
84
84
  sanitize_vpath,
85
85
  sendfile_kern,
86
86
  sendfile_py,
87
+ ub64dec,
88
+ ub64enc,
89
+ ujoin,
87
90
  undot,
88
91
  unescape_cookie,
89
92
  unquotep,
@@ -213,6 +216,13 @@ class HttpCli(object):
213
216
  ka["favico"] = self.args.favico
214
217
  ka["s_name"] = self.args.bname
215
218
  ka["s_doctitle"] = self.args.doctitle
219
+ ka["tcolor"] = self.vn.flags["tcolor"]
220
+
221
+ zso = self.vn.flags.get("html_head")
222
+ if zso:
223
+ ka["this"] = self
224
+ self._build_html_head(zso, ka)
225
+
216
226
  ka["html_head"] = self.html_head
217
227
  return tpl.render(**ka) # type: ignore
218
228
 
@@ -360,6 +370,21 @@ class HttpCli(object):
360
370
  if "&" in self.req and "?" not in self.req:
361
371
  self.hint = "did you mean '?' instead of '&'"
362
372
 
373
+ if self.args.uqe and "/.uqe/" in self.req:
374
+ try:
375
+ vpath, query = self.req.split("?")[0].split("/.uqe/")
376
+ query = query.split("/")[0] # discard trailing junk
377
+ # (usually a "filename" to trick discord into behaving)
378
+ query = ub64dec(query.encode("utf-8")).decode("utf-8", "replace")
379
+ if query.startswith("/"):
380
+ self.req = "%s/?%s" % (vpath, query[1:])
381
+ else:
382
+ self.req = "%s?%s" % (vpath, query)
383
+ except Exception as ex:
384
+ t = "bad uqe in request [%s]: %r" % (self.req, ex)
385
+ self.loud_reply(t, status=400)
386
+ return False
387
+
363
388
  # split req into vpath + uparam
364
389
  uparam = {}
365
390
  if "?" not in self.req:
@@ -426,7 +451,8 @@ class HttpCli(object):
426
451
  cookie_pw = ""
427
452
 
428
453
  if len(uparam) > 10 or len(cookies) > 50:
429
- raise Pebkac(400, "u wot m8")
454
+ self.loud_reply("u wot m8", status=400)
455
+ return False
430
456
 
431
457
  self.uparam = uparam
432
458
  self.cookies = cookies
@@ -714,6 +740,31 @@ class HttpCli(object):
714
740
  or ("; Trident/" in self.ua and not k304)
715
741
  )
716
742
 
743
+ def _build_html_head(self, maybe_html , kv ) :
744
+ html = str(maybe_html)
745
+ is_jinja = html[:2] in "%@%"
746
+ if is_jinja:
747
+ html = html.replace("%", "", 1)
748
+
749
+ if html.startswith("@"):
750
+ with open(html[1:], "rb") as f:
751
+ html = f.read().decode("utf-8")
752
+
753
+ if html.startswith("%"):
754
+ html = html[1:]
755
+ is_jinja = True
756
+
757
+ if is_jinja:
758
+ print("applying jinja")
759
+ with self.conn.hsrv.mutex:
760
+ if html not in self.conn.hsrv.j2:
761
+ j2env = jinja2.Environment()
762
+ tpl = j2env.from_string(html)
763
+ self.conn.hsrv.j2[html] = tpl
764
+ html = self.conn.hsrv.j2[html].render(**kv)
765
+
766
+ self.html_head += html + "\n"
767
+
717
768
  def send_headers(
718
769
  self,
719
770
  length ,
@@ -2247,6 +2298,10 @@ class HttpCli(object):
2247
2298
  def handle_login(self) :
2248
2299
  assert self.parser
2249
2300
  pwd = self.parser.require("cppwd", 64)
2301
+ try:
2302
+ uhash = self.parser.require("uhash", 256)
2303
+ except:
2304
+ uhash = ""
2250
2305
  self.parser.drop()
2251
2306
 
2252
2307
  self.out_headerlist = [
@@ -2259,6 +2314,11 @@ class HttpCli(object):
2259
2314
 
2260
2315
  dst += self.ourlq()
2261
2316
 
2317
+ uhash = uhash.lstrip("#")
2318
+ if uhash not in ("", "-"):
2319
+ dst += "&" if "?" in dst else "?"
2320
+ dst += "_=1#" + html_escape(uhash, True, True)
2321
+
2262
2322
  msg = self.get_pwd_cookie(pwd)
2263
2323
  html = self.j2s("msg", h1=msg, h2='<a href="' + dst + '">ack</a>', redir=dst)
2264
2324
  self.reply(html.encode("utf-8"))
@@ -3184,7 +3244,7 @@ class HttpCli(object):
3184
3244
  data_end = file_size
3185
3245
  break
3186
3246
 
3187
- if num_need != len(job["need"]):
3247
+ if num_need != len(job["need"]) and data_end - lower < 8 * M:
3188
3248
  num_need = len(job["need"])
3189
3249
  data_end = 0
3190
3250
  for cid in job["hash"]:
@@ -3480,7 +3540,6 @@ class HttpCli(object):
3480
3540
  targs = {
3481
3541
  "r": self.args.SR if self.is_vproxied else "",
3482
3542
  "ts": self.conn.hsrv.cachebuster(),
3483
- "html_head": self.html_head,
3484
3543
  "edit": "edit" in self.uparam,
3485
3544
  "title": html_escape(self.vpath, crlf=True),
3486
3545
  "lastmod": int(ts_md * 1000),
@@ -3491,6 +3550,13 @@ class HttpCli(object):
3491
3550
  "md": boundary,
3492
3551
  "arg_base": arg_base,
3493
3552
  }
3553
+
3554
+ zfv = self.vn.flags.get("html_head")
3555
+ if zfv:
3556
+ targs["this"] = self
3557
+ self._build_html_head(zfv, targs)
3558
+
3559
+ targs["html_head"] = self.html_head
3494
3560
  zs = template.render(**targs).encode("utf-8", "replace")
3495
3561
  html = zs.split(boundary.encode("utf-8"))
3496
3562
  if len(html) != 2:
@@ -3606,8 +3672,6 @@ class HttpCli(object):
3606
3672
  self.reply(zb, mime="text/plain; charset=utf-8")
3607
3673
  return True
3608
3674
 
3609
- self.html_head += self.vn.flags.get("html_head", "")
3610
-
3611
3675
  html = self.j2s(
3612
3676
  "splash",
3613
3677
  this=self,
@@ -3652,7 +3716,7 @@ class HttpCli(object):
3652
3716
  return True
3653
3717
 
3654
3718
  def set_cfg_reset(self) :
3655
- for k in ("k304", "js", "idxh", "cppwd", "cppws"):
3719
+ for k in ("k304", "js", "idxh", "dots", "cppwd", "cppws"):
3656
3720
  cookie = gencookie(k, "x", self.args.R, False)
3657
3721
  self.out_headerlist.append(("Set-Cookie", cookie))
3658
3722
 
@@ -3863,7 +3927,7 @@ class HttpCli(object):
3863
3927
  allvols = [x for x in allvols if "e2d" in x.flags]
3864
3928
 
3865
3929
  for vol in allvols:
3866
- cur = idx.get_cur(vol.realpath)
3930
+ cur = idx.get_cur(vol)
3867
3931
  if not cur:
3868
3932
  continue
3869
3933
 
@@ -4078,7 +4142,17 @@ class HttpCli(object):
4078
4142
  e2d = "e2d" in vn.flags
4079
4143
  e2t = "e2t" in vn.flags
4080
4144
 
4081
- self.html_head += vn.flags.get("html_head", "")
4145
+ add_og = "og" in vn.flags
4146
+ if add_og:
4147
+ if "th" in self.uparam or "raw" in self.uparam:
4148
+ og_ua = add_og = False
4149
+ elif self.args.og_ua:
4150
+ og_ua = add_og = self.args.og_ua.search(self.ua)
4151
+ else:
4152
+ og_ua = False
4153
+ add_og = True
4154
+ og_fn = ""
4155
+
4082
4156
  if "b" in self.uparam:
4083
4157
  self.out_headers["X-Robots-Tag"] = "noindex, nofollow"
4084
4158
 
@@ -4086,13 +4160,15 @@ class HttpCli(object):
4086
4160
  is_dk = False
4087
4161
  fk_pass = False
4088
4162
  icur = None
4089
- if is_dir and (e2t or e2d):
4163
+ if (e2t or e2d) and (is_dir or add_og):
4090
4164
  idx = self.conn.get_u2idx()
4091
4165
  if idx and hasattr(idx, "p_end"):
4092
- icur = idx.get_cur(dbv.realpath)
4166
+ icur = idx.get_cur(dbv)
4093
4167
 
4094
4168
  th_fmt = self.uparam.get("th")
4095
- if self.can_read or (self.can_get and vn.flags.get("dk")):
4169
+ if self.can_read or (
4170
+ self.can_get and (vn.flags.get("dk") or "fk" not in vn.flags)
4171
+ ):
4096
4172
  if th_fmt is not None:
4097
4173
  nothumb = "dthumb" in dbv.flags
4098
4174
  if is_dir:
@@ -4139,7 +4215,7 @@ class HttpCli(object):
4139
4215
  elif self.can_write and th_fmt is not None:
4140
4216
  return self.tx_svg("upload\nonly")
4141
4217
 
4142
- elif self.can_get and self.avn:
4218
+ if not self.can_read and self.can_get and self.avn:
4143
4219
  axs = self.avn.axs
4144
4220
  if self.uname not in axs.uhtml:
4145
4221
  pass
@@ -4185,6 +4261,17 @@ class HttpCli(object):
4185
4261
  self.log(t % (correct, got, self.req, abspath), 6)
4186
4262
  return self.tx_404()
4187
4263
 
4264
+ if add_og:
4265
+ if og_ua or self.host not in self.headers.get("referer", ""):
4266
+ self.vpath, og_fn = vsplit(self.vpath)
4267
+ vpath = self.vpath
4268
+ vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)
4269
+ abspath = vn.dcanonical(rem)
4270
+ dbv, vrem = vn.get_dbv(rem)
4271
+ is_dir = stat.S_ISDIR(st.st_mode)
4272
+ is_dk = True
4273
+ vpnodes.pop()
4274
+
4188
4275
  if (
4189
4276
  (abspath.endswith(".md") or self.can_delete)
4190
4277
  and "nohtml" not in vn.flags
@@ -4196,9 +4283,10 @@ class HttpCli(object):
4196
4283
  ):
4197
4284
  return self.tx_md(vn, abspath)
4198
4285
 
4199
- return self.tx_file(
4200
- abspath, None if st.st_size or "nopipe" in vn.flags else vn.realpath
4201
- )
4286
+ if not add_og or not og_fn:
4287
+ return self.tx_file(
4288
+ abspath, None if st.st_size or "nopipe" in vn.flags else vn.realpath
4289
+ )
4202
4290
 
4203
4291
  elif is_dir and not self.can_read:
4204
4292
  if self._use_dirkey(abspath):
@@ -4245,7 +4333,11 @@ class HttpCli(object):
4245
4333
  is_ls = "ls" in self.uparam
4246
4334
  is_js = self.args.force_js or self.cookies.get("js") == "y"
4247
4335
 
4248
- if not is_ls and (self.ua.startswith("curl/") or self.ua.startswith("fetch")):
4336
+ if (
4337
+ not is_ls
4338
+ and not add_og
4339
+ and (self.ua.startswith("curl/") or self.ua.startswith("fetch"))
4340
+ ):
4249
4341
  self.uparam["ls"] = "v"
4250
4342
  is_ls = True
4251
4343
 
@@ -4319,6 +4411,7 @@ class HttpCli(object):
4319
4411
  "dsort": vf["sort"],
4320
4412
  "dcrop": vf["crop"],
4321
4413
  "dth3x": vf["th3x"],
4414
+ "dvol": self.args.au_vol,
4322
4415
  "themes": self.args.themes,
4323
4416
  "turbolvl": self.args.turbo,
4324
4417
  "u2j": self.args.u2j,
@@ -4370,7 +4463,7 @@ class HttpCli(object):
4370
4463
 
4371
4464
  for k in ["zip", "tar"]:
4372
4465
  v = self.uparam.get(k)
4373
- if v is not None:
4466
+ if v is not None and (not add_og or not og_fn):
4374
4467
  return self.tx_zip(k, v, self.vpath, vn, rem, [])
4375
4468
 
4376
4469
  fsroot, vfs_ls, vfs_virt = vn.ls(
@@ -4384,6 +4477,10 @@ class HttpCli(object):
4384
4477
  ls_names = [x[0] for x in vfs_ls]
4385
4478
  ls_names.extend(list(vfs_virt.keys()))
4386
4479
 
4480
+ if add_og and og_fn and not self.can_read:
4481
+ ls_names = [og_fn]
4482
+ is_js = True
4483
+
4387
4484
  # check for old versions of files,
4388
4485
  # [num-backups, most-recent, hist-path]
4389
4486
  hist = {}
@@ -4445,12 +4542,14 @@ class HttpCli(object):
4445
4542
  margin = "DIR"
4446
4543
  elif add_dk:
4447
4544
  zs = absreal(fspath)
4448
- margin = '<a href="%s?k=%s&zip" rel="nofollow">zip</a>' % (
4545
+ margin = '<a href="%s?k=%s&zip=crc" rel="nofollow">zip</a>' % (
4449
4546
  quotep(href),
4450
4547
  self.gen_fk(2, self.args.dk_salt, zs, 0, 0)[:add_dk],
4451
4548
  )
4452
4549
  else:
4453
- margin = '<a href="%s?zip" rel="nofollow">zip</a>' % (quotep(href),)
4550
+ margin = '<a href="%s?zip=crc" rel="nofollow">zip</a>' % (
4551
+ quotep(href),
4552
+ )
4454
4553
  elif fn in hist:
4455
4554
  margin = '<a href="%s.hist/%s">#%s</a>' % (
4456
4555
  base,
@@ -4594,6 +4693,9 @@ class HttpCli(object):
4594
4693
  else:
4595
4694
  taglist = list(tagset)
4596
4695
 
4696
+ if not files and not dirs and not readme and not logues[0] and not logues[1]:
4697
+ logues[1] = "this folder is empty"
4698
+
4597
4699
  if is_ls:
4598
4700
  ls_ret["dirs"] = dirs
4599
4701
  ls_ret["files"] = files
@@ -4645,6 +4747,148 @@ class HttpCli(object):
4645
4747
  if "mth" in vn.flags:
4646
4748
  j2a["def_hcols"] = list(vn.flags["mth"])
4647
4749
 
4750
+ if add_og and "raw" not in self.uparam:
4751
+ j2a["this"] = self
4752
+ cgv["og_fn"] = og_fn
4753
+ if og_fn and vn.flags.get("og_tpl"):
4754
+ tpl = vn.flags["og_tpl"]
4755
+ if "EXT" in tpl:
4756
+ zs = og_fn.split(".")[-1].lower()
4757
+ tpl2 = tpl.replace("EXT", zs)
4758
+ if os.path.exists(tpl2):
4759
+ tpl = tpl2
4760
+ with self.conn.hsrv.mutex:
4761
+ if tpl not in self.conn.hsrv.j2:
4762
+ tdir, tname = os.path.split(tpl)
4763
+ j2env = jinja2.Environment()
4764
+ j2env.loader = jinja2.FileSystemLoader(tdir)
4765
+ self.conn.hsrv.j2[tpl] = j2env.get_template(tname)
4766
+ thumb = ""
4767
+ is_pic = is_vid = is_au = False
4768
+ covernames = self.args.th_coversd
4769
+ for fn in ls_names:
4770
+ if fn.lower() in covernames:
4771
+ thumb = fn
4772
+ break
4773
+ if og_fn:
4774
+ ext = og_fn.split(".")[-1].lower()
4775
+ if ext in self.thumbcli.thumbable:
4776
+ is_pic = (
4777
+ ext in self.thumbcli.fmt_pil
4778
+ or ext in self.thumbcli.fmt_vips
4779
+ or ext in self.thumbcli.fmt_ffi
4780
+ )
4781
+ is_vid = ext in self.thumbcli.fmt_ffv
4782
+ is_au = ext in self.thumbcli.fmt_ffa
4783
+ if not thumb or not is_au:
4784
+ thumb = og_fn
4785
+ file = next((x for x in files if x["name"] == og_fn), None)
4786
+ else:
4787
+ file = None
4788
+
4789
+ url_base = "%s://%s/%s" % (
4790
+ "https" if self.is_https else "http",
4791
+ self.host,
4792
+ self.args.RS + quotep(vpath),
4793
+ )
4794
+ j2a["og_is_pic"] = is_pic
4795
+ j2a["og_is_vid"] = is_vid
4796
+ j2a["og_is_au"] = is_au
4797
+ if thumb:
4798
+ fmt = vn.flags.get("og_th", "j")
4799
+ th_base = ujoin(url_base, quotep(thumb))
4800
+ query = "th=%s&cache" % (fmt,)
4801
+ query = ub64enc(query.encode("utf-8")).decode("utf-8")
4802
+ # discord looks at file extension, not content-type...
4803
+ query += "/a.jpg" if "j" in fmt else "/a.webp"
4804
+ j2a["og_thumb"] = "%s/.uqe/%s" % (th_base, query)
4805
+
4806
+ j2a["og_fn"] = og_fn
4807
+ j2a["og_file"] = file
4808
+ if og_fn:
4809
+ og_fn_q = quotep(og_fn)
4810
+ query = ub64enc(b"raw").decode("utf-8")
4811
+ if "." in og_fn:
4812
+ query += "/a.%s" % (og_fn.split(".")[-1])
4813
+
4814
+ j2a["og_url"] = ujoin(url_base, og_fn_q)
4815
+ j2a["og_raw"] = j2a["og_url"] + "/.uqe/" + query
4816
+ else:
4817
+ j2a["og_url"] = j2a["og_raw"] = url_base
4818
+
4819
+ if not vn.flags.get("og_no_head"):
4820
+ ogh = {"twitter:card": "summary"}
4821
+
4822
+ title = str(vn.flags.get("og_title") or "")
4823
+
4824
+ if thumb:
4825
+ ogh["og:image"] = j2a["og_thumb"]
4826
+
4827
+ zso = vn.flags.get("og_desc") or ""
4828
+ if zso != "-":
4829
+ ogh["og:description"] = str(zso)
4830
+
4831
+ zs = vn.flags.get("og_site") or self.args.name
4832
+ if zs not in ("", "-"):
4833
+ ogh["og:site_name"] = zs
4834
+
4835
+ tagmap = {}
4836
+ if is_au:
4837
+ title = str(vn.flags.get("og_title_a") or "")
4838
+ ogh["og:type"] = "music.song"
4839
+ ogh["og:audio"] = j2a["og_raw"]
4840
+ tagmap = {
4841
+ "artist": "og:music:musician",
4842
+ "album": "og:music:album",
4843
+ ".dur": "og:music:duration",
4844
+ }
4845
+ elif is_vid:
4846
+ title = str(vn.flags.get("og_title_v") or "")
4847
+ ogh["og:type"] = "video.other"
4848
+ ogh["og:video"] = j2a["og_raw"]
4849
+ tagmap = {
4850
+ "title": "og:title",
4851
+ ".dur": "og:video:duration",
4852
+ }
4853
+ elif is_pic:
4854
+ title = str(vn.flags.get("og_title_i") or "")
4855
+ ogh["twitter:card"] = "summary_large_image"
4856
+ ogh["twitter:image"] = ogh["og:image"] = j2a["og_raw"]
4857
+
4858
+ try:
4859
+ for k, v in file["tags"].items():
4860
+ zs = "{{ %s }}" % (k,)
4861
+ title = title.replace(zs, str(v))
4862
+ except:
4863
+ pass
4864
+ title = re.sub(r"\{\{ [^}]+ \}\}", "", title)
4865
+ while title.startswith(" - "):
4866
+ title = title[3:]
4867
+ while title.endswith(" - "):
4868
+ title = title[:3]
4869
+
4870
+ if vn.flags.get("og_s_title") or not title:
4871
+ title = str(vn.flags.get("og_title") or "")
4872
+
4873
+ for tag, hname in tagmap.items():
4874
+ try:
4875
+ v = file["tags"][tag]
4876
+ if not v:
4877
+ continue
4878
+ ogh[hname] = int(v) if tag == ".dur" else v
4879
+ except:
4880
+ pass
4881
+
4882
+ ogh["og:title"] = title
4883
+
4884
+ oghs = [
4885
+ '\t<meta property="%s" content="%s">'
4886
+ % (k, html_escape(str(v), True, True))
4887
+ for k, v in ogh.items()
4888
+ ]
4889
+ zs = self.html_head + "\n%s\n" % ("\n".join(oghs),)
4890
+ self.html_head = zs.replace("\n\n", "\n")
4891
+
4648
4892
  html = self.j2s(tpl, **j2a)
4649
4893
  self.reply(html.encode("utf-8", "replace"))
4650
4894
  return True
copyparty/metrics.py CHANGED
@@ -179,7 +179,7 @@ class Metrics(object):
179
179
  tnbytes = 0
180
180
  tnfiles = 0
181
181
  for vpath, vol in allvols:
182
- cur = idx.get_cur(vol.realpath)
182
+ cur = idx.get_cur(vol)
183
183
  if not cur:
184
184
  continue
185
185
 
copyparty/svchub.py CHANGED
@@ -520,7 +520,7 @@ class SvcHub(object):
520
520
  al.exp_md = odfusion(exp, al.exp_md.replace(" ", ","))
521
521
  al.exp_lg = odfusion(exp, al.exp_lg.replace(" ", ","))
522
522
 
523
- for k in ["no_hash", "no_idx"]:
523
+ for k in ["no_hash", "no_idx", "og_ua"]:
524
524
  ptn = getattr(self.args, k)
525
525
  if ptn:
526
526
  setattr(self.args, k, re.compile(ptn))
@@ -551,6 +551,10 @@ class SvcHub(object):
551
551
  except:
552
552
  raise Exception("invalid --mv-retry [%s]" % (self.args.mv_retry,))
553
553
 
554
+ al.tcolor = al.tcolor.lstrip("#")
555
+ if len(al.tcolor) == 3: # fc5 => ffcc55
556
+ al.tcolor = "".join([x * 2 for x in al.tcolor])
557
+
554
558
  return True
555
559
 
556
560
  def _ipa2re(self, txt) :
copyparty/tcpsrv.py CHANGED
@@ -460,6 +460,12 @@ class TcpSrv(object):
460
460
  sys.stderr.flush()
461
461
 
462
462
  def _qr(self, t1 , t2 ) :
463
+ t2c = {zs: zli for zs, zli in t2.items() if zs in ("127.0.0.1", "::1")}
464
+ t2b = {zs: zli for zs, zli in t2.items() if ":" in zs and zs not in t2c}
465
+ t2 = {zs: zli for zs, zli in t2.items() if zs not in t2b and zs not in t2c}
466
+ t2.update(t2b) # first ipv4, then ipv6...
467
+ t2.update(t2c) # ...and finally localhost
468
+
463
469
  ip = None
464
470
  ips = list(t1) + list(t2)
465
471
  qri = self.args.qri
copyparty/u2idx.py CHANGED
@@ -59,6 +59,17 @@ class U2idx(object):
59
59
  def log(self, msg , c = 0) :
60
60
  self.log_func("u2idx", msg, c)
61
61
 
62
+ def shutdown(self) :
63
+ for cur in self.cur.values():
64
+ db = cur.connection
65
+ try:
66
+ db.interrupt()
67
+ except:
68
+ pass
69
+
70
+ cur.close()
71
+ db.close()
72
+
62
73
  def fsearch(
63
74
  self, uname , vols , body
64
75
  ) :
@@ -78,14 +89,18 @@ class U2idx(object):
78
89
  except:
79
90
  raise Pebkac(500, min_ex())
80
91
 
81
- def get_cur(self, ptop ) :
92
+ def get_cur(self, vn ) :
82
93
  if not HAVE_SQLITE3:
83
94
  return None
84
95
 
85
- cur = self.cur.get(ptop)
96
+ cur = self.cur.get(vn.realpath)
86
97
  if cur:
87
98
  return cur
88
99
 
100
+ if "e2d" not in vn.flags:
101
+ return None
102
+
103
+ ptop = vn.realpath
89
104
  histpath = self.asrv.vfs.histtab.get(ptop)
90
105
  if not histpath:
91
106
  self.log("no histpath for [{}]".format(ptop))
@@ -314,7 +329,7 @@ class U2idx(object):
314
329
  ptop = vol.realpath
315
330
  flags = vol.flags
316
331
 
317
- cur = self.get_cur(ptop)
332
+ cur = self.get_cur(vol)
318
333
  if not cur:
319
334
  continue
320
335
 
copyparty/up2k.py CHANGED
@@ -183,7 +183,7 @@ class Up2k(object):
183
183
  t = "could not initialize sqlite3, will use in-memory registry only"
184
184
  self.log(t, 3)
185
185
 
186
- self.fstab = Fstab(self.log_func)
186
+ self.fstab = Fstab(self.log_func, self.args)
187
187
  self.gen_fk = self._gen_fk if self.args.log_fk else gen_filekey
188
188
 
189
189
  if self.args.hash_mt < 2:
@@ -4376,6 +4376,18 @@ class Up2k(object):
4376
4376
  for x in list(self.spools):
4377
4377
  self._unspool(x)
4378
4378
 
4379
+ for cur in self.cur.values():
4380
+ db = cur.connection
4381
+ try:
4382
+ db.interrupt()
4383
+ except:
4384
+ pass
4385
+
4386
+ cur.close()
4387
+ db.close()
4388
+
4389
+ self.registry = {}
4390
+
4379
4391
 
4380
4392
  def up2k_chunksize(filesize ) :
4381
4393
  chunksize = 1024 * 1024
copyparty/util.py CHANGED
@@ -35,6 +35,9 @@ from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, VT100, WINDOWS
35
35
  from .__version__ import S_BUILD_DT, S_VERSION
36
36
  from .stolen import surrogateescape
37
37
 
38
+ ub64dec = base64.urlsafe_b64decode
39
+ ub64enc = base64.urlsafe_b64encode
40
+
38
41
  try:
39
42
  from datetime import datetime, timezone
40
43
 
@@ -2010,6 +2013,7 @@ def vsplit(vpath ) :
2010
2013
  return vpath.rsplit("/", 1) # type: ignore
2011
2014
 
2012
2015
 
2016
+ # vpath-join
2013
2017
  def vjoin(rd , fn ) :
2014
2018
  if rd and fn:
2015
2019
  return rd + "/" + fn
@@ -2017,6 +2021,14 @@ def vjoin(rd , fn ) :
2017
2021
  return rd or fn
2018
2022
 
2019
2023
 
2024
+ # url-join
2025
+ def ujoin(rd , fn ) :
2026
+ if rd and fn:
2027
+ return rd.rstrip("/") + "/" + fn.lstrip("/")
2028
+ else:
2029
+ return rd or fn
2030
+
2031
+
2020
2032
  def _w8dec2(txt ) :
2021
2033
  """decodes filesystem-bytes to wtf8"""
2022
2034
  return surrogateescape.decodefilename(txt)
Binary file
Binary file
@@ -6,7 +6,7 @@
6
6
  <title>{{ title }}</title>
7
7
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
8
8
  <meta name="viewport" content="width=device-width, initial-scale=0.8, minimum-scale=0.6">
9
- <meta name="theme-color" content="#333">
9
+ <meta name="theme-color" content="#{{ tcolor }}">
10
10
  <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
11
11
  <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/browser.css?_={{ ts }}">
12
12
  {{ html_head }}
Binary file
copyparty/web/md.html CHANGED
@@ -3,7 +3,7 @@
3
3
  <title>📝 {{ title }}</title>
4
4
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=0.7">
6
- <meta name="theme-color" content="#333">
6
+ <meta name="theme-color" content="#{{ tcolor }}">
7
7
  <link rel="stylesheet" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
8
8
  <link rel="stylesheet" href="{{ r }}/.cpr/md.css?_={{ ts }}">
9
9
  {%- if edit %}
copyparty/web/mde.html CHANGED
@@ -3,7 +3,7 @@
3
3
  <title>📝 {{ title }}</title>
4
4
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=0.7">
6
- <meta name="theme-color" content="#333">
6
+ <meta name="theme-color" content="#{{ tcolor }}">
7
7
  <link rel="stylesheet" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
8
8
  <link rel="stylesheet" href="{{ r }}/.cpr/mde.css?_={{ ts }}">
9
9
  <link rel="stylesheet" href="{{ r }}/.cpr/deps/mini-fa.css?_={{ ts }}">
copyparty/web/msg.html CHANGED
@@ -6,7 +6,7 @@
6
6
  <title>{{ s_doctitle }}</title>
7
7
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
8
8
  <meta name="viewport" content="width=device-width, initial-scale=0.8">
9
- <meta name="theme-color" content="#333">
9
+ <meta name="theme-color" content="#{{ tcolor }}">
10
10
  <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/msg.css?_={{ ts }}">
11
11
  {{ html_head }}
12
12
  </head>
copyparty/web/splash.html CHANGED
@@ -6,7 +6,7 @@
6
6
  <title>{{ s_doctitle }}</title>
7
7
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
8
8
  <meta name="viewport" content="width=device-width, initial-scale=0.8">
9
- <meta name="theme-color" content="#333">
9
+ <meta name="theme-color" content="#{{ tcolor }}">
10
10
  <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/splash.css?_={{ ts }}">
11
11
  <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
12
12
  {{ html_head }}
@@ -95,6 +95,7 @@
95
95
  <form method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}">
96
96
  <input type="hidden" name="act" value="login" />
97
97
  <input type="password" name="cppwd" placeholder=" password" />
98
+ <input type="hidden" name="uhash" id="uhash" value="x" />
98
99
  <input type="submit" value="Login" />
99
100
  {% if ahttps %}
100
101
  <a id="w" href="{{ ahttps }}">switch to https</a>
Binary file
copyparty/web/svcs.html CHANGED
@@ -6,7 +6,7 @@
6
6
  <title>{{ s_doctitle }}</title>
7
7
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
8
8
  <meta name="viewport" content="width=device-width, initial-scale=0.8">
9
- <meta name="theme-color" content="#333">
9
+ <meta name="theme-color" content="#{{ tcolor }}">
10
10
  <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/splash.css?_={{ ts }}">
11
11
  <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
12
12
  <style>ul{padding-left:1.3em}li{margin:.4em 0}</style>
copyparty/web/util.js.gz CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: copyparty
3
- Version: 1.13.0
3
+ Version: 1.13.1
4
4
  Summary: Portable file server with accelerated resumable uploads, deduplication, WebDAV, FTP, zeroconf, media indexer, video thumbnails, audio transcoding, and write-only folders
5
5
  Author-email: ed <copyparty@ocv.me>
6
6
  License: MIT
@@ -101,6 +101,7 @@ turn almost any device into a file server with resumable uploads/downloads using
101
101
  * [audio equalizer](#audio-equalizer) - and [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression)
102
102
  * [fix unreliable playback on android](#fix-unreliable-playback-on-android) - due to phone / app settings
103
103
  * [markdown viewer](#markdown-viewer) - and there are *two* editors
104
+ * [markdown vars](#markdown-vars) - dynamic docs with serverside variable expansion
104
105
  * [other tricks](#other-tricks)
105
106
  * [searching](#searching) - search by size, date, path/name, mp3-tags, ...
106
107
  * [server config](#server-config) - using arguments or config files, or a mix of both
@@ -114,6 +115,7 @@ turn almost any device into a file server with resumable uploads/downloads using
114
115
  * [tftp server](#tftp-server) - a TFTP server (read/write) can be started using `--tftp 3969`
115
116
  * [smb server](#smb-server) - unsafe, slow, not recommended for wan
116
117
  * [browser ux](#browser-ux) - tweaking the ui
118
+ * [opengraph](#opengraph) - discord and social-media embeds
117
119
  * [file indexing](#file-indexing) - enables dedup and music search ++
118
120
  * [exclude-patterns](#exclude-patterns) - to save some time
119
121
  * [filesystem guards](#filesystem-guards) - avoid traversing into other filesystems
@@ -162,8 +164,9 @@ turn almost any device into a file server with resumable uploads/downloads using
162
164
  * [dependencies](#dependencies) - mandatory deps
163
165
  * [optional dependencies](#optional-dependencies) - install these to enable bonus features
164
166
  * [optional gpl stuff](#optional-gpl-stuff)
165
- * [sfx](#sfx) - the self-contained "binary"
167
+ * [sfx](#sfx) - the self-contained "binary" (recommended!)
166
168
  * [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+)
169
+ * [zipapp](#zipapp) - another emergency alternative, [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz)
167
170
  * [install on android](#install-on-android)
168
171
  * [reporting bugs](#reporting-bugs) - ideas for context to include, and where to submit them
169
172
  * [devnotes](#devnotes) - for build instructions etc, see [./docs/devnotes.md](./docs/devnotes.md)
@@ -177,6 +180,7 @@ just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/
177
180
  * or if you cannot install python, you can use [copyparty.exe](#copypartyexe) instead
178
181
  * or install [on arch](#arch-package) ╱ [on NixOS](#nixos-module) ╱ [through nix](#nix-package)
179
182
  * or if you are on android, [install copyparty in termux](#install-on-android)
183
+ * or if your computer is messed up and nothing else works, [try the pyz](#zipapp)
180
184
  * or if you prefer to [use docker](./scripts/docker/) 🐋 you can do that too
181
185
  * docker has all deps built-in, so skip this step:
182
186
 
@@ -288,9 +292,11 @@ also see [comparison to similar software](./docs/versus.md)
288
292
  * client support
289
293
  * ☑ [folder sync](#folder-sync)
290
294
  * ☑ [curl-friendly](https://user-images.githubusercontent.com/241032/215322619-ea5fd606-3654-40ad-94ee-2bc058647bb2.png)
295
+ * ☑ [opengraph](#opengraph) (discord embeds)
291
296
  * markdown
292
297
  * ☑ [viewer](#markdown-viewer)
293
298
  * ☑ editor (sure why not)
299
+ * ☑ [variables](#markdown-vars)
294
300
 
295
301
  PS: something missing? post any crazy ideas you've got as a [feature request](https://github.com/9001/copyparty/issues/new?assignees=9001&labels=enhancement&template=feature_request.md) or [discussion](https://github.com/9001/copyparty/discussions/new?category=ideas) 🤙
296
302
 
@@ -668,9 +674,15 @@ cool trick: download a folder by appending url-params `?tar&opus` or `?tar&mp3`
668
674
 
669
675
  ## uploading
670
676
 
671
- drag files/folders into the web-browser to upload (or use the [command-line uploader](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy))
677
+ drag files/folders into the web-browser to upload
672
678
 
673
- this initiates an upload using `up2k`; there are two uploaders available:
679
+ dragdrop is the recommended way, but you may also:
680
+
681
+ * select some files (not folders) in your file explorer and press CTRL-V inside the browser window
682
+ * use the [command-line uploader](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy)
683
+ * upload using [curl or sharex](#client-examples)
684
+
685
+ when uploading files through dragdrop or CTRL-V, this initiates an upload using `up2k`; there are two browser-based uploaders available:
674
686
  * `[🎈] bup`, the basic uploader, supports almost every browser since netscape 4.0
675
687
  * `[🚀] up2k`, the good / fancy one
676
688
 
@@ -897,6 +909,13 @@ other notes,
897
909
  * the document preview has a max-width which is the same as an A4 paper when printed
898
910
 
899
911
 
912
+ ### markdown vars
913
+
914
+ dynamic docs with serverside variable expansion to replace stuff like `{{self.ip}}` with the client's IP, or `{{srv.htime}}` with the current time on the server
915
+
916
+ see [./srv/expand/](./srv/expand/) for usage and examples
917
+
918
+
900
919
  ## other tricks
901
920
 
902
921
  * you can link a particular timestamp in an audio file by adding it to the URL, such as `&20` / `&20s` / `&1m20` / `&t=1:20` after the `.../#af-c8960dab`
@@ -1121,7 +1140,22 @@ tweaking the ui
1121
1140
  * to sort in music order (album, track, artist, title) with filename as fallback, you could `--sort tags/Cirle,tags/.tn,tags/Artist,tags/Title,href`
1122
1141
  * to sort by upload date, first enable showing the upload date in the listing with `-e2d -mte +.up_at` and then `--sort tags/.up_at`
1123
1142
 
1124
- see [./docs/rice](./docs/rice) for more
1143
+ see [./docs/rice](./docs/rice) for more, including how to add stuff (css/`<meta>`/...) to the html `<head>` tag
1144
+
1145
+
1146
+ ## opengraph
1147
+
1148
+ discord and social-media embeds
1149
+
1150
+ can be enabled globally with `--og` or per-volume with volflag `og`
1151
+
1152
+ note that this disables hotlinking because the opengraph spec demands it; to sneak past this intentional limitation, you can enable opengraph selectively by user-agent, for example `--og-ua '(Discord|Twitter|Slack)bot'` (or volflag `og_ua`)
1153
+
1154
+ you can also hotlink files regardless by appending `?raw` to the url
1155
+
1156
+ NOTE: because discord (and maybe others) strip query args such as `?raw` in opengraph tags, any links which require a filekey or dirkey will not work
1157
+
1158
+ if you want to entirely replace the copyparty response with your own jinja2 template, give the template filepath to `--og-tpl` or volflag `og_tpl` (all members of `HttpCli` are available through the `this` object)
1125
1159
 
1126
1160
 
1127
1161
  ## file indexing
@@ -2035,7 +2069,7 @@ these are standalone programs and will never be imported / evaluated by copypart
2035
2069
 
2036
2070
  # sfx
2037
2071
 
2038
- the self-contained "binary" [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) will unpack itself and run copyparty, assuming you have python installed of course
2072
+ the self-contained "binary" (recommended!) [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) will unpack itself and run copyparty, assuming you have python installed of course
2039
2073
 
2040
2074
  you can reduce the sfx size by repacking it; see [./docs/devnotes.md#sfx-repack](./docs/devnotes.md#sfx-repack)
2041
2075
 
@@ -2062,6 +2096,16 @@ meanwhile [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/d
2062
2096
  then again, if you are already into downloading shady binaries from the internet, you may also want my [minimal builds](./scripts/pyinstaller#ffmpeg) of [ffmpeg](https://ocv.me/stuff/bin/ffmpeg.exe) and [ffprobe](https://ocv.me/stuff/bin/ffprobe.exe) which enables copyparty to extract multimedia-info, do audio-transcoding, and thumbnails/spectrograms/waveforms, however it's much better to instead grab a [recent official build](https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z) every once ina while if you can afford the size
2063
2097
 
2064
2098
 
2099
+ ## zipapp
2100
+
2101
+ another emergency alternative, [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz) has less features, requires python 3.7 or newer, worse compression, and more importantly is unable to benefit from more recent versions of jinja2 and such (which makes it less secure)... lots of drawbacks with this one really -- but it *may* just work if the regular sfx fails to start because the computer is messed up in certain funky ways, so it's worth a shot if all else fails
2102
+
2103
+ run it by doubleclicking it, or try typing `python copyparty.pyz` in your terminal/console/commandline/telex if that fails
2104
+
2105
+ it is a python [zipapp](https://docs.python.org/3/library/zipapp.html) meaning it doesn't have to unpack its own python code anywhere to run, so if the filesystem is busted it has a better chance of getting somewhere
2106
+ * but note that it currently still needs to extract the web-resources somewhere (they'll land in the default TEMP-folder of your OS)
2107
+
2108
+
2065
2109
  # install on android
2066
2110
 
2067
2111
  install [Termux](https://termux.com/) + its companion app `Termux:API` (see [ocv.me/termux](https://ocv.me/termux/)) and then copy-paste this into Termux (long-tap) all at once:
@@ -1,22 +1,22 @@
1
1
  copyparty/__init__.py,sha256=fUINM1abqDGzCCH_JcXdOnLdKOV-SrTI2Xo2QgQW2P4,1703
2
- copyparty/__main__.py,sha256=u0RNQ2iZwlKCqngQofFjDsYyKKD50s5q_oht8RONSuU,95386
3
- copyparty/__version__.py,sha256=Zw2SLEDiKYiPpg31tkxLz17OfRS4iKV7v7yLmGIf3fE,255
4
- copyparty/authsrv.py,sha256=-MZQnSsrEGD8cTtR90qT5-xoc9xD8PP8cjvBL75HMKo,84501
2
+ copyparty/__main__.py,sha256=7fOoPuMQe4KzHAGS5K7KI35-fyj-3rERZVM0dTInOV4,98648
3
+ copyparty/__version__.py,sha256=Ev7V864ayuRrHZmn9gLsfpBYVJs05PYyOWl3-oCL_uU,254
4
+ copyparty/authsrv.py,sha256=UaWwotBSZBEst8LENH7BIaVSL7k2EnUrVzVftTEvtmU,84896
5
5
  copyparty/broker_mp.py,sha256=4mEZC5tiHUazJMgYuwInNo2dxS7jrbzrGb1qs2UBt9k,3948
6
6
  copyparty/broker_mpw.py,sha256=4ZI7bJYOwUibeAJVv9_FPGNmHrr9eOtkj_Kz0JEppTU,3197
7
7
  copyparty/broker_thr.py,sha256=eKr--HJGig5zqvNGwH9UoBG9Nvi9mT2axrRmJwknd0s,1759
8
8
  copyparty/broker_util.py,sha256=CnX_LAhQQqouONcDLtVkVlcBX3Z6pWuKDQDmmbHGEg4,1489
9
9
  copyparty/cert.py,sha256=nCeDdzcCpvjPPUcxT4Oh7wvL_8zvddu4oXtbA-zOb8g,7607
10
- copyparty/cfg.py,sha256=LawUJv8faoWrFWPudtWpcRlakrW6dp8uUKMYR86Nza8,9305
10
+ copyparty/cfg.py,sha256=PSv7T4nm0_ISlijbAP7re61ucvr37xObv8_495ynPws,9584
11
11
  copyparty/dxml.py,sha256=lZpg-kn-kQsXRtNY1n6fRaS-b7uXzMCyv8ovKnhZcZc,1548
12
- copyparty/fsutil.py,sha256=c4fTvmclKbVABNsjU4rGddsjCgRwi9YExAyo-06ATc8,3932
12
+ copyparty/fsutil.py,sha256=NEdhYYgQxDQ7MmgTbtjMKorikCjDls2AXVX16EH2JfQ,4613
13
13
  copyparty/ftpd.py,sha256=OIExjfqOEw-Y_ygez6cIZUQec4SFOmoxEH_WOVvw-aE,15961
14
- copyparty/httpcli.py,sha256=aIMmB_kMvI7PbDvHw5WCmMVMdHPuLJ2rer_NPlgMuvE,155100
14
+ copyparty/httpcli.py,sha256=nuJOPt2T9mjLLRreaCRUmpjoLN5nNkzsXJcCtzn963Q,164338
15
15
  copyparty/httpconn.py,sha256=6MOQgBtOGrlVRr6ZiHBKYzkzcls-YWwaWEtqE6DweM0,6873
16
16
  copyparty/httpsrv.py,sha256=Xf6wI5V25gzAoyEpiKH8VjEFwUqTzW5z8pcRfo2J40c,16421
17
17
  copyparty/ico.py,sha256=AYHdK6NlYBfBgafVYXia3jHQ9XHZdUL1D8WftLMAzIU,3545
18
18
  copyparty/mdns.py,sha256=CcraggbDxTT1ntYzD8Ebgqmw5Q4HkyZcfh5ymtCV_ak,17469
19
- copyparty/metrics.py,sha256=OqXFkAuoVhayGAGd_Sv-OQ9SVmdXYV8M7CxitkzE3lo,8854
19
+ copyparty/metrics.py,sha256=O8qiPNDxNjub_PI8C8Qu9rBQ_z0J1mnKonqkcTeAtf4,8845
20
20
  copyparty/mtag.py,sha256=wiXd26ZSYgOu4lkRDn4KLaqo6H2V7cpqUMepTHTCfKE,16851
21
21
  copyparty/multicast.py,sha256=Ha27l2oATEa-Qo2WOzkeRgjAm6G_YDCfbVJWR-ao2UE,12319
22
22
  copyparty/pwhash.py,sha256=D82y8emnwpHDQq7Cr8lNuppHshbNA9ptcR2XsGOOk6E,3937
@@ -24,15 +24,15 @@ copyparty/smbd.py,sha256=iACj5pbiKsX7bVu20BK3ebPQLB_qA7WS2l-ytrSfT3Y,14054
24
24
  copyparty/ssdp.py,sha256=H6ZftXttydcnBxcg2-Prm4P-XiybgT3xiJRUXU1pbrE,6343
25
25
  copyparty/star.py,sha256=K4NuzyfT4956uoW6GJSQ2II-JsSV57apQZwRZ4mjFoo,3790
26
26
  copyparty/sutil.py,sha256=_G4TM0YFa1vXzhRypHJ88QBdZWtYgDbom4CZjGvGIwc,3074
27
- copyparty/svchub.py,sha256=FiX6B70K5XVhCDuQDty8ZXhujThfkFcH3brp1rsEr-A,32258
27
+ copyparty/svchub.py,sha256=9DTZLgrHRMrlnq6pc_3UngNpxtdlT2SWnhlE2dPaoBk,32419
28
28
  copyparty/szip.py,sha256=631TsEwGKV22yAnusJtvE-9fGFWr61HPGBinu-jk1QA,8591
29
- copyparty/tcpsrv.py,sha256=2LGUqOBAIrsmL-1pwrbsPXR71gutHccqRp-hjzt91Us,17289
29
+ copyparty/tcpsrv.py,sha256=ym6rda7svl_M4DjNesHMI1_6wO7Csu01UV1zGzXEMxI,17637
30
30
  copyparty/tftpd.py,sha256=7EHAZ9LnjAXupwRNIENJ2eA8Q0lFynnwwbziV3fyzns,13157
31
31
  copyparty/th_cli.py,sha256=eSW7sBiaZAsh_XffXFzb035CTSbS3J3Q0G-BMzQGuSY,4385
32
32
  copyparty/th_srv.py,sha256=C2ZBE6ddINCuYDympRQQmhj0ULdlD6HOM6qNK-UB4so,27191
33
- copyparty/u2idx.py,sha256=JBEqKX1ZM8GIvQrDYb5VQ_5QiFNFsjWF6H9drHlPVEY,12709
34
- copyparty/up2k.py,sha256=co26DRvhYgwHpwbvuXUDQlCWSMjDZiIE29KVXHMwiKs,142833
35
- copyparty/util.py,sha256=f9e6vxtnsHxBkctkcJX_iaAg_2AWk4K4p67MMzox144,82942
33
+ copyparty/u2idx.py,sha256=uEUcEbye1jzGlQfEJkLtD060XA6Rv_6lXLgeg6oAU5M,13033
34
+ copyparty/up2k.py,sha256=D-E5y27lJopCtrxUjEi1-oH-kqrOnHL2hQO1Q_X6tHw,143080
35
+ copyparty/util.py,sha256=N8rIBtCX_-oEhfLURsyhf3WfrdUzsdsN4Q1qkFtYKHg,83167
36
36
  copyparty/bos/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
37
  copyparty/bos/bos.py,sha256=Wb7eWsXJgR5AFlBR9ZOyKrLTwy-Kct9RrGiOu4Jo37Y,1622
38
38
  copyparty/bos/path.py,sha256=yEjCq2ki9CvxA5sCT8pS0keEXwugs0ZeUyUhdBziOCI,777
@@ -54,31 +54,31 @@ copyparty/stolen/ifaddr/__init__.py,sha256=_BUN7eM5oD2Jgib6B22tEFSb20fD9urNPPaAl
54
54
  copyparty/stolen/ifaddr/_posix.py,sha256=-67NdfGrCktfQPakT2fLbjl2U00QMvyBGkSvrUuTOrU,2626
55
55
  copyparty/stolen/ifaddr/_shared.py,sha256=cJACl8cOxQ-HSYphZTzKMAjAx_TAFyJwUPjfD102Xqw,6111
56
56
  copyparty/stolen/ifaddr/_win32.py,sha256=EE-QyoBgeB7lYQ6z62VjXNaRozaYfCkaJBHGNA8QtZM,4026
57
- copyparty/web/baguettebox.js.gz,sha256=Qcx5ZJWWCU4S1J0ULVXuVKWnm_SuCiEknMlt_uwIkJ8,7830
58
- copyparty/web/browser.css.gz,sha256=VruUcE9yZm8bpJrPml1lcnJwznaR43Db864qW8Rv4d4,11470
59
- copyparty/web/browser.html,sha256=uAejLJd11rV_tQx3h2nHnJ1XY6zn1JV-meIAv74Lc8o,4873
60
- copyparty/web/browser.js.gz,sha256=_ePuKLuMIrJbKNcwqXcipJHiVzK5ZiaG5JtEExTy440,67893
57
+ copyparty/web/baguettebox.js.gz,sha256=HdRHC_4Lvepp1DrRwusdcxvAn8IKGMdrKdggGIshKek,7869
58
+ copyparty/web/browser.css.gz,sha256=GGyPK9BBOX63x9XWqO2jrXewHtfAeZ-Jo0BBxqTOGUM,11491
59
+ copyparty/web/browser.html,sha256=-tLasq2GKe9mUceqXG4PczQ7odBMrX0qlWuyaA9SjPI,4882
60
+ copyparty/web/browser.js.gz,sha256=9w6hKIv_v8QERNxRQ5DOdZxBijStf-YE8H7SaWLbi7Y,68707
61
61
  copyparty/web/browser2.html,sha256=ciQlgr9GWuIapdsRBFNRvRFvN5T_5n920LqDMbsj5-g,1605
62
62
  copyparty/web/cf.html,sha256=lJThtNFNAQT1ClCHHlivAkDGE0LutedwopXD62Z8Nys,589
63
63
  copyparty/web/dbg-audio.js.gz,sha256=Ma-KZtK8LnmiwNvNKFKXMPYl_Nn_3U7GsJ6-DRWC2HE,688
64
64
  copyparty/web/md.css.gz,sha256=UZpN0J7ubVM05CZkbZYkQRJeGgJt_GNDEzKTGSQd8h4,2032
65
- copyparty/web/md.html,sha256=qnJpj_5-MoVYr9j5Rhy7dS40wctqdWbjELmNa-J9cUY,4110
65
+ copyparty/web/md.html,sha256=35oLUnDYsAdiW7Zg-iKFEXzEl_bGbnoAxUrNgJL46_o,4119
66
66
  copyparty/web/md.js.gz,sha256=AHRQ3a-PZq_UiGh4CjNwXRllJCvA0IqqYmeHhFWhCig,4179
67
67
  copyparty/web/md2.css.gz,sha256=uIVHKScThdbcfhXNSHgKZnALYpxbnXC-WuEzOJ20Lpc,699
68
68
  copyparty/web/md2.js.gz,sha256=8xLixaTfTXC808538OOSLhp9AqKowYaunjDeBsbiBEw,8350
69
69
  copyparty/web/mde.css.gz,sha256=2SkAEDKIRPqywNJ8t_heQaeBQ_R73Rf-pQI_bDoKF6o,942
70
- copyparty/web/mde.html,sha256=FMMq4ySXoOrQV5E836KmQCry3COOhMu0DSstAdJZL_g,1678
70
+ copyparty/web/mde.html,sha256=v0MsEinom5LmZzUM-Ht26IEUkrFzMX57XpCyIQXctAg,1687
71
71
  copyparty/web/mde.js.gz,sha256=kN2eUSvr4mFuksfK4-4LimJmWdwsao39Sea2lWtu8L0,2224
72
72
  copyparty/web/msg.css.gz,sha256=u90fXYAVrMD-jqwf5XFVC1ptSpSHZUe8Mez6PX101P8,300
73
- copyparty/web/msg.html,sha256=XDg51WLO7RruZnoFnKpeJ33k47-tBHP3bR7l55Jwre4,896
73
+ copyparty/web/msg.html,sha256=HcBeXXpcF2JKwcj8KD3dGCvONMnTZ6lXYmm4SYgBMlA,905
74
74
  copyparty/web/splash.css.gz,sha256=zgDs-SY3VrInsXeARRPcGHziVOUs-1hUtSObzybwD1g,1006
75
- copyparty/web/splash.html,sha256=mPhMBTO3BMaie5lGJGloS6b8HhoujUzDZYAosfDX8fg,3846
76
- copyparty/web/splash.js.gz,sha256=2R8UYlAN8WpIABg8clgWckWqgD8nKtz3eGZFu2y1g88,1420
77
- copyparty/web/svcs.html,sha256=s7vUSrCrELC3iTemksodRBhQpssO7s4xW1vA-CX6vU8,11702
75
+ copyparty/web/splash.html,sha256=z5OrfZqA5RBxeY86BJiQ5NZNHIIDHDvPlTuht-Q0v64,3917
76
+ copyparty/web/splash.js.gz,sha256=kPLyo_LaoEdRswRNHU32G4rQY8x5Jes4iAIIvK2ug9U,1442
77
+ copyparty/web/svcs.html,sha256=Lniv3ndzV1ALGOdvMNKg6za5rafrqltuwoknYbExRxM,11711
78
78
  copyparty/web/svcs.js.gz,sha256=k81ZvZ3I-f4fMHKrNGGOgOlvXnCBz0mVjD-8mieoWCA,520
79
79
  copyparty/web/ui.css.gz,sha256=skuzZHqTU0ag5hButpQmKI9wM7ro-UJ2PnpTodTWYF4,2616
80
80
  copyparty/web/up2k.js.gz,sha256=3IKVXjZq7byJWFKyHVylIIbWozsJ6IL7CrOUCibE8BY,22114
81
- copyparty/web/util.js.gz,sha256=3Ys57MotfGguhuCdDhj3afyX8wlz3ZgG5ZrnY4_Zqcw,14377
81
+ copyparty/web/util.js.gz,sha256=eNRKtW7fM9AvipRdJGQyy9mbQIqMda73o8Bo64UWr7s,14416
82
82
  copyparty/web/w.hash.js.gz,sha256=__hBMd5oZWfTrb8ZCJNT21isoSqyrxKE6qdaKGQVAhc,1060
83
83
  copyparty/web/a/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
84
84
  copyparty/web/a/partyfuse.py,sha256=MuRkaSuYsdfWfBFMOkbPwDXqSvNTw3sd7QhhlKCDZ8I,32311
@@ -102,9 +102,9 @@ copyparty/web/deps/prismd.css.gz,sha256=ObUlksQVr-OuYlTz-I4B23TeBg2QDVVGRnWBz8cV
102
102
  copyparty/web/deps/scp.woff2,sha256=w99BDU5i8MukkMEL-iW0YO9H4vFFZSPWxbkH70ytaAg,8612
103
103
  copyparty/web/deps/sha512.ac.js.gz,sha256=lFZaCLumgWxrvEuDr4bqdKHsqjX82AbVAb7_F45Yk88,7033
104
104
  copyparty/web/deps/sha512.hw.js.gz,sha256=vqoXeracj-99Z5MfY3jK2N4WiSzYQdfjy0RnUlQDhSU,8110
105
- copyparty-1.13.0.dist-info/LICENSE,sha256=gOr4h33pCsBEg9uIy9AYmb7qlocL4V9t2uPJS5wllr0,1072
106
- copyparty-1.13.0.dist-info/METADATA,sha256=sa7IcvkdDV532CI8LFuQJg8dzUbplA45cB8Qmn3Ppmw,119305
107
- copyparty-1.13.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
108
- copyparty-1.13.0.dist-info/entry_points.txt,sha256=4zw6a3rqASywQomiYLObjjlxybaI65LYYOTJwgKz7b0,128
109
- copyparty-1.13.0.dist-info/top_level.txt,sha256=LnYUPsDyk-8kFgM6YJLG4h820DQekn81cObKSu9g-sI,10
110
- copyparty-1.13.0.dist-info/RECORD,,
105
+ copyparty-1.13.1.dist-info/LICENSE,sha256=gOr4h33pCsBEg9uIy9AYmb7qlocL4V9t2uPJS5wllr0,1072
106
+ copyparty-1.13.1.dist-info/METADATA,sha256=HMm1ZQpKi3_hfKuT0-Rowdv0kdJXGsK2AK4YU7yaW7Q,122167
107
+ copyparty-1.13.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
108
+ copyparty-1.13.1.dist-info/entry_points.txt,sha256=4zw6a3rqASywQomiYLObjjlxybaI65LYYOTJwgKz7b0,128
109
+ copyparty-1.13.1.dist-info/top_level.txt,sha256=LnYUPsDyk-8kFgM6YJLG4h820DQekn81cObKSu9g-sI,10
110
+ copyparty-1.13.1.dist-info/RECORD,,