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