##// END OF EJS Templates
store: use "vfs.exists()" instead of "os.path.exists()"
FUJIWARA Katsunori -
r19903:ca875b27 default
parent child Browse files
Show More
@@ -1,536 +1,536
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 i18n import _
9 9 import scmutil, util, parsers
10 10 import os, stat, errno
11 11
12 12 _sha = util.sha1
13 13
14 14 # This avoids a collision between a file named foo and a dir named
15 15 # foo.i or foo.d
16 16 def _encodedir(path):
17 17 '''
18 18 >>> _encodedir('data/foo.i')
19 19 'data/foo.i'
20 20 >>> _encodedir('data/foo.i/bla.i')
21 21 'data/foo.i.hg/bla.i'
22 22 >>> _encodedir('data/foo.i.hg/bla.i')
23 23 'data/foo.i.hg.hg/bla.i'
24 24 >>> _encodedir('data/foo.i\\ndata/foo.i/bla.i\\ndata/foo.i.hg/bla.i\\n')
25 25 'data/foo.i\\ndata/foo.i.hg/bla.i\\ndata/foo.i.hg.hg/bla.i\\n'
26 26 '''
27 27 return (path
28 28 .replace(".hg/", ".hg.hg/")
29 29 .replace(".i/", ".i.hg/")
30 30 .replace(".d/", ".d.hg/"))
31 31
32 32 encodedir = getattr(parsers, 'encodedir', _encodedir)
33 33
34 34 def decodedir(path):
35 35 '''
36 36 >>> decodedir('data/foo.i')
37 37 'data/foo.i'
38 38 >>> decodedir('data/foo.i.hg/bla.i')
39 39 'data/foo.i/bla.i'
40 40 >>> decodedir('data/foo.i.hg.hg/bla.i')
41 41 'data/foo.i.hg/bla.i'
42 42 '''
43 43 if ".hg/" not in path:
44 44 return path
45 45 return (path
46 46 .replace(".d.hg/", ".d/")
47 47 .replace(".i.hg/", ".i/")
48 48 .replace(".hg.hg/", ".hg/"))
49 49
50 50 def _buildencodefun():
51 51 '''
52 52 >>> enc, dec = _buildencodefun()
53 53
54 54 >>> enc('nothing/special.txt')
55 55 'nothing/special.txt'
56 56 >>> dec('nothing/special.txt')
57 57 'nothing/special.txt'
58 58
59 59 >>> enc('HELLO')
60 60 '_h_e_l_l_o'
61 61 >>> dec('_h_e_l_l_o')
62 62 'HELLO'
63 63
64 64 >>> enc('hello:world?')
65 65 'hello~3aworld~3f'
66 66 >>> dec('hello~3aworld~3f')
67 67 'hello:world?'
68 68
69 69 >>> enc('the\x07quick\xADshot')
70 70 'the~07quick~adshot'
71 71 >>> dec('the~07quick~adshot')
72 72 'the\\x07quick\\xadshot'
73 73 '''
74 74 e = '_'
75 75 winreserved = [ord(x) for x in '\\:*?"<>|']
76 76 cmap = dict([(chr(x), chr(x)) for x in xrange(127)])
77 77 for x in (range(32) + range(126, 256) + winreserved):
78 78 cmap[chr(x)] = "~%02x" % x
79 79 for x in range(ord("A"), ord("Z") + 1) + [ord(e)]:
80 80 cmap[chr(x)] = e + chr(x).lower()
81 81 dmap = {}
82 82 for k, v in cmap.iteritems():
83 83 dmap[v] = k
84 84 def decode(s):
85 85 i = 0
86 86 while i < len(s):
87 87 for l in xrange(1, 4):
88 88 try:
89 89 yield dmap[s[i:i + l]]
90 90 i += l
91 91 break
92 92 except KeyError:
93 93 pass
94 94 else:
95 95 raise KeyError
96 96 return (lambda s: ''.join([cmap[c] for c in s]),
97 97 lambda s: ''.join(list(decode(s))))
98 98
99 99 _encodefname, _decodefname = _buildencodefun()
100 100
101 101 def encodefilename(s):
102 102 '''
103 103 >>> encodefilename('foo.i/bar.d/bla.hg/hi:world?/HELLO')
104 104 'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o'
105 105 '''
106 106 return _encodefname(encodedir(s))
107 107
108 108 def decodefilename(s):
109 109 '''
110 110 >>> decodefilename('foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o')
111 111 'foo.i/bar.d/bla.hg/hi:world?/HELLO'
112 112 '''
113 113 return decodedir(_decodefname(s))
114 114
115 115 def _buildlowerencodefun():
116 116 '''
117 117 >>> f = _buildlowerencodefun()
118 118 >>> f('nothing/special.txt')
119 119 'nothing/special.txt'
120 120 >>> f('HELLO')
121 121 'hello'
122 122 >>> f('hello:world?')
123 123 'hello~3aworld~3f'
124 124 >>> f('the\x07quick\xADshot')
125 125 'the~07quick~adshot'
126 126 '''
127 127 winreserved = [ord(x) for x in '\\:*?"<>|']
128 128 cmap = dict([(chr(x), chr(x)) for x in xrange(127)])
129 129 for x in (range(32) + range(126, 256) + winreserved):
130 130 cmap[chr(x)] = "~%02x" % x
131 131 for x in range(ord("A"), ord("Z") + 1):
132 132 cmap[chr(x)] = chr(x).lower()
133 133 return lambda s: "".join([cmap[c] for c in s])
134 134
135 135 lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun()
136 136
137 137 # Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9
138 138 _winres3 = ('aux', 'con', 'prn', 'nul') # length 3
139 139 _winres4 = ('com', 'lpt') # length 4 (with trailing 1..9)
140 140 def _auxencode(path, dotencode):
141 141 '''
142 142 Encodes filenames containing names reserved by Windows or which end in
143 143 period or space. Does not touch other single reserved characters c.
144 144 Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here.
145 145 Additionally encodes space or period at the beginning, if dotencode is
146 146 True. Parameter path is assumed to be all lowercase.
147 147 A segment only needs encoding if a reserved name appears as a
148 148 basename (e.g. "aux", "aux.foo"). A directory or file named "foo.aux"
149 149 doesn't need encoding.
150 150
151 151 >>> s = '.foo/aux.txt/txt.aux/con/prn/nul/foo.'
152 152 >>> _auxencode(s.split('/'), True)
153 153 ['~2efoo', 'au~78.txt', 'txt.aux', 'co~6e', 'pr~6e', 'nu~6c', 'foo~2e']
154 154 >>> s = '.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.'
155 155 >>> _auxencode(s.split('/'), False)
156 156 ['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e']
157 157 >>> _auxencode(['foo. '], True)
158 158 ['foo.~20']
159 159 >>> _auxencode([' .foo'], True)
160 160 ['~20.foo']
161 161 '''
162 162 for i, n in enumerate(path):
163 163 if not n:
164 164 continue
165 165 if dotencode and n[0] in '. ':
166 166 n = "~%02x" % ord(n[0]) + n[1:]
167 167 path[i] = n
168 168 else:
169 169 l = n.find('.')
170 170 if l == -1:
171 171 l = len(n)
172 172 if ((l == 3 and n[:3] in _winres3) or
173 173 (l == 4 and n[3] <= '9' and n[3] >= '1'
174 174 and n[:3] in _winres4)):
175 175 # encode third letter ('aux' -> 'au~78')
176 176 ec = "~%02x" % ord(n[2])
177 177 n = n[0:2] + ec + n[3:]
178 178 path[i] = n
179 179 if n[-1] in '. ':
180 180 # encode last period or space ('foo...' -> 'foo..~2e')
181 181 path[i] = n[:-1] + "~%02x" % ord(n[-1])
182 182 return path
183 183
184 184 _maxstorepathlen = 120
185 185 _dirprefixlen = 8
186 186 _maxshortdirslen = 8 * (_dirprefixlen + 1) - 4
187 187
188 188 def _hashencode(path, dotencode):
189 189 digest = _sha(path).hexdigest()
190 190 le = lowerencode(path).split('/')[1:]
191 191 parts = _auxencode(le, dotencode)
192 192 basename = parts[-1]
193 193 _root, ext = os.path.splitext(basename)
194 194 sdirs = []
195 195 sdirslen = 0
196 196 for p in parts[:-1]:
197 197 d = p[:_dirprefixlen]
198 198 if d[-1] in '. ':
199 199 # Windows can't access dirs ending in period or space
200 200 d = d[:-1] + '_'
201 201 if sdirslen == 0:
202 202 t = len(d)
203 203 else:
204 204 t = sdirslen + 1 + len(d)
205 205 if t > _maxshortdirslen:
206 206 break
207 207 sdirs.append(d)
208 208 sdirslen = t
209 209 dirs = '/'.join(sdirs)
210 210 if len(dirs) > 0:
211 211 dirs += '/'
212 212 res = 'dh/' + dirs + digest + ext
213 213 spaceleft = _maxstorepathlen - len(res)
214 214 if spaceleft > 0:
215 215 filler = basename[:spaceleft]
216 216 res = 'dh/' + dirs + filler + digest + ext
217 217 return res
218 218
219 219 def _hybridencode(path, dotencode):
220 220 '''encodes path with a length limit
221 221
222 222 Encodes all paths that begin with 'data/', according to the following.
223 223
224 224 Default encoding (reversible):
225 225
226 226 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
227 227 characters are encoded as '~xx', where xx is the two digit hex code
228 228 of the character (see encodefilename).
229 229 Relevant path components consisting of Windows reserved filenames are
230 230 masked by encoding the third character ('aux' -> 'au~78', see _auxencode).
231 231
232 232 Hashed encoding (not reversible):
233 233
234 234 If the default-encoded path is longer than _maxstorepathlen, a
235 235 non-reversible hybrid hashing of the path is done instead.
236 236 This encoding uses up to _dirprefixlen characters of all directory
237 237 levels of the lowerencoded path, but not more levels than can fit into
238 238 _maxshortdirslen.
239 239 Then follows the filler followed by the sha digest of the full path.
240 240 The filler is the beginning of the basename of the lowerencoded path
241 241 (the basename is everything after the last path separator). The filler
242 242 is as long as possible, filling in characters from the basename until
243 243 the encoded path has _maxstorepathlen characters (or all chars of the
244 244 basename have been taken).
245 245 The extension (e.g. '.i' or '.d') is preserved.
246 246
247 247 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
248 248 encoding was used.
249 249 '''
250 250 path = encodedir(path)
251 251 ef = _encodefname(path).split('/')
252 252 res = '/'.join(_auxencode(ef, dotencode))
253 253 if len(res) > _maxstorepathlen:
254 254 res = _hashencode(path, dotencode)
255 255 return res
256 256
257 257 def _pathencode(path):
258 258 de = encodedir(path)
259 259 if len(path) > _maxstorepathlen:
260 260 return _hashencode(de, True)
261 261 ef = _encodefname(de).split('/')
262 262 res = '/'.join(_auxencode(ef, True))
263 263 if len(res) > _maxstorepathlen:
264 264 return _hashencode(de, True)
265 265 return res
266 266
267 267 _pathencode = getattr(parsers, 'pathencode', _pathencode)
268 268
269 269 def _plainhybridencode(f):
270 270 return _hybridencode(f, False)
271 271
272 272 def _calcmode(vfs):
273 273 try:
274 274 # files in .hg/ will be created using this mode
275 275 mode = vfs.stat().st_mode
276 276 # avoid some useless chmods
277 277 if (0777 & ~util.umask) == (0777 & mode):
278 278 mode = None
279 279 except OSError:
280 280 mode = None
281 281 return mode
282 282
283 283 _data = ('data 00manifest.d 00manifest.i 00changelog.d 00changelog.i'
284 284 ' phaseroots obsstore')
285 285
286 286 class basicstore(object):
287 287 '''base class for local repository stores'''
288 288 def __init__(self, path, vfstype):
289 289 vfs = vfstype(path)
290 290 self.path = vfs.base
291 291 self.createmode = _calcmode(vfs)
292 292 vfs.createmode = self.createmode
293 293 self.rawvfs = vfs
294 294 self.vfs = scmutil.filtervfs(vfs, encodedir)
295 295 self.opener = self.vfs
296 296
297 297 def join(self, f):
298 298 return self.path + '/' + encodedir(f)
299 299
300 300 def _walk(self, relpath, recurse):
301 301 '''yields (unencoded, encoded, size)'''
302 302 path = self.path
303 303 if relpath:
304 304 path += '/' + relpath
305 305 striplen = len(self.path) + 1
306 306 l = []
307 307 if self.rawvfs.isdir(path):
308 308 visit = [path]
309 309 readdir = self.rawvfs.readdir
310 310 while visit:
311 311 p = visit.pop()
312 312 for f, kind, st in readdir(p, stat=True):
313 313 fp = p + '/' + f
314 314 if kind == stat.S_IFREG and f[-2:] in ('.d', '.i'):
315 315 n = util.pconvert(fp[striplen:])
316 316 l.append((decodedir(n), n, st.st_size))
317 317 elif kind == stat.S_IFDIR and recurse:
318 318 visit.append(fp)
319 319 l.sort()
320 320 return l
321 321
322 322 def datafiles(self):
323 323 return self._walk('data', True)
324 324
325 325 def topfiles(self):
326 326 # yield manifest before changelog
327 327 return reversed(self._walk('', False))
328 328
329 329 def walk(self):
330 330 '''yields (unencoded, encoded, size)'''
331 331 # yield data files first
332 332 for x in self.datafiles():
333 333 yield x
334 334 for x in self.topfiles():
335 335 yield x
336 336
337 337 def copylist(self):
338 338 return ['requires'] + _data.split()
339 339
340 340 def write(self):
341 341 pass
342 342
343 343 def __contains__(self, path):
344 344 '''Checks if the store contains path'''
345 345 path = "/".join(("data", path))
346 346 # file?
347 if os.path.exists(self.join(path + ".i")):
347 if self.vfs.exists(path + ".i"):
348 348 return True
349 349 # dir?
350 350 if not path.endswith("/"):
351 351 path = path + "/"
352 return os.path.exists(self.join(path))
352 return self.vfs.exists(path)
353 353
354 354 class encodedstore(basicstore):
355 355 def __init__(self, path, vfstype):
356 356 vfs = vfstype(path + '/store')
357 357 self.path = vfs.base
358 358 self.createmode = _calcmode(vfs)
359 359 vfs.createmode = self.createmode
360 360 self.rawvfs = vfs
361 361 self.vfs = scmutil.filtervfs(vfs, encodefilename)
362 362 self.opener = self.vfs
363 363
364 364 def datafiles(self):
365 365 for a, b, size in self._walk('data', True):
366 366 try:
367 367 a = decodefilename(a)
368 368 except KeyError:
369 369 a = None
370 370 yield a, b, size
371 371
372 372 def join(self, f):
373 373 return self.path + '/' + encodefilename(f)
374 374
375 375 def copylist(self):
376 376 return (['requires', '00changelog.i'] +
377 377 ['store/' + f for f in _data.split()])
378 378
379 379 class fncache(object):
380 380 # the filename used to be partially encoded
381 381 # hence the encodedir/decodedir dance
382 382 def __init__(self, vfs):
383 383 self.vfs = vfs
384 384 self.entries = None
385 385 self._dirty = False
386 386
387 387 def _load(self):
388 388 '''fill the entries from the fncache file'''
389 389 self._dirty = False
390 390 try:
391 391 fp = self.vfs('fncache', mode='rb')
392 392 except IOError:
393 393 # skip nonexistent file
394 394 self.entries = set()
395 395 return
396 396 self.entries = set(decodedir(fp.read()).splitlines())
397 397 if '' in self.entries:
398 398 fp.seek(0)
399 399 for n, line in enumerate(fp):
400 400 if not line.rstrip('\n'):
401 401 t = _('invalid entry in fncache, line %s') % (n + 1)
402 402 raise util.Abort(t)
403 403 fp.close()
404 404
405 405 def _write(self, files, atomictemp):
406 406 fp = self.vfs('fncache', mode='wb', atomictemp=atomictemp)
407 407 if files:
408 408 fp.write(encodedir('\n'.join(files) + '\n'))
409 409 fp.close()
410 410 self._dirty = False
411 411
412 412 def rewrite(self, files):
413 413 self._write(files, False)
414 414 self.entries = set(files)
415 415
416 416 def write(self):
417 417 if self._dirty:
418 418 self._write(self.entries, True)
419 419
420 420 def add(self, fn):
421 421 if self.entries is None:
422 422 self._load()
423 423 if fn not in self.entries:
424 424 self._dirty = True
425 425 self.entries.add(fn)
426 426
427 427 def __contains__(self, fn):
428 428 if self.entries is None:
429 429 self._load()
430 430 return fn in self.entries
431 431
432 432 def __iter__(self):
433 433 if self.entries is None:
434 434 self._load()
435 435 return iter(self.entries)
436 436
437 437 class _fncachevfs(scmutil.abstractvfs, scmutil.auditvfs):
438 438 def __init__(self, vfs, fnc, encode):
439 439 scmutil.auditvfs.__init__(self, vfs)
440 440 self.fncache = fnc
441 441 self.encode = encode
442 442
443 443 def __call__(self, path, mode='r', *args, **kw):
444 444 if mode not in ('r', 'rb') and path.startswith('data/'):
445 445 self.fncache.add(path)
446 446 return self.vfs(self.encode(path), mode, *args, **kw)
447 447
448 448 def join(self, path):
449 449 if path:
450 450 return self.vfs.join(self.encode(path))
451 451 else:
452 452 return self.vfs.join(path)
453 453
454 454 class fncachestore(basicstore):
455 455 def __init__(self, path, vfstype, dotencode):
456 456 if dotencode:
457 457 encode = _pathencode
458 458 else:
459 459 encode = _plainhybridencode
460 460 self.encode = encode
461 461 vfs = vfstype(path + '/store')
462 462 self.path = vfs.base
463 463 self.pathsep = self.path + '/'
464 464 self.createmode = _calcmode(vfs)
465 465 vfs.createmode = self.createmode
466 466 self.rawvfs = vfs
467 467 fnc = fncache(vfs)
468 468 self.fncache = fnc
469 469 self.vfs = _fncachevfs(vfs, fnc, encode)
470 470 self.opener = self.vfs
471 471
472 472 def join(self, f):
473 473 return self.pathsep + self.encode(f)
474 474
475 475 def getsize(self, path):
476 476 return self.rawvfs.stat(path).st_size
477 477
478 478 def datafiles(self):
479 479 rewrite = False
480 480 existing = []
481 481 for f in sorted(self.fncache):
482 482 ef = self.encode(f)
483 483 try:
484 484 yield f, ef, self.getsize(ef)
485 485 existing.append(f)
486 486 except OSError, err:
487 487 if err.errno != errno.ENOENT:
488 488 raise
489 489 # nonexistent entry
490 490 rewrite = True
491 491 if rewrite:
492 492 # rewrite fncache to remove nonexistent entries
493 493 # (may be caused by rollback / strip)
494 494 self.fncache.rewrite(existing)
495 495
496 496 def copylist(self):
497 497 d = ('data dh fncache phaseroots obsstore'
498 498 ' 00manifest.d 00manifest.i 00changelog.d 00changelog.i')
499 499 return (['requires', '00changelog.i'] +
500 500 ['store/' + f for f in d.split()])
501 501
502 502 def write(self):
503 503 self.fncache.write()
504 504
505 505 def _exists(self, f):
506 506 ef = self.encode(f)
507 507 try:
508 508 self.getsize(ef)
509 509 return True
510 510 except OSError, err:
511 511 if err.errno != errno.ENOENT:
512 512 raise
513 513 # nonexistent entry
514 514 return False
515 515
516 516 def __contains__(self, path):
517 517 '''Checks if the store contains path'''
518 518 path = "/".join(("data", path))
519 519 # check for files (exact match)
520 520 e = path + '.i'
521 521 if e in self.fncache and self._exists(e):
522 522 return True
523 523 # now check for directories (prefix match)
524 524 if not path.endswith('/'):
525 525 path += '/'
526 526 for e in self.fncache:
527 527 if e.startswith(path) and self._exists(e):
528 528 return True
529 529 return False
530 530
531 531 def store(requirements, path, vfstype):
532 532 if 'store' in requirements:
533 533 if 'fncache' in requirements:
534 534 return fncachestore(path, vfstype, 'dotencode' in requirements)
535 535 return encodedstore(path, vfstype)
536 536 return basicstore(path, vfstype)
General Comments 0
You need to be logged in to leave comments. Login now