##// END OF EJS Templates
vfs: drop the 'mustaudit' API...
marmoute -
r33257:6fb5c509 default
parent child Browse files
Show More
@@ -1,643 +1,627 b''
1 1 # vfs.py - Mercurial 'vfs' classes
2 2 #
3 3 # Copyright 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 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 tempfile
15 15 import threading
16 16
17 17 from .i18n import _
18 18 from . import (
19 19 error,
20 20 pathutil,
21 21 pycompat,
22 22 util,
23 23 )
24 24
25 25 class abstractvfs(object):
26 26 """Abstract base class; cannot be instantiated"""
27 27
28 28 def __init__(self, *args, **kwargs):
29 29 '''Prevent instantiation; don't call this from subclasses.'''
30 30 raise NotImplementedError('attempted instantiating ' + str(type(self)))
31 31
32 32 def tryread(self, path):
33 33 '''gracefully return an empty string for missing files'''
34 34 try:
35 35 return self.read(path)
36 36 except IOError as inst:
37 37 if inst.errno != errno.ENOENT:
38 38 raise
39 39 return ""
40 40
41 41 def tryreadlines(self, path, mode='rb'):
42 42 '''gracefully return an empty array for missing files'''
43 43 try:
44 44 return self.readlines(path, mode=mode)
45 45 except IOError as inst:
46 46 if inst.errno != errno.ENOENT:
47 47 raise
48 48 return []
49 49
50 50 @util.propertycache
51 51 def open(self):
52 52 '''Open ``path`` file, which is relative to vfs root.
53 53
54 54 Newly created directories are marked as "not to be indexed by
55 55 the content indexing service", if ``notindexed`` is specified
56 56 for "write" mode access.
57 57 '''
58 58 return self.__call__
59 59
60 60 def read(self, path):
61 61 with self(path, 'rb') as fp:
62 62 return fp.read()
63 63
64 64 def readlines(self, path, mode='rb'):
65 65 with self(path, mode=mode) as fp:
66 66 return fp.readlines()
67 67
68 68 def write(self, path, data, backgroundclose=False):
69 69 with self(path, 'wb', backgroundclose=backgroundclose) as fp:
70 70 return fp.write(data)
71 71
72 72 def writelines(self, path, data, mode='wb', notindexed=False):
73 73 with self(path, mode=mode, notindexed=notindexed) as fp:
74 74 return fp.writelines(data)
75 75
76 76 def append(self, path, data):
77 77 with self(path, 'ab') as fp:
78 78 return fp.write(data)
79 79
80 80 def basename(self, path):
81 81 """return base element of a path (as os.path.basename would do)
82 82
83 83 This exists to allow handling of strange encoding if needed."""
84 84 return os.path.basename(path)
85 85
86 86 def chmod(self, path, mode):
87 87 return os.chmod(self.join(path), mode)
88 88
89 89 def dirname(self, path):
90 90 """return dirname element of a path (as os.path.dirname would do)
91 91
92 92 This exists to allow handling of strange encoding if needed."""
93 93 return os.path.dirname(path)
94 94
95 95 def exists(self, path=None):
96 96 return os.path.exists(self.join(path))
97 97
98 98 def fstat(self, fp):
99 99 return util.fstat(fp)
100 100
101 101 def isdir(self, path=None):
102 102 return os.path.isdir(self.join(path))
103 103
104 104 def isfile(self, path=None):
105 105 return os.path.isfile(self.join(path))
106 106
107 107 def islink(self, path=None):
108 108 return os.path.islink(self.join(path))
109 109
110 110 def isfileorlink(self, path=None):
111 111 '''return whether path is a regular file or a symlink
112 112
113 113 Unlike isfile, this doesn't follow symlinks.'''
114 114 try:
115 115 st = self.lstat(path)
116 116 except OSError:
117 117 return False
118 118 mode = st.st_mode
119 119 return stat.S_ISREG(mode) or stat.S_ISLNK(mode)
120 120
121 121 def reljoin(self, *paths):
122 122 """join various elements of a path together (as os.path.join would do)
123 123
124 124 The vfs base is not injected so that path stay relative. This exists
125 125 to allow handling of strange encoding if needed."""
126 126 return os.path.join(*paths)
127 127
128 128 def split(self, path):
129 129 """split top-most element of a path (as os.path.split would do)
130 130
131 131 This exists to allow handling of strange encoding if needed."""
132 132 return os.path.split(path)
133 133
134 134 def lexists(self, path=None):
135 135 return os.path.lexists(self.join(path))
136 136
137 137 def lstat(self, path=None):
138 138 return os.lstat(self.join(path))
139 139
140 140 def listdir(self, path=None):
141 141 return os.listdir(self.join(path))
142 142
143 143 def makedir(self, path=None, notindexed=True):
144 144 return util.makedir(self.join(path), notindexed)
145 145
146 146 def makedirs(self, path=None, mode=None):
147 147 return util.makedirs(self.join(path), mode)
148 148
149 149 def makelock(self, info, path):
150 150 return util.makelock(info, self.join(path))
151 151
152 152 def mkdir(self, path=None):
153 153 return os.mkdir(self.join(path))
154 154
155 155 def mkstemp(self, suffix='', prefix='tmp', dir=None, text=False):
156 156 fd, name = tempfile.mkstemp(suffix=suffix, prefix=prefix,
157 157 dir=self.join(dir), text=text)
158 158 dname, fname = util.split(name)
159 159 if dir:
160 160 return fd, os.path.join(dir, fname)
161 161 else:
162 162 return fd, fname
163 163
164 164 def readdir(self, path=None, stat=None, skip=None):
165 165 return util.listdir(self.join(path), stat, skip)
166 166
167 167 def readlock(self, path):
168 168 return util.readlock(self.join(path))
169 169
170 170 def rename(self, src, dst, checkambig=False):
171 171 """Rename from src to dst
172 172
173 173 checkambig argument is used with util.filestat, and is useful
174 174 only if destination file is guarded by any lock
175 175 (e.g. repo.lock or repo.wlock).
176 176 """
177 177 srcpath = self.join(src)
178 178 dstpath = self.join(dst)
179 179 oldstat = checkambig and util.filestat.frompath(dstpath)
180 180 if oldstat and oldstat.stat:
181 181 def dorename(spath, dpath):
182 182 ret = util.rename(spath, dpath)
183 183 newstat = util.filestat.frompath(dpath)
184 184 if newstat.isambig(oldstat):
185 185 # stat of renamed file is ambiguous to original one
186 186 return ret, newstat.avoidambig(dpath, oldstat)
187 187 return ret, True
188 188 ret, avoided = dorename(srcpath, dstpath)
189 189 if not avoided:
190 190 # simply copy to change owner of srcpath (see issue5418)
191 191 util.copyfile(dstpath, srcpath)
192 192 ret, avoided = dorename(srcpath, dstpath)
193 193 return ret
194 194 return util.rename(srcpath, dstpath)
195 195
196 196 def readlink(self, path):
197 197 return os.readlink(self.join(path))
198 198
199 199 def removedirs(self, path=None):
200 200 """Remove a leaf directory and all empty intermediate ones
201 201 """
202 202 return util.removedirs(self.join(path))
203 203
204 204 def rmtree(self, path=None, ignore_errors=False, forcibly=False):
205 205 """Remove a directory tree recursively
206 206
207 207 If ``forcibly``, this tries to remove READ-ONLY files, too.
208 208 """
209 209 if forcibly:
210 210 def onerror(function, path, excinfo):
211 211 if function is not os.remove:
212 212 raise
213 213 # read-only files cannot be unlinked under Windows
214 214 s = os.stat(path)
215 215 if (s.st_mode & stat.S_IWRITE) != 0:
216 216 raise
217 217 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
218 218 os.remove(path)
219 219 else:
220 220 onerror = None
221 221 return shutil.rmtree(self.join(path),
222 222 ignore_errors=ignore_errors, onerror=onerror)
223 223
224 224 def setflags(self, path, l, x):
225 225 return util.setflags(self.join(path), l, x)
226 226
227 227 def stat(self, path=None):
228 228 return os.stat(self.join(path))
229 229
230 230 def unlink(self, path=None):
231 231 return util.unlink(self.join(path))
232 232
233 233 def tryunlink(self, path=None):
234 234 """Attempt to remove a file, ignoring missing file errors."""
235 235 util.tryunlink(self.join(path))
236 236
237 237 def unlinkpath(self, path=None, ignoremissing=False):
238 238 return util.unlinkpath(self.join(path), ignoremissing=ignoremissing)
239 239
240 240 def utime(self, path=None, t=None):
241 241 return os.utime(self.join(path), t)
242 242
243 243 def walk(self, path=None, onerror=None):
244 244 """Yield (dirpath, dirs, files) tuple for each directories under path
245 245
246 246 ``dirpath`` is relative one from the root of this vfs. This
247 247 uses ``os.sep`` as path separator, even you specify POSIX
248 248 style ``path``.
249 249
250 250 "The root of this vfs" is represented as empty ``dirpath``.
251 251 """
252 252 root = os.path.normpath(self.join(None))
253 253 # when dirpath == root, dirpath[prefixlen:] becomes empty
254 254 # because len(dirpath) < prefixlen.
255 255 prefixlen = len(pathutil.normasprefix(root))
256 256 for dirpath, dirs, files in os.walk(self.join(path), onerror=onerror):
257 257 yield (dirpath[prefixlen:], dirs, files)
258 258
259 259 @contextlib.contextmanager
260 260 def backgroundclosing(self, ui, expectedcount=-1):
261 261 """Allow files to be closed asynchronously.
262 262
263 263 When this context manager is active, ``backgroundclose`` can be passed
264 264 to ``__call__``/``open`` to result in the file possibly being closed
265 265 asynchronously, on a background thread.
266 266 """
267 267 # This is an arbitrary restriction and could be changed if we ever
268 268 # have a use case.
269 269 vfs = getattr(self, 'vfs', self)
270 270 if getattr(vfs, '_backgroundfilecloser', None):
271 271 raise error.Abort(
272 272 _('can only have 1 active background file closer'))
273 273
274 274 with backgroundfilecloser(ui, expectedcount=expectedcount) as bfc:
275 275 try:
276 276 vfs._backgroundfilecloser = bfc
277 277 yield bfc
278 278 finally:
279 279 vfs._backgroundfilecloser = None
280 280
281 281 class vfs(abstractvfs):
282 282 '''Operate files relative to a base directory
283 283
284 284 This class is used to hide the details of COW semantics and
285 285 remote file access from higher level code.
286 286 '''
287 287 def __init__(self, base, audit=True, expandpath=False, realpath=False):
288 288 if expandpath:
289 289 base = util.expandpath(base)
290 290 if realpath:
291 291 base = os.path.realpath(base)
292 292 self.base = base
293 self.mustaudit = audit
294 self.createmode = None
295 self._trustnlink = None
296
297 @property
298 def mustaudit(self):
299 return self._audit
300
301 @mustaudit.setter
302 def mustaudit(self, onoff):
303 self._audit = onoff
304 if onoff:
293 self._audit = audit
294 if audit:
305 295 self.audit = pathutil.pathauditor(self.base)
306 296 else:
307 297 self.audit = util.always
298 self.createmode = None
299 self._trustnlink = None
308 300
309 301 @util.propertycache
310 302 def _cansymlink(self):
311 303 return util.checklink(self.base)
312 304
313 305 @util.propertycache
314 306 def _chmod(self):
315 307 return util.checkexec(self.base)
316 308
317 309 def _fixfilemode(self, name):
318 310 if self.createmode is None or not self._chmod:
319 311 return
320 312 os.chmod(name, self.createmode & 0o666)
321 313
322 314 def __call__(self, path, mode="r", text=False, atomictemp=False,
323 315 notindexed=False, backgroundclose=False, checkambig=False,
324 316 auditpath=True):
325 317 '''Open ``path`` file, which is relative to vfs root.
326 318
327 319 Newly created directories are marked as "not to be indexed by
328 320 the content indexing service", if ``notindexed`` is specified
329 321 for "write" mode access.
330 322
331 323 If ``backgroundclose`` is passed, the file may be closed asynchronously.
332 324 It can only be used if the ``self.backgroundclosing()`` context manager
333 325 is active. This should only be specified if the following criteria hold:
334 326
335 327 1. There is a potential for writing thousands of files. Unless you
336 328 are writing thousands of files, the performance benefits of
337 329 asynchronously closing files is not realized.
338 330 2. Files are opened exactly once for the ``backgroundclosing``
339 331 active duration and are therefore free of race conditions between
340 332 closing a file on a background thread and reopening it. (If the
341 333 file were opened multiple times, there could be unflushed data
342 334 because the original file handle hasn't been flushed/closed yet.)
343 335
344 336 ``checkambig`` argument is passed to atomictemplfile (valid
345 337 only for writing), and is useful only if target file is
346 338 guarded by any lock (e.g. repo.lock or repo.wlock).
347 339 '''
348 340 if auditpath:
349 341 if self._audit:
350 342 r = util.checkosfilename(path)
351 343 if r:
352 344 raise error.Abort("%s: %r" % (r, path))
353 345 self.audit(path)
354 346 f = self.join(path)
355 347
356 348 if not text and "b" not in mode:
357 349 mode += "b" # for that other OS
358 350
359 351 nlink = -1
360 352 if mode not in ('r', 'rb'):
361 353 dirname, basename = util.split(f)
362 354 # If basename is empty, then the path is malformed because it points
363 355 # to a directory. Let the posixfile() call below raise IOError.
364 356 if basename:
365 357 if atomictemp:
366 358 util.makedirs(dirname, self.createmode, notindexed)
367 359 return util.atomictempfile(f, mode, self.createmode,
368 360 checkambig=checkambig)
369 361 try:
370 362 if 'w' in mode:
371 363 util.unlink(f)
372 364 nlink = 0
373 365 else:
374 366 # nlinks() may behave differently for files on Windows
375 367 # shares if the file is open.
376 368 with util.posixfile(f):
377 369 nlink = util.nlinks(f)
378 370 if nlink < 1:
379 371 nlink = 2 # force mktempcopy (issue1922)
380 372 except (OSError, IOError) as e:
381 373 if e.errno != errno.ENOENT:
382 374 raise
383 375 nlink = 0
384 376 util.makedirs(dirname, self.createmode, notindexed)
385 377 if nlink > 0:
386 378 if self._trustnlink is None:
387 379 self._trustnlink = nlink > 1 or util.checknlink(f)
388 380 if nlink > 1 or not self._trustnlink:
389 381 util.rename(util.mktempcopy(f), f)
390 382 fp = util.posixfile(f, mode)
391 383 if nlink == 0:
392 384 self._fixfilemode(f)
393 385
394 386 if checkambig:
395 387 if mode in ('r', 'rb'):
396 388 raise error.Abort(_('implementation error: mode %s is not'
397 389 ' valid for checkambig=True') % mode)
398 390 fp = checkambigatclosing(fp)
399 391
400 392 if backgroundclose:
401 393 if not self._backgroundfilecloser:
402 394 raise error.Abort(_('backgroundclose can only be used when a '
403 395 'backgroundclosing context manager is active')
404 396 )
405 397
406 398 fp = delayclosedfile(fp, self._backgroundfilecloser)
407 399
408 400 return fp
409 401
410 402 def symlink(self, src, dst):
411 403 self.audit(dst)
412 404 linkname = self.join(dst)
413 405 util.tryunlink(linkname)
414 406
415 407 util.makedirs(os.path.dirname(linkname), self.createmode)
416 408
417 409 if self._cansymlink:
418 410 try:
419 411 os.symlink(src, linkname)
420 412 except OSError as err:
421 413 raise OSError(err.errno, _('could not symlink to %r: %s') %
422 414 (src, err.strerror), linkname)
423 415 else:
424 416 self.write(dst, src)
425 417
426 418 def join(self, path, *insidef):
427 419 if path:
428 420 return os.path.join(self.base, path, *insidef)
429 421 else:
430 422 return self.base
431 423
432 424 opener = vfs
433 425
434 426 class auditvfs(object):
435 427 def __init__(self, vfs):
436 428 self.vfs = vfs
437 429
438 430 @property
439 def mustaudit(self):
440 return self.vfs.mustaudit
441
442 @mustaudit.setter
443 def mustaudit(self, onoff):
444 self.vfs.mustaudit = onoff
445
446 @property
447 431 def options(self):
448 432 return self.vfs.options
449 433
450 434 @options.setter
451 435 def options(self, value):
452 436 self.vfs.options = value
453 437
454 438 class filtervfs(abstractvfs, auditvfs):
455 439 '''Wrapper vfs for filtering filenames with a function.'''
456 440
457 441 def __init__(self, vfs, filter):
458 442 auditvfs.__init__(self, vfs)
459 443 self._filter = filter
460 444
461 445 def __call__(self, path, *args, **kwargs):
462 446 return self.vfs(self._filter(path), *args, **kwargs)
463 447
464 448 def join(self, path, *insidef):
465 449 if path:
466 450 return self.vfs.join(self._filter(self.vfs.reljoin(path, *insidef)))
467 451 else:
468 452 return self.vfs.join(path)
469 453
470 454 filteropener = filtervfs
471 455
472 456 class readonlyvfs(abstractvfs, auditvfs):
473 457 '''Wrapper vfs preventing any writing.'''
474 458
475 459 def __init__(self, vfs):
476 460 auditvfs.__init__(self, vfs)
477 461
478 462 def __call__(self, path, mode='r', *args, **kw):
479 463 if mode not in ('r', 'rb'):
480 464 raise error.Abort(_('this vfs is read only'))
481 465 return self.vfs(path, mode, *args, **kw)
482 466
483 467 def join(self, path, *insidef):
484 468 return self.vfs.join(path, *insidef)
485 469
486 470 class closewrapbase(object):
487 471 """Base class of wrapper, which hooks closing
488 472
489 473 Do not instantiate outside of the vfs layer.
490 474 """
491 475 def __init__(self, fh):
492 476 object.__setattr__(self, r'_origfh', fh)
493 477
494 478 def __getattr__(self, attr):
495 479 return getattr(self._origfh, attr)
496 480
497 481 def __setattr__(self, attr, value):
498 482 return setattr(self._origfh, attr, value)
499 483
500 484 def __delattr__(self, attr):
501 485 return delattr(self._origfh, attr)
502 486
503 487 def __enter__(self):
504 488 return self._origfh.__enter__()
505 489
506 490 def __exit__(self, exc_type, exc_value, exc_tb):
507 491 raise NotImplementedError('attempted instantiating ' + str(type(self)))
508 492
509 493 def close(self):
510 494 raise NotImplementedError('attempted instantiating ' + str(type(self)))
511 495
512 496 class delayclosedfile(closewrapbase):
513 497 """Proxy for a file object whose close is delayed.
514 498
515 499 Do not instantiate outside of the vfs layer.
516 500 """
517 501 def __init__(self, fh, closer):
518 502 super(delayclosedfile, self).__init__(fh)
519 503 object.__setattr__(self, r'_closer', closer)
520 504
521 505 def __exit__(self, exc_type, exc_value, exc_tb):
522 506 self._closer.close(self._origfh)
523 507
524 508 def close(self):
525 509 self._closer.close(self._origfh)
526 510
527 511 class backgroundfilecloser(object):
528 512 """Coordinates background closing of file handles on multiple threads."""
529 513 def __init__(self, ui, expectedcount=-1):
530 514 self._running = False
531 515 self._entered = False
532 516 self._threads = []
533 517 self._threadexception = None
534 518
535 519 # Only Windows/NTFS has slow file closing. So only enable by default
536 520 # on that platform. But allow to be enabled elsewhere for testing.
537 521 defaultenabled = pycompat.osname == 'nt'
538 522 enabled = ui.configbool('worker', 'backgroundclose', defaultenabled)
539 523
540 524 if not enabled:
541 525 return
542 526
543 527 # There is overhead to starting and stopping the background threads.
544 528 # Don't do background processing unless the file count is large enough
545 529 # to justify it.
546 530 minfilecount = ui.configint('worker', 'backgroundcloseminfilecount')
547 531 # FUTURE dynamically start background threads after minfilecount closes.
548 532 # (We don't currently have any callers that don't know their file count)
549 533 if expectedcount > 0 and expectedcount < minfilecount:
550 534 return
551 535
552 536 maxqueue = ui.configint('worker', 'backgroundclosemaxqueue')
553 537 threadcount = ui.configint('worker', 'backgroundclosethreadcount')
554 538
555 539 ui.debug('starting %d threads for background file closing\n' %
556 540 threadcount)
557 541
558 542 self._queue = util.queue(maxsize=maxqueue)
559 543 self._running = True
560 544
561 545 for i in range(threadcount):
562 546 t = threading.Thread(target=self._worker, name='backgroundcloser')
563 547 self._threads.append(t)
564 548 t.start()
565 549
566 550 def __enter__(self):
567 551 self._entered = True
568 552 return self
569 553
570 554 def __exit__(self, exc_type, exc_value, exc_tb):
571 555 self._running = False
572 556
573 557 # Wait for threads to finish closing so open files don't linger for
574 558 # longer than lifetime of context manager.
575 559 for t in self._threads:
576 560 t.join()
577 561
578 562 def _worker(self):
579 563 """Main routine for worker thread."""
580 564 while True:
581 565 try:
582 566 fh = self._queue.get(block=True, timeout=0.100)
583 567 # Need to catch or the thread will terminate and
584 568 # we could orphan file descriptors.
585 569 try:
586 570 fh.close()
587 571 except Exception as e:
588 572 # Stash so can re-raise from main thread later.
589 573 self._threadexception = e
590 574 except util.empty:
591 575 if not self._running:
592 576 break
593 577
594 578 def close(self, fh):
595 579 """Schedule a file for closing."""
596 580 if not self._entered:
597 581 raise error.Abort(_('can only call close() when context manager '
598 582 'active'))
599 583
600 584 # If a background thread encountered an exception, raise now so we fail
601 585 # fast. Otherwise we may potentially go on for minutes until the error
602 586 # is acted on.
603 587 if self._threadexception:
604 588 e = self._threadexception
605 589 self._threadexception = None
606 590 raise e
607 591
608 592 # If we're not actively running, close synchronously.
609 593 if not self._running:
610 594 fh.close()
611 595 return
612 596
613 597 self._queue.put(fh, block=True, timeout=None)
614 598
615 599 class checkambigatclosing(closewrapbase):
616 600 """Proxy for a file object, to avoid ambiguity of file stat
617 601
618 602 See also util.filestat for detail about "ambiguity of file stat".
619 603
620 604 This proxy is useful only if the target file is guarded by any
621 605 lock (e.g. repo.lock or repo.wlock)
622 606
623 607 Do not instantiate outside of the vfs layer.
624 608 """
625 609 def __init__(self, fh):
626 610 super(checkambigatclosing, self).__init__(fh)
627 611 object.__setattr__(self, r'_oldstat', util.filestat.frompath(fh.name))
628 612
629 613 def _checkambig(self):
630 614 oldstat = self._oldstat
631 615 if oldstat.stat:
632 616 newstat = util.filestat.frompath(self._origfh.name)
633 617 if newstat.isambig(oldstat):
634 618 # stat of changed file is ambiguous to original one
635 619 newstat.avoidambig(self._origfh.name, oldstat)
636 620
637 621 def __exit__(self, exc_type, exc_value, exc_tb):
638 622 self._origfh.__exit__(exc_type, exc_value, exc_tb)
639 623 self._checkambig()
640 624
641 625 def close(self):
642 626 self._origfh.close()
643 627 self._checkambig()
General Comments 0
You need to be logged in to leave comments. Login now