pathlib-next 0.2.1__tar.gz → 0.3.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/.gitignore +1 -0
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/PKG-INFO +1 -1
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/pyproject.toml +1 -1
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/src/example.py +5 -7
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/src/pathlib_next/mempath.py +5 -5
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/src/pathlib_next/path.py +20 -191
- pathlib_next-0.3.5/src/pathlib_next/protocols/__init__.py +2 -0
- pathlib_next-0.3.5/src/pathlib_next/protocols/fs.py +117 -0
- pathlib_next-0.3.5/src/pathlib_next/protocols/io.py +80 -0
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/src/pathlib_next/uri/schemes/http.py +7 -2
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/src/pathlib_next/utils/stat.py +2 -15
- pathlib_next-0.3.5/src/pathlib_next/utils/sync.py +258 -0
- pathlib_next-0.2.1/src/pathlib_next/utils/sync.py +0 -104
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/LICENSE +0 -0
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/README.md +0 -0
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/src/pathlib_next/__init__.py +0 -0
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/src/pathlib_next/fspath.py +0 -0
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/src/pathlib_next/uri/__init__.py +0 -0
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/src/pathlib_next/uri/query.py +0 -0
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/src/pathlib_next/uri/schemes/__init__.py +0 -0
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/src/pathlib_next/uri/schemes/file.py +0 -0
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/src/pathlib_next/uri/schemes/sftp.py +0 -0
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/src/pathlib_next/uri/source.py +0 -0
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/src/pathlib_next/utils/__init__.py +0 -0
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/src/pathlib_next/utils/glob.py +0 -0
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/tests/test_local.py +0 -0
- {pathlib_next-0.2.1 → pathlib_next-0.3.5}/tests/test_uri.py +0 -0
|
@@ -3,7 +3,7 @@ import os
|
|
|
3
3
|
from pathlib_next import Path, glob
|
|
4
4
|
from pathlib_next.mempath import MemPath
|
|
5
5
|
from pathlib_next.uri import Query, Source, Uri, UriPath
|
|
6
|
-
from pathlib_next.utils.sync import PathSyncer
|
|
6
|
+
from pathlib_next.utils.sync import PathAndStat, PathSyncer
|
|
7
7
|
|
|
8
8
|
rootless = Uri("sftp://root@sftpexample")
|
|
9
9
|
rootless.source
|
|
@@ -18,8 +18,6 @@ mempath.write_text("test")
|
|
|
18
18
|
check = mempath.read_text()
|
|
19
19
|
mempath.parent.rm(recursive=True)
|
|
20
20
|
|
|
21
|
-
test = list(os.scandir(local))
|
|
22
|
-
print(list(local.iterdir()))
|
|
23
21
|
query = Query({"test": "://$#!1", "test2&": [1, 2]})
|
|
24
22
|
q2 = Query(str(query)).to_dict()
|
|
25
23
|
for name, value in query:
|
|
@@ -53,17 +51,17 @@ authkeys = sftp_root / "root/.ssh/authorized_keys"
|
|
|
53
51
|
print(authkeys.as_posix())
|
|
54
52
|
|
|
55
53
|
|
|
56
|
-
def checksum(uri:
|
|
57
|
-
stat = uri.stat
|
|
54
|
+
def checksum(uri: PathAndStat):
|
|
55
|
+
stat = uri.stat
|
|
58
56
|
return hash(stat.st_size)
|
|
59
57
|
|
|
60
58
|
|
|
61
59
|
syncer = PathSyncer(checksum, remove_missing=False)
|
|
62
|
-
syncer.sync((sftp_root / "root/.ssh"), dest, dry_run=
|
|
60
|
+
syncer.sync((sftp_root / "root/.ssh"), dest, dry_run=False)
|
|
63
61
|
|
|
64
62
|
rocky_repo = UriPath("http://dl.rockylinux.org/pub")
|
|
65
63
|
|
|
66
|
-
glob_test = UriPath("file
|
|
64
|
+
glob_test = UriPath("file:src/**/*.py")
|
|
67
65
|
|
|
68
66
|
for path in glob.glob(glob_test, recursive=True):
|
|
69
67
|
print(path)
|
|
@@ -4,16 +4,16 @@ from io import IOBase
|
|
|
4
4
|
from urllib.parse import quote as _urlquote
|
|
5
5
|
|
|
6
6
|
from .path import Path, Pathname
|
|
7
|
-
from .utils.stat import FileStat
|
|
7
|
+
from .utils.stat import FileStat
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class MemPathBackend(dict): ...
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class MemBytesIO(io.BytesIO):
|
|
14
|
-
def __init__(self,
|
|
15
|
-
self._bytes =
|
|
16
|
-
super().__init__(
|
|
14
|
+
def __init__(self, dest: bytearray) -> None:
|
|
15
|
+
self._bytes = dest
|
|
16
|
+
super().__init__()
|
|
17
17
|
|
|
18
18
|
def close(self) -> None:
|
|
19
19
|
self.seek(0)
|
|
@@ -137,7 +137,7 @@ class MemPath(Path):
|
|
|
137
137
|
raise IsADirectoryError(self)
|
|
138
138
|
parent.pop(name)
|
|
139
139
|
|
|
140
|
-
def stat(self, *, follow_symlinks=True)
|
|
140
|
+
def stat(self, *, follow_symlinks=True):
|
|
141
141
|
parent, name = self._parent_container()
|
|
142
142
|
if not name:
|
|
143
143
|
return FileStat(is_dir=True)
|
|
@@ -6,17 +6,14 @@ operating systems.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import abc as _abc
|
|
9
|
-
import io as _io
|
|
10
9
|
import os as _os
|
|
11
10
|
import re as _re
|
|
12
|
-
import shutil as _shutil
|
|
13
|
-
import stat as _stat
|
|
14
11
|
import typing as _ty
|
|
15
|
-
from pathlib import _ignore_error
|
|
16
12
|
|
|
17
13
|
from . import utils as _utils
|
|
14
|
+
from .protocols import BinaryOpen, Chmod, Stat
|
|
18
15
|
from .utils import glob as _glob
|
|
19
|
-
from .utils.stat import
|
|
16
|
+
from .utils.stat import FileStat
|
|
20
17
|
|
|
21
18
|
P = _ty.TypeVar("P", bound="Path")
|
|
22
19
|
PN = _ty.TypeVar("PN", bound="Pathname")
|
|
@@ -128,7 +125,7 @@ class Pathname(FsPathLike, _ty.Generic[_P]):
|
|
|
128
125
|
|
|
129
126
|
@property
|
|
130
127
|
@_abc.abstractmethod
|
|
131
|
-
def segments(self) -> _ty.
|
|
128
|
+
def segments(self) -> _ty.Sequence[str]: ...
|
|
132
129
|
|
|
133
130
|
@property
|
|
134
131
|
@_abc.abstractmethod
|
|
@@ -140,8 +137,7 @@ class Pathname(FsPathLike, _ty.Generic[_P]):
|
|
|
140
137
|
def with_name(self, name: str) -> _ty.Self:
|
|
141
138
|
if not self.name:
|
|
142
139
|
raise ValueError("%r has an empty name" % (self,))
|
|
143
|
-
|
|
144
|
-
return self.with_segments(*segments)
|
|
140
|
+
return self.with_segments(*self.segments[:-1], name)
|
|
145
141
|
|
|
146
142
|
def with_stem(self, stem: str) -> _ty.Self:
|
|
147
143
|
"""Return a new path with the stem changed."""
|
|
@@ -191,8 +187,7 @@ class Pathname(FsPathLike, _ty.Generic[_P]):
|
|
|
191
187
|
|
|
192
188
|
@_utils.notimplemented
|
|
193
189
|
def is_absolute(self) -> bool:
|
|
194
|
-
"""True if the path is absolute
|
|
195
|
-
a drive)."""
|
|
190
|
+
"""True if the path is absolute"""
|
|
196
191
|
...
|
|
197
192
|
|
|
198
193
|
def match(self, path_pattern: str | _re.Pattern, *, case_sensitive=None):
|
|
@@ -204,7 +199,7 @@ class Pathname(FsPathLike, _ty.Generic[_P]):
|
|
|
204
199
|
path = str(self)
|
|
205
200
|
if not isinstance(path_pattern, _re.Pattern):
|
|
206
201
|
if isinstance(str, path_pattern):
|
|
207
|
-
path_pattern = type(self)(path_pattern)
|
|
202
|
+
path_pattern = type(self)(path_pattern)
|
|
208
203
|
path_pattern = _glob.compile_pattern(path_pattern, case_sensitive)
|
|
209
204
|
return path_pattern.match(path) is not None
|
|
210
205
|
|
|
@@ -221,7 +216,7 @@ class Pathname(FsPathLike, _ty.Generic[_P]):
|
|
|
221
216
|
PurePathLike = str | Pathname
|
|
222
217
|
|
|
223
218
|
|
|
224
|
-
class Path(Pathname):
|
|
219
|
+
class Path(Pathname, Chmod, Stat, BinaryOpen):
|
|
225
220
|
"""Base class for manipulating paths with I/O."""
|
|
226
221
|
|
|
227
222
|
__slots__ = ()
|
|
@@ -233,91 +228,9 @@ class Path(Pathname):
|
|
|
233
228
|
cls = LocalPath
|
|
234
229
|
return Pathname.__new__(cls)
|
|
235
230
|
|
|
236
|
-
@_utils.notimplemented
|
|
237
|
-
def stat(self, *, follow_symlinks=True) -> FileStatLike: ...
|
|
238
|
-
|
|
239
|
-
def lstat(self) -> FileStatLike:
|
|
240
|
-
"""
|
|
241
|
-
Like stat(), except if the path points to a symlink, the symlink's
|
|
242
|
-
status information is returned, rather than its target's.
|
|
243
|
-
"""
|
|
244
|
-
return self.stat(follow_symlinks=False)
|
|
245
|
-
|
|
246
|
-
def _st_mode(self, *, follow_symlinks=True):
|
|
247
|
-
try:
|
|
248
|
-
return self.stat().st_mode
|
|
249
|
-
except FileNotFoundError:
|
|
250
|
-
return None
|
|
251
|
-
except OSError as e:
|
|
252
|
-
if not _ignore_error(e):
|
|
253
|
-
raise
|
|
254
|
-
return None
|
|
255
|
-
except ValueError:
|
|
256
|
-
return None
|
|
257
|
-
|
|
258
|
-
# Convenience functions for querying the stat results
|
|
259
|
-
def exists(self, *, follow_symlinks=True):
|
|
260
|
-
"""
|
|
261
|
-
Whether this path exists.
|
|
262
|
-
"""
|
|
263
|
-
return self._st_mode(follow_symlinks=follow_symlinks) != None
|
|
264
|
-
|
|
265
231
|
def is_hidden(self):
|
|
266
232
|
return self.name.startswith(".")
|
|
267
233
|
|
|
268
|
-
def is_dir(self):
|
|
269
|
-
"""
|
|
270
|
-
Whether this path is a directory.
|
|
271
|
-
"""
|
|
272
|
-
try:
|
|
273
|
-
return _stat.S_ISDIR(self._st_mode() or 0)
|
|
274
|
-
except NotImplementedError:
|
|
275
|
-
pass
|
|
276
|
-
try:
|
|
277
|
-
next(self.iterdir(), None)
|
|
278
|
-
return True
|
|
279
|
-
except NotADirectoryError:
|
|
280
|
-
return False
|
|
281
|
-
|
|
282
|
-
raise NotImplementedError("is_dir")
|
|
283
|
-
|
|
284
|
-
def is_file(self):
|
|
285
|
-
"""
|
|
286
|
-
Whether this path is a regular file (also True for symlinks pointing
|
|
287
|
-
to regular files).
|
|
288
|
-
"""
|
|
289
|
-
return _stat.S_ISREG(self._st_mode() or 0)
|
|
290
|
-
|
|
291
|
-
def is_symlink(self):
|
|
292
|
-
"""
|
|
293
|
-
Whether this path is a symbolic link.
|
|
294
|
-
"""
|
|
295
|
-
return _stat.S_ISLNK(self._st_mode(follow_symlinks=True) or 0)
|
|
296
|
-
|
|
297
|
-
def is_block_device(self):
|
|
298
|
-
"""
|
|
299
|
-
Whether this path is a block device.
|
|
300
|
-
"""
|
|
301
|
-
return _stat.S_ISBLK(self._st_mode() or 0)
|
|
302
|
-
|
|
303
|
-
def is_char_device(self):
|
|
304
|
-
"""
|
|
305
|
-
Whether this path is a character device.
|
|
306
|
-
"""
|
|
307
|
-
return _stat.S_ISCHR(self._st_mode() or 0)
|
|
308
|
-
|
|
309
|
-
def is_fifo(self):
|
|
310
|
-
"""
|
|
311
|
-
Whether this path is a FIFO.
|
|
312
|
-
"""
|
|
313
|
-
return _stat.S_ISFIFO(self._st_mode() or 0)
|
|
314
|
-
|
|
315
|
-
def is_socket(self):
|
|
316
|
-
"""
|
|
317
|
-
Whether this path is a socket.
|
|
318
|
-
"""
|
|
319
|
-
return _stat.S_ISSOCK(self._st_mode() or 0)
|
|
320
|
-
|
|
321
234
|
@_utils.notimplemented
|
|
322
235
|
def samefile(self, other_path: str | _ty.Self):
|
|
323
236
|
"""Return whether other_path is the same or not as this file
|
|
@@ -325,72 +238,6 @@ class Path(Pathname):
|
|
|
325
238
|
"""
|
|
326
239
|
...
|
|
327
240
|
|
|
328
|
-
@_utils.notimplemented
|
|
329
|
-
def _open(
|
|
330
|
-
self,
|
|
331
|
-
mode="r",
|
|
332
|
-
buffering=-1,
|
|
333
|
-
) -> _io.IOBase:
|
|
334
|
-
"""
|
|
335
|
-
All operations should be binary
|
|
336
|
-
To be used only by UriPath.open() to obtain binary stream
|
|
337
|
-
"""
|
|
338
|
-
...
|
|
339
|
-
|
|
340
|
-
def open(
|
|
341
|
-
self,
|
|
342
|
-
mode="r",
|
|
343
|
-
buffering=-1,
|
|
344
|
-
encoding: str = None,
|
|
345
|
-
errors: str = None,
|
|
346
|
-
newline: str = None,
|
|
347
|
-
) -> _io.IOBase:
|
|
348
|
-
"""
|
|
349
|
-
Open the file pointed by this path and return a file object, as
|
|
350
|
-
the built-in open() function does.
|
|
351
|
-
"""
|
|
352
|
-
fh = self._open(mode.replace("b", ""), buffering)
|
|
353
|
-
if "b" not in mode:
|
|
354
|
-
encoding = _io.text_encoding(encoding)
|
|
355
|
-
fh = _io.TextIOWrapper(fh, encoding, errors, newline)
|
|
356
|
-
return fh
|
|
357
|
-
|
|
358
|
-
def read_bytes(self) -> bytes:
|
|
359
|
-
"""
|
|
360
|
-
Open the file in bytes mode, read it, and close the file.
|
|
361
|
-
"""
|
|
362
|
-
with self.open(mode="rb") as f:
|
|
363
|
-
return f.read()
|
|
364
|
-
|
|
365
|
-
def read_text(self, encoding: str = None, errors: str = None) -> str:
|
|
366
|
-
"""
|
|
367
|
-
Open the file in text mode, read it, and close the file.
|
|
368
|
-
"""
|
|
369
|
-
with self.open(mode="r", encoding=encoding, errors=errors) as f:
|
|
370
|
-
return f.read()
|
|
371
|
-
|
|
372
|
-
def write_bytes(self, data: bytes):
|
|
373
|
-
"""
|
|
374
|
-
Open the file in bytes mode, write to it, and close the file.
|
|
375
|
-
"""
|
|
376
|
-
# type-check for the buffer interface before truncating the file
|
|
377
|
-
view = memoryview(data)
|
|
378
|
-
with self.open(mode="wb") as f:
|
|
379
|
-
return f.write(view)
|
|
380
|
-
|
|
381
|
-
def write_text(
|
|
382
|
-
self, data: str, encoding: str = None, errors: str = None, newline: str = None
|
|
383
|
-
):
|
|
384
|
-
"""
|
|
385
|
-
Open the file in text mode, write to it, and close the file.
|
|
386
|
-
"""
|
|
387
|
-
if not isinstance(data, str):
|
|
388
|
-
raise TypeError("data must be str, not %s" % data.__class__.__name__)
|
|
389
|
-
with self.open(
|
|
390
|
-
mode="w", encoding=encoding, errors=errors, newline=newline
|
|
391
|
-
) as f:
|
|
392
|
-
return f.write(data)
|
|
393
|
-
|
|
394
241
|
def __iter__(self):
|
|
395
242
|
return self.iterdir()
|
|
396
243
|
|
|
@@ -448,13 +295,13 @@ class Path(Pathname):
|
|
|
448
295
|
filenames: "list[str]" = []
|
|
449
296
|
for entry in scandir_it:
|
|
450
297
|
try:
|
|
451
|
-
|
|
452
|
-
is_dir =
|
|
298
|
+
stat = FileStat.from_path(entry, follow_symlink=follow_symlinks)
|
|
299
|
+
is_dir = stat.is_dir()
|
|
453
300
|
except OSError:
|
|
454
301
|
# Carried over from os.path.isdir().
|
|
455
302
|
is_dir = False
|
|
456
303
|
|
|
457
|
-
if is_dir
|
|
304
|
+
if is_dir:
|
|
458
305
|
dirnames.append(entry.name)
|
|
459
306
|
else:
|
|
460
307
|
filenames.append(entry.name)
|
|
@@ -494,28 +341,12 @@ class Path(Pathname):
|
|
|
494
341
|
except FileNotFoundError:
|
|
495
342
|
if not parents or self.parent == self:
|
|
496
343
|
raise
|
|
497
|
-
self.parent.mkdir(parents=True, exist_ok=
|
|
498
|
-
self.mkdir(mode, parents=False, exist_ok=
|
|
499
|
-
except
|
|
500
|
-
# Cannot rely on checking for EEXIST, since the operating system
|
|
501
|
-
# could give priority to other errors like EACCES or EROFS
|
|
344
|
+
self.parent.mkdir(parents=True, exist_ok=False)
|
|
345
|
+
self.mkdir(mode, parents=False, exist_ok=False)
|
|
346
|
+
except FileExistsError:
|
|
502
347
|
if not exist_ok or not self.is_dir():
|
|
503
348
|
raise
|
|
504
349
|
|
|
505
|
-
@_utils.notimplemented
|
|
506
|
-
def chmod(self, mode: int, *, follow_symlinks=True):
|
|
507
|
-
"""
|
|
508
|
-
Change the permissions of the path, like os.chmod().
|
|
509
|
-
"""
|
|
510
|
-
...
|
|
511
|
-
|
|
512
|
-
def lchmod(self, mode: int):
|
|
513
|
-
"""
|
|
514
|
-
Like chmod(), except if the path points to a symlink, the symlink's
|
|
515
|
-
permissions are changed, rather than its target's.
|
|
516
|
-
"""
|
|
517
|
-
self.chmod(mode, follow_symlinks=False)
|
|
518
|
-
|
|
519
350
|
@_utils.notimplemented
|
|
520
351
|
def unlink(self, missing_ok=False):
|
|
521
352
|
"""
|
|
@@ -540,11 +371,11 @@ class Path(Pathname):
|
|
|
540
371
|
ignore_error if not callable(ignore_error) else ignore_error
|
|
541
372
|
)
|
|
542
373
|
try:
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
374
|
+
stat = FileStat.from_path(self)
|
|
375
|
+
if stat is None:
|
|
376
|
+
if not missing_ok:
|
|
377
|
+
raise FileNotFoundError(self)
|
|
378
|
+
elif stat.is_dir():
|
|
548
379
|
if recursive:
|
|
549
380
|
for child in self.iterdir():
|
|
550
381
|
child.rm(recursive=recursive, ignore_error=ignore_error)
|
|
@@ -570,9 +401,7 @@ class Path(Pathname):
|
|
|
570
401
|
target.unlink()
|
|
571
402
|
else:
|
|
572
403
|
raise FileExistsError(target)
|
|
573
|
-
|
|
574
|
-
with target.open("wb") as output, src.open("rb") as input:
|
|
575
|
-
_shutil.copyfileobj(input, output)
|
|
404
|
+
BinaryOpen.copy(src, target)
|
|
576
405
|
|
|
577
406
|
try:
|
|
578
407
|
stat = src.stat()
|
|
@@ -598,7 +427,7 @@ class Path(Pathname):
|
|
|
598
427
|
except NotImplementedError:
|
|
599
428
|
pass
|
|
600
429
|
|
|
601
|
-
|
|
430
|
+
BinaryOpen.copy(src, target)
|
|
602
431
|
src.unlink()
|
|
603
432
|
|
|
604
433
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import abc as _abc
|
|
2
|
+
import stat as _stat
|
|
3
|
+
import typing as _ty
|
|
4
|
+
|
|
5
|
+
from .. import utils as _utils
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FileStatLike(_ty.Protocol):
|
|
9
|
+
"""Minimum properties stat like object shouuld provide"""
|
|
10
|
+
|
|
11
|
+
__slots__ = ()
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
@_abc.abstractmethod
|
|
15
|
+
def st_mode(self) -> int: ...
|
|
16
|
+
@property
|
|
17
|
+
@_abc.abstractmethod
|
|
18
|
+
def st_size(self) -> int: ...
|
|
19
|
+
@property
|
|
20
|
+
@_abc.abstractmethod
|
|
21
|
+
def st_mtime(self) -> int: ...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Stat(_ty.Protocol):
|
|
25
|
+
"""Any object that can implement Stat and utilities functions based on it"""
|
|
26
|
+
|
|
27
|
+
__slots__ = ()
|
|
28
|
+
|
|
29
|
+
@_utils.notimplemented
|
|
30
|
+
def stat(self, *, follow_symlinks=True) -> FileStatLike: ...
|
|
31
|
+
|
|
32
|
+
def lstat(self) -> FileStatLike:
|
|
33
|
+
"""
|
|
34
|
+
Like stat(), except if the path points to a symlink, the symlink's
|
|
35
|
+
status information is returned, rather than its target's.
|
|
36
|
+
"""
|
|
37
|
+
return self.stat(follow_symlinks=False)
|
|
38
|
+
|
|
39
|
+
def _st_mode(self, *, follow_symlinks=True):
|
|
40
|
+
"""
|
|
41
|
+
Utility function only for internal use if this object,
|
|
42
|
+
not required nor to be expected in any implementations of the protocol
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
return self.stat().st_mode
|
|
46
|
+
except FileNotFoundError:
|
|
47
|
+
return None
|
|
48
|
+
except ValueError:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
# Convenience functions for querying the stat results
|
|
52
|
+
def exists(self, *, follow_symlinks=True):
|
|
53
|
+
"""
|
|
54
|
+
Whether this path exists.
|
|
55
|
+
"""
|
|
56
|
+
return self._st_mode(follow_symlinks=follow_symlinks) != None
|
|
57
|
+
|
|
58
|
+
def is_dir(self):
|
|
59
|
+
"""
|
|
60
|
+
Whether this path is a directory.
|
|
61
|
+
"""
|
|
62
|
+
return _stat.S_ISDIR(self._st_mode() or 0)
|
|
63
|
+
|
|
64
|
+
def is_file(self):
|
|
65
|
+
"""
|
|
66
|
+
Whether this path is a regular file (also True for symlinks pointing
|
|
67
|
+
to regular files).
|
|
68
|
+
"""
|
|
69
|
+
return _stat.S_ISREG(self._st_mode() or 0)
|
|
70
|
+
|
|
71
|
+
def is_symlink(self):
|
|
72
|
+
"""
|
|
73
|
+
Whether this path is a symbolic link.
|
|
74
|
+
"""
|
|
75
|
+
return _stat.S_ISLNK(self._st_mode(follow_symlinks=False) or 0)
|
|
76
|
+
|
|
77
|
+
def is_block_device(self):
|
|
78
|
+
"""
|
|
79
|
+
Whether this path is a block device.
|
|
80
|
+
"""
|
|
81
|
+
return _stat.S_ISBLK(self._st_mode() or 0)
|
|
82
|
+
|
|
83
|
+
def is_char_device(self):
|
|
84
|
+
"""
|
|
85
|
+
Whether this path is a character device.
|
|
86
|
+
"""
|
|
87
|
+
return _stat.S_ISCHR(self._st_mode() or 0)
|
|
88
|
+
|
|
89
|
+
def is_fifo(self):
|
|
90
|
+
"""
|
|
91
|
+
Whether this path is a FIFO.
|
|
92
|
+
"""
|
|
93
|
+
return _stat.S_ISFIFO(self._st_mode() or 0)
|
|
94
|
+
|
|
95
|
+
def is_socket(self):
|
|
96
|
+
"""
|
|
97
|
+
Whether this path is a socket.
|
|
98
|
+
"""
|
|
99
|
+
return _stat.S_ISSOCK(self._st_mode() or 0)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Chmod(_ty.Protocol):
|
|
103
|
+
__slots__ = ()
|
|
104
|
+
|
|
105
|
+
@_utils.notimplemented
|
|
106
|
+
def chmod(self, mode: int, *, follow_symlinks=True):
|
|
107
|
+
"""
|
|
108
|
+
Change the permissions of the path, like os.chmod().
|
|
109
|
+
"""
|
|
110
|
+
...
|
|
111
|
+
|
|
112
|
+
def lchmod(self, mode: int):
|
|
113
|
+
"""
|
|
114
|
+
Like chmod(), except if the path points to a symlink, the symlink's
|
|
115
|
+
permissions are changed, rather than its target's.
|
|
116
|
+
"""
|
|
117
|
+
self.chmod(mode, follow_symlinks=False)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import io as _io
|
|
2
|
+
import shutil as _shutil
|
|
3
|
+
import typing as _ty
|
|
4
|
+
|
|
5
|
+
from .. import utils as _utils
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BinaryOpen(_ty.Protocol):
|
|
9
|
+
"""Protocol for objects that support open->io.IoBase"""
|
|
10
|
+
|
|
11
|
+
__slots__ = ()
|
|
12
|
+
|
|
13
|
+
@_utils.notimplemented
|
|
14
|
+
def _open(
|
|
15
|
+
self,
|
|
16
|
+
mode="r",
|
|
17
|
+
buffering=-1,
|
|
18
|
+
) -> _io.IOBase:
|
|
19
|
+
"""
|
|
20
|
+
All operations should be binary
|
|
21
|
+
To be used only by open() to obtain binary stream to provide implementations for all methods
|
|
22
|
+
"""
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
def open(
|
|
26
|
+
self,
|
|
27
|
+
mode="r",
|
|
28
|
+
buffering=-1,
|
|
29
|
+
encoding: str = None,
|
|
30
|
+
errors: str = None,
|
|
31
|
+
newline: str = None,
|
|
32
|
+
) -> _io.IOBase:
|
|
33
|
+
"""
|
|
34
|
+
Open the a handle to an object that implement io.IOBase
|
|
35
|
+
"""
|
|
36
|
+
fh = self._open(mode.replace("b", ""), buffering)
|
|
37
|
+
if "b" not in mode:
|
|
38
|
+
encoding = _io.text_encoding(encoding)
|
|
39
|
+
fh = _io.TextIOWrapper(fh, encoding, errors, newline)
|
|
40
|
+
return fh
|
|
41
|
+
|
|
42
|
+
def read_bytes(self) -> bytes:
|
|
43
|
+
"""
|
|
44
|
+
Open in bytes mode, read it, and close the file.
|
|
45
|
+
"""
|
|
46
|
+
with self.open(mode="rb") as f:
|
|
47
|
+
return f.read()
|
|
48
|
+
|
|
49
|
+
def read_text(self, encoding: str = None, errors: str = None) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Open in text mode, read it, and close the file.
|
|
52
|
+
"""
|
|
53
|
+
with self.open(mode="r", encoding=encoding, errors=errors) as f:
|
|
54
|
+
return f.read()
|
|
55
|
+
|
|
56
|
+
def write_bytes(self, data: bytes):
|
|
57
|
+
"""
|
|
58
|
+
Open in bytes mode, write to it, and close the file.
|
|
59
|
+
"""
|
|
60
|
+
# type-check for the buffer interface before truncating the file
|
|
61
|
+
view = memoryview(data)
|
|
62
|
+
with self.open(mode="wb") as f:
|
|
63
|
+
return f.write(view)
|
|
64
|
+
|
|
65
|
+
def write_text(
|
|
66
|
+
self, data: str, encoding: str = None, errors: str = None, newline: str = None
|
|
67
|
+
):
|
|
68
|
+
"""
|
|
69
|
+
Open in text mode, write to it, and close the file.
|
|
70
|
+
"""
|
|
71
|
+
if not isinstance(data, str):
|
|
72
|
+
raise TypeError("data must be str, not %s" % data.__class__.__name__)
|
|
73
|
+
with self.open(
|
|
74
|
+
mode="w", encoding=encoding, errors=errors, newline=newline
|
|
75
|
+
) as f:
|
|
76
|
+
return f.write(data)
|
|
77
|
+
|
|
78
|
+
def copy(self, target: "BinaryOpen"):
|
|
79
|
+
with target.open("wb") as output, self.open("rb") as input:
|
|
80
|
+
_shutil.copyfileobj(input, output)
|
|
@@ -6,6 +6,9 @@ import bs4 as _bs4
|
|
|
6
6
|
import requests as _req
|
|
7
7
|
from htmllistparse import parse as _htmlparse
|
|
8
8
|
|
|
9
|
+
if _ty.TYPE_CHECKING:
|
|
10
|
+
from urllib3.response import HTTPResponse
|
|
11
|
+
|
|
9
12
|
from ... import utils as _utils
|
|
10
13
|
from ...utils.stat import FileStat
|
|
11
14
|
from .. import UriPath
|
|
@@ -119,10 +122,12 @@ class HttpPath(UriPath):
|
|
|
119
122
|
raise NotImplementedError(mode)
|
|
120
123
|
buffer_size = _io.DEFAULT_BUFFER_SIZE if buffering < 0 else buffering
|
|
121
124
|
req = self.backend.request("GET", self.as_uri(), stream=True)
|
|
125
|
+
resp: "HTTPResponse" = req.raw
|
|
126
|
+
resp.auto_close = False
|
|
122
127
|
return (
|
|
123
|
-
|
|
128
|
+
resp
|
|
124
129
|
if buffer_size == 0
|
|
125
|
-
else _io.BufferedReader(
|
|
130
|
+
else _io.BufferedReader(resp, buffer_size=buffer_size)
|
|
126
131
|
)
|
|
127
132
|
|
|
128
133
|
def is_dir(self):
|
|
@@ -3,6 +3,7 @@ import stat as _stat
|
|
|
3
3
|
import typing as _ty
|
|
4
4
|
|
|
5
5
|
from .. import utils as _utils
|
|
6
|
+
from ..protocols import FileStatLike as _FStat
|
|
6
7
|
|
|
7
8
|
if _ty.TYPE_CHECKING:
|
|
8
9
|
from os import stat_result as _os_stat
|
|
@@ -10,21 +11,7 @@ if _ty.TYPE_CHECKING:
|
|
|
10
11
|
from ..path import Path
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
class
|
|
14
|
-
__slots__ = ()
|
|
15
|
-
|
|
16
|
-
@property
|
|
17
|
-
@_abc.abstractmethod
|
|
18
|
-
def st_mode(self) -> int: ...
|
|
19
|
-
@property
|
|
20
|
-
@_abc.abstractmethod
|
|
21
|
-
def st_size(self) -> int: ...
|
|
22
|
-
@property
|
|
23
|
-
@_abc.abstractmethod
|
|
24
|
-
def st_mtime(self) -> int: ...
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class FileStat(FileStatLike):
|
|
14
|
+
class FileStat(_FStat):
|
|
28
15
|
__slots__ = (
|
|
29
16
|
"st_mode",
|
|
30
17
|
"st_nlink",
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import enum as _enum
|
|
2
|
+
import typing as _ty
|
|
3
|
+
|
|
4
|
+
from ..path import Path
|
|
5
|
+
from ..utils.stat import FileStat
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SyncEvent(_enum.Enum):
|
|
9
|
+
Copy = 1
|
|
10
|
+
RemovedMissing = 2
|
|
11
|
+
TypeMismatch = 6
|
|
12
|
+
SyncStart = 5
|
|
13
|
+
Synced = 3
|
|
14
|
+
CreatedDirectory = 4
|
|
15
|
+
CheckTargetChild = _enum.auto()
|
|
16
|
+
CheckTargetChildren = _enum.auto()
|
|
17
|
+
SyncChild = _enum.auto()
|
|
18
|
+
SyncChildren = _enum.auto()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PathAndStat(object):
|
|
22
|
+
__slots__ = ("_path", "_stat")
|
|
23
|
+
|
|
24
|
+
def __init__(self, path: Path, *, follow_symlink=None) -> None:
|
|
25
|
+
self._path = path
|
|
26
|
+
self.refresh(follow_symlink)
|
|
27
|
+
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
return str(self.path)
|
|
30
|
+
|
|
31
|
+
def __repr__(self) -> str:
|
|
32
|
+
return str((self.path, self._stat))
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def path(self):
|
|
36
|
+
return self._path
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def stat(self):
|
|
40
|
+
return self._stat
|
|
41
|
+
|
|
42
|
+
def exists(self):
|
|
43
|
+
return self.stat != None
|
|
44
|
+
|
|
45
|
+
def refresh(self, follow_symlink: bool):
|
|
46
|
+
self._stat = FileStat.from_path(self.path, follow_symlink=follow_symlink)
|
|
47
|
+
|
|
48
|
+
def __getattr__(self, name: str):
|
|
49
|
+
if name.startswith("is_"):
|
|
50
|
+
if self.stat:
|
|
51
|
+
return getattr(self.stat, name)
|
|
52
|
+
else:
|
|
53
|
+
return lambda *args, **kwargs: False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if _ty.TYPE_CHECKING:
|
|
57
|
+
|
|
58
|
+
class PathAndStat(PathAndStat, FileStat): ...
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _OnPathSyncerError(_ty.Protocol):
|
|
62
|
+
def __call__(
|
|
63
|
+
self,
|
|
64
|
+
error: Exception,
|
|
65
|
+
source: PathAndStat,
|
|
66
|
+
target: PathAndStat,
|
|
67
|
+
event: SyncEvent,
|
|
68
|
+
) -> bool: ...
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class PathSyncer(object):
|
|
72
|
+
__slots__ = (
|
|
73
|
+
"checksum",
|
|
74
|
+
"_hook",
|
|
75
|
+
"remove_missing",
|
|
76
|
+
"follow_symlinks",
|
|
77
|
+
"ignore_error",
|
|
78
|
+
)
|
|
79
|
+
EVENT_LOG_FORMAT = "[{event}] Source:{source} Target:{target} DryRun:{dry_run}"
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
checksum: _ty.Callable[[PathAndStat], int],
|
|
84
|
+
/,
|
|
85
|
+
remove_missing: bool = False,
|
|
86
|
+
follow_symlinks: bool = True,
|
|
87
|
+
hook: _ty.Callable[[PathAndStat, PathAndStat, SyncEvent, bool], None] = None,
|
|
88
|
+
ignore_error: _OnPathSyncerError | bool = False,
|
|
89
|
+
) -> None:
|
|
90
|
+
self.checksum = checksum
|
|
91
|
+
self.remove_missing = remove_missing
|
|
92
|
+
self._hook = hook
|
|
93
|
+
self.follow_symlinks = follow_symlinks
|
|
94
|
+
if not callable(ignore_error):
|
|
95
|
+
_ignore_error = lambda *args, **kwargs: bool(ignore_error)
|
|
96
|
+
else:
|
|
97
|
+
_ignore_error = ignore_error
|
|
98
|
+
self.ignore_error = _ty.cast(_OnPathSyncerError, _ignore_error)
|
|
99
|
+
|
|
100
|
+
def log(self, msg: str, **kwargs: str):
|
|
101
|
+
print(msg.format_map(kwargs))
|
|
102
|
+
|
|
103
|
+
def hook(
|
|
104
|
+
self,
|
|
105
|
+
source: PathAndStat,
|
|
106
|
+
target: PathAndStat,
|
|
107
|
+
event: SyncEvent,
|
|
108
|
+
dry_run: bool,
|
|
109
|
+
do: _ty.Callable[[], None] = None,
|
|
110
|
+
):
|
|
111
|
+
if not dry_run and do:
|
|
112
|
+
try:
|
|
113
|
+
do()
|
|
114
|
+
except Exception as e:
|
|
115
|
+
if self.ignore_error(e, source, target, event):
|
|
116
|
+
return e
|
|
117
|
+
raise
|
|
118
|
+
if self._hook:
|
|
119
|
+
self._hook(source, target, event, dry_run)
|
|
120
|
+
self.log(
|
|
121
|
+
self.EVENT_LOG_FORMAT,
|
|
122
|
+
event=event,
|
|
123
|
+
source=source,
|
|
124
|
+
target=target,
|
|
125
|
+
dry_run=dry_run,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def sync(
|
|
129
|
+
self,
|
|
130
|
+
source: Path | PathAndStat,
|
|
131
|
+
target: Path | PathAndStat,
|
|
132
|
+
/,
|
|
133
|
+
dry_run: bool = False,
|
|
134
|
+
ignore_error: (
|
|
135
|
+
bool | _ty.Callable[[Exception, PathAndStat, PathAndStat], None]
|
|
136
|
+
) = False,
|
|
137
|
+
):
|
|
138
|
+
checksum = self.checksum
|
|
139
|
+
|
|
140
|
+
def start():
|
|
141
|
+
nonlocal source, target
|
|
142
|
+
source = (
|
|
143
|
+
PathAndStat(source, follow_symlink=self.follow_symlinks)
|
|
144
|
+
if not isinstance(source, PathAndStat)
|
|
145
|
+
else source
|
|
146
|
+
)
|
|
147
|
+
target = (
|
|
148
|
+
PathAndStat(target, follow_symlink=self.follow_symlinks)
|
|
149
|
+
if not isinstance(target, PathAndStat)
|
|
150
|
+
else target
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if self.hook(source, target, SyncEvent.SyncStart, False, start):
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
if not source.exists():
|
|
157
|
+
if self.remove_missing:
|
|
158
|
+
if self.hook(
|
|
159
|
+
source,
|
|
160
|
+
target,
|
|
161
|
+
SyncEvent.RemovedMissing,
|
|
162
|
+
dry_run,
|
|
163
|
+
lambda: target.path.rm(recursive=True, missing_ok=True),
|
|
164
|
+
):
|
|
165
|
+
return
|
|
166
|
+
elif source.is_symlink():
|
|
167
|
+
error = NotImplementedError("symlink sync not implemented yet")
|
|
168
|
+
if not ignore_error(error, source, target, None):
|
|
169
|
+
raise error
|
|
170
|
+
return
|
|
171
|
+
elif source.is_file():
|
|
172
|
+
synced = False
|
|
173
|
+
if target.is_file():
|
|
174
|
+
if checksum(target) == checksum(source):
|
|
175
|
+
synced = True
|
|
176
|
+
if not synced:
|
|
177
|
+
|
|
178
|
+
def copy():
|
|
179
|
+
if target.is_file() or target.is_symlink():
|
|
180
|
+
target.path.unlink()
|
|
181
|
+
else:
|
|
182
|
+
if target.exists():
|
|
183
|
+
target.path.rm(recursive=target.is_dir())
|
|
184
|
+
source.path.copy(target.path)
|
|
185
|
+
|
|
186
|
+
if self.hook(source, target, SyncEvent.Copy, dry_run, copy):
|
|
187
|
+
return
|
|
188
|
+
else:
|
|
189
|
+
if target.is_file():
|
|
190
|
+
if self.hook(
|
|
191
|
+
source,
|
|
192
|
+
target,
|
|
193
|
+
SyncEvent.TypeMismatch,
|
|
194
|
+
dry_run,
|
|
195
|
+
lambda: target.path.unlink(),
|
|
196
|
+
):
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
target._stat = None
|
|
200
|
+
|
|
201
|
+
if not target.exists():
|
|
202
|
+
if self.hook(
|
|
203
|
+
source,
|
|
204
|
+
target,
|
|
205
|
+
SyncEvent.CreatedDirectory,
|
|
206
|
+
dry_run,
|
|
207
|
+
lambda: target.path.mkdir(),
|
|
208
|
+
):
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
if self.remove_missing:
|
|
212
|
+
|
|
213
|
+
def checkchildren():
|
|
214
|
+
for child in target.path.iterdir():
|
|
215
|
+
|
|
216
|
+
def checkchild():
|
|
217
|
+
if not (source.path / child.name).exists():
|
|
218
|
+
self.hook(
|
|
219
|
+
source,
|
|
220
|
+
target,
|
|
221
|
+
SyncEvent.RemovedMissing,
|
|
222
|
+
dry_run,
|
|
223
|
+
lambda: child.rm(recursive=True),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
self.hook(
|
|
227
|
+
source,
|
|
228
|
+
target,
|
|
229
|
+
SyncEvent.CheckTargetChild,
|
|
230
|
+
False,
|
|
231
|
+
checkchild,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
self.hook(
|
|
235
|
+
source,
|
|
236
|
+
target,
|
|
237
|
+
SyncEvent.CheckTargetChildren,
|
|
238
|
+
False,
|
|
239
|
+
checkchildren,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def sync_children():
|
|
243
|
+
for child in source.path.iterdir():
|
|
244
|
+
self.hook(
|
|
245
|
+
source,
|
|
246
|
+
target,
|
|
247
|
+
SyncEvent.SyncChild,
|
|
248
|
+
False,
|
|
249
|
+
lambda: self.sync(
|
|
250
|
+
child,
|
|
251
|
+
target.path / (child.name or child.parent.name),
|
|
252
|
+
dry_run,
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
self.hook(source, target, SyncEvent.SyncChildren, False, sync_children)
|
|
257
|
+
|
|
258
|
+
self.hook(source, target, SyncEvent.Synced, dry_run)
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import enum as _enum
|
|
2
|
-
import typing as _ty
|
|
3
|
-
|
|
4
|
-
from ..path import Path
|
|
5
|
-
from ..utils.stat import FileStat
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class SyncEvent(_enum.Enum):
|
|
9
|
-
Copy = 1
|
|
10
|
-
RemovedMissing = 2
|
|
11
|
-
SyncStart = 5
|
|
12
|
-
Synced = 3
|
|
13
|
-
CreatedDirectory = 4
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class PathSyncer(object):
|
|
17
|
-
__slots__ = ("checksum", "_hook", "remove_missing", "follow_symlinks")
|
|
18
|
-
EVENT_LOG_FORMAT = "[{event}] Source:{source} Target:{target} DryRun:{dry_run}"
|
|
19
|
-
|
|
20
|
-
def __init__(
|
|
21
|
-
self,
|
|
22
|
-
checksum: _ty.Callable[[Path], int],
|
|
23
|
-
/,
|
|
24
|
-
remove_missing: bool = False,
|
|
25
|
-
follow_symlinks: bool = True,
|
|
26
|
-
hook: _ty.Callable[[Path, Path, SyncEvent, bool], None] = None,
|
|
27
|
-
) -> None:
|
|
28
|
-
self.checksum = checksum
|
|
29
|
-
self.remove_missing = remove_missing
|
|
30
|
-
self._hook = hook
|
|
31
|
-
self.follow_symlinks = follow_symlinks
|
|
32
|
-
|
|
33
|
-
def log(self, msg: str, **kwargs: str):
|
|
34
|
-
print(msg.format_map(kwargs))
|
|
35
|
-
|
|
36
|
-
def hook(
|
|
37
|
-
self,
|
|
38
|
-
source: Path,
|
|
39
|
-
target: Path,
|
|
40
|
-
event: SyncEvent,
|
|
41
|
-
dry_run: bool,
|
|
42
|
-
):
|
|
43
|
-
if self._hook:
|
|
44
|
-
self._hook(source, target, event, dry_run)
|
|
45
|
-
self.log(
|
|
46
|
-
self.EVENT_LOG_FORMAT,
|
|
47
|
-
event=event,
|
|
48
|
-
source=source,
|
|
49
|
-
target=target,
|
|
50
|
-
dry_run=dry_run,
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
def sync(self, source: Path, target: Path, /, dry_run: bool = False):
|
|
54
|
-
checksum = self.checksum
|
|
55
|
-
self.hook(source, target, SyncEvent.SyncStart, dry_run)
|
|
56
|
-
|
|
57
|
-
src_stat = FileStat.from_path(source, follow_symlink=self.follow_symlinks)
|
|
58
|
-
tgt_stat = FileStat.from_path(target, follow_symlink=self.follow_symlinks)
|
|
59
|
-
|
|
60
|
-
if src_stat is None:
|
|
61
|
-
if self.remove_missing:
|
|
62
|
-
if not dry_run:
|
|
63
|
-
target.rm(recursive=True, missing_ok=True)
|
|
64
|
-
self.hook(source, target, SyncEvent.RemovedMissing, dry_run)
|
|
65
|
-
elif src_stat.is_symlink():
|
|
66
|
-
raise NotImplementedError("symlink sync not implemented yet")
|
|
67
|
-
elif src_stat.is_file():
|
|
68
|
-
synced = False
|
|
69
|
-
if tgt_stat and tgt_stat.is_file():
|
|
70
|
-
if checksum(target) == checksum(source):
|
|
71
|
-
synced = True
|
|
72
|
-
if not synced:
|
|
73
|
-
if not dry_run:
|
|
74
|
-
if tgt_stat:
|
|
75
|
-
if tgt_stat.is_file() or tgt_stat.is_symlink():
|
|
76
|
-
target.unlink()
|
|
77
|
-
else:
|
|
78
|
-
if target:
|
|
79
|
-
target.rm(recursive=tgt_stat.is_dir())
|
|
80
|
-
source.copy(target)
|
|
81
|
-
self.hook(source, target, SyncEvent.Copy, dry_run)
|
|
82
|
-
else:
|
|
83
|
-
t_exists = tgt_stat != None
|
|
84
|
-
if tgt_stat and tgt_stat.is_file():
|
|
85
|
-
if not dry_run:
|
|
86
|
-
target.unlink()
|
|
87
|
-
t_exists = False
|
|
88
|
-
|
|
89
|
-
if not t_exists:
|
|
90
|
-
if not dry_run:
|
|
91
|
-
target.mkdir()
|
|
92
|
-
self.hook(source, target, SyncEvent.CreatedDirectory, dry_run)
|
|
93
|
-
|
|
94
|
-
if self.remove_missing:
|
|
95
|
-
for child in target.iterdir():
|
|
96
|
-
if not (source / child.name).exists():
|
|
97
|
-
if not dry_run:
|
|
98
|
-
child.rm(recursive=True)
|
|
99
|
-
self.hook(source, target, SyncEvent.RemovedMissing, dry_run)
|
|
100
|
-
|
|
101
|
-
for child in source.iterdir():
|
|
102
|
-
self.sync(child, target / child.name, dry_run)
|
|
103
|
-
|
|
104
|
-
self.hook(source, target, SyncEvent.Synced, dry_run)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|