python3-cyberfusion-sync-support 1.2.7.3.1__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.
@@ -0,0 +1,42 @@
1
+ Metadata-Version: 2.1
2
+ Name: python3-cyberfusion-sync-support
3
+ Version: 1.2.7.3.1
4
+ Summary: Library for syncing objects (e.g. directories).
5
+ Home-page: https://github.com/CyberfusionIO/python3-cyberfusion-sync-support
6
+ Author: Cyberfusion
7
+ Author-email: support@cyberfusion.io
8
+ Platform: linux
9
+ Description-Content-Type: text/markdown
10
+
11
+ # python3-cyberfusion-sync-support
12
+
13
+ Library for syncing objects (e.g. directories).
14
+
15
+ # Install
16
+
17
+ ## PyPI
18
+
19
+ Run the following command to install the package from PyPI:
20
+
21
+ pip3 install python3-cyberfusion-sync-support
22
+
23
+ ## Generic
24
+
25
+ Run the following command to create a source distribution:
26
+
27
+ python3 setup.py sdist
28
+
29
+ ## Debian
30
+
31
+ Run the following commands to build a Debian package:
32
+
33
+ mk-build-deps -i -t 'apt -o Debug::pkgProblemResolver=yes --no-install-recommends -y'
34
+ dpkg-buildpackage -us -uc
35
+
36
+ # Configure
37
+
38
+ No configuration is supported.
39
+
40
+ # Usage
41
+
42
+ See code.
@@ -0,0 +1,32 @@
1
+ # python3-cyberfusion-sync-support
2
+
3
+ Library for syncing objects (e.g. directories).
4
+
5
+ # Install
6
+
7
+ ## PyPI
8
+
9
+ Run the following command to install the package from PyPI:
10
+
11
+ pip3 install python3-cyberfusion-sync-support
12
+
13
+ ## Generic
14
+
15
+ Run the following command to create a source distribution:
16
+
17
+ python3 setup.py sdist
18
+
19
+ ## Debian
20
+
21
+ Run the following commands to build a Debian package:
22
+
23
+ mk-build-deps -i -t 'apt -o Debug::pkgProblemResolver=yes --no-install-recommends -y'
24
+ dpkg-buildpackage -us -uc
25
+
26
+ # Configure
27
+
28
+ No configuration is supported.
29
+
30
+ # Usage
31
+
32
+ See code.
@@ -0,0 +1,21 @@
1
+ [tool.isort]
2
+ profile = "black"
3
+ line_length = 79
4
+ known_first_party = ["cyberfusion"]
5
+ default_section = "THIRDPARTY"
6
+
7
+ [tool.black]
8
+ line-length = 79
9
+ exclude = '''
10
+ (
11
+ /(
12
+ \.eggs # exclude a few common directories in the
13
+ | \.git # root of the project
14
+ | \.hg
15
+ | \.mypy_cache
16
+ | \.tox
17
+ | \.venv
18
+ | venv
19
+ )/
20
+ )
21
+ '''
@@ -0,0 +1,12 @@
1
+ [aliases]
2
+ test = pytest
3
+
4
+ [tool:pytest]
5
+ norecursedirs = .git build dist *.egg __pycache__ .cache
6
+ testpaths = tests
7
+ junit_suite_name = python3-cyberfusion-sync-support
8
+
9
+ [egg_info]
10
+ tag_build =
11
+ tag_date = 0
12
+
@@ -0,0 +1,23 @@
1
+ """A setuptools based setup module."""
2
+
3
+ from setuptools import setup
4
+
5
+ with open("README.md", "r", encoding="utf-8") as fh:
6
+ long_description = fh.read()
7
+
8
+ setup(
9
+ name="python3-cyberfusion-sync-support",
10
+ version="1.2.7.3.1",
11
+ description="Library for syncing objects (e.g. directories).",
12
+ long_description=long_description,
13
+ long_description_content_type="text/markdown",
14
+ author="Cyberfusion",
15
+ author_email="support@cyberfusion.io",
16
+ url="https://github.com/CyberfusionIO/python3-cyberfusion-sync-support",
17
+ platforms=["linux"],
18
+ packages=[
19
+ "cyberfusion.SyncSupport",
20
+ ],
21
+ package_dir={"": "src"},
22
+ data_files=[],
23
+ )
@@ -0,0 +1,5 @@
1
+ """Constants."""
2
+
3
+ import os
4
+
5
+ PATH_ARCHIVE = os.path.join(os.path.sep, "tmp", "sync-support-archives")
@@ -0,0 +1,28 @@
1
+ """Exceptions."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ class SyncSupportError(Exception):
7
+ """Error occurred using this library."""
8
+
9
+ pass
10
+
11
+
12
+ @dataclass
13
+ class IllegalMemberError(SyncSupportError):
14
+ """Archive contains member that is not in store path."""
15
+
16
+ member_name: str
17
+
18
+
19
+ class StorePathNotRelativeError(SyncSupportError):
20
+ """Store path is not relative (to UNIX user home directory)."""
21
+
22
+ pass
23
+
24
+
25
+ class FilesystemPathNotRelativeError(SyncSupportError):
26
+ """Filesystem path is not relative (to UNIX user home directory)."""
27
+
28
+ pass
@@ -0,0 +1,367 @@
1
+ """Classes for interaction with UNIX users."""
2
+
3
+ import getpass
4
+ import os
5
+ import shutil
6
+ import tarfile
7
+ from pathlib import Path, PosixPath
8
+ from typing import List, Optional, Tuple
9
+
10
+ from cached_property import cached_property
11
+
12
+ from cyberfusion.Common import generate_random_string, get_md5_hash
13
+ from cyberfusion.Common.FilesystemComparison import (
14
+ get_directories_only_in_right_directory,
15
+ get_files_only_in_right_directory,
16
+ )
17
+ from cyberfusion.SyncSupport import PATH_ARCHIVE
18
+ from cyberfusion.SyncSupport.exceptions import (
19
+ FilesystemPathNotRelativeError,
20
+ IllegalMemberError,
21
+ StorePathNotRelativeError,
22
+ )
23
+
24
+
25
+ class UNIXUserHomeDirectoryArchive:
26
+ """Abstraction of archive of UNIX user home directory."""
27
+
28
+ EXTENSION_FILE_TAR_GZ = "tar.gz"
29
+
30
+ def __init__(
31
+ self,
32
+ *,
33
+ store_path: str,
34
+ exclude_paths: Optional[List[str]] = None,
35
+ archive_path_root_directory: str = PATH_ARCHIVE,
36
+ ) -> None:
37
+ """Set attributes.
38
+
39
+ The archive is written to a file inside root_directory. The default path
40
+ is are automatically cleaned up using systemd-tmpfiles, if this library
41
+ is installed as a Debian package.
42
+ """
43
+ self.store_path = store_path
44
+ self.exclude_paths = exclude_paths if exclude_paths is not None else []
45
+ self.archive_path_root_directory = archive_path_root_directory
46
+
47
+ @property
48
+ def store_path(self) -> str:
49
+ """Set store path.
50
+
51
+ Path in archive is relative, so is path without leading slash.
52
+ """
53
+ return self._store_path
54
+
55
+ @store_path.setter
56
+ def store_path(self, value: str) -> None:
57
+ """Set store path.
58
+
59
+ Checks if store path is relative.
60
+ """
61
+ if os.path.isabs(value):
62
+ raise StorePathNotRelativeError
63
+
64
+ self._store_path = value
65
+
66
+ @property
67
+ def home_directory(self) -> Path:
68
+ """Set home directory."""
69
+ return Path.home()
70
+
71
+ @property
72
+ def username(self) -> str:
73
+ """Set username."""
74
+ return getpass.getuser()
75
+
76
+ @cached_property
77
+ def archive_path(self) -> str:
78
+ """Set archive path."""
79
+ user_directory = os.path.join(
80
+ self.archive_path_root_directory,
81
+ self.username,
82
+ )
83
+
84
+ os.makedirs(user_directory, exist_ok=True)
85
+
86
+ return (
87
+ os.path.join(
88
+ user_directory,
89
+ f"archive-{generate_random_string().lower()}",
90
+ )
91
+ + "."
92
+ + self.EXTENSION_FILE_TAR_GZ
93
+ )
94
+
95
+ def create(self) -> Tuple[str, str]:
96
+ """Create archive of store path."""
97
+
98
+ # Create file with correct permissions
99
+
100
+ with open(self.archive_path, "w"):
101
+ pass
102
+
103
+ os.chmod(self.archive_path, 0o600)
104
+
105
+ # Create archive
106
+
107
+ with tarfile.open(self.archive_path, "w:gz") as tar:
108
+ tar.add(
109
+ os.path.join(self.home_directory, self.store_path),
110
+ filter=lambda x: None if x.name in self.exclude_paths else x,
111
+ # self.store_path is relative to home directory, so this makes
112
+ # paths inside the archive relative to home directory
113
+ arcname=self.store_path,
114
+ )
115
+
116
+ return self.archive_path, get_md5_hash(self.archive_path)
117
+
118
+
119
+ class UNIXUserHomeDirectoryArchiveRestoration:
120
+ """Abstraction of UNIX user home directory archive restore process.
121
+
122
+ Restores path in archive to path on local filesystem.
123
+
124
+ This function extracts the given path in the archive to a temporary directory.
125
+ It then replaces the path on the local filesystem with the temporary directory.
126
+ """
127
+
128
+ # Ensure all restore-related directories start with this prefix. The prefix
129
+ # might be used by other systems to recognise directories that are related
130
+ # to an archive restore.
131
+
132
+ PREFIX_RESTORE_FILESYSTEM_OBJECT = ".archive-restore-"
133
+
134
+ def __init__(
135
+ self,
136
+ *,
137
+ store_path: str,
138
+ filesystem_path: str,
139
+ archive_path: str,
140
+ temporary_path_root_path: str,
141
+ exclude_paths: Optional[List[str]] = None,
142
+ ):
143
+ """Set attributes."""
144
+ self.archive_path = archive_path
145
+ self.store_path = store_path
146
+ self.filesystem_path = filesystem_path
147
+ self.exclude_paths = exclude_paths if exclude_paths is not None else []
148
+ self.temporary_path_root_path = temporary_path_root_path
149
+
150
+ @property
151
+ def filesystem_path(self) -> str:
152
+ """Set filesystem path.
153
+
154
+ Path on local filesystem is absolute, so is path with leading slash.
155
+ """
156
+ return self._filesystem_path
157
+
158
+ @filesystem_path.setter
159
+ def filesystem_path(self, value: str) -> None:
160
+ """Set filesystem path.
161
+
162
+ Path on local filesystem is absolute, so is path with leading slash.
163
+ """
164
+ if os.path.isabs(value):
165
+ raise FilesystemPathNotRelativeError
166
+
167
+ self._filesystem_path = os.path.join(self.home_directory, value)
168
+
169
+ @property
170
+ def store_path(self) -> str:
171
+ """Set store path.
172
+
173
+ Path in archive is relative, so is path without leading slash.
174
+ """
175
+ return self._store_path
176
+
177
+ @store_path.setter
178
+ def store_path(self, value: str) -> None:
179
+ """Set store path.
180
+
181
+ Checks if store path is relative.
182
+ """
183
+ if os.path.isabs(value):
184
+ raise StorePathNotRelativeError
185
+
186
+ self._store_path = value
187
+
188
+ @property
189
+ def home_directory(self) -> Path:
190
+ """Set home directory."""
191
+ return Path.home()
192
+
193
+ @cached_property
194
+ def temporary_path(
195
+ self,
196
+ ) -> str:
197
+ """Generate and create temporary path."""
198
+ temporary_path = os.path.join(
199
+ self.temporary_path_root_path,
200
+ self.PREFIX_RESTORE_FILESYSTEM_OBJECT
201
+ + "tmp."
202
+ + os.path.basename(self.filesystem_path)
203
+ + "-"
204
+ + generate_random_string(8),
205
+ )
206
+
207
+ os.mkdir(temporary_path)
208
+ os.chmod(temporary_path, 0o700)
209
+
210
+ return temporary_path
211
+
212
+ @cached_property
213
+ def old_path(self) -> str:
214
+ """Set old path."""
215
+
216
+ # Add dot prefix to prevent access, and add random string in case filesystem
217
+ # object without random string already exists
218
+
219
+ return os.path.join(
220
+ Path(self.filesystem_path).parent,
221
+ self.PREFIX_RESTORE_FILESYSTEM_OBJECT
222
+ + "old."
223
+ + os.path.basename(self.filesystem_path)
224
+ + "-"
225
+ + generate_random_string(8),
226
+ )
227
+
228
+ @cached_property
229
+ def new_path(self) -> str:
230
+ """Set new path."""
231
+
232
+ # Add dot prefix to prevent access, and add random string in case filesystem
233
+ # object without random string already exists
234
+
235
+ return os.path.join(
236
+ Path(self.filesystem_path).parent,
237
+ self.PREFIX_RESTORE_FILESYSTEM_OBJECT
238
+ + "new."
239
+ + os.path.basename(self.filesystem_path)
240
+ + "-"
241
+ + generate_random_string(8),
242
+ )
243
+
244
+ def _extract(self) -> None:
245
+ """Extract store path to temporary path."""
246
+ tar = tarfile.open(self.archive_path)
247
+
248
+ # As the entire archive is extracted by tar.extractall, we must check
249
+ # here that the archive contains only expected members, i.e. those in
250
+ # the store path. If the archive contains any member that is not in
251
+ # the given store path, an exception will be raised instead of silently
252
+ # continuing because, when creating the archive with UNIXUserHomeDirectoryArchive,
253
+ # this should never happen.
254
+
255
+ for member in tar.getmembers():
256
+ # Member *is* the store path itself, which is the only member
257
+ # allowed outside of the store path
258
+
259
+ if member.name == self.store_path:
260
+ continue
261
+
262
+ # Member is inside the given store path
263
+
264
+ if Path(self.store_path) in PosixPath(member.name).parents:
265
+ continue
266
+
267
+ raise IllegalMemberError(member.name)
268
+
269
+ # Extract entire archive
270
+
271
+ tar.extractall(self.temporary_path)
272
+
273
+ tar.close()
274
+
275
+ @property
276
+ def username(self) -> str:
277
+ """Set username."""
278
+ return getpass.getuser()
279
+
280
+ def _copy(self) -> None:
281
+ """Copy files that are in the filesystem path but not in the archive (i.e. removed) and are excluded (excluding excludes from deletion)."""
282
+ for f in get_files_only_in_right_directory(
283
+ os.path.join(self.temporary_path, self.store_path),
284
+ self.filesystem_path,
285
+ ):
286
+ if (
287
+ os.path.relpath(f, self.home_directory)
288
+ not in self.exclude_paths
289
+ ):
290
+ continue
291
+
292
+ destination_path = os.path.join(
293
+ self.temporary_path,
294
+ self.store_path,
295
+ os.path.relpath(f, self.filesystem_path),
296
+ )
297
+
298
+ shutil.copyfile(f, destination_path)
299
+
300
+ for f in get_directories_only_in_right_directory(
301
+ os.path.join(self.temporary_path, self.store_path),
302
+ self.filesystem_path,
303
+ ):
304
+ if (
305
+ os.path.relpath(f, self.home_directory)
306
+ not in self.exclude_paths
307
+ ):
308
+ continue
309
+
310
+ destination_path = os.path.join(
311
+ self.temporary_path,
312
+ self.store_path,
313
+ os.path.relpath(f, self.filesystem_path),
314
+ )
315
+
316
+ shutil.copytree(f, destination_path, ignore_dangling_symlinks=True)
317
+
318
+ def replace(self) -> None:
319
+ """Replace object on local filesystem with object from archive.
320
+
321
+ This is a nearly atomic process. I.e. there is almost no downtime when
322
+ replacing.
323
+ """
324
+
325
+ # Extract archive. The filesystem path remains untouched until this is
326
+ # completed. This ensures that the filesystem is not left in a broken
327
+ # state if the extraction fails.
328
+
329
+ self._extract()
330
+
331
+ # Copy deleted files that were excluded
332
+
333
+ if os.path.lexists(self.filesystem_path):
334
+ self._copy()
335
+
336
+ # Move the extracted filesystem objects to the new path. As these may be
337
+ # on different filesystems, the move could take a while. We restore to the
338
+ # new path instead of to the filesystem path. If we restored to the filesystem
339
+ # path, we would have to get the original filesystem object out of the way,
340
+ # leaving the filesystem structure in a 'broken' state, while the move could
341
+ # take a while. In order to prevent downtime, we restore to this temporary
342
+ # new directory first.
343
+
344
+ shutil.move(
345
+ os.path.join(self.temporary_path, self.store_path),
346
+ self.new_path,
347
+ )
348
+
349
+ # If the filesystem path already exists, move it out of the way so that
350
+ # we can move the new path to it. This procedure is also followed for
351
+ # regular files. Unlike non-empty directories, regular files can be
352
+ # overwritten without having to ensure the filesystem object does not
353
+ # exist at the path, unless the regular file is write-protected (e.g.
354
+ # if it has permissions 0400).
355
+
356
+ if os.path.lexists(self.filesystem_path):
357
+ os.rename(self.filesystem_path, self.old_path)
358
+
359
+ # Move the new path to the filesystem path. This completes the restore.
360
+
361
+ os.rename(self.new_path, self.filesystem_path)
362
+
363
+ # Remove the old path if it exists (it exists if the filesystem path
364
+ # existed before doing the restore, see above).
365
+
366
+ if os.path.lexists(self.old_path):
367
+ shutil.rmtree(self.old_path)
@@ -0,0 +1,42 @@
1
+ Metadata-Version: 2.1
2
+ Name: python3-cyberfusion-sync-support
3
+ Version: 1.2.7.3.1
4
+ Summary: Library for syncing objects (e.g. directories).
5
+ Home-page: https://github.com/CyberfusionIO/python3-cyberfusion-sync-support
6
+ Author: Cyberfusion
7
+ Author-email: support@cyberfusion.io
8
+ Platform: linux
9
+ Description-Content-Type: text/markdown
10
+
11
+ # python3-cyberfusion-sync-support
12
+
13
+ Library for syncing objects (e.g. directories).
14
+
15
+ # Install
16
+
17
+ ## PyPI
18
+
19
+ Run the following command to install the package from PyPI:
20
+
21
+ pip3 install python3-cyberfusion-sync-support
22
+
23
+ ## Generic
24
+
25
+ Run the following command to create a source distribution:
26
+
27
+ python3 setup.py sdist
28
+
29
+ ## Debian
30
+
31
+ Run the following commands to build a Debian package:
32
+
33
+ mk-build-deps -i -t 'apt -o Debug::pkgProblemResolver=yes --no-install-recommends -y'
34
+ dpkg-buildpackage -us -uc
35
+
36
+ # Configure
37
+
38
+ No configuration is supported.
39
+
40
+ # Usage
41
+
42
+ See code.
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.cfg
4
+ setup.py
5
+ src/cyberfusion/SyncSupport/__init__.py
6
+ src/cyberfusion/SyncSupport/exceptions.py
7
+ src/cyberfusion/SyncSupport/unix_users.py
8
+ src/python3_cyberfusion_sync_support.egg-info/PKG-INFO
9
+ src/python3_cyberfusion_sync_support.egg-info/SOURCES.txt
10
+ src/python3_cyberfusion_sync_support.egg-info/dependency_links.txt
11
+ src/python3_cyberfusion_sync_support.egg-info/top_level.txt