##// END OF EJS Templates
store: recommend using `hg debugrebuildfncache` is fncache is corrupted...
Pulkit Goyal -
r42156:3e7cfa17 default
parent child Browse files
Show More
@@ -1,652 +1,654
1 1 # store.py - repository store handling for Mercurial
2 2 #
3 3 # Copyright 2008 Matt Mackall <mpm@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 hashlib
13 13 import os
14 14 import stat
15 15
16 16 from .i18n import _
17 17 from . import (
18 18 error,
19 19 node,
20 20 policy,
21 21 pycompat,
22 22 util,
23 23 vfs as vfsmod,
24 24 )
25 25
26 26 parsers = policy.importmod(r'parsers')
27 27 # how much bytes should be read from fncache in one read
28 28 # It is done to prevent loading large fncache files into memory
29 29 fncache_chunksize = 10 ** 6
30 30
31 31 def _matchtrackedpath(path, matcher):
32 32 """parses a fncache entry and returns whether the entry is tracking a path
33 33 matched by matcher or not.
34 34
35 35 If matcher is None, returns True"""
36 36
37 37 if matcher is None:
38 38 return True
39 39 path = decodedir(path)
40 40 if path.startswith('data/'):
41 41 return matcher(path[len('data/'):-len('.i')])
42 42 elif path.startswith('meta/'):
43 43 return matcher.visitdir(path[len('meta/'):-len('/00manifest.i')] or '.')
44 44
45 45 raise error.ProgrammingError("cannot decode path %s" % path)
46 46
47 47 # This avoids a collision between a file named foo and a dir named
48 48 # foo.i or foo.d
49 49 def _encodedir(path):
50 50 '''
51 51 >>> _encodedir(b'data/foo.i')
52 52 'data/foo.i'
53 53 >>> _encodedir(b'data/foo.i/bla.i')
54 54 'data/foo.i.hg/bla.i'
55 55 >>> _encodedir(b'data/foo.i.hg/bla.i')
56 56 'data/foo.i.hg.hg/bla.i'
57 57 >>> _encodedir(b'data/foo.i\\ndata/foo.i/bla.i\\ndata/foo.i.hg/bla.i\\n')
58 58 'data/foo.i\\ndata/foo.i.hg/bla.i\\ndata/foo.i.hg.hg/bla.i\\n'
59 59 '''
60 60 return (path
61 61 .replace(".hg/", ".hg.hg/")
62 62 .replace(".i/", ".i.hg/")
63 63 .replace(".d/", ".d.hg/"))
64 64
65 65 encodedir = getattr(parsers, 'encodedir', _encodedir)
66 66
67 67 def decodedir(path):
68 68 '''
69 69 >>> decodedir(b'data/foo.i')
70 70 'data/foo.i'
71 71 >>> decodedir(b'data/foo.i.hg/bla.i')
72 72 'data/foo.i/bla.i'
73 73 >>> decodedir(b'data/foo.i.hg.hg/bla.i')
74 74 'data/foo.i.hg/bla.i'
75 75 '''
76 76 if ".hg/" not in path:
77 77 return path
78 78 return (path
79 79 .replace(".d.hg/", ".d/")
80 80 .replace(".i.hg/", ".i/")
81 81 .replace(".hg.hg/", ".hg/"))
82 82
83 83 def _reserved():
84 84 ''' characters that are problematic for filesystems
85 85
86 86 * ascii escapes (0..31)
87 87 * ascii hi (126..255)
88 88 * windows specials
89 89
90 90 these characters will be escaped by encodefunctions
91 91 '''
92 92 winreserved = [ord(x) for x in u'\\:*?"<>|']
93 93 for x in range(32):
94 94 yield x
95 95 for x in range(126, 256):
96 96 yield x
97 97 for x in winreserved:
98 98 yield x
99 99
100 100 def _buildencodefun():
101 101 '''
102 102 >>> enc, dec = _buildencodefun()
103 103
104 104 >>> enc(b'nothing/special.txt')
105 105 'nothing/special.txt'
106 106 >>> dec(b'nothing/special.txt')
107 107 'nothing/special.txt'
108 108
109 109 >>> enc(b'HELLO')
110 110 '_h_e_l_l_o'
111 111 >>> dec(b'_h_e_l_l_o')
112 112 'HELLO'
113 113
114 114 >>> enc(b'hello:world?')
115 115 'hello~3aworld~3f'
116 116 >>> dec(b'hello~3aworld~3f')
117 117 'hello:world?'
118 118
119 119 >>> enc(b'the\\x07quick\\xADshot')
120 120 'the~07quick~adshot'
121 121 >>> dec(b'the~07quick~adshot')
122 122 'the\\x07quick\\xadshot'
123 123 '''
124 124 e = '_'
125 125 xchr = pycompat.bytechr
126 126 asciistr = list(map(xchr, range(127)))
127 127 capitals = list(range(ord("A"), ord("Z") + 1))
128 128
129 129 cmap = dict((x, x) for x in asciistr)
130 130 for x in _reserved():
131 131 cmap[xchr(x)] = "~%02x" % x
132 132 for x in capitals + [ord(e)]:
133 133 cmap[xchr(x)] = e + xchr(x).lower()
134 134
135 135 dmap = {}
136 136 for k, v in cmap.iteritems():
137 137 dmap[v] = k
138 138 def decode(s):
139 139 i = 0
140 140 while i < len(s):
141 141 for l in pycompat.xrange(1, 4):
142 142 try:
143 143 yield dmap[s[i:i + l]]
144 144 i += l
145 145 break
146 146 except KeyError:
147 147 pass
148 148 else:
149 149 raise KeyError
150 150 return (lambda s: ''.join([cmap[s[c:c + 1]]
151 151 for c in pycompat.xrange(len(s))]),
152 152 lambda s: ''.join(list(decode(s))))
153 153
154 154 _encodefname, _decodefname = _buildencodefun()
155 155
156 156 def encodefilename(s):
157 157 '''
158 158 >>> encodefilename(b'foo.i/bar.d/bla.hg/hi:world?/HELLO')
159 159 'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o'
160 160 '''
161 161 return _encodefname(encodedir(s))
162 162
163 163 def decodefilename(s):
164 164 '''
165 165 >>> decodefilename(b'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o')
166 166 'foo.i/bar.d/bla.hg/hi:world?/HELLO'
167 167 '''
168 168 return decodedir(_decodefname(s))
169 169
170 170 def _buildlowerencodefun():
171 171 '''
172 172 >>> f = _buildlowerencodefun()
173 173 >>> f(b'nothing/special.txt')
174 174 'nothing/special.txt'
175 175 >>> f(b'HELLO')
176 176 'hello'
177 177 >>> f(b'hello:world?')
178 178 'hello~3aworld~3f'
179 179 >>> f(b'the\\x07quick\\xADshot')
180 180 'the~07quick~adshot'
181 181 '''
182 182 xchr = pycompat.bytechr
183 183 cmap = dict([(xchr(x), xchr(x)) for x in pycompat.xrange(127)])
184 184 for x in _reserved():
185 185 cmap[xchr(x)] = "~%02x" % x
186 186 for x in range(ord("A"), ord("Z") + 1):
187 187 cmap[xchr(x)] = xchr(x).lower()
188 188 def lowerencode(s):
189 189 return "".join([cmap[c] for c in pycompat.iterbytestr(s)])
190 190 return lowerencode
191 191
192 192 lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun()
193 193
194 194 # Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9
195 195 _winres3 = ('aux', 'con', 'prn', 'nul') # length 3
196 196 _winres4 = ('com', 'lpt') # length 4 (with trailing 1..9)
197 197 def _auxencode(path, dotencode):
198 198 '''
199 199 Encodes filenames containing names reserved by Windows or which end in
200 200 period or space. Does not touch other single reserved characters c.
201 201 Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here.
202 202 Additionally encodes space or period at the beginning, if dotencode is
203 203 True. Parameter path is assumed to be all lowercase.
204 204 A segment only needs encoding if a reserved name appears as a
205 205 basename (e.g. "aux", "aux.foo"). A directory or file named "foo.aux"
206 206 doesn't need encoding.
207 207
208 208 >>> s = b'.foo/aux.txt/txt.aux/con/prn/nul/foo.'
209 209 >>> _auxencode(s.split(b'/'), True)
210 210 ['~2efoo', 'au~78.txt', 'txt.aux', 'co~6e', 'pr~6e', 'nu~6c', 'foo~2e']
211 211 >>> s = b'.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.'
212 212 >>> _auxencode(s.split(b'/'), False)
213 213 ['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e']
214 214 >>> _auxencode([b'foo. '], True)
215 215 ['foo.~20']
216 216 >>> _auxencode([b' .foo'], True)
217 217 ['~20.foo']
218 218 '''
219 219 for i, n in enumerate(path):
220 220 if not n:
221 221 continue
222 222 if dotencode and n[0] in '. ':
223 223 n = "~%02x" % ord(n[0:1]) + n[1:]
224 224 path[i] = n
225 225 else:
226 226 l = n.find('.')
227 227 if l == -1:
228 228 l = len(n)
229 229 if ((l == 3 and n[:3] in _winres3) or
230 230 (l == 4 and n[3:4] <= '9' and n[3:4] >= '1'
231 231 and n[:3] in _winres4)):
232 232 # encode third letter ('aux' -> 'au~78')
233 233 ec = "~%02x" % ord(n[2:3])
234 234 n = n[0:2] + ec + n[3:]
235 235 path[i] = n
236 236 if n[-1] in '. ':
237 237 # encode last period or space ('foo...' -> 'foo..~2e')
238 238 path[i] = n[:-1] + "~%02x" % ord(n[-1:])
239 239 return path
240 240
241 241 _maxstorepathlen = 120
242 242 _dirprefixlen = 8
243 243 _maxshortdirslen = 8 * (_dirprefixlen + 1) - 4
244 244
245 245 def _hashencode(path, dotencode):
246 246 digest = node.hex(hashlib.sha1(path).digest())
247 247 le = lowerencode(path[5:]).split('/') # skips prefix 'data/' or 'meta/'
248 248 parts = _auxencode(le, dotencode)
249 249 basename = parts[-1]
250 250 _root, ext = os.path.splitext(basename)
251 251 sdirs = []
252 252 sdirslen = 0
253 253 for p in parts[:-1]:
254 254 d = p[:_dirprefixlen]
255 255 if d[-1] in '. ':
256 256 # Windows can't access dirs ending in period or space
257 257 d = d[:-1] + '_'
258 258 if sdirslen == 0:
259 259 t = len(d)
260 260 else:
261 261 t = sdirslen + 1 + len(d)
262 262 if t > _maxshortdirslen:
263 263 break
264 264 sdirs.append(d)
265 265 sdirslen = t
266 266 dirs = '/'.join(sdirs)
267 267 if len(dirs) > 0:
268 268 dirs += '/'
269 269 res = 'dh/' + dirs + digest + ext
270 270 spaceleft = _maxstorepathlen - len(res)
271 271 if spaceleft > 0:
272 272 filler = basename[:spaceleft]
273 273 res = 'dh/' + dirs + filler + digest + ext
274 274 return res
275 275
276 276 def _hybridencode(path, dotencode):
277 277 '''encodes path with a length limit
278 278
279 279 Encodes all paths that begin with 'data/', according to the following.
280 280
281 281 Default encoding (reversible):
282 282
283 283 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
284 284 characters are encoded as '~xx', where xx is the two digit hex code
285 285 of the character (see encodefilename).
286 286 Relevant path components consisting of Windows reserved filenames are
287 287 masked by encoding the third character ('aux' -> 'au~78', see _auxencode).
288 288
289 289 Hashed encoding (not reversible):
290 290
291 291 If the default-encoded path is longer than _maxstorepathlen, a
292 292 non-reversible hybrid hashing of the path is done instead.
293 293 This encoding uses up to _dirprefixlen characters of all directory
294 294 levels of the lowerencoded path, but not more levels than can fit into
295 295 _maxshortdirslen.
296 296 Then follows the filler followed by the sha digest of the full path.
297 297 The filler is the beginning of the basename of the lowerencoded path
298 298 (the basename is everything after the last path separator). The filler
299 299 is as long as possible, filling in characters from the basename until
300 300 the encoded path has _maxstorepathlen characters (or all chars of the
301 301 basename have been taken).
302 302 The extension (e.g. '.i' or '.d') is preserved.
303 303
304 304 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
305 305 encoding was used.
306 306 '''
307 307 path = encodedir(path)
308 308 ef = _encodefname(path).split('/')
309 309 res = '/'.join(_auxencode(ef, dotencode))
310 310 if len(res) > _maxstorepathlen:
311 311 res = _hashencode(path, dotencode)
312 312 return res
313 313
314 314 def _pathencode(path):
315 315 de = encodedir(path)
316 316 if len(path) > _maxstorepathlen:
317 317 return _hashencode(de, True)
318 318 ef = _encodefname(de).split('/')
319 319 res = '/'.join(_auxencode(ef, True))
320 320 if len(res) > _maxstorepathlen:
321 321 return _hashencode(de, True)
322 322 return res
323 323
324 324 _pathencode = getattr(parsers, 'pathencode', _pathencode)
325 325
326 326 def _plainhybridencode(f):
327 327 return _hybridencode(f, False)
328 328
329 329 def _calcmode(vfs):
330 330 try:
331 331 # files in .hg/ will be created using this mode
332 332 mode = vfs.stat().st_mode
333 333 # avoid some useless chmods
334 334 if (0o777 & ~util.umask) == (0o777 & mode):
335 335 mode = None
336 336 except OSError:
337 337 mode = None
338 338 return mode
339 339
340 340 _data = ('narrowspec data meta 00manifest.d 00manifest.i'
341 341 ' 00changelog.d 00changelog.i phaseroots obsstore')
342 342
343 343 def isrevlog(f, kind, st):
344 344 return kind == stat.S_IFREG and f[-2:] in ('.i', '.d')
345 345
346 346 class basicstore(object):
347 347 '''base class for local repository stores'''
348 348 def __init__(self, path, vfstype):
349 349 vfs = vfstype(path)
350 350 self.path = vfs.base
351 351 self.createmode = _calcmode(vfs)
352 352 vfs.createmode = self.createmode
353 353 self.rawvfs = vfs
354 354 self.vfs = vfsmod.filtervfs(vfs, encodedir)
355 355 self.opener = self.vfs
356 356
357 357 def join(self, f):
358 358 return self.path + '/' + encodedir(f)
359 359
360 360 def _walk(self, relpath, recurse, filefilter=isrevlog):
361 361 '''yields (unencoded, encoded, size)'''
362 362 path = self.path
363 363 if relpath:
364 364 path += '/' + relpath
365 365 striplen = len(self.path) + 1
366 366 l = []
367 367 if self.rawvfs.isdir(path):
368 368 visit = [path]
369 369 readdir = self.rawvfs.readdir
370 370 while visit:
371 371 p = visit.pop()
372 372 for f, kind, st in readdir(p, stat=True):
373 373 fp = p + '/' + f
374 374 if filefilter(f, kind, st):
375 375 n = util.pconvert(fp[striplen:])
376 376 l.append((decodedir(n), n, st.st_size))
377 377 elif kind == stat.S_IFDIR and recurse:
378 378 visit.append(fp)
379 379 l.sort()
380 380 return l
381 381
382 382 def datafiles(self, matcher=None):
383 383 return self._walk('data', True) + self._walk('meta', True)
384 384
385 385 def topfiles(self):
386 386 # yield manifest before changelog
387 387 return reversed(self._walk('', False))
388 388
389 389 def walk(self, matcher=None):
390 390 '''yields (unencoded, encoded, size)
391 391
392 392 if a matcher is passed, storage files of only those tracked paths
393 393 are passed with matches the matcher
394 394 '''
395 395 # yield data files first
396 396 for x in self.datafiles(matcher):
397 397 yield x
398 398 for x in self.topfiles():
399 399 yield x
400 400
401 401 def copylist(self):
402 402 return ['requires'] + _data.split()
403 403
404 404 def write(self, tr):
405 405 pass
406 406
407 407 def invalidatecaches(self):
408 408 pass
409 409
410 410 def markremoved(self, fn):
411 411 pass
412 412
413 413 def __contains__(self, path):
414 414 '''Checks if the store contains path'''
415 415 path = "/".join(("data", path))
416 416 # file?
417 417 if self.vfs.exists(path + ".i"):
418 418 return True
419 419 # dir?
420 420 if not path.endswith("/"):
421 421 path = path + "/"
422 422 return self.vfs.exists(path)
423 423
424 424 class encodedstore(basicstore):
425 425 def __init__(self, path, vfstype):
426 426 vfs = vfstype(path + '/store')
427 427 self.path = vfs.base
428 428 self.createmode = _calcmode(vfs)
429 429 vfs.createmode = self.createmode
430 430 self.rawvfs = vfs
431 431 self.vfs = vfsmod.filtervfs(vfs, encodefilename)
432 432 self.opener = self.vfs
433 433
434 434 def datafiles(self, matcher=None):
435 435 for a, b, size in super(encodedstore, self).datafiles():
436 436 try:
437 437 a = decodefilename(a)
438 438 except KeyError:
439 439 a = None
440 440 if a is not None and not _matchtrackedpath(a, matcher):
441 441 continue
442 442 yield a, b, size
443 443
444 444 def join(self, f):
445 445 return self.path + '/' + encodefilename(f)
446 446
447 447 def copylist(self):
448 448 return (['requires', '00changelog.i'] +
449 449 ['store/' + f for f in _data.split()])
450 450
451 451 class fncache(object):
452 452 # the filename used to be partially encoded
453 453 # hence the encodedir/decodedir dance
454 454 def __init__(self, vfs):
455 455 self.vfs = vfs
456 456 self.entries = None
457 457 self._dirty = False
458 458 # set of new additions to fncache
459 459 self.addls = set()
460 460
461 461 def _load(self):
462 462 '''fill the entries from the fncache file'''
463 463 self._dirty = False
464 464 try:
465 465 fp = self.vfs('fncache', mode='rb')
466 466 except IOError:
467 467 # skip nonexistent file
468 468 self.entries = set()
469 469 return
470 470
471 471 self.entries = set()
472 472 chunk = b''
473 473 for c in iter(functools.partial(fp.read, fncache_chunksize), b''):
474 474 chunk += c
475 475 try:
476 476 p = chunk.rindex(b'\n')
477 477 self.entries.update(decodedir(chunk[:p + 1]).splitlines())
478 478 chunk = chunk[p + 1:]
479 479 except ValueError:
480 480 # substring '\n' not found, maybe the entry is bigger than the
481 481 # chunksize, so let's keep iterating
482 482 pass
483 483
484 484 if chunk:
485 raise error.Abort(_("fncache does not ends with a newline"))
485 raise error.Abort(_("fncache does not ends with a newline"),
486 hint=_("use 'hg debugrebuildfncache' to rebuild"
487 " the fncache"))
486 488 self._checkentries(fp)
487 489 fp.close()
488 490
489 491 def _checkentries(self, fp):
490 492 """ make sure there is no empty string in entries """
491 493 if '' in self.entries:
492 494 fp.seek(0)
493 495 for n, line in enumerate(util.iterfile(fp)):
494 496 if not line.rstrip('\n'):
495 497 t = _('invalid entry in fncache, line %d') % (n + 1)
496 498 raise error.Abort(t)
497 499
498 500 def write(self, tr):
499 501 if self._dirty:
500 502 assert self.entries is not None
501 503 self.entries = self.entries | self.addls
502 504 self.addls = set()
503 505 tr.addbackup('fncache')
504 506 fp = self.vfs('fncache', mode='wb', atomictemp=True)
505 507 if self.entries:
506 508 fp.write(encodedir('\n'.join(self.entries) + '\n'))
507 509 fp.close()
508 510 self._dirty = False
509 511 if self.addls:
510 512 # if we have just new entries, let's append them to the fncache
511 513 tr.addbackup('fncache')
512 514 fp = self.vfs('fncache', mode='ab', atomictemp=True)
513 515 if self.addls:
514 516 fp.write(encodedir('\n'.join(self.addls) + '\n'))
515 517 fp.close()
516 518 self.entries = None
517 519 self.addls = set()
518 520
519 521 def add(self, fn):
520 522 if self.entries is None:
521 523 self._load()
522 524 if fn not in self.entries:
523 525 self.addls.add(fn)
524 526
525 527 def remove(self, fn):
526 528 if self.entries is None:
527 529 self._load()
528 530 if fn in self.addls:
529 531 self.addls.remove(fn)
530 532 return
531 533 try:
532 534 self.entries.remove(fn)
533 535 self._dirty = True
534 536 except KeyError:
535 537 pass
536 538
537 539 def __contains__(self, fn):
538 540 if fn in self.addls:
539 541 return True
540 542 if self.entries is None:
541 543 self._load()
542 544 return fn in self.entries
543 545
544 546 def __iter__(self):
545 547 if self.entries is None:
546 548 self._load()
547 549 return iter(self.entries | self.addls)
548 550
549 551 class _fncachevfs(vfsmod.proxyvfs):
550 552 def __init__(self, vfs, fnc, encode):
551 553 vfsmod.proxyvfs.__init__(self, vfs)
552 554 self.fncache = fnc
553 555 self.encode = encode
554 556
555 557 def __call__(self, path, mode='r', *args, **kw):
556 558 encoded = self.encode(path)
557 559 if mode not in ('r', 'rb') and (path.startswith('data/') or
558 560 path.startswith('meta/')):
559 561 # do not trigger a fncache load when adding a file that already is
560 562 # known to exist.
561 563 notload = self.fncache.entries is None and self.vfs.exists(encoded)
562 564 if notload and 'a' in mode and not self.vfs.stat(encoded).st_size:
563 565 # when appending to an existing file, if the file has size zero,
564 566 # it should be considered as missing. Such zero-size files are
565 567 # the result of truncation when a transaction is aborted.
566 568 notload = False
567 569 if not notload:
568 570 self.fncache.add(path)
569 571 return self.vfs(encoded, mode, *args, **kw)
570 572
571 573 def join(self, path):
572 574 if path:
573 575 return self.vfs.join(self.encode(path))
574 576 else:
575 577 return self.vfs.join(path)
576 578
577 579 class fncachestore(basicstore):
578 580 def __init__(self, path, vfstype, dotencode):
579 581 if dotencode:
580 582 encode = _pathencode
581 583 else:
582 584 encode = _plainhybridencode
583 585 self.encode = encode
584 586 vfs = vfstype(path + '/store')
585 587 self.path = vfs.base
586 588 self.pathsep = self.path + '/'
587 589 self.createmode = _calcmode(vfs)
588 590 vfs.createmode = self.createmode
589 591 self.rawvfs = vfs
590 592 fnc = fncache(vfs)
591 593 self.fncache = fnc
592 594 self.vfs = _fncachevfs(vfs, fnc, encode)
593 595 self.opener = self.vfs
594 596
595 597 def join(self, f):
596 598 return self.pathsep + self.encode(f)
597 599
598 600 def getsize(self, path):
599 601 return self.rawvfs.stat(path).st_size
600 602
601 603 def datafiles(self, matcher=None):
602 604 for f in sorted(self.fncache):
603 605 if not _matchtrackedpath(f, matcher):
604 606 continue
605 607 ef = self.encode(f)
606 608 try:
607 609 yield f, ef, self.getsize(ef)
608 610 except OSError as err:
609 611 if err.errno != errno.ENOENT:
610 612 raise
611 613
612 614 def copylist(self):
613 615 d = ('narrowspec data meta dh fncache phaseroots obsstore'
614 616 ' 00manifest.d 00manifest.i 00changelog.d 00changelog.i')
615 617 return (['requires', '00changelog.i'] +
616 618 ['store/' + f for f in d.split()])
617 619
618 620 def write(self, tr):
619 621 self.fncache.write(tr)
620 622
621 623 def invalidatecaches(self):
622 624 self.fncache.entries = None
623 625 self.fncache.addls = set()
624 626
625 627 def markremoved(self, fn):
626 628 self.fncache.remove(fn)
627 629
628 630 def _exists(self, f):
629 631 ef = self.encode(f)
630 632 try:
631 633 self.getsize(ef)
632 634 return True
633 635 except OSError as err:
634 636 if err.errno != errno.ENOENT:
635 637 raise
636 638 # nonexistent entry
637 639 return False
638 640
639 641 def __contains__(self, path):
640 642 '''Checks if the store contains path'''
641 643 path = "/".join(("data", path))
642 644 # check for files (exact match)
643 645 e = path + '.i'
644 646 if e in self.fncache and self._exists(e):
645 647 return True
646 648 # now check for directories (prefix match)
647 649 if not path.endswith('/'):
648 650 path += '/'
649 651 for e in self.fncache:
650 652 if e.startswith(path) and self._exists(e):
651 653 return True
652 654 return False
General Comments 0
You need to be logged in to leave comments. Login now