copyparty 1.13.0__py3-none-any.whl → 1.13.2__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
@@ -43,11 +43,13 @@ from .util import (
43
43
  DEF_MTH,
44
44
  IMPLICATIONS,
45
45
  JINJA_VER,
46
+ MIMES,
46
47
  PARTFTPY_VER,
47
48
  PY_DESC,
48
49
  PYFTPD_VER,
49
50
  SQLITE_VER,
50
51
  UNPLICATIONS,
52
+ Daemon,
51
53
  align_tab,
52
54
  ansi_re,
53
55
  dedent,
@@ -464,6 +466,16 @@ def disable_quickedit() :
464
466
  cmode(True, mode | 4)
465
467
 
466
468
 
469
+ def sfx_tpoke(top ):
470
+ files = [os.path.join(dp, p) for dp, dd, df in os.walk(top) for p in dd + df]
471
+ while True:
472
+ t = int(time.time())
473
+ for f in [top] + files:
474
+ os.utime(f, (t, t))
475
+
476
+ time.sleep(78123)
477
+
478
+
467
479
  def showlic() :
468
480
  p = os.path.join(E.mod, "res", "COPYING.txt")
469
481
  if not os.path.exists(p):
@@ -814,7 +826,7 @@ def build_flags_desc():
814
826
  v = v.replace("\n", "\n ")
815
827
  ret += "\n \033[36m{}\033[35m {}".format(k, v)
816
828
 
817
- return ret + "\033[0m"
829
+ return ret
818
830
 
819
831
 
820
832
  # fmt: off
@@ -832,6 +844,8 @@ def add_general(ap, nc, srvname):
832
844
  ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,get", help="how to handle url-form POSTs; see \033[33m--help-urlform\033[0m")
833
845
  ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="server terminal title, for example [\033[32m$ip-10.1.2.\033[0m] or [\033[32m$ip-]")
834
846
  ap2.add_argument("--name", metavar="TXT", type=u, default=srvname, help="server name (displayed topleft in browser and in mDNS)")
847
+ ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="map file \033[33mEXT\033[0mension to \033[33mMIME\033[0mtype, for example [\033[32mjpg=image/jpeg\033[0m]")
848
+ ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit")
835
849
  ap2.add_argument("--license", action="store_true", help="show licenses and exit")
836
850
  ap2.add_argument("--version", action="store_true", help="show versions and exit")
837
851
 
@@ -854,6 +868,7 @@ def add_fs(ap):
854
868
  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
869
  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
870
  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)")
871
+ 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
872
 
858
873
 
859
874
  def add_upload(ap):
@@ -1187,7 +1202,8 @@ def add_thumbnail(ap):
1187
1202
  ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips")
1188
1203
  ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,dds,dib,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
1189
1204
  ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg")
1190
- ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,m4a,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,ogg,okt,opus,ra,s3m,tak,tta,ulaw,wav,wma,wv,xm,xpk", help="audio formats to decode using ffmpeg")
1205
+ ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
1206
+ ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz", help="audio formats to decompress before passing to ffmpeg")
1191
1207
 
1192
1208
 
1193
1209
  def add_transcoding(ap):
@@ -1250,19 +1266,38 @@ def add_txt(ap):
1250
1266
  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
1267
 
1252
1268
 
1269
+ def add_og(ap):
1270
+ ap2 = ap.add_argument_group('og / open graph / discord-embed options')
1271
+ 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)")
1272
+ 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)")
1273
+ 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)")
1274
+ 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)")
1275
+ 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)")
1276
+ 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)")
1277
+ 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)")
1278
+ ap2.add_argument("--og-title-v", metavar="T", type=u, default="{{ title }}", help="video title format; takes any metadata key (volflag=og_title_v)")
1279
+ ap2.add_argument("--og-title-i", metavar="T", type=u, default="{{ title }}", help="image title format; takes any metadata key (volflag=og_title_i)")
1280
+ ap2.add_argument("--og-s-title", action="store_true", help="force default title; do not read from tags (volflag=og_s_title)")
1281
+ 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)")
1282
+ 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)")
1283
+ 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)")
1284
+ 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")
1285
+
1286
+
1253
1287
  def add_ui(ap, retry):
1254
1288
  ap2 = ap.add_argument_group('ui options')
1255
1289
  ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
1256
1290
  ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language; one of the following: \033[32meng nor\033[0m")
1257
1291
  ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..7)")
1258
1292
  ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed")
1293
+ ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent")
1259
1294
  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
1295
  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
1296
  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
1297
  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
1298
  ap2.add_argument("--js-browser", metavar="L", type=u, help="URL to additional JS to include")
1264
1299
  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")
1300
+ 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
1301
  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
1302
  ap2.add_argument("--textfiles", metavar="CSV", type=u, default="txt,nfo,diz,cue,readme", help="file extensions to present as plaintext")
1268
1303
  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 +1385,7 @@ def run_argparse(
1350
1385
  add_hooks(ap)
1351
1386
  add_stats(ap)
1352
1387
  add_txt(ap)
1388
+ add_og(ap)
1353
1389
  add_ui(ap, retry)
1354
1390
  add_admin(ap)
1355
1391
  add_logging(ap)
@@ -1378,18 +1414,22 @@ def run_argparse(
1378
1414
  k2 = "help_" + k.replace("-", "_")
1379
1415
  if vars(ret)[k2]:
1380
1416
  lprint("# %s help page (%s)" % (k, h))
1381
- lprint(t + "\033[0m")
1417
+ lprint(t.rstrip() + "\033[0m")
1382
1418
  sys.exit(0)
1383
1419
 
1384
1420
  return ret
1385
1421
 
1386
1422
 
1387
- def main(argv = None) :
1423
+ def main(argv = None, rsrc = None) :
1388
1424
  time.strptime("19970815", "%Y%m%d") # python#7980
1389
1425
  if WINDOWS:
1390
1426
  os.system("rem") # enables colors
1391
1427
 
1392
1428
  init_E(E)
1429
+
1430
+ if rsrc: # pyz
1431
+ E.mod = rsrc
1432
+
1393
1433
  if argv is None:
1394
1434
  argv = sys.argv
1395
1435
 
@@ -1413,9 +1453,19 @@ def main(argv = None) :
1413
1453
  showlic()
1414
1454
  sys.exit(0)
1415
1455
 
1456
+ if "--mimes" in argv:
1457
+ print("\n".join("%8s %s" % (k, v) for k, v in sorted(MIMES.items())))
1458
+ sys.exit(0)
1459
+
1416
1460
  if EXE:
1417
1461
  print("pybin: {}\n".format(pybin), end="")
1418
1462
 
1463
+ for n, zs in enumerate(argv):
1464
+ if zs.startswith("--sfx-tpoke="):
1465
+ Daemon(sfx_tpoke, "sfx-tpoke", (zs.split("=", 1)[1],))
1466
+ argv.pop(n)
1467
+ break
1468
+
1419
1469
  ensure_locale()
1420
1470
 
1421
1471
  ensure_webdeps()
copyparty/__version__.py CHANGED
@@ -1,8 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
- VERSION = (1, 13, 0)
3
+ VERSION = (1, 13, 2)
4
4
  CODENAME = "race the beam"
5
- BUILD_DT = (2024, 4, 20)
5
+ BUILD_DT = (2024, 5, 10)
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
@@ -17,7 +17,9 @@ from .bos import bos
17
17
  from .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap
18
18
  from .pwhash import PWHash
19
19
  from .util import (
20
+ EXTS,
20
21
  IMPLICATIONS,
22
+ MIMES,
21
23
  SQLITE_VER,
22
24
  UNPLICATIONS,
23
25
  UTC,
@@ -1435,6 +1437,7 @@ class AuthSrv(object):
1435
1437
  elif "" not in mount:
1436
1438
  # there's volumes but no root; make root inaccessible
1437
1439
  vfs = VFS(self.log_func, "", "", AXS(), {})
1440
+ vfs.flags["tcolor"] = self.args.tcolor
1438
1441
  vfs.flags["d2d"] = True
1439
1442
 
1440
1443
  maxdepth = 0
@@ -1720,7 +1723,11 @@ class AuthSrv(object):
1720
1723
  if self.args.e2d or "e2ds" in vol.flags:
1721
1724
  vol.flags["e2d"] = True
1722
1725
 
1723
- for ga, vf in [["no_hash", "nohash"], ["no_idx", "noidx"]]:
1726
+ for ga, vf in [
1727
+ ["no_hash", "nohash"],
1728
+ ["no_idx", "noidx"],
1729
+ ["og_ua", "og_ua"],
1730
+ ]:
1724
1731
  if vf in vol.flags:
1725
1732
  ptn = re.compile(vol.flags.pop(vf))
1726
1733
  else:
@@ -1766,6 +1773,13 @@ class AuthSrv(object):
1766
1773
  t = 'volume "/%s" has invalid %stry [%s]'
1767
1774
  raise Exception(t % (vol.vpath, k, vol.flags.get(k + "try")))
1768
1775
 
1776
+ if vol.flags.get("og"):
1777
+ self.args.uqe = True
1778
+
1779
+ zs = str(vol.flags.get("tcolor", "")).lstrip("#")
1780
+ if len(zs) == 3: # fc5 => ffcc55
1781
+ vol.flags["tcolor"] = "".join([x * 2 for x in zs])
1782
+
1769
1783
  for k1, k2 in IMPLICATIONS:
1770
1784
  if k1 in vol.flags:
1771
1785
  vol.flags[k2] = True
@@ -2046,6 +2060,13 @@ class AuthSrv(object):
2046
2060
 
2047
2061
  self.re_pwd = re.compile(zs)
2048
2062
 
2063
+ # to ensure it propagates into tcpsrv with mp on
2064
+ if self.args.mime:
2065
+ for zs in self.args.mime:
2066
+ ext, mime = zs.split("=", 1)
2067
+ MIMES[ext] = mime
2068
+ EXTS.update({v: k for k, v in MIMES.items()})
2069
+
2049
2070
  def setup_pwhash(self, acct ) :
2050
2071
  self.ah = PWHash(self.args)
2051
2072
  if not self.ah.on:
@@ -2403,7 +2424,7 @@ def expand_config_file(
2403
2424
  if not cnames:
2404
2425
  t = "warning: tried to read config-files from folder '%s' but it does not contain any "
2405
2426
  if names:
2406
- t += ".conf files; the following files were ignored: %s"
2427
+ t += ".conf files; the following files/subfolders were ignored: %s"
2407
2428
  t = t % (fp, ", ".join(names[:8]))
2408
2429
  else:
2409
2430
  t += "files at all"
copyparty/broker_mp.py CHANGED
@@ -53,11 +53,8 @@ class BrokerMp(object):
53
53
  def shutdown(self) :
54
54
  self.log("broker", "shutting down")
55
55
  for n, proc in enumerate(self.procs):
56
- thr = threading.Thread(
57
- target=proc.q_pend.put((0, "shutdown", [])),
58
- name="mp-shutdown-{}-{}".format(n, len(self.procs)),
59
- )
60
- thr.start()
56
+ name = "mp-shut-%d-%d" % (n, len(self.procs))
57
+ Daemon(proc.q_pend.put, name, ((0, "shutdown", []),))
61
58
 
62
59
  with self.mutex:
63
60
  procs = self.procs
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",
@@ -177,6 +190,7 @@ flagcats = {
177
190
  "dvthumb": "disables video thumbnails",
178
191
  "dathumb": "disables audio thumbnails (spectrograms)",
179
192
  "dithumb": "disables image thumbnails",
193
+ "pngquant": "compress audio waveforms 33% better",
180
194
  "thsize": "thumbnail res; WxH",
181
195
  "crop": "center-cropping (y/n/fy/fn)",
182
196
  "th3x": "3x resolution (y/n/fy/fn)",
@@ -201,7 +215,7 @@ flagcats = {
201
215
  "grid": "show grid/thumbnails by default",
202
216
  "sort": "default sort order",
203
217
  "unlist": "dont list files matching REGEX",
204
- "html_head=TXT": "includes TXT in the <head>",
218
+ "html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH",
205
219
  "robots": "allows indexing by search engines (default)",
206
220
  "norobots": "kindly asks search engines to leave",
207
221
  "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