rclone-api 1.0.27__tar.gz → 1.0.29__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {rclone_api-1.0.27 → rclone_api-1.0.29}/PKG-INFO +1 -1
- {rclone_api-1.0.27 → rclone_api-1.0.29}/pyproject.toml +1 -1
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api/__init__.py +3 -0
- rclone_api-1.0.29/src/rclone_api/diff.py +119 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api/exec.py +2 -1
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api/process.py +8 -1
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api/rclone.py +24 -2
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api.egg-info/PKG-INFO +1 -1
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api.egg-info/SOURCES.txt +2 -0
- rclone_api-1.0.29/tests/test_diff.py +87 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/tests/test_mount_s3.py +3 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/tests/test_mount_webdav.py +3 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/.aiderignore +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/.github/workflows/lint.yml +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/.github/workflows/push_macos.yml +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/.github/workflows/push_ubuntu.yml +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/.github/workflows/push_win.yml +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/.gitignore +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/.pylintrc +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/.vscode/launch.json +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/.vscode/settings.json +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/.vscode/tasks.json +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/LICENSE +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/MANIFEST.in +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/README.md +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/clean +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/install +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/lint +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/requirements.testing.txt +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/setup.cfg +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/setup.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api/assets/example.txt +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api/cli.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api/cmd/list_files.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api/config.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api/convert.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api/dir.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api/dir_listing.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api/file.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api/filelist.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api/remote.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api/rpath.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api/util.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api/walk.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api.egg-info/dependency_links.txt +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api.egg-info/entry_points.txt +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api.egg-info/requires.txt +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/src/rclone_api.egg-info/top_level.txt +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/test +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/tests/test_cmd_list_files.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/tests/test_copy.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/tests/test_is_synced.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/tests/test_ls.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/tests/test_mount.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/tests/test_obscure.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/tests/test_remotes.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/tests/test_serve_webdav.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/tests/test_walk.py +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/tox.ini +0 -0
- {rclone_api-1.0.27 → rclone_api-1.0.29}/upload_package.sh +0 -0
@@ -1,4 +1,5 @@
|
|
1
1
|
from .config import Config
|
2
|
+
from .diff import DiffItem, DiffType
|
2
3
|
from .dir import Dir
|
3
4
|
from .dir_listing import DirListing
|
4
5
|
from .file import File
|
@@ -18,4 +19,6 @@ __all__ = [
|
|
18
19
|
"DirListing",
|
19
20
|
"FileList",
|
20
21
|
"Process",
|
22
|
+
"DiffItem",
|
23
|
+
"DiffType",
|
21
24
|
]
|
@@ -0,0 +1,119 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from enum import Enum
|
3
|
+
from queue import Queue
|
4
|
+
from threading import Thread
|
5
|
+
from typing import Generator
|
6
|
+
|
7
|
+
from rclone_api.process import Process
|
8
|
+
|
9
|
+
|
10
|
+
class DiffType(Enum):
|
11
|
+
EQUAL = "="
|
12
|
+
MISSING_ON_SRC = (
|
13
|
+
"-" # means path was missing on the source, so only in the destination
|
14
|
+
)
|
15
|
+
MISSING_ON_DST = (
|
16
|
+
"+" # means path was missing on the destination, so only in the source
|
17
|
+
)
|
18
|
+
DIFFERENT = "*" # means path was present in source and destination but different.
|
19
|
+
ERROR = "!" # means there was an error
|
20
|
+
|
21
|
+
|
22
|
+
@dataclass
|
23
|
+
class DiffItem:
|
24
|
+
type: DiffType
|
25
|
+
path: str
|
26
|
+
src_prefix: str
|
27
|
+
dst_prefix: str
|
28
|
+
|
29
|
+
def __str__(self) -> str:
|
30
|
+
return f"{self.type.value} {self.path}"
|
31
|
+
|
32
|
+
def __repr__(self) -> str:
|
33
|
+
return f"{self.type.name} {self.path}"
|
34
|
+
|
35
|
+
|
36
|
+
def _classify_diff(line: str, src_slug: str, dst_slug: str) -> DiffItem | None:
|
37
|
+
def _new(type: DiffType, path: str) -> DiffItem:
|
38
|
+
return DiffItem(type, path, src_prefix=src_slug, dst_prefix=dst_slug)
|
39
|
+
|
40
|
+
suffix = line[1:].strip() if len(line) > 0 else ""
|
41
|
+
if line.startswith(DiffType.EQUAL.value):
|
42
|
+
return _new(DiffType.EQUAL, suffix)
|
43
|
+
if line.startswith(DiffType.MISSING_ON_SRC.value):
|
44
|
+
return _new(DiffType.MISSING_ON_SRC, suffix)
|
45
|
+
if line.startswith(DiffType.MISSING_ON_DST.value):
|
46
|
+
# return DiffItem(DiffType.MISSING_ON_DST, f"{src_slug}/{suffix}")
|
47
|
+
return _new(DiffType.MISSING_ON_DST, suffix)
|
48
|
+
if line.startswith(DiffType.DIFFERENT.value):
|
49
|
+
# return DiffItem(DiffType.DIFFERENT, suffix)
|
50
|
+
return _new(DiffType.DIFFERENT, suffix)
|
51
|
+
if line.startswith(DiffType.ERROR.value):
|
52
|
+
# return DiffItem(DiffType.ERROR, suffix)
|
53
|
+
return _new(DiffType.ERROR, suffix)
|
54
|
+
return None
|
55
|
+
|
56
|
+
|
57
|
+
def _async_diff_stream_from_running_process(
|
58
|
+
running_process: Process,
|
59
|
+
src_slug: str,
|
60
|
+
dst_slug: str,
|
61
|
+
output: Queue[DiffItem | None],
|
62
|
+
) -> None:
|
63
|
+
count = 0
|
64
|
+
first_few_lines: list[str] = []
|
65
|
+
try:
|
66
|
+
assert running_process.stdout is not None
|
67
|
+
n_max = 10
|
68
|
+
for line in iter(running_process.stdout.readline, b""):
|
69
|
+
try:
|
70
|
+
line_str = line.decode("utf-8").strip()
|
71
|
+
if len(first_few_lines) < n_max:
|
72
|
+
first_few_lines.append(line_str)
|
73
|
+
# _classify_line_type
|
74
|
+
diff_item: DiffItem | None = _classify_diff(
|
75
|
+
line_str, src_slug, dst_slug
|
76
|
+
)
|
77
|
+
if diff_item is None:
|
78
|
+
# Some other output that we don't care about, debug print etc.
|
79
|
+
continue
|
80
|
+
output.put(diff_item)
|
81
|
+
count += 1
|
82
|
+
# print(f"unhandled: {line_str}")
|
83
|
+
except UnicodeDecodeError:
|
84
|
+
print("UnicodeDecodeError")
|
85
|
+
continue
|
86
|
+
output.put(None)
|
87
|
+
print("done")
|
88
|
+
except KeyboardInterrupt:
|
89
|
+
import _thread
|
90
|
+
|
91
|
+
print("KeyboardInterrupt")
|
92
|
+
output.put(None)
|
93
|
+
_thread.interrupt_main()
|
94
|
+
if count == 0:
|
95
|
+
first_lines_str = "\n".join(first_few_lines)
|
96
|
+
raise ValueError(
|
97
|
+
f"No output from rclone check, first few lines: {first_lines_str}"
|
98
|
+
)
|
99
|
+
|
100
|
+
|
101
|
+
def diff_stream_from_running_process(
|
102
|
+
running_process: Process,
|
103
|
+
src_slug: str,
|
104
|
+
dst_slug: str,
|
105
|
+
) -> Generator[DiffItem, None, None]:
|
106
|
+
output: Queue[DiffItem | None] = Queue()
|
107
|
+
# process_output_to_diff_stream(running_process, src_slug, dst_slug, output)
|
108
|
+
thread = Thread(
|
109
|
+
target=_async_diff_stream_from_running_process,
|
110
|
+
args=(running_process, src_slug, dst_slug, output),
|
111
|
+
daemon=True,
|
112
|
+
)
|
113
|
+
thread.start()
|
114
|
+
while True:
|
115
|
+
item = output.get()
|
116
|
+
if item is None:
|
117
|
+
break
|
118
|
+
yield item
|
119
|
+
thread.join(timeout=5)
|
@@ -19,7 +19,7 @@ class RcloneExec:
|
|
19
19
|
|
20
20
|
return rclone_execute(cmd, self.rclone_config, self.rclone_exe, check=check)
|
21
21
|
|
22
|
-
def launch_process(self, cmd: list[str]) -> Process:
|
22
|
+
def launch_process(self, cmd: list[str], capture: bool | None) -> Process:
|
23
23
|
"""Launch rclone process."""
|
24
24
|
|
25
25
|
args: ProcessArgs = ProcessArgs(
|
@@ -27,6 +27,7 @@ class RcloneExec:
|
|
27
27
|
rclone_conf=self.rclone_config,
|
28
28
|
rclone_exe=self.rclone_exe,
|
29
29
|
cmd_list=cmd,
|
30
|
+
capture_stdout=capture,
|
30
31
|
)
|
31
32
|
process = Process(args)
|
32
33
|
return process
|
@@ -56,6 +56,7 @@ class ProcessArgs:
|
|
56
56
|
rclone_exe: Path
|
57
57
|
cmd_list: list[str]
|
58
58
|
verbose: bool | None = None
|
59
|
+
capture_stdout: bool | None = None
|
59
60
|
|
60
61
|
|
61
62
|
class Process:
|
@@ -84,7 +85,13 @@ class Process:
|
|
84
85
|
if verbose:
|
85
86
|
cmd_str = subprocess.list2cmdline(self.cmd)
|
86
87
|
print(f"Running: {cmd_str}")
|
87
|
-
|
88
|
+
kwargs: dict = {}
|
89
|
+
kwargs["shell"] = False
|
90
|
+
if args.capture_stdout:
|
91
|
+
kwargs["stdout"] = subprocess.PIPE
|
92
|
+
kwargs["stderr"] = subprocess.STDOUT
|
93
|
+
|
94
|
+
self.process = subprocess.Popen(self.cmd, **kwargs) # type: ignore
|
88
95
|
|
89
96
|
def cleanup(self) -> None:
|
90
97
|
if self.tempdir and self.needs_cleanup:
|
@@ -14,6 +14,7 @@ from typing import Generator
|
|
14
14
|
from rclone_api import Dir
|
15
15
|
from rclone_api.config import Config
|
16
16
|
from rclone_api.convert import convert_to_filestr_list, convert_to_str
|
17
|
+
from rclone_api.diff import DiffItem, diff_stream_from_running_process
|
17
18
|
from rclone_api.dir_listing import DirListing
|
18
19
|
from rclone_api.exec import RcloneExec
|
19
20
|
from rclone_api.file import File
|
@@ -41,8 +42,8 @@ class Rclone:
|
|
41
42
|
def _run(self, cmd: list[str], check: bool = True) -> subprocess.CompletedProcess:
|
42
43
|
return self._exec.execute(cmd, check=check)
|
43
44
|
|
44
|
-
def _launch_process(self, cmd: list[str]) -> Process:
|
45
|
-
return self._exec.launch_process(cmd)
|
45
|
+
def _launch_process(self, cmd: list[str], capture: bool | None = None) -> Process:
|
46
|
+
return self._exec.launch_process(cmd, capture=capture)
|
46
47
|
|
47
48
|
def obscure(self, password: str) -> str:
|
48
49
|
"""Obscure a password for use in rclone config files."""
|
@@ -108,6 +109,27 @@ class Rclone:
|
|
108
109
|
out = [Remote(name=t, rclone=self) for t in tmp]
|
109
110
|
return out
|
110
111
|
|
112
|
+
def diff(self, src: str, dst: str) -> Generator[DiffItem, None, None]:
|
113
|
+
"""Be extra careful with the src and dst values. If you are off by one
|
114
|
+
parent directory, you will get a huge amount of false diffs."""
|
115
|
+
cmd = [
|
116
|
+
"check",
|
117
|
+
src,
|
118
|
+
dst,
|
119
|
+
"--checkers",
|
120
|
+
"1000",
|
121
|
+
"--log-level",
|
122
|
+
"INFO",
|
123
|
+
"--combined",
|
124
|
+
"-",
|
125
|
+
]
|
126
|
+
proc = self._launch_process(cmd, capture=True)
|
127
|
+
item: DiffItem
|
128
|
+
for item in diff_stream_from_running_process(proc, src_slug=src, dst_slug=src):
|
129
|
+
if item is None:
|
130
|
+
break
|
131
|
+
yield item
|
132
|
+
|
111
133
|
def walk(
|
112
134
|
self, path: Dir | Remote | str, max_depth: int = -1, breadth_first: bool = True
|
113
135
|
) -> Generator[DirListing, None, None]:
|
@@ -24,6 +24,7 @@ src/rclone_api/__init__.py
|
|
24
24
|
src/rclone_api/cli.py
|
25
25
|
src/rclone_api/config.py
|
26
26
|
src/rclone_api/convert.py
|
27
|
+
src/rclone_api/diff.py
|
27
28
|
src/rclone_api/dir.py
|
28
29
|
src/rclone_api/dir_listing.py
|
29
30
|
src/rclone_api/exec.py
|
@@ -45,6 +46,7 @@ src/rclone_api/assets/example.txt
|
|
45
46
|
src/rclone_api/cmd/list_files.py
|
46
47
|
tests/test_cmd_list_files.py
|
47
48
|
tests/test_copy.py
|
49
|
+
tests/test_diff.py
|
48
50
|
tests/test_is_synced.py
|
49
51
|
tests/test_ls.py
|
50
52
|
tests/test_mount.py
|
@@ -0,0 +1,87 @@
|
|
1
|
+
"""
|
2
|
+
Unit test file.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import os
|
6
|
+
import unittest
|
7
|
+
|
8
|
+
from dotenv import load_dotenv
|
9
|
+
|
10
|
+
from rclone_api import Config, Rclone
|
11
|
+
from rclone_api.diff import DiffItem, DiffType
|
12
|
+
|
13
|
+
load_dotenv()
|
14
|
+
|
15
|
+
BUCKET_NAME = os.getenv("BUCKET_NAME") # Default if not in .env
|
16
|
+
|
17
|
+
|
18
|
+
def _generate_rclone_config() -> Config:
|
19
|
+
|
20
|
+
# BUCKET_NAME = os.getenv("BUCKET_NAME", "TorrentBooks") # Default if not in .env
|
21
|
+
|
22
|
+
# Load additional environment variables
|
23
|
+
BUCKET_KEY_SECRET = os.getenv("BUCKET_KEY_SECRET")
|
24
|
+
BUCKET_KEY_PUBLIC = os.getenv("BUCKET_KEY_PUBLIC")
|
25
|
+
# BUCKET_URL = os.getenv("BUCKET_URL")
|
26
|
+
BUCKET_URL = "sfo3.digitaloceanspaces.com"
|
27
|
+
|
28
|
+
config_text = f"""
|
29
|
+
[dst]
|
30
|
+
type = s3
|
31
|
+
provider = DigitalOcean
|
32
|
+
access_key_id = {BUCKET_KEY_PUBLIC}
|
33
|
+
secret_access_key = {BUCKET_KEY_SECRET}
|
34
|
+
endpoint = {BUCKET_URL}
|
35
|
+
"""
|
36
|
+
|
37
|
+
out = Config(config_text)
|
38
|
+
return out
|
39
|
+
|
40
|
+
|
41
|
+
class RcloneDiffTests(unittest.TestCase):
|
42
|
+
"""Test rclone functionality."""
|
43
|
+
|
44
|
+
def setUp(self) -> None:
|
45
|
+
"""Check if all required environment variables are set before running tests."""
|
46
|
+
required_vars = [
|
47
|
+
"BUCKET_NAME",
|
48
|
+
"BUCKET_KEY_SECRET",
|
49
|
+
"BUCKET_KEY_PUBLIC",
|
50
|
+
"BUCKET_URL",
|
51
|
+
]
|
52
|
+
missing = [var for var in required_vars if not os.getenv(var)]
|
53
|
+
if missing:
|
54
|
+
self.skipTest(
|
55
|
+
f"Missing required environment variables: {', '.join(missing)}"
|
56
|
+
)
|
57
|
+
os.environ["RCLONE_API_VERBOSE"] = "1"
|
58
|
+
|
59
|
+
def test_diff(self) -> None:
|
60
|
+
"""Test copying a single file to remote storage."""
|
61
|
+
rclone = Rclone(_generate_rclone_config())
|
62
|
+
item: DiffItem
|
63
|
+
all: list[DiffItem] = []
|
64
|
+
for item in rclone.diff("dst:rclone-api-unit-test", "dst:rclone-api-unit-test"):
|
65
|
+
self.assertEqual(
|
66
|
+
item.type, DiffType.EQUAL
|
67
|
+
) # should be equal because same repo
|
68
|
+
all.append(item)
|
69
|
+
self.assertGreater(len(all), 10)
|
70
|
+
msg = "\n".join([str(item) for item in all])
|
71
|
+
print(msg)
|
72
|
+
|
73
|
+
all.clear()
|
74
|
+
for item in rclone.diff(
|
75
|
+
"dst:rclone-api-unit-test/test", "dst:rclone-api-unit-test/test"
|
76
|
+
):
|
77
|
+
self.assertEqual(item.type, DiffType.EQUAL)
|
78
|
+
print(item)
|
79
|
+
all.append(item)
|
80
|
+
|
81
|
+
msg = "\n".join([str(item) for item in all])
|
82
|
+
print(msg)
|
83
|
+
print("done")
|
84
|
+
|
85
|
+
|
86
|
+
if __name__ == "__main__":
|
87
|
+
unittest.main()
|
@@ -13,6 +13,8 @@ from rclone_api import Config, Process, Rclone
|
|
13
13
|
|
14
14
|
load_dotenv()
|
15
15
|
|
16
|
+
_ENABLED = False
|
17
|
+
|
16
18
|
|
17
19
|
def _generate_rclone_config() -> Config:
|
18
20
|
# Load environment variables
|
@@ -60,6 +62,7 @@ class RcloneMountS3Tests(unittest.TestCase):
|
|
60
62
|
os.environ["RCLONE_API_VERBOSE"] = "1"
|
61
63
|
self.rclone = Rclone(_generate_rclone_config())
|
62
64
|
|
65
|
+
@unittest.skipUnless(_ENABLED, "Test is disabled by default")
|
63
66
|
def test_mount(self) -> None:
|
64
67
|
"""Test mounting a remote bucket."""
|
65
68
|
remote_path = f"dst:{self.bucket_name}"
|
@@ -10,6 +10,8 @@ from dotenv import load_dotenv
|
|
10
10
|
|
11
11
|
from rclone_api import Config, Process, Rclone, Remote
|
12
12
|
|
13
|
+
_ENABLED = False
|
14
|
+
|
13
15
|
load_dotenv()
|
14
16
|
|
15
17
|
BUCKET_NAME = os.getenv("BUCKET_NAME") # Default if not in .env
|
@@ -66,6 +68,7 @@ class RcloneMountWebdavTester(unittest.TestCase):
|
|
66
68
|
)
|
67
69
|
os.environ["RCLONE_API_VERBOSE"] = "1"
|
68
70
|
|
71
|
+
@unittest.skipIf(not _ENABLED, "Test not enabled")
|
69
72
|
def test_serve_webdav_and_mount(self) -> None:
|
70
73
|
"""Test basic NFS serve functionality."""
|
71
74
|
port = 8090
|
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
|
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
|
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
|