dfsync 0.4.9__tar.gz → 0.4.11__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.
- {dfsync-0.4.9 → dfsync-0.4.11}/PKG-INFO +5 -3
- {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/backends/kube.py +2 -2
- {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/backends/rsync.py +14 -4
- {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/filters.py +13 -8
- dfsync-0.4.11/dfsync/lib.py +120 -0
- {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/monitor.py +61 -24
- dfsync-0.4.11/dfsync/sentry.py +9 -0
- dfsync-0.4.11/dfsync/test_filters.py +28 -0
- {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/test_lib.py +22 -2
- {dfsync-0.4.9 → dfsync-0.4.11}/pyproject.toml +3 -3
- dfsync-0.4.9/dfsync/lib.py +0 -49
- {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/__init__.py +0 -0
- {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/backends/__init__.py +0 -0
- {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/backends/kube_exec.py +0 -0
- {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/char_ui.py +0 -0
- {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/chcli.py +0 -0
- {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/check_dns.py +0 -0
- {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/cli.py +0 -0
- {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/config.py +0 -0
- {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/diffui.py +0 -0
- {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/distribution.py +0 -0
- {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/exp.py +0 -0
- {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/kube_credentials.py +0 -0
- {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/test_dfsync.py +0 -0
- {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/test_distribution.py +0 -0
- {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/test_kube_credentials.py +0 -0
- {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/transactions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: dfsync
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.11
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Mihai Balint
|
|
6
6
|
Author-email: balint.mihai@gmail.com
|
|
@@ -11,12 +11,14 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
-
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
16
|
Requires-Dist: click (>=8,<9)
|
|
16
17
|
Requires-Dist: click-default-group (>=1.2.2,<2.0.0)
|
|
17
18
|
Requires-Dist: gitpython (>=3.1.14,<4.0.0)
|
|
18
19
|
Requires-Dist: kubernetes (>=28.1.0,<29.0.0)
|
|
19
20
|
Requires-Dist: paramiko[invoke] (>=3.2.0,<4.0.0)
|
|
21
|
+
Requires-Dist: sentry-sdk (>=2.26.0,<3.0.0)
|
|
20
22
|
Requires-Dist: tenacity (>=8.0.1,<9.0.0)
|
|
21
23
|
Requires-Dist: toml (>=0.10.2,<0.11.0)
|
|
22
24
|
Requires-Dist: watchdog (>=2.0.2,<3.0.0)
|
|
@@ -569,7 +569,7 @@ class KubeReDeployer:
|
|
|
569
569
|
try:
|
|
570
570
|
resp = self._exec(pod, spec, status, Generic.check_can_exec())
|
|
571
571
|
return
|
|
572
|
-
except:
|
|
572
|
+
except Exception:
|
|
573
573
|
self._uncrash(pod, spec, status)
|
|
574
574
|
|
|
575
575
|
def _sniff_image_distro(self, pod, spec, status):
|
|
@@ -596,7 +596,7 @@ class KubeReDeployer:
|
|
|
596
596
|
return False
|
|
597
597
|
|
|
598
598
|
return len(resp) > 0
|
|
599
|
-
except:
|
|
599
|
+
except Exception:
|
|
600
600
|
return False
|
|
601
601
|
|
|
602
602
|
def stabilize_deployments(self, image_base):
|
|
@@ -10,6 +10,8 @@ EVENT_TYPE_MAP = {
|
|
|
10
10
|
"full-sync": "Full Sync",
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
RSYNC_CONFIG = {"RSYNC_EXECUTABLE": "rsync"}
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
class StopStreamReader(Exception):
|
|
15
17
|
pass
|
|
@@ -174,7 +176,7 @@ class FileRsync:
|
|
|
174
176
|
rsync_cmd = self._get_rsync_cmd_on_full_sync(src_paths, destination_dir, blocking_io, rsh)
|
|
175
177
|
else:
|
|
176
178
|
rsync_cmd = [
|
|
177
|
-
|
|
179
|
+
get_rsync_executable(),
|
|
178
180
|
"-Rvx",
|
|
179
181
|
"--temp-dir=/tmp",
|
|
180
182
|
"--delay-updates",
|
|
@@ -227,7 +229,7 @@ class FileRsync:
|
|
|
227
229
|
if return_code != 23:
|
|
228
230
|
raise subprocess.CalledProcessError(returncode=return_code, cmd=rsync_cmd)
|
|
229
231
|
|
|
230
|
-
except:
|
|
232
|
+
except Exception:
|
|
231
233
|
echo("Sync failed")
|
|
232
234
|
env_str = "N/A"
|
|
233
235
|
if rsh_env:
|
|
@@ -260,7 +262,7 @@ class FileRsync:
|
|
|
260
262
|
src_dir = "{}/".format(src_dir)
|
|
261
263
|
|
|
262
264
|
cmd = [
|
|
263
|
-
|
|
265
|
+
get_rsync_executable(),
|
|
264
266
|
"-rvx",
|
|
265
267
|
"--delete",
|
|
266
268
|
"--filter=-,s *",
|
|
@@ -276,7 +278,7 @@ class FileRsync:
|
|
|
276
278
|
def _get_rsync_cmd_on_full_sync(self, src_file_paths: list, destination_dir: str, blocking_io: list, rsh: list):
|
|
277
279
|
filters = ["--filter=- {}".format(f) for f in list_files_to_ignore()]
|
|
278
280
|
return [
|
|
279
|
-
|
|
281
|
+
get_rsync_executable(),
|
|
280
282
|
"-rvx",
|
|
281
283
|
"--temp-dir=/tmp",
|
|
282
284
|
"--delay-updates",
|
|
@@ -312,3 +314,11 @@ def sanitize_relative_path(path):
|
|
|
312
314
|
|
|
313
315
|
|
|
314
316
|
rsync_backend = FileRsync
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def get_rsync_executable() -> str:
|
|
320
|
+
return RSYNC_CONFIG["RSYNC_EXECUTABLE"]
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def set_rsync_executable(executable_path: str) -> str:
|
|
324
|
+
return RSYNC_CONFIG["RSYNC_EXECUTABLE"]
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import black
|
|
2
1
|
import fnmatch
|
|
3
2
|
import os.path
|
|
4
3
|
import git.exc
|
|
@@ -75,7 +74,9 @@ class EmacsBufferFilter(UserConfigFilter):
|
|
|
75
74
|
self._ignore_message = "Emacs buffer backup"
|
|
76
75
|
|
|
77
76
|
|
|
78
|
-
class
|
|
77
|
+
class PythonSyntaxFilter(LoggingFilter):
|
|
78
|
+
"""Reject .py files that do not parse, so a half-saved file never reaches the pod."""
|
|
79
|
+
|
|
79
80
|
def __init__(self):
|
|
80
81
|
super().__init__()
|
|
81
82
|
|
|
@@ -87,17 +88,17 @@ class PythonBlackFilter(LoggingFilter):
|
|
|
87
88
|
parent_path, file_name = os.path.split(os.path.expanduser(src_file_path))
|
|
88
89
|
for pattern in PYTHON_PATTERNS:
|
|
89
90
|
if fnmatch.fnmatch(file_name, pattern):
|
|
90
|
-
return self.
|
|
91
|
+
return self._syntax_check(src_file_path)
|
|
91
92
|
return False
|
|
92
93
|
|
|
93
|
-
def
|
|
94
|
+
def _syntax_check(self, src_file_path):
|
|
94
95
|
try:
|
|
95
|
-
with open(src_file_path, "
|
|
96
|
+
with open(src_file_path, "rb") as f:
|
|
96
97
|
contents = f.read()
|
|
97
|
-
|
|
98
|
+
compile(contents, src_file_path, "exec", dont_inherit=True)
|
|
98
99
|
return False
|
|
99
100
|
|
|
100
|
-
except (FileNotFoundError, PermissionError
|
|
101
|
+
except (FileNotFoundError, PermissionError):
|
|
101
102
|
return False
|
|
102
103
|
|
|
103
104
|
except Exception as e:
|
|
@@ -106,6 +107,10 @@ class PythonBlackFilter(LoggingFilter):
|
|
|
106
107
|
return True
|
|
107
108
|
|
|
108
109
|
|
|
110
|
+
# Kept for code that imported the old name
|
|
111
|
+
PythonBlackFilter = PythonSyntaxFilter
|
|
112
|
+
|
|
113
|
+
|
|
109
114
|
class UntrackedGitFilesFilter(LoggingFilter):
|
|
110
115
|
def __init__(self):
|
|
111
116
|
super().__init__()
|
|
@@ -192,7 +197,7 @@ class UntrackedGitFilesFilter(LoggingFilter):
|
|
|
192
197
|
)
|
|
193
198
|
|
|
194
199
|
|
|
195
|
-
USER_FILTERS = [EmacsBufferFilter(),
|
|
200
|
+
USER_FILTERS = [EmacsBufferFilter(), PythonSyntaxFilter()]
|
|
196
201
|
GIT_FILTER = UntrackedGitFilesFilter()
|
|
197
202
|
ALL_FILTERS = [
|
|
198
203
|
exclude_watchdog_directory_events,
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from enum import Enum
|
|
3
|
+
import platform
|
|
4
|
+
import subprocess
|
|
5
|
+
import time
|
|
6
|
+
import threading
|
|
7
|
+
import zlib
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ThreadedOperationsManager:
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self.threads = []
|
|
13
|
+
|
|
14
|
+
def stop(self, *args, **kwargs):
|
|
15
|
+
for thread in self.threads:
|
|
16
|
+
thread.stop(*args, **kwargs)
|
|
17
|
+
|
|
18
|
+
def register_threaded_operation(self, op):
|
|
19
|
+
self.threads.append(op)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
thread_manager = ThreadedOperationsManager()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ControlledThreadedOperation:
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self._running = False
|
|
28
|
+
self._thread = threading.Thread(target=self.run)
|
|
29
|
+
thread_manager.register_threaded_operation(self)
|
|
30
|
+
|
|
31
|
+
def start(self):
|
|
32
|
+
self._running = True
|
|
33
|
+
self._thread.start()
|
|
34
|
+
|
|
35
|
+
def stop(self, *args, **kwargs):
|
|
36
|
+
self._running = False
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def is_running(self):
|
|
40
|
+
return self._running
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def is_completed(self):
|
|
44
|
+
return self._running is False
|
|
45
|
+
|
|
46
|
+
def run(self):
|
|
47
|
+
while self._running:
|
|
48
|
+
try:
|
|
49
|
+
self._run_once()
|
|
50
|
+
except Exception:
|
|
51
|
+
time.sleep(0.001)
|
|
52
|
+
|
|
53
|
+
def _run_once(self):
|
|
54
|
+
time.sleep(0.001)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class RsyncFlavour(Enum):
|
|
58
|
+
SAMBA_RSYNC = "SAMBA_RSYNC"
|
|
59
|
+
OPEN_RSYNC = "OPENRSYNC"
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def executable(self):
|
|
63
|
+
if platform.system() == "Darwin": # Check if running on macOS
|
|
64
|
+
if self == RsyncFlavour.SAMBA_RSYNC:
|
|
65
|
+
return "/opt/homebrew/bin/rsync"
|
|
66
|
+
elif self == RsyncFlavour.OPEN_RSYNC:
|
|
67
|
+
return "/usr/bin/rsync"
|
|
68
|
+
|
|
69
|
+
# Default fallback for other platforms or if paths don't exist
|
|
70
|
+
return "rsync"
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def exists_installed(self):
|
|
74
|
+
try:
|
|
75
|
+
# Check if the executable exists and is executable
|
|
76
|
+
result = subprocess.run(
|
|
77
|
+
["which", self.executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False
|
|
78
|
+
)
|
|
79
|
+
if result.returncode != 0:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
# Additionally verify it can be executed
|
|
83
|
+
result = subprocess.run(
|
|
84
|
+
[self.executable, "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False
|
|
85
|
+
)
|
|
86
|
+
return result.returncode == 0
|
|
87
|
+
except Exception:
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def check_rsync() -> RsyncFlavour:
|
|
92
|
+
try:
|
|
93
|
+
result = subprocess.run(["rsync", "--version"], capture_output=True, text=True)
|
|
94
|
+
version_text = result.stdout
|
|
95
|
+
|
|
96
|
+
if "rsync.samba.org/" in version_text:
|
|
97
|
+
return RsyncFlavour.SAMBA_RSYNC
|
|
98
|
+
else:
|
|
99
|
+
# Default to OpenRsync if we can't positively identify Samba rsync
|
|
100
|
+
return RsyncFlavour.OPEN_RSYNC
|
|
101
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
102
|
+
# If rsync isn't available or fails, default to Samba rsync
|
|
103
|
+
return RsyncFlavour.SAMBA_RSYNC
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
NONCE = "Rah4liteaR1geughuC8eushaOhtiW0xu"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def dsn_pack(unpacked_dsn: str) -> str:
|
|
110
|
+
# XOR with NONCE, compress and base64 encode
|
|
111
|
+
xored = "".join(chr(ord(a) ^ ord(b)) for a, b in zip(unpacked_dsn, NONCE * (len(unpacked_dsn) // len(NONCE) + 1)))
|
|
112
|
+
compressed = zlib.compress(xored.encode())
|
|
113
|
+
return base64.b64encode(compressed).decode()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def dsn_unpack(packed_dsn: str) -> str:
|
|
117
|
+
# Reverse the packing process
|
|
118
|
+
decoded = base64.b64decode(packed_dsn)
|
|
119
|
+
decompressed = zlib.decompress(decoded).decode()
|
|
120
|
+
return "".join(chr(ord(a) ^ ord(b)) for a, b in zip(decompressed, NONCE * (len(decompressed) // len(NONCE) + 1)))
|
|
@@ -1,31 +1,37 @@
|
|
|
1
|
-
import
|
|
1
|
+
import logging
|
|
2
2
|
import os
|
|
3
3
|
import os.path
|
|
4
|
+
import queue
|
|
4
5
|
import sys
|
|
5
6
|
import time
|
|
6
|
-
import logging
|
|
7
|
-
import queue
|
|
8
|
-
|
|
9
|
-
from click_default_group import DefaultGroup
|
|
10
7
|
from contextlib import contextmanager
|
|
11
8
|
from functools import partial
|
|
12
|
-
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
import sentry_sdk
|
|
12
|
+
from click_default_group import DefaultGroup
|
|
13
13
|
from watchdog.events import FileSystemEventHandler
|
|
14
|
+
from watchdog.observers import Observer
|
|
14
15
|
|
|
15
16
|
import dfsync.filters as filters
|
|
16
|
-
|
|
17
|
+
import dfsync.lib as lib
|
|
18
|
+
from dfsync.backends import kube_backend, rsync_backend
|
|
19
|
+
from dfsync.backends.rsync import set_rsync_executable
|
|
20
|
+
from dfsync.char_ui import KeyController
|
|
21
|
+
from dfsync.config import read_config
|
|
17
22
|
from dfsync.distribution import (
|
|
23
|
+
AsyncVersionChecker,
|
|
18
24
|
get_installed_version,
|
|
19
25
|
get_latest_version,
|
|
20
|
-
is_older_version,
|
|
21
|
-
AsyncVersionChecker,
|
|
22
26
|
is_installed_in_editable_mode,
|
|
27
|
+
is_older_version,
|
|
23
28
|
update_package,
|
|
24
29
|
)
|
|
25
|
-
from dfsync.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
from dfsync.kube_credentials import (
|
|
31
|
+
contextualize_kube_credentials,
|
|
32
|
+
normalized_k8s_url,
|
|
33
|
+
update_local_kube_config,
|
|
34
|
+
)
|
|
29
35
|
|
|
30
36
|
logging.basicConfig(level=logging.WARN)
|
|
31
37
|
|
|
@@ -34,6 +40,7 @@ BACKENDS = {
|
|
|
34
40
|
"file-rsync": rsync_backend,
|
|
35
41
|
"kube-rsync": kube_backend,
|
|
36
42
|
}
|
|
43
|
+
SENTRY_DSN = "eJyzEpVxkQ+O9gpKYAoIdwyKFinjYnZ04gnSjXXhTg/xFEwOiA+Li/ENULSP4+Rh5JCS0Y33lpThZlDmZ2MwlhOUqg0CAPv5DeE="
|
|
37
44
|
|
|
38
45
|
|
|
39
46
|
class IgnoreEvent(Exception):
|
|
@@ -44,7 +51,11 @@ class RelatedLocationsError(ValueError):
|
|
|
44
51
|
pass
|
|
45
52
|
|
|
46
53
|
|
|
47
|
-
class
|
|
54
|
+
class UnsupportedRsyncError(ValueError):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class FileChangedEventHandler(lib.ControlledThreadedOperation, FileSystemEventHandler):
|
|
48
59
|
def __init__(
|
|
49
60
|
self,
|
|
50
61
|
backend: str = "log",
|
|
@@ -258,11 +269,12 @@ def main():
|
|
|
258
269
|
pass
|
|
259
270
|
|
|
260
271
|
|
|
261
|
-
@main.command()
|
|
272
|
+
@main.command(add_help_option=False)
|
|
262
273
|
def version():
|
|
263
274
|
"""
|
|
264
275
|
Print the currently installed version
|
|
265
276
|
"""
|
|
277
|
+
sentry_sdk.set_tag("command", "version")
|
|
266
278
|
_version()
|
|
267
279
|
|
|
268
280
|
|
|
@@ -279,21 +291,21 @@ def _version():
|
|
|
279
291
|
click.echo(f"Latest version is {latest_version}, please update using `dfsync self-update`")
|
|
280
292
|
|
|
281
293
|
|
|
282
|
-
@main.command(name="help",
|
|
283
|
-
|
|
284
|
-
def help(help):
|
|
294
|
+
@main.command(name="help", add_help_option=False)
|
|
295
|
+
def help():
|
|
285
296
|
"""
|
|
286
297
|
Print the usage/help for dfsync
|
|
287
298
|
"""
|
|
288
|
-
with click.Context(sync, info_name="dfsync") as ctx:
|
|
299
|
+
with click.Context(sync, info_name="dfsync", max_content_width=999) as ctx:
|
|
289
300
|
click.echo(sync.get_help(ctx))
|
|
290
301
|
|
|
291
302
|
|
|
292
|
-
@main.command()
|
|
303
|
+
@main.command(add_help_option=False)
|
|
293
304
|
def self_update():
|
|
294
305
|
"""
|
|
295
306
|
Update dfsync to the latest version published on pypi
|
|
296
307
|
"""
|
|
308
|
+
sentry_sdk.set_tag("command", "self_update")
|
|
297
309
|
latest_version = get_latest_version()
|
|
298
310
|
installed_version = get_installed_version()
|
|
299
311
|
if not is_older_version(installed_version, latest_version):
|
|
@@ -316,6 +328,7 @@ def import_kube_host(kube_host=None, credentials=None):
|
|
|
316
328
|
|
|
317
329
|
ssh user@kube-host -C "sudo cat /root/.kube/config" | dfsync import-kube-host --kube-host=https://kube-host:6443
|
|
318
330
|
"""
|
|
331
|
+
sentry_sdk.set_tag("command", "import_kube_host")
|
|
319
332
|
patch = contextualize_kube_credentials(normalized_k8s_url(kube_host), credentials)
|
|
320
333
|
update_local_kube_config(patch)
|
|
321
334
|
|
|
@@ -363,6 +376,7 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync, ver
|
|
|
363
376
|
* editor-aware: changes to temporary files created by source code editors will be ignored
|
|
364
377
|
* transparent: every action is diligently logged in the console
|
|
365
378
|
"""
|
|
379
|
+
sentry_sdk.set_tag("command", "sync")
|
|
366
380
|
logging.basicConfig(
|
|
367
381
|
level=logging.WARN,
|
|
368
382
|
format="%(asctime)s - %(message)s",
|
|
@@ -389,12 +403,22 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync, ver
|
|
|
389
403
|
click.echo(f"Source file/dirs not found: {', '.join(missing)}")
|
|
390
404
|
|
|
391
405
|
backend, destination_dir = split_destination(destination_dir)
|
|
406
|
+
sentry_sdk.set_tag("sync-backend", backend)
|
|
392
407
|
try:
|
|
393
408
|
if len(paths) == 0:
|
|
394
409
|
raise ValueError("No source file/dirs found")
|
|
395
410
|
elif len(missing) > 0:
|
|
396
411
|
click.echo(f"Using source file/dirs {', '.join(paths)}")
|
|
397
412
|
|
|
413
|
+
if backend == "kube-rsync":
|
|
414
|
+
installed_rsync = lib.check_rsync()
|
|
415
|
+
if installed_rsync != lib.RsyncFlavour.SAMBA_RSYNC and lib.RsyncFlavour.SAMBA_RSYNC.exists_installed:
|
|
416
|
+
set_rsync_executable(lib.RsyncFlavour.SAMBA_RSYNC.executable)
|
|
417
|
+
elif installed_rsync != lib.RsyncFlavour.SAMBA_RSYNC:
|
|
418
|
+
click.echo(f"Found rsync: {installed_rsync.executable}")
|
|
419
|
+
raise UnsupportedRsyncError()
|
|
420
|
+
|
|
421
|
+
sentry_sdk.set_tag("sync-backend", backend)
|
|
398
422
|
backend_options = dict(
|
|
399
423
|
destination_dir=destination_dir,
|
|
400
424
|
supervisor=supervisor,
|
|
@@ -423,6 +447,14 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync, ver
|
|
|
423
447
|
click.echo(str(e))
|
|
424
448
|
return -1
|
|
425
449
|
|
|
450
|
+
except UnsupportedRsyncError:
|
|
451
|
+
click.echo(
|
|
452
|
+
"\nStarting from Mac OSX Sequoia 15.4, Apple are shipping openrsync instead of the GPL-licensed rsync from Samba"
|
|
453
|
+
"\nThe Apple version of rsync has less features and is currently not supported by dfsync"
|
|
454
|
+
"\nPlease use homebrew to install the GPL-licensed rsync from Samba with this command `brew install rsync`."
|
|
455
|
+
)
|
|
456
|
+
return -1
|
|
457
|
+
|
|
426
458
|
except ValueError as e:
|
|
427
459
|
click.echo(
|
|
428
460
|
"Trying {} from source: {} to destination: '{}'\n".format(
|
|
@@ -432,6 +464,7 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync, ver
|
|
|
432
464
|
)
|
|
433
465
|
)
|
|
434
466
|
click.echo(str(e))
|
|
467
|
+
sentry_sdk.capture_exception(e)
|
|
435
468
|
return -1
|
|
436
469
|
|
|
437
470
|
checker = AsyncVersionChecker()
|
|
@@ -455,7 +488,7 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync, ver
|
|
|
455
488
|
controller.on_key(
|
|
456
489
|
"x",
|
|
457
490
|
description="to exit",
|
|
458
|
-
action=partial(thread_manager.stop, "Exiting."),
|
|
491
|
+
action=partial(lib.thread_manager.stop, "Exiting."),
|
|
459
492
|
)
|
|
460
493
|
|
|
461
494
|
try:
|
|
@@ -481,16 +514,16 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync, ver
|
|
|
481
514
|
controller.raise_exceptions()
|
|
482
515
|
|
|
483
516
|
except KeyboardInterrupt:
|
|
484
|
-
thread_manager.stop()
|
|
517
|
+
lib.thread_manager.stop()
|
|
485
518
|
click.echo("Received [Ctrl-C], exiting.")
|
|
486
519
|
|
|
487
520
|
except:
|
|
488
|
-
thread_manager.stop()
|
|
521
|
+
lib.thread_manager.stop()
|
|
489
522
|
raise
|
|
490
523
|
|
|
491
524
|
finally:
|
|
492
525
|
observer.stop()
|
|
493
|
-
thread_manager.stop()
|
|
526
|
+
lib.thread_manager.stop()
|
|
494
527
|
backend_engine.on_monitor_exit(**backend_options)
|
|
495
528
|
if observer.ident is not None:
|
|
496
529
|
# only join the observer if it was previously started
|
|
@@ -498,4 +531,8 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync, ver
|
|
|
498
531
|
|
|
499
532
|
|
|
500
533
|
if __name__ == "__main__":
|
|
534
|
+
sentry_sdk.init(
|
|
535
|
+
lib.dsn_unpack(SENTRY_DSN),
|
|
536
|
+
release=f"dfsync@{get_installed_version()}",
|
|
537
|
+
)
|
|
501
538
|
sys.exit(main() or 0)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from dfsync.filters import USER_FILTERS, PythonBlackFilter, PythonSyntaxFilter
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_rejects_python_that_does_not_parse(tmp_path):
|
|
5
|
+
src = tmp_path / "broken.py"
|
|
6
|
+
src.write_text("def f(:\n")
|
|
7
|
+
assert PythonSyntaxFilter().is_filtered(src_file_path=str(src))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_syncs_valid_python_even_if_not_formatted(tmp_path):
|
|
11
|
+
src = tmp_path / "ugly.py"
|
|
12
|
+
src.write_text("x = [1,\n2]\n")
|
|
13
|
+
assert not PythonSyntaxFilter().is_filtered(src_file_path=str(src))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_ignores_files_that_are_not_python(tmp_path):
|
|
17
|
+
src = tmp_path / "notes.txt"
|
|
18
|
+
src.write_text("def f(:\n")
|
|
19
|
+
assert not PythonSyntaxFilter().is_filtered(src_file_path=str(src))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_syncs_a_deleted_file(tmp_path):
|
|
23
|
+
assert not PythonSyntaxFilter().is_filtered(src_file_path=str(tmp_path / "gone.py"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_old_name_still_works():
|
|
27
|
+
assert PythonBlackFilter is PythonSyntaxFilter
|
|
28
|
+
assert any(isinstance(f, PythonSyntaxFilter) for f in USER_FILTERS)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
from dfsync.lib import ControlledThreadedOperation, ThreadedOperationsManager
|
|
1
|
+
from dfsync.lib import ControlledThreadedOperation, ThreadedOperationsManager, dsn_pack, dsn_unpack
|
|
3
2
|
|
|
4
3
|
|
|
5
4
|
def test_threaded_operations_manager():
|
|
@@ -26,3 +25,24 @@ def test_controlled_threaded_operation():
|
|
|
26
25
|
operation.stop()
|
|
27
26
|
assert not operation.is_running
|
|
28
27
|
assert operation.is_completed
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_dsn_pack_unpack():
|
|
31
|
+
# Test basic packing and unpacking
|
|
32
|
+
original_dsn = "test_dsn_string"
|
|
33
|
+
packed = dsn_pack(original_dsn)
|
|
34
|
+
unpacked = dsn_unpack(packed)
|
|
35
|
+
assert unpacked == original_dsn
|
|
36
|
+
|
|
37
|
+
# Test with empty string
|
|
38
|
+
assert dsn_unpack(dsn_pack("")) == ""
|
|
39
|
+
|
|
40
|
+
# Test with longer string
|
|
41
|
+
long_dsn = "https://very.long.dsn.string.with.special/characters?key=value&other=123"
|
|
42
|
+
assert dsn_unpack(dsn_pack(long_dsn)) == long_dsn
|
|
43
|
+
|
|
44
|
+
# Test that packed value is different from original
|
|
45
|
+
assert dsn_pack("test") != "test"
|
|
46
|
+
|
|
47
|
+
# Test that consecutive packs produce same result
|
|
48
|
+
assert dsn_pack("test") == dsn_pack("test")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "dfsync"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.11"
|
|
4
4
|
description = ""
|
|
5
5
|
authors = ["Mihai Balint <balint.mihai@gmail.com>"]
|
|
6
6
|
|
|
@@ -14,9 +14,9 @@ toml = "^0.10.2"
|
|
|
14
14
|
tenacity = "^8.0.1"
|
|
15
15
|
click-default-group = "^1.2.2"
|
|
16
16
|
paramiko = {version = "^3.2.0", extras = ["invoke"]}
|
|
17
|
-
|
|
17
|
+
sentry-sdk = "^2.26.0"
|
|
18
18
|
|
|
19
|
-
[tool.poetry.dev
|
|
19
|
+
[tool.poetry.group.dev.dependencies]
|
|
20
20
|
pytest = "^7.4"
|
|
21
21
|
pytest_mock = "^3.12"
|
|
22
22
|
ipython = "^8"
|
dfsync-0.4.9/dfsync/lib.py
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import time
|
|
2
|
-
import threading
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class ThreadedOperationsManager:
|
|
6
|
-
def __init__(self):
|
|
7
|
-
self.threads = []
|
|
8
|
-
|
|
9
|
-
def stop(self, *args, **kwargs):
|
|
10
|
-
for thread in self.threads:
|
|
11
|
-
thread.stop(*args, **kwargs)
|
|
12
|
-
|
|
13
|
-
def register_threaded_operation(self, op):
|
|
14
|
-
self.threads.append(op)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
thread_manager = ThreadedOperationsManager()
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class ControlledThreadedOperation:
|
|
21
|
-
def __init__(self):
|
|
22
|
-
self._running = False
|
|
23
|
-
self._thread = threading.Thread(target=self.run)
|
|
24
|
-
thread_manager.register_threaded_operation(self)
|
|
25
|
-
|
|
26
|
-
def start(self):
|
|
27
|
-
self._running = True
|
|
28
|
-
self._thread.start()
|
|
29
|
-
|
|
30
|
-
def stop(self, *args, **kwargs):
|
|
31
|
-
self._running = False
|
|
32
|
-
|
|
33
|
-
@property
|
|
34
|
-
def is_running(self):
|
|
35
|
-
return self._running
|
|
36
|
-
|
|
37
|
-
@property
|
|
38
|
-
def is_completed(self):
|
|
39
|
-
return self._running is False
|
|
40
|
-
|
|
41
|
-
def run(self):
|
|
42
|
-
while self._running:
|
|
43
|
-
try:
|
|
44
|
-
self._run_once()
|
|
45
|
-
except Exception:
|
|
46
|
-
time.sleep(0.001)
|
|
47
|
-
|
|
48
|
-
def _run_once(self):
|
|
49
|
-
time.sleep(0.001)
|
|
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
|
|
File without changes
|
|
File without changes
|