##// END OF EJS Templates
vfs: add a `register_file` method on the vfs class...
marmoute -
r48236:9ab54aa5 default
parent child Browse files
Show More
@@ -1,824 +1,829 b''
1 1 # store.py - repository store handling for Mercurial
2 2 #
3 3 # Copyright 2008 Olivia Mackall <olivia@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import functools
12 12 import os
13 13 import re
14 14 import stat
15 15
16 16 from .i18n import _
17 17 from .pycompat import getattr
18 18 from .node import hex
19 19 from . import (
20 20 changelog,
21 21 error,
22 22 manifest,
23 23 policy,
24 24 pycompat,
25 25 util,
26 26 vfs as vfsmod,
27 27 )
28 28 from .utils import hashutil
29 29
30 30 parsers = policy.importmod('parsers')
31 31 # how much bytes should be read from fncache in one read
32 32 # It is done to prevent loading large fncache files into memory
33 33 fncache_chunksize = 10 ** 6
34 34
35 35
36 36 def _matchtrackedpath(path, matcher):
37 37 """parses a fncache entry and returns whether the entry is tracking a path
38 38 matched by matcher or not.
39 39
40 40 If matcher is None, returns True"""
41 41
42 42 if matcher is None:
43 43 return True
44 44 path = decodedir(path)
45 45 if path.startswith(b'data/'):
46 46 return matcher(path[len(b'data/') : -len(b'.i')])
47 47 elif path.startswith(b'meta/'):
48 48 return matcher.visitdir(path[len(b'meta/') : -len(b'/00manifest.i')])
49 49
50 50 raise error.ProgrammingError(b"cannot decode path %s" % path)
51 51
52 52
53 53 # This avoids a collision between a file named foo and a dir named
54 54 # foo.i or foo.d
55 55 def _encodedir(path):
56 56 """
57 57 >>> _encodedir(b'data/foo.i')
58 58 'data/foo.i'
59 59 >>> _encodedir(b'data/foo.i/bla.i')
60 60 'data/foo.i.hg/bla.i'
61 61 >>> _encodedir(b'data/foo.i.hg/bla.i')
62 62 'data/foo.i.hg.hg/bla.i'
63 63 >>> _encodedir(b'data/foo.i\\ndata/foo.i/bla.i\\ndata/foo.i.hg/bla.i\\n')
64 64 'data/foo.i\\ndata/foo.i.hg/bla.i\\ndata/foo.i.hg.hg/bla.i\\n'
65 65 """
66 66 return (
67 67 path.replace(b".hg/", b".hg.hg/")
68 68 .replace(b".i/", b".i.hg/")
69 69 .replace(b".d/", b".d.hg/")
70 70 )
71 71
72 72
73 73 encodedir = getattr(parsers, 'encodedir', _encodedir)
74 74
75 75
76 76 def decodedir(path):
77 77 """
78 78 >>> decodedir(b'data/foo.i')
79 79 'data/foo.i'
80 80 >>> decodedir(b'data/foo.i.hg/bla.i')
81 81 'data/foo.i/bla.i'
82 82 >>> decodedir(b'data/foo.i.hg.hg/bla.i')
83 83 'data/foo.i.hg/bla.i'
84 84 """
85 85 if b".hg/" not in path:
86 86 return path
87 87 return (
88 88 path.replace(b".d.hg/", b".d/")
89 89 .replace(b".i.hg/", b".i/")
90 90 .replace(b".hg.hg/", b".hg/")
91 91 )
92 92
93 93
94 94 def _reserved():
95 95 """characters that are problematic for filesystems
96 96
97 97 * ascii escapes (0..31)
98 98 * ascii hi (126..255)
99 99 * windows specials
100 100
101 101 these characters will be escaped by encodefunctions
102 102 """
103 103 winreserved = [ord(x) for x in u'\\:*?"<>|']
104 104 for x in range(32):
105 105 yield x
106 106 for x in range(126, 256):
107 107 yield x
108 108 for x in winreserved:
109 109 yield x
110 110
111 111
112 112 def _buildencodefun():
113 113 """
114 114 >>> enc, dec = _buildencodefun()
115 115
116 116 >>> enc(b'nothing/special.txt')
117 117 'nothing/special.txt'
118 118 >>> dec(b'nothing/special.txt')
119 119 'nothing/special.txt'
120 120
121 121 >>> enc(b'HELLO')
122 122 '_h_e_l_l_o'
123 123 >>> dec(b'_h_e_l_l_o')
124 124 'HELLO'
125 125
126 126 >>> enc(b'hello:world?')
127 127 'hello~3aworld~3f'
128 128 >>> dec(b'hello~3aworld~3f')
129 129 'hello:world?'
130 130
131 131 >>> enc(b'the\\x07quick\\xADshot')
132 132 'the~07quick~adshot'
133 133 >>> dec(b'the~07quick~adshot')
134 134 'the\\x07quick\\xadshot'
135 135 """
136 136 e = b'_'
137 137 xchr = pycompat.bytechr
138 138 asciistr = list(map(xchr, range(127)))
139 139 capitals = list(range(ord(b"A"), ord(b"Z") + 1))
140 140
141 141 cmap = {x: x for x in asciistr}
142 142 for x in _reserved():
143 143 cmap[xchr(x)] = b"~%02x" % x
144 144 for x in capitals + [ord(e)]:
145 145 cmap[xchr(x)] = e + xchr(x).lower()
146 146
147 147 dmap = {}
148 148 for k, v in pycompat.iteritems(cmap):
149 149 dmap[v] = k
150 150
151 151 def decode(s):
152 152 i = 0
153 153 while i < len(s):
154 154 for l in pycompat.xrange(1, 4):
155 155 try:
156 156 yield dmap[s[i : i + l]]
157 157 i += l
158 158 break
159 159 except KeyError:
160 160 pass
161 161 else:
162 162 raise KeyError
163 163
164 164 return (
165 165 lambda s: b''.join(
166 166 [cmap[s[c : c + 1]] for c in pycompat.xrange(len(s))]
167 167 ),
168 168 lambda s: b''.join(list(decode(s))),
169 169 )
170 170
171 171
172 172 _encodefname, _decodefname = _buildencodefun()
173 173
174 174
175 175 def encodefilename(s):
176 176 """
177 177 >>> encodefilename(b'foo.i/bar.d/bla.hg/hi:world?/HELLO')
178 178 'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o'
179 179 """
180 180 return _encodefname(encodedir(s))
181 181
182 182
183 183 def decodefilename(s):
184 184 """
185 185 >>> decodefilename(b'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o')
186 186 'foo.i/bar.d/bla.hg/hi:world?/HELLO'
187 187 """
188 188 return decodedir(_decodefname(s))
189 189
190 190
191 191 def _buildlowerencodefun():
192 192 """
193 193 >>> f = _buildlowerencodefun()
194 194 >>> f(b'nothing/special.txt')
195 195 'nothing/special.txt'
196 196 >>> f(b'HELLO')
197 197 'hello'
198 198 >>> f(b'hello:world?')
199 199 'hello~3aworld~3f'
200 200 >>> f(b'the\\x07quick\\xADshot')
201 201 'the~07quick~adshot'
202 202 """
203 203 xchr = pycompat.bytechr
204 204 cmap = {xchr(x): xchr(x) for x in pycompat.xrange(127)}
205 205 for x in _reserved():
206 206 cmap[xchr(x)] = b"~%02x" % x
207 207 for x in range(ord(b"A"), ord(b"Z") + 1):
208 208 cmap[xchr(x)] = xchr(x).lower()
209 209
210 210 def lowerencode(s):
211 211 return b"".join([cmap[c] for c in pycompat.iterbytestr(s)])
212 212
213 213 return lowerencode
214 214
215 215
216 216 lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun()
217 217
218 218 # Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9
219 219 _winres3 = (b'aux', b'con', b'prn', b'nul') # length 3
220 220 _winres4 = (b'com', b'lpt') # length 4 (with trailing 1..9)
221 221
222 222
223 223 def _auxencode(path, dotencode):
224 224 """
225 225 Encodes filenames containing names reserved by Windows or which end in
226 226 period or space. Does not touch other single reserved characters c.
227 227 Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here.
228 228 Additionally encodes space or period at the beginning, if dotencode is
229 229 True. Parameter path is assumed to be all lowercase.
230 230 A segment only needs encoding if a reserved name appears as a
231 231 basename (e.g. "aux", "aux.foo"). A directory or file named "foo.aux"
232 232 doesn't need encoding.
233 233
234 234 >>> s = b'.foo/aux.txt/txt.aux/con/prn/nul/foo.'
235 235 >>> _auxencode(s.split(b'/'), True)
236 236 ['~2efoo', 'au~78.txt', 'txt.aux', 'co~6e', 'pr~6e', 'nu~6c', 'foo~2e']
237 237 >>> s = b'.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.'
238 238 >>> _auxencode(s.split(b'/'), False)
239 239 ['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e']
240 240 >>> _auxencode([b'foo. '], True)
241 241 ['foo.~20']
242 242 >>> _auxencode([b' .foo'], True)
243 243 ['~20.foo']
244 244 """
245 245 for i, n in enumerate(path):
246 246 if not n:
247 247 continue
248 248 if dotencode and n[0] in b'. ':
249 249 n = b"~%02x" % ord(n[0:1]) + n[1:]
250 250 path[i] = n
251 251 else:
252 252 l = n.find(b'.')
253 253 if l == -1:
254 254 l = len(n)
255 255 if (l == 3 and n[:3] in _winres3) or (
256 256 l == 4
257 257 and n[3:4] <= b'9'
258 258 and n[3:4] >= b'1'
259 259 and n[:3] in _winres4
260 260 ):
261 261 # encode third letter ('aux' -> 'au~78')
262 262 ec = b"~%02x" % ord(n[2:3])
263 263 n = n[0:2] + ec + n[3:]
264 264 path[i] = n
265 265 if n[-1] in b'. ':
266 266 # encode last period or space ('foo...' -> 'foo..~2e')
267 267 path[i] = n[:-1] + b"~%02x" % ord(n[-1:])
268 268 return path
269 269
270 270
271 271 _maxstorepathlen = 120
272 272 _dirprefixlen = 8
273 273 _maxshortdirslen = 8 * (_dirprefixlen + 1) - 4
274 274
275 275
276 276 def _hashencode(path, dotencode):
277 277 digest = hex(hashutil.sha1(path).digest())
278 278 le = lowerencode(path[5:]).split(b'/') # skips prefix 'data/' or 'meta/'
279 279 parts = _auxencode(le, dotencode)
280 280 basename = parts[-1]
281 281 _root, ext = os.path.splitext(basename)
282 282 sdirs = []
283 283 sdirslen = 0
284 284 for p in parts[:-1]:
285 285 d = p[:_dirprefixlen]
286 286 if d[-1] in b'. ':
287 287 # Windows can't access dirs ending in period or space
288 288 d = d[:-1] + b'_'
289 289 if sdirslen == 0:
290 290 t = len(d)
291 291 else:
292 292 t = sdirslen + 1 + len(d)
293 293 if t > _maxshortdirslen:
294 294 break
295 295 sdirs.append(d)
296 296 sdirslen = t
297 297 dirs = b'/'.join(sdirs)
298 298 if len(dirs) > 0:
299 299 dirs += b'/'
300 300 res = b'dh/' + dirs + digest + ext
301 301 spaceleft = _maxstorepathlen - len(res)
302 302 if spaceleft > 0:
303 303 filler = basename[:spaceleft]
304 304 res = b'dh/' + dirs + filler + digest + ext
305 305 return res
306 306
307 307
308 308 def _hybridencode(path, dotencode):
309 309 """encodes path with a length limit
310 310
311 311 Encodes all paths that begin with 'data/', according to the following.
312 312
313 313 Default encoding (reversible):
314 314
315 315 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
316 316 characters are encoded as '~xx', where xx is the two digit hex code
317 317 of the character (see encodefilename).
318 318 Relevant path components consisting of Windows reserved filenames are
319 319 masked by encoding the third character ('aux' -> 'au~78', see _auxencode).
320 320
321 321 Hashed encoding (not reversible):
322 322
323 323 If the default-encoded path is longer than _maxstorepathlen, a
324 324 non-reversible hybrid hashing of the path is done instead.
325 325 This encoding uses up to _dirprefixlen characters of all directory
326 326 levels of the lowerencoded path, but not more levels than can fit into
327 327 _maxshortdirslen.
328 328 Then follows the filler followed by the sha digest of the full path.
329 329 The filler is the beginning of the basename of the lowerencoded path
330 330 (the basename is everything after the last path separator). The filler
331 331 is as long as possible, filling in characters from the basename until
332 332 the encoded path has _maxstorepathlen characters (or all chars of the
333 333 basename have been taken).
334 334 The extension (e.g. '.i' or '.d') is preserved.
335 335
336 336 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
337 337 encoding was used.
338 338 """
339 339 path = encodedir(path)
340 340 ef = _encodefname(path).split(b'/')
341 341 res = b'/'.join(_auxencode(ef, dotencode))
342 342 if len(res) > _maxstorepathlen:
343 343 res = _hashencode(path, dotencode)
344 344 return res
345 345
346 346
347 347 def _pathencode(path):
348 348 de = encodedir(path)
349 349 if len(path) > _maxstorepathlen:
350 350 return _hashencode(de, True)
351 351 ef = _encodefname(de).split(b'/')
352 352 res = b'/'.join(_auxencode(ef, True))
353 353 if len(res) > _maxstorepathlen:
354 354 return _hashencode(de, True)
355 355 return res
356 356
357 357
358 358 _pathencode = getattr(parsers, 'pathencode', _pathencode)
359 359
360 360
361 361 def _plainhybridencode(f):
362 362 return _hybridencode(f, False)
363 363
364 364
365 365 def _calcmode(vfs):
366 366 try:
367 367 # files in .hg/ will be created using this mode
368 368 mode = vfs.stat().st_mode
369 369 # avoid some useless chmods
370 370 if (0o777 & ~util.umask) == (0o777 & mode):
371 371 mode = None
372 372 except OSError:
373 373 mode = None
374 374 return mode
375 375
376 376
377 377 _data = [
378 378 b'bookmarks',
379 379 b'narrowspec',
380 380 b'data',
381 381 b'meta',
382 382 b'00manifest.d',
383 383 b'00manifest.i',
384 384 b'00changelog.d',
385 385 b'00changelog.i',
386 386 b'phaseroots',
387 387 b'obsstore',
388 388 b'requires',
389 389 ]
390 390
391 391 REVLOG_FILES_MAIN_EXT = (b'.i', b'i.tmpcensored')
392 392 REVLOG_FILES_OTHER_EXT = (
393 393 b'.idx',
394 394 b'.d',
395 395 b'.dat',
396 396 b'.n',
397 397 b'.nd',
398 398 b'.sda',
399 399 b'd.tmpcensored',
400 400 )
401 401 # files that are "volatile" and might change between listing and streaming
402 402 #
403 403 # note: the ".nd" file are nodemap data and won't "change" but they might be
404 404 # deleted.
405 405 REVLOG_FILES_VOLATILE_EXT = (b'.n', b'.nd')
406 406
407 407 # some exception to the above matching
408 408 EXCLUDED = re.compile(b'.*undo\.[^/]+\.(nd?|i)$')
409 409
410 410
411 411 def is_revlog(f, kind, st):
412 412 if kind != stat.S_IFREG:
413 413 return None
414 414 return revlog_type(f)
415 415
416 416
417 417 def revlog_type(f):
418 418 if f.endswith(REVLOG_FILES_MAIN_EXT) and EXCLUDED.match(f) is None:
419 419 return FILEFLAGS_REVLOG_MAIN
420 420 elif f.endswith(REVLOG_FILES_OTHER_EXT) and EXCLUDED.match(f) is None:
421 421 t = FILETYPE_FILELOG_OTHER
422 422 if f.endswith(REVLOG_FILES_VOLATILE_EXT):
423 423 t |= FILEFLAGS_VOLATILE
424 424 return t
425 425 return None
426 426
427 427
428 428 # the file is part of changelog data
429 429 FILEFLAGS_CHANGELOG = 1 << 13
430 430 # the file is part of manifest data
431 431 FILEFLAGS_MANIFESTLOG = 1 << 12
432 432 # the file is part of filelog data
433 433 FILEFLAGS_FILELOG = 1 << 11
434 434 # file that are not directly part of a revlog
435 435 FILEFLAGS_OTHER = 1 << 10
436 436
437 437 # the main entry point for a revlog
438 438 FILEFLAGS_REVLOG_MAIN = 1 << 1
439 439 # a secondary file for a revlog
440 440 FILEFLAGS_REVLOG_OTHER = 1 << 0
441 441
442 442 # files that are "volatile" and might change between listing and streaming
443 443 FILEFLAGS_VOLATILE = 1 << 20
444 444
445 445 FILETYPE_CHANGELOG_MAIN = FILEFLAGS_CHANGELOG | FILEFLAGS_REVLOG_MAIN
446 446 FILETYPE_CHANGELOG_OTHER = FILEFLAGS_CHANGELOG | FILEFLAGS_REVLOG_OTHER
447 447 FILETYPE_MANIFESTLOG_MAIN = FILEFLAGS_MANIFESTLOG | FILEFLAGS_REVLOG_MAIN
448 448 FILETYPE_MANIFESTLOG_OTHER = FILEFLAGS_MANIFESTLOG | FILEFLAGS_REVLOG_OTHER
449 449 FILETYPE_FILELOG_MAIN = FILEFLAGS_FILELOG | FILEFLAGS_REVLOG_MAIN
450 450 FILETYPE_FILELOG_OTHER = FILEFLAGS_FILELOG | FILEFLAGS_REVLOG_OTHER
451 451 FILETYPE_OTHER = FILEFLAGS_OTHER
452 452
453 453
454 454 class basicstore(object):
455 455 '''base class for local repository stores'''
456 456
457 457 def __init__(self, path, vfstype):
458 458 vfs = vfstype(path)
459 459 self.path = vfs.base
460 460 self.createmode = _calcmode(vfs)
461 461 vfs.createmode = self.createmode
462 462 self.rawvfs = vfs
463 463 self.vfs = vfsmod.filtervfs(vfs, encodedir)
464 464 self.opener = self.vfs
465 465
466 466 def join(self, f):
467 467 return self.path + b'/' + encodedir(f)
468 468
469 469 def _walk(self, relpath, recurse):
470 470 '''yields (unencoded, encoded, size)'''
471 471 path = self.path
472 472 if relpath:
473 473 path += b'/' + relpath
474 474 striplen = len(self.path) + 1
475 475 l = []
476 476 if self.rawvfs.isdir(path):
477 477 visit = [path]
478 478 readdir = self.rawvfs.readdir
479 479 while visit:
480 480 p = visit.pop()
481 481 for f, kind, st in readdir(p, stat=True):
482 482 fp = p + b'/' + f
483 483 rl_type = is_revlog(f, kind, st)
484 484 if rl_type is not None:
485 485 n = util.pconvert(fp[striplen:])
486 486 l.append((rl_type, decodedir(n), n, st.st_size))
487 487 elif kind == stat.S_IFDIR and recurse:
488 488 visit.append(fp)
489 489 l.sort()
490 490 return l
491 491
492 492 def changelog(self, trypending, concurrencychecker=None):
493 493 return changelog.changelog(
494 494 self.vfs,
495 495 trypending=trypending,
496 496 concurrencychecker=concurrencychecker,
497 497 )
498 498
499 499 def manifestlog(self, repo, storenarrowmatch):
500 500 rootstore = manifest.manifestrevlog(repo.nodeconstants, self.vfs)
501 501 return manifest.manifestlog(self.vfs, repo, rootstore, storenarrowmatch)
502 502
503 503 def datafiles(self, matcher=None):
504 504 files = self._walk(b'data', True) + self._walk(b'meta', True)
505 505 for (t, u, e, s) in files:
506 506 yield (FILEFLAGS_FILELOG | t, u, e, s)
507 507
508 508 def topfiles(self):
509 509 # yield manifest before changelog
510 510 files = reversed(self._walk(b'', False))
511 511 for (t, u, e, s) in files:
512 512 if u.startswith(b'00changelog'):
513 513 yield (FILEFLAGS_CHANGELOG | t, u, e, s)
514 514 elif u.startswith(b'00manifest'):
515 515 yield (FILEFLAGS_MANIFESTLOG | t, u, e, s)
516 516 else:
517 517 yield (FILETYPE_OTHER | t, u, e, s)
518 518
519 519 def walk(self, matcher=None):
520 520 """return file related to data storage (ie: revlogs)
521 521
522 522 yields (file_type, unencoded, encoded, size)
523 523
524 524 if a matcher is passed, storage files of only those tracked paths
525 525 are passed with matches the matcher
526 526 """
527 527 # yield data files first
528 528 for x in self.datafiles(matcher):
529 529 yield x
530 530 for x in self.topfiles():
531 531 yield x
532 532
533 533 def copylist(self):
534 534 return _data
535 535
536 536 def write(self, tr):
537 537 pass
538 538
539 539 def invalidatecaches(self):
540 540 pass
541 541
542 542 def markremoved(self, fn):
543 543 pass
544 544
545 545 def __contains__(self, path):
546 546 '''Checks if the store contains path'''
547 547 path = b"/".join((b"data", path))
548 548 # file?
549 549 if self.vfs.exists(path + b".i"):
550 550 return True
551 551 # dir?
552 552 if not path.endswith(b"/"):
553 553 path = path + b"/"
554 554 return self.vfs.exists(path)
555 555
556 556
557 557 class encodedstore(basicstore):
558 558 def __init__(self, path, vfstype):
559 559 vfs = vfstype(path + b'/store')
560 560 self.path = vfs.base
561 561 self.createmode = _calcmode(vfs)
562 562 vfs.createmode = self.createmode
563 563 self.rawvfs = vfs
564 564 self.vfs = vfsmod.filtervfs(vfs, encodefilename)
565 565 self.opener = self.vfs
566 566
567 567 def datafiles(self, matcher=None):
568 568 for t, a, b, size in super(encodedstore, self).datafiles():
569 569 try:
570 570 a = decodefilename(a)
571 571 except KeyError:
572 572 a = None
573 573 if a is not None and not _matchtrackedpath(a, matcher):
574 574 continue
575 575 yield t, a, b, size
576 576
577 577 def join(self, f):
578 578 return self.path + b'/' + encodefilename(f)
579 579
580 580 def copylist(self):
581 581 return [b'requires', b'00changelog.i'] + [b'store/' + f for f in _data]
582 582
583 583
584 584 class fncache(object):
585 585 # the filename used to be partially encoded
586 586 # hence the encodedir/decodedir dance
587 587 def __init__(self, vfs):
588 588 self.vfs = vfs
589 589 self.entries = None
590 590 self._dirty = False
591 591 # set of new additions to fncache
592 592 self.addls = set()
593 593
594 594 def ensureloaded(self, warn=None):
595 595 """read the fncache file if not already read.
596 596
597 597 If the file on disk is corrupted, raise. If warn is provided,
598 598 warn and keep going instead."""
599 599 if self.entries is None:
600 600 self._load(warn)
601 601
602 602 def _load(self, warn=None):
603 603 '''fill the entries from the fncache file'''
604 604 self._dirty = False
605 605 try:
606 606 fp = self.vfs(b'fncache', mode=b'rb')
607 607 except IOError:
608 608 # skip nonexistent file
609 609 self.entries = set()
610 610 return
611 611
612 612 self.entries = set()
613 613 chunk = b''
614 614 for c in iter(functools.partial(fp.read, fncache_chunksize), b''):
615 615 chunk += c
616 616 try:
617 617 p = chunk.rindex(b'\n')
618 618 self.entries.update(decodedir(chunk[: p + 1]).splitlines())
619 619 chunk = chunk[p + 1 :]
620 620 except ValueError:
621 621 # substring '\n' not found, maybe the entry is bigger than the
622 622 # chunksize, so let's keep iterating
623 623 pass
624 624
625 625 if chunk:
626 626 msg = _(b"fncache does not ends with a newline")
627 627 if warn:
628 628 warn(msg + b'\n')
629 629 else:
630 630 raise error.Abort(
631 631 msg,
632 632 hint=_(
633 633 b"use 'hg debugrebuildfncache' to "
634 634 b"rebuild the fncache"
635 635 ),
636 636 )
637 637 self._checkentries(fp, warn)
638 638 fp.close()
639 639
640 640 def _checkentries(self, fp, warn):
641 641 """make sure there is no empty string in entries"""
642 642 if b'' in self.entries:
643 643 fp.seek(0)
644 644 for n, line in enumerate(util.iterfile(fp)):
645 645 if not line.rstrip(b'\n'):
646 646 t = _(b'invalid entry in fncache, line %d') % (n + 1)
647 647 if warn:
648 648 warn(t + b'\n')
649 649 else:
650 650 raise error.Abort(t)
651 651
652 652 def write(self, tr):
653 653 if self._dirty:
654 654 assert self.entries is not None
655 655 self.entries = self.entries | self.addls
656 656 self.addls = set()
657 657 tr.addbackup(b'fncache')
658 658 fp = self.vfs(b'fncache', mode=b'wb', atomictemp=True)
659 659 if self.entries:
660 660 fp.write(encodedir(b'\n'.join(self.entries) + b'\n'))
661 661 fp.close()
662 662 self._dirty = False
663 663 if self.addls:
664 664 # if we have just new entries, let's append them to the fncache
665 665 tr.addbackup(b'fncache')
666 666 fp = self.vfs(b'fncache', mode=b'ab', atomictemp=True)
667 667 if self.addls:
668 668 fp.write(encodedir(b'\n'.join(self.addls) + b'\n'))
669 669 fp.close()
670 670 self.entries = None
671 671 self.addls = set()
672 672
673 673 def add(self, fn):
674 674 if self.entries is None:
675 675 self._load()
676 676 if fn not in self.entries:
677 677 self.addls.add(fn)
678 678
679 679 def remove(self, fn):
680 680 if self.entries is None:
681 681 self._load()
682 682 if fn in self.addls:
683 683 self.addls.remove(fn)
684 684 return
685 685 try:
686 686 self.entries.remove(fn)
687 687 self._dirty = True
688 688 except KeyError:
689 689 pass
690 690
691 691 def __contains__(self, fn):
692 692 if fn in self.addls:
693 693 return True
694 694 if self.entries is None:
695 695 self._load()
696 696 return fn in self.entries
697 697
698 698 def __iter__(self):
699 699 if self.entries is None:
700 700 self._load()
701 701 return iter(self.entries | self.addls)
702 702
703 703
704 704 class _fncachevfs(vfsmod.proxyvfs):
705 705 def __init__(self, vfs, fnc, encode):
706 706 vfsmod.proxyvfs.__init__(self, vfs)
707 707 self.fncache = fnc
708 708 self.encode = encode
709 709
710 710 def __call__(self, path, mode=b'r', *args, **kw):
711 711 encoded = self.encode(path)
712 712 if mode not in (b'r', b'rb') and (
713 713 path.startswith(b'data/') or path.startswith(b'meta/')
714 714 ):
715 715 # do not trigger a fncache load when adding a file that already is
716 716 # known to exist.
717 717 notload = self.fncache.entries is None and self.vfs.exists(encoded)
718 718 if notload and b'r+' in mode and not self.vfs.stat(encoded).st_size:
719 719 # when appending to an existing file, if the file has size zero,
720 720 # it should be considered as missing. Such zero-size files are
721 721 # the result of truncation when a transaction is aborted.
722 722 notload = False
723 723 if not notload:
724 724 self.fncache.add(path)
725 725 return self.vfs(encoded, mode, *args, **kw)
726 726
727 727 def join(self, path):
728 728 if path:
729 729 return self.vfs.join(self.encode(path))
730 730 else:
731 731 return self.vfs.join(path)
732 732
733 def register_file(self, path):
734 """generic hook point to lets fncache steer its stew"""
735 if path.startswith(b'data/') or path.startswith(b'meta/'):
736 self.fncache.add(path)
737
733 738
734 739 class fncachestore(basicstore):
735 740 def __init__(self, path, vfstype, dotencode):
736 741 if dotencode:
737 742 encode = _pathencode
738 743 else:
739 744 encode = _plainhybridencode
740 745 self.encode = encode
741 746 vfs = vfstype(path + b'/store')
742 747 self.path = vfs.base
743 748 self.pathsep = self.path + b'/'
744 749 self.createmode = _calcmode(vfs)
745 750 vfs.createmode = self.createmode
746 751 self.rawvfs = vfs
747 752 fnc = fncache(vfs)
748 753 self.fncache = fnc
749 754 self.vfs = _fncachevfs(vfs, fnc, encode)
750 755 self.opener = self.vfs
751 756
752 757 def join(self, f):
753 758 return self.pathsep + self.encode(f)
754 759
755 760 def getsize(self, path):
756 761 return self.rawvfs.stat(path).st_size
757 762
758 763 def datafiles(self, matcher=None):
759 764 for f in sorted(self.fncache):
760 765 if not _matchtrackedpath(f, matcher):
761 766 continue
762 767 ef = self.encode(f)
763 768 try:
764 769 t = revlog_type(f)
765 770 assert t is not None, f
766 771 t |= FILEFLAGS_FILELOG
767 772 yield t, f, ef, self.getsize(ef)
768 773 except OSError as err:
769 774 if err.errno != errno.ENOENT:
770 775 raise
771 776
772 777 def copylist(self):
773 778 d = (
774 779 b'bookmarks',
775 780 b'narrowspec',
776 781 b'data',
777 782 b'meta',
778 783 b'dh',
779 784 b'fncache',
780 785 b'phaseroots',
781 786 b'obsstore',
782 787 b'00manifest.d',
783 788 b'00manifest.i',
784 789 b'00changelog.d',
785 790 b'00changelog.i',
786 791 b'requires',
787 792 )
788 793 return [b'requires', b'00changelog.i'] + [b'store/' + f for f in d]
789 794
790 795 def write(self, tr):
791 796 self.fncache.write(tr)
792 797
793 798 def invalidatecaches(self):
794 799 self.fncache.entries = None
795 800 self.fncache.addls = set()
796 801
797 802 def markremoved(self, fn):
798 803 self.fncache.remove(fn)
799 804
800 805 def _exists(self, f):
801 806 ef = self.encode(f)
802 807 try:
803 808 self.getsize(ef)
804 809 return True
805 810 except OSError as err:
806 811 if err.errno != errno.ENOENT:
807 812 raise
808 813 # nonexistent entry
809 814 return False
810 815
811 816 def __contains__(self, path):
812 817 '''Checks if the store contains path'''
813 818 path = b"/".join((b"data", path))
814 819 # check for files (exact match)
815 820 e = path + b'.i'
816 821 if e in self.fncache and self._exists(e):
817 822 return True
818 823 # now check for directories (prefix match)
819 824 if not path.endswith(b'/'):
820 825 path += b'/'
821 826 for e in self.fncache:
822 827 if e.startswith(path) and self._exists(e):
823 828 return True
824 829 return False
@@ -1,751 +1,754 b''
1 1 # vfs.py - Mercurial 'vfs' classes
2 2 #
3 3 # Copyright Olivia Mackall <olivia@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 from __future__ import absolute_import
8 8
9 9 import contextlib
10 10 import errno
11 11 import os
12 12 import shutil
13 13 import stat
14 14 import threading
15 15
16 16 from .i18n import _
17 17 from .pycompat import (
18 18 delattr,
19 19 getattr,
20 20 setattr,
21 21 )
22 22 from . import (
23 23 encoding,
24 24 error,
25 25 pathutil,
26 26 pycompat,
27 27 util,
28 28 )
29 29
30 30
31 31 def _avoidambig(path, oldstat):
32 32 """Avoid file stat ambiguity forcibly
33 33
34 34 This function causes copying ``path`` file, if it is owned by
35 35 another (see issue5418 and issue5584 for detail).
36 36 """
37 37
38 38 def checkandavoid():
39 39 newstat = util.filestat.frompath(path)
40 40 # return whether file stat ambiguity is (already) avoided
41 41 return not newstat.isambig(oldstat) or newstat.avoidambig(path, oldstat)
42 42
43 43 if not checkandavoid():
44 44 # simply copy to change owner of path to get privilege to
45 45 # advance mtime (see issue5418)
46 46 util.rename(util.mktempcopy(path), path)
47 47 checkandavoid()
48 48
49 49
50 50 class abstractvfs(object):
51 51 """Abstract base class; cannot be instantiated"""
52 52
53 53 def __init__(self, *args, **kwargs):
54 54 '''Prevent instantiation; don't call this from subclasses.'''
55 55 raise NotImplementedError('attempted instantiating ' + str(type(self)))
56 56
57 57 def __call__(self, path, mode=b'rb', **kwargs):
58 58 raise NotImplementedError
59 59
60 60 def _auditpath(self, path, mode):
61 61 raise NotImplementedError
62 62
63 63 def join(self, path, *insidef):
64 64 raise NotImplementedError
65 65
66 66 def tryread(self, path):
67 67 '''gracefully return an empty string for missing files'''
68 68 try:
69 69 return self.read(path)
70 70 except IOError as inst:
71 71 if inst.errno != errno.ENOENT:
72 72 raise
73 73 return b""
74 74
75 75 def tryreadlines(self, path, mode=b'rb'):
76 76 '''gracefully return an empty array for missing files'''
77 77 try:
78 78 return self.readlines(path, mode=mode)
79 79 except IOError as inst:
80 80 if inst.errno != errno.ENOENT:
81 81 raise
82 82 return []
83 83
84 84 @util.propertycache
85 85 def open(self):
86 86 """Open ``path`` file, which is relative to vfs root.
87 87
88 88 Newly created directories are marked as "not to be indexed by
89 89 the content indexing service", if ``notindexed`` is specified
90 90 for "write" mode access.
91 91 """
92 92 return self.__call__
93 93
94 94 def read(self, path):
95 95 with self(path, b'rb') as fp:
96 96 return fp.read()
97 97
98 98 def readlines(self, path, mode=b'rb'):
99 99 with self(path, mode=mode) as fp:
100 100 return fp.readlines()
101 101
102 102 def write(self, path, data, backgroundclose=False, **kwargs):
103 103 with self(path, b'wb', backgroundclose=backgroundclose, **kwargs) as fp:
104 104 return fp.write(data)
105 105
106 106 def writelines(self, path, data, mode=b'wb', notindexed=False):
107 107 with self(path, mode=mode, notindexed=notindexed) as fp:
108 108 return fp.writelines(data)
109 109
110 110 def append(self, path, data):
111 111 with self(path, b'ab') as fp:
112 112 return fp.write(data)
113 113
114 114 def basename(self, path):
115 115 """return base element of a path (as os.path.basename would do)
116 116
117 117 This exists to allow handling of strange encoding if needed."""
118 118 return os.path.basename(path)
119 119
120 120 def chmod(self, path, mode):
121 121 return os.chmod(self.join(path), mode)
122 122
123 123 def dirname(self, path):
124 124 """return dirname element of a path (as os.path.dirname would do)
125 125
126 126 This exists to allow handling of strange encoding if needed."""
127 127 return os.path.dirname(path)
128 128
129 129 def exists(self, path=None):
130 130 return os.path.exists(self.join(path))
131 131
132 132 def fstat(self, fp):
133 133 return util.fstat(fp)
134 134
135 135 def isdir(self, path=None):
136 136 return os.path.isdir(self.join(path))
137 137
138 138 def isfile(self, path=None):
139 139 return os.path.isfile(self.join(path))
140 140
141 141 def islink(self, path=None):
142 142 return os.path.islink(self.join(path))
143 143
144 144 def isfileorlink(self, path=None):
145 145 """return whether path is a regular file or a symlink
146 146
147 147 Unlike isfile, this doesn't follow symlinks."""
148 148 try:
149 149 st = self.lstat(path)
150 150 except OSError:
151 151 return False
152 152 mode = st.st_mode
153 153 return stat.S_ISREG(mode) or stat.S_ISLNK(mode)
154 154
155 155 def reljoin(self, *paths):
156 156 """join various elements of a path together (as os.path.join would do)
157 157
158 158 The vfs base is not injected so that path stay relative. This exists
159 159 to allow handling of strange encoding if needed."""
160 160 return os.path.join(*paths)
161 161
162 162 def split(self, path):
163 163 """split top-most element of a path (as os.path.split would do)
164 164
165 165 This exists to allow handling of strange encoding if needed."""
166 166 return os.path.split(path)
167 167
168 168 def lexists(self, path=None):
169 169 return os.path.lexists(self.join(path))
170 170
171 171 def lstat(self, path=None):
172 172 return os.lstat(self.join(path))
173 173
174 174 def listdir(self, path=None):
175 175 return os.listdir(self.join(path))
176 176
177 177 def makedir(self, path=None, notindexed=True):
178 178 return util.makedir(self.join(path), notindexed)
179 179
180 180 def makedirs(self, path=None, mode=None):
181 181 return util.makedirs(self.join(path), mode)
182 182
183 183 def makelock(self, info, path):
184 184 return util.makelock(info, self.join(path))
185 185
186 186 def mkdir(self, path=None):
187 187 return os.mkdir(self.join(path))
188 188
189 189 def mkstemp(self, suffix=b'', prefix=b'tmp', dir=None):
190 190 fd, name = pycompat.mkstemp(
191 191 suffix=suffix, prefix=prefix, dir=self.join(dir)
192 192 )
193 193 dname, fname = util.split(name)
194 194 if dir:
195 195 return fd, os.path.join(dir, fname)
196 196 else:
197 197 return fd, fname
198 198
199 199 def readdir(self, path=None, stat=None, skip=None):
200 200 return util.listdir(self.join(path), stat, skip)
201 201
202 202 def readlock(self, path):
203 203 return util.readlock(self.join(path))
204 204
205 205 def rename(self, src, dst, checkambig=False):
206 206 """Rename from src to dst
207 207
208 208 checkambig argument is used with util.filestat, and is useful
209 209 only if destination file is guarded by any lock
210 210 (e.g. repo.lock or repo.wlock).
211 211
212 212 To avoid file stat ambiguity forcibly, checkambig=True involves
213 213 copying ``src`` file, if it is owned by another. Therefore, use
214 214 checkambig=True only in limited cases (see also issue5418 and
215 215 issue5584 for detail).
216 216 """
217 217 self._auditpath(dst, b'w')
218 218 srcpath = self.join(src)
219 219 dstpath = self.join(dst)
220 220 oldstat = checkambig and util.filestat.frompath(dstpath)
221 221 if oldstat and oldstat.stat:
222 222 ret = util.rename(srcpath, dstpath)
223 223 _avoidambig(dstpath, oldstat)
224 224 return ret
225 225 return util.rename(srcpath, dstpath)
226 226
227 227 def readlink(self, path):
228 228 return util.readlink(self.join(path))
229 229
230 230 def removedirs(self, path=None):
231 231 """Remove a leaf directory and all empty intermediate ones"""
232 232 return util.removedirs(self.join(path))
233 233
234 234 def rmdir(self, path=None):
235 235 """Remove an empty directory."""
236 236 return os.rmdir(self.join(path))
237 237
238 238 def rmtree(self, path=None, ignore_errors=False, forcibly=False):
239 239 """Remove a directory tree recursively
240 240
241 241 If ``forcibly``, this tries to remove READ-ONLY files, too.
242 242 """
243 243 if forcibly:
244 244
245 245 def onerror(function, path, excinfo):
246 246 if function is not os.remove:
247 247 raise
248 248 # read-only files cannot be unlinked under Windows
249 249 s = os.stat(path)
250 250 if (s.st_mode & stat.S_IWRITE) != 0:
251 251 raise
252 252 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
253 253 os.remove(path)
254 254
255 255 else:
256 256 onerror = None
257 257 return shutil.rmtree(
258 258 self.join(path), ignore_errors=ignore_errors, onerror=onerror
259 259 )
260 260
261 261 def setflags(self, path, l, x):
262 262 return util.setflags(self.join(path), l, x)
263 263
264 264 def stat(self, path=None):
265 265 return os.stat(self.join(path))
266 266
267 267 def unlink(self, path=None):
268 268 return util.unlink(self.join(path))
269 269
270 270 def tryunlink(self, path=None):
271 271 """Attempt to remove a file, ignoring missing file errors."""
272 272 util.tryunlink(self.join(path))
273 273
274 274 def unlinkpath(self, path=None, ignoremissing=False, rmdir=True):
275 275 return util.unlinkpath(
276 276 self.join(path), ignoremissing=ignoremissing, rmdir=rmdir
277 277 )
278 278
279 279 def utime(self, path=None, t=None):
280 280 return os.utime(self.join(path), t)
281 281
282 282 def walk(self, path=None, onerror=None):
283 283 """Yield (dirpath, dirs, files) tuple for each directories under path
284 284
285 285 ``dirpath`` is relative one from the root of this vfs. This
286 286 uses ``os.sep`` as path separator, even you specify POSIX
287 287 style ``path``.
288 288
289 289 "The root of this vfs" is represented as empty ``dirpath``.
290 290 """
291 291 root = os.path.normpath(self.join(None))
292 292 # when dirpath == root, dirpath[prefixlen:] becomes empty
293 293 # because len(dirpath) < prefixlen.
294 294 prefixlen = len(pathutil.normasprefix(root))
295 295 for dirpath, dirs, files in os.walk(self.join(path), onerror=onerror):
296 296 yield (dirpath[prefixlen:], dirs, files)
297 297
298 298 @contextlib.contextmanager
299 299 def backgroundclosing(self, ui, expectedcount=-1):
300 300 """Allow files to be closed asynchronously.
301 301
302 302 When this context manager is active, ``backgroundclose`` can be passed
303 303 to ``__call__``/``open`` to result in the file possibly being closed
304 304 asynchronously, on a background thread.
305 305 """
306 306 # Sharing backgroundfilecloser between threads is complex and using
307 307 # multiple instances puts us at risk of running out of file descriptors
308 308 # only allow to use backgroundfilecloser when in main thread.
309 309 if not isinstance(
310 310 threading.current_thread(),
311 311 threading._MainThread, # pytype: disable=module-attr
312 312 ):
313 313 yield
314 314 return
315 315 vfs = getattr(self, 'vfs', self)
316 316 if getattr(vfs, '_backgroundfilecloser', None):
317 317 raise error.Abort(
318 318 _(b'can only have 1 active background file closer')
319 319 )
320 320
321 321 with backgroundfilecloser(ui, expectedcount=expectedcount) as bfc:
322 322 try:
323 323 vfs._backgroundfilecloser = (
324 324 bfc # pytype: disable=attribute-error
325 325 )
326 326 yield bfc
327 327 finally:
328 328 vfs._backgroundfilecloser = (
329 329 None # pytype: disable=attribute-error
330 330 )
331 331
332 def register_file(self, path):
333 """generic hook point to lets fncache steer its stew"""
334
332 335
333 336 class vfs(abstractvfs):
334 337 """Operate files relative to a base directory
335 338
336 339 This class is used to hide the details of COW semantics and
337 340 remote file access from higher level code.
338 341
339 342 'cacheaudited' should be enabled only if (a) vfs object is short-lived, or
340 343 (b) the base directory is managed by hg and considered sort-of append-only.
341 344 See pathutil.pathauditor() for details.
342 345 """
343 346
344 347 def __init__(
345 348 self,
346 349 base,
347 350 audit=True,
348 351 cacheaudited=False,
349 352 expandpath=False,
350 353 realpath=False,
351 354 ):
352 355 if expandpath:
353 356 base = util.expandpath(base)
354 357 if realpath:
355 358 base = os.path.realpath(base)
356 359 self.base = base
357 360 self._audit = audit
358 361 if audit:
359 362 self.audit = pathutil.pathauditor(self.base, cached=cacheaudited)
360 363 else:
361 364 self.audit = lambda path, mode=None: True
362 365 self.createmode = None
363 366 self._trustnlink = None
364 367 self.options = {}
365 368
366 369 @util.propertycache
367 370 def _cansymlink(self):
368 371 return util.checklink(self.base)
369 372
370 373 @util.propertycache
371 374 def _chmod(self):
372 375 return util.checkexec(self.base)
373 376
374 377 def _fixfilemode(self, name):
375 378 if self.createmode is None or not self._chmod:
376 379 return
377 380 os.chmod(name, self.createmode & 0o666)
378 381
379 382 def _auditpath(self, path, mode):
380 383 if self._audit:
381 384 if os.path.isabs(path) and path.startswith(self.base):
382 385 path = os.path.relpath(path, self.base)
383 386 r = util.checkosfilename(path)
384 387 if r:
385 388 raise error.Abort(b"%s: %r" % (r, path))
386 389 self.audit(path, mode=mode)
387 390
388 391 def __call__(
389 392 self,
390 393 path,
391 394 mode=b"r",
392 395 atomictemp=False,
393 396 notindexed=False,
394 397 backgroundclose=False,
395 398 checkambig=False,
396 399 auditpath=True,
397 400 makeparentdirs=True,
398 401 ):
399 402 """Open ``path`` file, which is relative to vfs root.
400 403
401 404 By default, parent directories are created as needed. Newly created
402 405 directories are marked as "not to be indexed by the content indexing
403 406 service", if ``notindexed`` is specified for "write" mode access.
404 407 Set ``makeparentdirs=False`` to not create directories implicitly.
405 408
406 409 If ``backgroundclose`` is passed, the file may be closed asynchronously.
407 410 It can only be used if the ``self.backgroundclosing()`` context manager
408 411 is active. This should only be specified if the following criteria hold:
409 412
410 413 1. There is a potential for writing thousands of files. Unless you
411 414 are writing thousands of files, the performance benefits of
412 415 asynchronously closing files is not realized.
413 416 2. Files are opened exactly once for the ``backgroundclosing``
414 417 active duration and are therefore free of race conditions between
415 418 closing a file on a background thread and reopening it. (If the
416 419 file were opened multiple times, there could be unflushed data
417 420 because the original file handle hasn't been flushed/closed yet.)
418 421
419 422 ``checkambig`` argument is passed to atomictempfile (valid
420 423 only for writing), and is useful only if target file is
421 424 guarded by any lock (e.g. repo.lock or repo.wlock).
422 425
423 426 To avoid file stat ambiguity forcibly, checkambig=True involves
424 427 copying ``path`` file opened in "append" mode (e.g. for
425 428 truncation), if it is owned by another. Therefore, use
426 429 combination of append mode and checkambig=True only in limited
427 430 cases (see also issue5418 and issue5584 for detail).
428 431 """
429 432 if auditpath:
430 433 self._auditpath(path, mode)
431 434 f = self.join(path)
432 435
433 436 if b"b" not in mode:
434 437 mode += b"b" # for that other OS
435 438
436 439 nlink = -1
437 440 if mode not in (b'r', b'rb'):
438 441 dirname, basename = util.split(f)
439 442 # If basename is empty, then the path is malformed because it points
440 443 # to a directory. Let the posixfile() call below raise IOError.
441 444 if basename:
442 445 if atomictemp:
443 446 if makeparentdirs:
444 447 util.makedirs(dirname, self.createmode, notindexed)
445 448 return util.atomictempfile(
446 449 f, mode, self.createmode, checkambig=checkambig
447 450 )
448 451 try:
449 452 if b'w' in mode:
450 453 util.unlink(f)
451 454 nlink = 0
452 455 else:
453 456 # nlinks() may behave differently for files on Windows
454 457 # shares if the file is open.
455 458 with util.posixfile(f):
456 459 nlink = util.nlinks(f)
457 460 if nlink < 1:
458 461 nlink = 2 # force mktempcopy (issue1922)
459 462 except (OSError, IOError) as e:
460 463 if e.errno != errno.ENOENT:
461 464 raise
462 465 nlink = 0
463 466 if makeparentdirs:
464 467 util.makedirs(dirname, self.createmode, notindexed)
465 468 if nlink > 0:
466 469 if self._trustnlink is None:
467 470 self._trustnlink = nlink > 1 or util.checknlink(f)
468 471 if nlink > 1 or not self._trustnlink:
469 472 util.rename(util.mktempcopy(f), f)
470 473 fp = util.posixfile(f, mode)
471 474 if nlink == 0:
472 475 self._fixfilemode(f)
473 476
474 477 if checkambig:
475 478 if mode in (b'r', b'rb'):
476 479 raise error.Abort(
477 480 _(
478 481 b'implementation error: mode %s is not'
479 482 b' valid for checkambig=True'
480 483 )
481 484 % mode
482 485 )
483 486 fp = checkambigatclosing(fp)
484 487
485 488 if backgroundclose and isinstance(
486 489 threading.current_thread(),
487 490 threading._MainThread, # pytype: disable=module-attr
488 491 ):
489 492 if (
490 493 not self._backgroundfilecloser # pytype: disable=attribute-error
491 494 ):
492 495 raise error.Abort(
493 496 _(
494 497 b'backgroundclose can only be used when a '
495 498 b'backgroundclosing context manager is active'
496 499 )
497 500 )
498 501
499 502 fp = delayclosedfile(
500 503 fp,
501 504 self._backgroundfilecloser, # pytype: disable=attribute-error
502 505 )
503 506
504 507 return fp
505 508
506 509 def symlink(self, src, dst):
507 510 self.audit(dst)
508 511 linkname = self.join(dst)
509 512 util.tryunlink(linkname)
510 513
511 514 util.makedirs(os.path.dirname(linkname), self.createmode)
512 515
513 516 if self._cansymlink:
514 517 try:
515 518 os.symlink(src, linkname)
516 519 except OSError as err:
517 520 raise OSError(
518 521 err.errno,
519 522 _(b'could not symlink to %r: %s')
520 523 % (src, encoding.strtolocal(err.strerror)),
521 524 linkname,
522 525 )
523 526 else:
524 527 self.write(dst, src)
525 528
526 529 def join(self, path, *insidef):
527 530 if path:
528 531 return os.path.join(self.base, path, *insidef)
529 532 else:
530 533 return self.base
531 534
532 535
533 536 opener = vfs
534 537
535 538
536 539 class proxyvfs(abstractvfs):
537 540 def __init__(self, vfs):
538 541 self.vfs = vfs
539 542
540 543 def _auditpath(self, path, mode):
541 544 return self.vfs._auditpath(path, mode)
542 545
543 546 @property
544 547 def options(self):
545 548 return self.vfs.options
546 549
547 550 @options.setter
548 551 def options(self, value):
549 552 self.vfs.options = value
550 553
551 554
552 555 class filtervfs(proxyvfs, abstractvfs):
553 556 '''Wrapper vfs for filtering filenames with a function.'''
554 557
555 558 def __init__(self, vfs, filter):
556 559 proxyvfs.__init__(self, vfs)
557 560 self._filter = filter
558 561
559 562 def __call__(self, path, *args, **kwargs):
560 563 return self.vfs(self._filter(path), *args, **kwargs)
561 564
562 565 def join(self, path, *insidef):
563 566 if path:
564 567 return self.vfs.join(self._filter(self.vfs.reljoin(path, *insidef)))
565 568 else:
566 569 return self.vfs.join(path)
567 570
568 571
569 572 filteropener = filtervfs
570 573
571 574
572 575 class readonlyvfs(proxyvfs):
573 576 '''Wrapper vfs preventing any writing.'''
574 577
575 578 def __init__(self, vfs):
576 579 proxyvfs.__init__(self, vfs)
577 580
578 581 def __call__(self, path, mode=b'r', *args, **kw):
579 582 if mode not in (b'r', b'rb'):
580 583 raise error.Abort(_(b'this vfs is read only'))
581 584 return self.vfs(path, mode, *args, **kw)
582 585
583 586 def join(self, path, *insidef):
584 587 return self.vfs.join(path, *insidef)
585 588
586 589
587 590 class closewrapbase(object):
588 591 """Base class of wrapper, which hooks closing
589 592
590 593 Do not instantiate outside of the vfs layer.
591 594 """
592 595
593 596 def __init__(self, fh):
594 597 object.__setattr__(self, '_origfh', fh)
595 598
596 599 def __getattr__(self, attr):
597 600 return getattr(self._origfh, attr)
598 601
599 602 def __setattr__(self, attr, value):
600 603 return setattr(self._origfh, attr, value)
601 604
602 605 def __delattr__(self, attr):
603 606 return delattr(self._origfh, attr)
604 607
605 608 def __enter__(self):
606 609 self._origfh.__enter__()
607 610 return self
608 611
609 612 def __exit__(self, exc_type, exc_value, exc_tb):
610 613 raise NotImplementedError('attempted instantiating ' + str(type(self)))
611 614
612 615 def close(self):
613 616 raise NotImplementedError('attempted instantiating ' + str(type(self)))
614 617
615 618
616 619 class delayclosedfile(closewrapbase):
617 620 """Proxy for a file object whose close is delayed.
618 621
619 622 Do not instantiate outside of the vfs layer.
620 623 """
621 624
622 625 def __init__(self, fh, closer):
623 626 super(delayclosedfile, self).__init__(fh)
624 627 object.__setattr__(self, '_closer', closer)
625 628
626 629 def __exit__(self, exc_type, exc_value, exc_tb):
627 630 self._closer.close(self._origfh)
628 631
629 632 def close(self):
630 633 self._closer.close(self._origfh)
631 634
632 635
633 636 class backgroundfilecloser(object):
634 637 """Coordinates background closing of file handles on multiple threads."""
635 638
636 639 def __init__(self, ui, expectedcount=-1):
637 640 self._running = False
638 641 self._entered = False
639 642 self._threads = []
640 643 self._threadexception = None
641 644
642 645 # Only Windows/NTFS has slow file closing. So only enable by default
643 646 # on that platform. But allow to be enabled elsewhere for testing.
644 647 defaultenabled = pycompat.iswindows
645 648 enabled = ui.configbool(b'worker', b'backgroundclose', defaultenabled)
646 649
647 650 if not enabled:
648 651 return
649 652
650 653 # There is overhead to starting and stopping the background threads.
651 654 # Don't do background processing unless the file count is large enough
652 655 # to justify it.
653 656 minfilecount = ui.configint(b'worker', b'backgroundcloseminfilecount')
654 657 # FUTURE dynamically start background threads after minfilecount closes.
655 658 # (We don't currently have any callers that don't know their file count)
656 659 if expectedcount > 0 and expectedcount < minfilecount:
657 660 return
658 661
659 662 maxqueue = ui.configint(b'worker', b'backgroundclosemaxqueue')
660 663 threadcount = ui.configint(b'worker', b'backgroundclosethreadcount')
661 664
662 665 ui.debug(
663 666 b'starting %d threads for background file closing\n' % threadcount
664 667 )
665 668
666 669 self._queue = pycompat.queue.Queue(maxsize=maxqueue)
667 670 self._running = True
668 671
669 672 for i in range(threadcount):
670 673 t = threading.Thread(target=self._worker, name='backgroundcloser')
671 674 self._threads.append(t)
672 675 t.start()
673 676
674 677 def __enter__(self):
675 678 self._entered = True
676 679 return self
677 680
678 681 def __exit__(self, exc_type, exc_value, exc_tb):
679 682 self._running = False
680 683
681 684 # Wait for threads to finish closing so open files don't linger for
682 685 # longer than lifetime of context manager.
683 686 for t in self._threads:
684 687 t.join()
685 688
686 689 def _worker(self):
687 690 """Main routine for worker thread."""
688 691 while True:
689 692 try:
690 693 fh = self._queue.get(block=True, timeout=0.100)
691 694 # Need to catch or the thread will terminate and
692 695 # we could orphan file descriptors.
693 696 try:
694 697 fh.close()
695 698 except Exception as e:
696 699 # Stash so can re-raise from main thread later.
697 700 self._threadexception = e
698 701 except pycompat.queue.Empty:
699 702 if not self._running:
700 703 break
701 704
702 705 def close(self, fh):
703 706 """Schedule a file for closing."""
704 707 if not self._entered:
705 708 raise error.Abort(
706 709 _(b'can only call close() when context manager active')
707 710 )
708 711
709 712 # If a background thread encountered an exception, raise now so we fail
710 713 # fast. Otherwise we may potentially go on for minutes until the error
711 714 # is acted on.
712 715 if self._threadexception:
713 716 e = self._threadexception
714 717 self._threadexception = None
715 718 raise e
716 719
717 720 # If we're not actively running, close synchronously.
718 721 if not self._running:
719 722 fh.close()
720 723 return
721 724
722 725 self._queue.put(fh, block=True, timeout=None)
723 726
724 727
725 728 class checkambigatclosing(closewrapbase):
726 729 """Proxy for a file object, to avoid ambiguity of file stat
727 730
728 731 See also util.filestat for detail about "ambiguity of file stat".
729 732
730 733 This proxy is useful only if the target file is guarded by any
731 734 lock (e.g. repo.lock or repo.wlock)
732 735
733 736 Do not instantiate outside of the vfs layer.
734 737 """
735 738
736 739 def __init__(self, fh):
737 740 super(checkambigatclosing, self).__init__(fh)
738 741 object.__setattr__(self, '_oldstat', util.filestat.frompath(fh.name))
739 742
740 743 def _checkambig(self):
741 744 oldstat = self._oldstat
742 745 if oldstat.stat:
743 746 _avoidambig(self._origfh.name, oldstat)
744 747
745 748 def __exit__(self, exc_type, exc_value, exc_tb):
746 749 self._origfh.__exit__(exc_type, exc_value, exc_tb)
747 750 self._checkambig()
748 751
749 752 def close(self):
750 753 self._origfh.close()
751 754 self._checkambig()
General Comments 0
You need to be logged in to leave comments. Login now