dploy 0.1.3__py3-none-any.whl
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.
- dploy/__init__.py +35 -0
- dploy/__main__.py +17 -0
- dploy/actions.py +217 -0
- dploy/cli.py +114 -0
- dploy/error.py +163 -0
- dploy/ignore.py +67 -0
- dploy/linkcmd.py +81 -0
- dploy/main.py +125 -0
- dploy/stowcmd.py +442 -0
- dploy/utils.py +123 -0
- dploy/version.py +5 -0
- dploy-0.1.3.dist-info/METADATA +78 -0
- dploy-0.1.3.dist-info/RECORD +16 -0
- dploy-0.1.3.dist-info/WHEEL +4 -0
- dploy-0.1.3.dist-info/entry_points.txt +3 -0
- dploy-0.1.3.dist-info/licenses/LICENSE.txt +19 -0
dploy/main.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The logic and workings behind the stow and unstow sub-commands
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
import pathlib
|
|
7
|
+
from dploy import actions
|
|
8
|
+
from dploy import error
|
|
9
|
+
from dploy import ignore
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# pylint: disable=too-few-public-methods
|
|
13
|
+
class Input:
|
|
14
|
+
"""
|
|
15
|
+
Input validator abstract base class
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, errors, subcmd):
|
|
19
|
+
self.errors = errors
|
|
20
|
+
self.subcmd = subcmd
|
|
21
|
+
|
|
22
|
+
def is_valid(self, sources, dest):
|
|
23
|
+
"""
|
|
24
|
+
Checks if the passes in source and dest are valid
|
|
25
|
+
"""
|
|
26
|
+
is_input_valid = True
|
|
27
|
+
if not self._is_there_duplicate_sources(sources) and self._is_valid_dest(dest):
|
|
28
|
+
for source in sources:
|
|
29
|
+
if not self._is_valid_source(source):
|
|
30
|
+
is_input_valid = False
|
|
31
|
+
else:
|
|
32
|
+
is_input_valid = False
|
|
33
|
+
|
|
34
|
+
return is_input_valid
|
|
35
|
+
|
|
36
|
+
def _is_there_duplicate_sources(self, sources):
|
|
37
|
+
"""
|
|
38
|
+
Checks sources to see if there are any duplicates
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
is_there_duplicates = False
|
|
42
|
+
|
|
43
|
+
tally = defaultdict(int)
|
|
44
|
+
for source in sources:
|
|
45
|
+
tally[source] += 1
|
|
46
|
+
|
|
47
|
+
for source, count in tally.items():
|
|
48
|
+
if count > 1:
|
|
49
|
+
is_there_duplicates = True
|
|
50
|
+
self.errors.add(error.DuplicateSource(self.subcmd, source))
|
|
51
|
+
|
|
52
|
+
return is_there_duplicates
|
|
53
|
+
|
|
54
|
+
def _is_valid_dest(self, dest):
|
|
55
|
+
"""
|
|
56
|
+
Abstract method to check if the dest input to a sub-command is valid
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
def _is_valid_source(self, source):
|
|
61
|
+
"""
|
|
62
|
+
Abstract method to check if the source input to a sub-command is valid
|
|
63
|
+
"""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# pylint: disable=too-few-public-methods
|
|
68
|
+
class AbstractBaseSubCommand:
|
|
69
|
+
"""
|
|
70
|
+
An abstract class to unify shared functionality in stow commands
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
# pylint: disable=too-many-arguments
|
|
74
|
+
def __init__(self, subcmd, sources, dest, is_silent, is_dry_run, ignore_patterns):
|
|
75
|
+
self.subcmd = subcmd
|
|
76
|
+
|
|
77
|
+
self.actions = actions.Actions(is_silent, is_dry_run)
|
|
78
|
+
self.errors = error.Errors(is_silent)
|
|
79
|
+
|
|
80
|
+
self.is_silent = is_silent
|
|
81
|
+
self.is_dry_run = is_dry_run
|
|
82
|
+
|
|
83
|
+
self.dest_input = pathlib.Path(dest)
|
|
84
|
+
source_inputs = [pathlib.Path(source) for source in sources]
|
|
85
|
+
|
|
86
|
+
if self._is_valid_input(source_inputs, self.dest_input):
|
|
87
|
+
for source in source_inputs:
|
|
88
|
+
self.ignore = ignore.Ignore(ignore_patterns, source)
|
|
89
|
+
|
|
90
|
+
if self.ignore.should_ignore(source):
|
|
91
|
+
self.ignore.ignore(source)
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
self._collect_actions(source, self.dest_input)
|
|
95
|
+
|
|
96
|
+
self._check_for_other_actions()
|
|
97
|
+
self._execute_actions()
|
|
98
|
+
|
|
99
|
+
def _check_for_other_actions(self):
|
|
100
|
+
"""
|
|
101
|
+
Abstract method for examine the existing action to see if more actions
|
|
102
|
+
need to be added or if some actions need to be removed.
|
|
103
|
+
"""
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
def _is_valid_input(self, sources, dest):
|
|
107
|
+
"""
|
|
108
|
+
Abstract method to check if the input to a sub-command is valid
|
|
109
|
+
"""
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
def _collect_actions(self, source, dest):
|
|
113
|
+
"""
|
|
114
|
+
Abstract method that collects the actions required to complete a
|
|
115
|
+
sub-command.
|
|
116
|
+
"""
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
def _execute_actions(self):
|
|
120
|
+
"""
|
|
121
|
+
Either executes collected actions by a sub command or raises collected
|
|
122
|
+
exceptions.
|
|
123
|
+
"""
|
|
124
|
+
self.errors.handle()
|
|
125
|
+
self.actions.execute()
|
dploy/stowcmd.py
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The logic and workings behind the stow and unstow sub-commands
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from collections import Counter
|
|
6
|
+
import pathlib
|
|
7
|
+
from dploy import actions
|
|
8
|
+
from dploy import utils
|
|
9
|
+
from dploy import error
|
|
10
|
+
from dploy import main
|
|
11
|
+
from dploy import ignore
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# pylint: disable=too-few-public-methods
|
|
15
|
+
class AbstractBaseStow(main.AbstractBaseSubCommand):
|
|
16
|
+
"""
|
|
17
|
+
Abstract Base class that contains the shared logic for all of the stow
|
|
18
|
+
commands
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
# pylint: disable=too-many-arguments
|
|
22
|
+
def __init__(self, subcmd, source, dest, is_silent, is_dry_run, ignore_patterns):
|
|
23
|
+
self.is_unfolding = False
|
|
24
|
+
super().__init__(subcmd, source, dest, is_silent, is_dry_run, ignore_patterns)
|
|
25
|
+
|
|
26
|
+
def _is_valid_input(self, sources, dest):
|
|
27
|
+
"""
|
|
28
|
+
Check to see if the input is valid
|
|
29
|
+
"""
|
|
30
|
+
return StowInput(self.errors, self.subcmd).is_valid(sources, dest)
|
|
31
|
+
|
|
32
|
+
def get_directory_contents(self, directory):
|
|
33
|
+
"""
|
|
34
|
+
Get the contents of a directory while handling errors that may occur
|
|
35
|
+
"""
|
|
36
|
+
contents = []
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
contents = utils.get_directory_contents(directory)
|
|
40
|
+
except PermissionError:
|
|
41
|
+
self.errors.add(error.PermissionDenied(self.subcmd, directory))
|
|
42
|
+
except FileNotFoundError:
|
|
43
|
+
self.errors.add(error.NoSuchFileOrDirectory(self.subcmd, directory))
|
|
44
|
+
except NotADirectoryError:
|
|
45
|
+
self.errors.add(error.NoSuchDirectory(self.subcmd, directory))
|
|
46
|
+
|
|
47
|
+
return contents
|
|
48
|
+
|
|
49
|
+
def _are_same_file(self, source, dest):
|
|
50
|
+
"""
|
|
51
|
+
Abstract method that handles the case when the source and dest are the
|
|
52
|
+
same file when collecting actions
|
|
53
|
+
"""
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
def _are_directories(self, source, dest):
|
|
57
|
+
"""
|
|
58
|
+
Abstract method that handles the case when the source and dest are directories
|
|
59
|
+
same file when collecting actions
|
|
60
|
+
"""
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
def _are_other(self, source, dest):
|
|
64
|
+
"""
|
|
65
|
+
Abstract method that handles all other cases what to do if no particular
|
|
66
|
+
condition is true cases are found
|
|
67
|
+
"""
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
def _collect_actions_existing_dest(self, source, dest):
|
|
71
|
+
"""
|
|
72
|
+
_collect_actions() helper to collect required actions to perform a stow
|
|
73
|
+
command when the destination already exists
|
|
74
|
+
"""
|
|
75
|
+
if utils.is_same_file(dest, source):
|
|
76
|
+
if dest.is_symlink() or self.is_unfolding:
|
|
77
|
+
self._are_same_file(source, dest)
|
|
78
|
+
else:
|
|
79
|
+
self.errors.add(error.SourceIsSameAsDest(self.subcmd, dest.parent))
|
|
80
|
+
|
|
81
|
+
elif dest.is_dir() and source.is_dir:
|
|
82
|
+
self._are_directories(source, dest)
|
|
83
|
+
else:
|
|
84
|
+
self.errors.add(error.ConflictsWithExistingFile(self.subcmd, source, dest))
|
|
85
|
+
|
|
86
|
+
def _collect_actions(self, source, dest):
|
|
87
|
+
"""
|
|
88
|
+
Concrete method to collect required actions to perform a stow
|
|
89
|
+
sub-command
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
if self.ignore.should_ignore(source):
|
|
93
|
+
self.ignore.ignore(source)
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
if not StowInput(self.errors, self.subcmd).is_valid_collection_input(
|
|
97
|
+
source, dest
|
|
98
|
+
):
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
sources = self.get_directory_contents(source)
|
|
102
|
+
|
|
103
|
+
for subsources in sources:
|
|
104
|
+
if self.ignore.should_ignore(subsources):
|
|
105
|
+
self.ignore.ignore(subsources)
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
dest_path = dest / pathlib.Path(subsources.name)
|
|
109
|
+
|
|
110
|
+
does_dest_path_exist = False
|
|
111
|
+
try:
|
|
112
|
+
does_dest_path_exist = dest_path.exists()
|
|
113
|
+
except PermissionError:
|
|
114
|
+
self.errors.add(error.PermissionDenied(self.subcmd, dest_path))
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
if does_dest_path_exist:
|
|
118
|
+
self._collect_actions_existing_dest(subsources, dest_path)
|
|
119
|
+
elif dest_path.is_symlink():
|
|
120
|
+
self.errors.add(
|
|
121
|
+
error.ConflictsWithExistingLink(self.subcmd, subsources, dest_path)
|
|
122
|
+
)
|
|
123
|
+
elif not dest_path.parent.exists() and not self.is_unfolding:
|
|
124
|
+
self.errors.add(error.NoSuchDirectory(self.subcmd, dest_path.parent))
|
|
125
|
+
else:
|
|
126
|
+
self._are_other(subsources, dest_path)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# pylint: disable=too-few-public-methods
|
|
130
|
+
class Stow(AbstractBaseStow):
|
|
131
|
+
"""
|
|
132
|
+
Concrete class implementation of the stow sub-command
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
# pylint: disable=too-many-arguments
|
|
136
|
+
def __init__(
|
|
137
|
+
self, source, dest, is_silent=True, is_dry_run=False, ignore_patterns=None
|
|
138
|
+
):
|
|
139
|
+
super().__init__("stow", source, dest, is_silent, is_dry_run, ignore_patterns)
|
|
140
|
+
|
|
141
|
+
def _unfold(self, source, dest):
|
|
142
|
+
"""
|
|
143
|
+
Method unfold a destination directory
|
|
144
|
+
"""
|
|
145
|
+
self.is_unfolding = True
|
|
146
|
+
self.actions.add(actions.UnLink(self.subcmd, dest))
|
|
147
|
+
self.actions.add(actions.MakeDirectory(self.subcmd, dest))
|
|
148
|
+
self._collect_actions(source, dest)
|
|
149
|
+
self.is_unfolding = False
|
|
150
|
+
|
|
151
|
+
def _handle_duplicate_actions(self):
|
|
152
|
+
"""
|
|
153
|
+
check for symbolic link actions that would cause conflicting symbolic
|
|
154
|
+
links to the same destination. Also check for actions that conflict but
|
|
155
|
+
are candidates for unfolding instead.
|
|
156
|
+
"""
|
|
157
|
+
has_conflicts = False
|
|
158
|
+
dupes = self.actions.get_duplicates()
|
|
159
|
+
|
|
160
|
+
if len(dupes) == 0:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
for indices in dupes:
|
|
164
|
+
first_action = self.actions.actions[indices[0]]
|
|
165
|
+
remaining_actions = [self.actions.actions[i] for i in indices[1:]]
|
|
166
|
+
|
|
167
|
+
if first_action.source.is_dir():
|
|
168
|
+
self._unfold(first_action.source, first_action.dest)
|
|
169
|
+
|
|
170
|
+
for action in remaining_actions:
|
|
171
|
+
self.is_unfolding = True
|
|
172
|
+
self._collect_actions(action.source, action.dest)
|
|
173
|
+
self.is_unfolding = False
|
|
174
|
+
else:
|
|
175
|
+
duplicate_action_sources = [
|
|
176
|
+
str(self.actions.actions[i].source) for i in indices
|
|
177
|
+
]
|
|
178
|
+
self.errors.add(
|
|
179
|
+
error.ConflictsWithAnotherSource(
|
|
180
|
+
self.subcmd, duplicate_action_sources
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
has_conflicts = True
|
|
184
|
+
|
|
185
|
+
if has_conflicts:
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
# remove duplicates
|
|
189
|
+
for indices in dupes:
|
|
190
|
+
for index in reversed(indices[1:]):
|
|
191
|
+
del self.actions.actions[index]
|
|
192
|
+
|
|
193
|
+
self._handle_duplicate_actions()
|
|
194
|
+
|
|
195
|
+
def _check_for_other_actions(self):
|
|
196
|
+
self._handle_duplicate_actions()
|
|
197
|
+
|
|
198
|
+
def _are_same_file(self, source, dest):
|
|
199
|
+
"""
|
|
200
|
+
what to do if source and dest are the same files
|
|
201
|
+
"""
|
|
202
|
+
if self.is_unfolding:
|
|
203
|
+
self.actions.add(actions.SymbolicLink(self.subcmd, source, dest))
|
|
204
|
+
else:
|
|
205
|
+
self.actions.add(actions.AlreadyLinked(self.subcmd, source, dest))
|
|
206
|
+
|
|
207
|
+
def _are_directories(self, source, dest):
|
|
208
|
+
if dest.is_symlink():
|
|
209
|
+
self._unfold(dest.resolve(), dest)
|
|
210
|
+
self._collect_actions(source, dest)
|
|
211
|
+
|
|
212
|
+
def _are_other(self, source, dest):
|
|
213
|
+
self.actions.add(actions.SymbolicLink(self.subcmd, source, dest))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# pylint: disable=too-few-public-methods
|
|
217
|
+
class UnStow(AbstractBaseStow):
|
|
218
|
+
"""
|
|
219
|
+
Concrete class implementation of the unstow sub-command
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
# pylint: disable=too-many-arguments
|
|
223
|
+
def __init__(
|
|
224
|
+
self, source, dest, is_silent=True, is_dry_run=False, ignore_patterns=None
|
|
225
|
+
):
|
|
226
|
+
super().__init__("unstow", source, dest, is_silent, is_dry_run, ignore_patterns)
|
|
227
|
+
|
|
228
|
+
def _are_same_file(self, source, dest):
|
|
229
|
+
"""
|
|
230
|
+
what to do if source and dest are the same files
|
|
231
|
+
"""
|
|
232
|
+
self.actions.add(actions.UnLink(self.subcmd, dest))
|
|
233
|
+
|
|
234
|
+
def _are_directories(self, source, dest):
|
|
235
|
+
self._collect_actions(source, dest)
|
|
236
|
+
|
|
237
|
+
def _are_other(self, source, dest):
|
|
238
|
+
self.actions.add(actions.AlreadyUnlinked(self.subcmd, source, dest))
|
|
239
|
+
|
|
240
|
+
def _check_for_other_actions(self):
|
|
241
|
+
self._collect_folding_actions()
|
|
242
|
+
|
|
243
|
+
def _collect_folding_actions(self):
|
|
244
|
+
"""
|
|
245
|
+
find candidates for folding i.e. when a directory contains symlinks to
|
|
246
|
+
files that all share the same parent directory
|
|
247
|
+
"""
|
|
248
|
+
for parent in self.actions.get_unlink_target_parents():
|
|
249
|
+
items = utils.get_directory_contents(parent)
|
|
250
|
+
other_links_parents = []
|
|
251
|
+
other_links = []
|
|
252
|
+
source_parent = None
|
|
253
|
+
is_normal_files_detected = False
|
|
254
|
+
|
|
255
|
+
for item in items:
|
|
256
|
+
if item not in self.actions.get_unlink_targets():
|
|
257
|
+
does_item_exist = False
|
|
258
|
+
try:
|
|
259
|
+
does_item_exist = item.exists()
|
|
260
|
+
except PermissionError:
|
|
261
|
+
self.errors.add(error.PermissionDenied(self.subcmd, item))
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
if does_item_exist and item.is_symlink():
|
|
265
|
+
source_parent = item.resolve().parent
|
|
266
|
+
other_links_parents.append(item.resolve().parent)
|
|
267
|
+
other_links.append(item)
|
|
268
|
+
else:
|
|
269
|
+
is_normal_files_detected = True
|
|
270
|
+
break
|
|
271
|
+
|
|
272
|
+
if not is_normal_files_detected:
|
|
273
|
+
other_links_parent_count = len(Counter(other_links_parents))
|
|
274
|
+
|
|
275
|
+
if other_links_parent_count == 1:
|
|
276
|
+
assert source_parent is not None
|
|
277
|
+
if utils.is_same_files(
|
|
278
|
+
utils.get_directory_contents(source_parent), other_links
|
|
279
|
+
):
|
|
280
|
+
self._fold(source_parent, parent)
|
|
281
|
+
|
|
282
|
+
elif other_links_parent_count == 0 and not utils.is_same_file(
|
|
283
|
+
parent, self.dest_input
|
|
284
|
+
):
|
|
285
|
+
self.actions.add(actions.RemoveDirectory(self.subcmd, parent))
|
|
286
|
+
|
|
287
|
+
def _fold(self, source, dest):
|
|
288
|
+
"""
|
|
289
|
+
add the required actions for folding
|
|
290
|
+
"""
|
|
291
|
+
self._collect_actions(source, dest)
|
|
292
|
+
self.actions.add(actions.RemoveDirectory(self.subcmd, dest))
|
|
293
|
+
self.actions.add(actions.SymbolicLink(self.subcmd, source, dest))
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class StowInput(main.Input):
|
|
297
|
+
"""
|
|
298
|
+
Input validator for the link command
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
def _is_valid_dest(self, dest):
|
|
302
|
+
"""
|
|
303
|
+
Check if the test argument is valid
|
|
304
|
+
"""
|
|
305
|
+
result = True
|
|
306
|
+
|
|
307
|
+
if not dest.is_dir():
|
|
308
|
+
self.errors.add(error.NoSuchDirectoryToSubcmdInto(self.subcmd, dest))
|
|
309
|
+
result = False
|
|
310
|
+
else:
|
|
311
|
+
if not utils.is_directory_writable(dest):
|
|
312
|
+
self.errors.add(
|
|
313
|
+
error.InsufficientPermissionsToSubcmdTo(self.subcmd, dest)
|
|
314
|
+
)
|
|
315
|
+
result = False
|
|
316
|
+
|
|
317
|
+
if not utils.is_directory_readable(dest):
|
|
318
|
+
self.errors.add(
|
|
319
|
+
error.InsufficientPermissionsToSubcmdTo(self.subcmd, dest)
|
|
320
|
+
)
|
|
321
|
+
result = False
|
|
322
|
+
|
|
323
|
+
if not utils.is_directory_executable(dest):
|
|
324
|
+
self.errors.add(
|
|
325
|
+
error.InsufficientPermissionsToSubcmdTo(self.subcmd, dest)
|
|
326
|
+
)
|
|
327
|
+
result = False
|
|
328
|
+
|
|
329
|
+
return result
|
|
330
|
+
|
|
331
|
+
def _is_valid_source(self, source):
|
|
332
|
+
"""
|
|
333
|
+
Check if the source argument is valid
|
|
334
|
+
"""
|
|
335
|
+
result = True
|
|
336
|
+
|
|
337
|
+
if not source.is_dir():
|
|
338
|
+
self.errors.add(error.NoSuchDirectory(self.subcmd, source))
|
|
339
|
+
result = False
|
|
340
|
+
else:
|
|
341
|
+
if not utils.is_directory_readable(source):
|
|
342
|
+
self.errors.add(
|
|
343
|
+
error.InsufficientPermissionsToSubcmdFrom(self.subcmd, source)
|
|
344
|
+
)
|
|
345
|
+
result = False
|
|
346
|
+
|
|
347
|
+
if not utils.is_directory_executable(source):
|
|
348
|
+
self.errors.add(
|
|
349
|
+
error.InsufficientPermissionsToSubcmdFrom(self.subcmd, source)
|
|
350
|
+
)
|
|
351
|
+
result = False
|
|
352
|
+
|
|
353
|
+
return result
|
|
354
|
+
|
|
355
|
+
def is_valid_collection_input(self, source, dest):
|
|
356
|
+
"""
|
|
357
|
+
Helper to validate the source and dest parameters passed to
|
|
358
|
+
_collect_actions()
|
|
359
|
+
"""
|
|
360
|
+
result = True
|
|
361
|
+
if not self._is_valid_source(source):
|
|
362
|
+
result = False
|
|
363
|
+
|
|
364
|
+
if dest.exists():
|
|
365
|
+
if not self._is_valid_dest(dest):
|
|
366
|
+
result = False
|
|
367
|
+
return result
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
# pylint: disable=too-few-public-methods
|
|
371
|
+
class Clean(main.AbstractBaseSubCommand):
|
|
372
|
+
"""
|
|
373
|
+
Abstract Base class that contains the shared logic for all of the stow
|
|
374
|
+
commands
|
|
375
|
+
"""
|
|
376
|
+
|
|
377
|
+
# pylint: disable=too-many-arguments
|
|
378
|
+
def __init__(self, source, dest, is_silent, is_dry_run, ignore_patterns):
|
|
379
|
+
self.source = [pathlib.Path(s) for s in source]
|
|
380
|
+
self.dest = pathlib.Path(dest)
|
|
381
|
+
self.ignore_patterns = ignore_patterns
|
|
382
|
+
super().__init__("clean", source, dest, is_silent, is_dry_run, ignore_patterns)
|
|
383
|
+
|
|
384
|
+
def _is_valid_input(self, sources, dest):
|
|
385
|
+
"""
|
|
386
|
+
Check to see if the input is valid
|
|
387
|
+
"""
|
|
388
|
+
return StowInput(self.errors, self.subcmd).is_valid(sources, dest)
|
|
389
|
+
|
|
390
|
+
def get_directory_contents(self, directory):
|
|
391
|
+
"""
|
|
392
|
+
Get the contents of a directory while handling errors that may occur
|
|
393
|
+
"""
|
|
394
|
+
contents = []
|
|
395
|
+
|
|
396
|
+
try:
|
|
397
|
+
contents = utils.get_directory_contents(directory)
|
|
398
|
+
except PermissionError:
|
|
399
|
+
self.errors.add(error.PermissionDenied(self.subcmd, directory))
|
|
400
|
+
except FileNotFoundError:
|
|
401
|
+
self.errors.add(error.NoSuchFileOrDirectory(self.subcmd, directory))
|
|
402
|
+
except NotADirectoryError:
|
|
403
|
+
self.errors.add(error.NoSuchDirectory(self.subcmd, directory))
|
|
404
|
+
|
|
405
|
+
return contents
|
|
406
|
+
|
|
407
|
+
def _collect_clean_actions(self, source, source_names, dest):
|
|
408
|
+
subdests = utils.get_directory_contents(dest)
|
|
409
|
+
for subdest in subdests:
|
|
410
|
+
if subdest.is_symlink():
|
|
411
|
+
link_target = utils.readlink(subdest, absolute_target=True)
|
|
412
|
+
if not link_target.exists() and not source_names.isdisjoint(
|
|
413
|
+
set(link_target.parents)
|
|
414
|
+
):
|
|
415
|
+
self.actions.add(actions.UnLink(self.subcmd, subdest))
|
|
416
|
+
elif subdest.is_dir():
|
|
417
|
+
self._collect_clean_actions(source, source_names, subdest)
|
|
418
|
+
|
|
419
|
+
def _check_for_other_actions(self):
|
|
420
|
+
"""
|
|
421
|
+
Concrete method to collect required actions to perform a stow
|
|
422
|
+
sub-command
|
|
423
|
+
"""
|
|
424
|
+
valid_files = []
|
|
425
|
+
for a_file in self.source:
|
|
426
|
+
self.ignore = ignore.Ignore(self.ignore_patterns, a_file)
|
|
427
|
+
if self.ignore.should_ignore(a_file):
|
|
428
|
+
self.ignore.ignore(a_file)
|
|
429
|
+
continue
|
|
430
|
+
else:
|
|
431
|
+
valid_files.append(a_file)
|
|
432
|
+
|
|
433
|
+
if not StowInput(self.errors, self.subcmd).is_valid_collection_input(
|
|
434
|
+
a_file, self.dest
|
|
435
|
+
):
|
|
436
|
+
return
|
|
437
|
+
|
|
438
|
+
# NOTE: an option to make clean more aggressive is to change f.name to
|
|
439
|
+
# f.parent this could a be a good --option
|
|
440
|
+
files_names = [utils.get_absolute_path(f.name) for f in valid_files]
|
|
441
|
+
files_names_set = set(files_names)
|
|
442
|
+
self._collect_clean_actions(valid_files, files_names_set, self.dest)
|
dploy/utils.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A module that contains utility function mainly used for operations in the
|
|
3
|
+
missing from the pathlib module.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import pathlib
|
|
8
|
+
import shutil
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_directory_contents(directory):
|
|
12
|
+
"""
|
|
13
|
+
return a sorted list of the contents of a directory
|
|
14
|
+
"""
|
|
15
|
+
contents = []
|
|
16
|
+
|
|
17
|
+
for child in directory.iterdir():
|
|
18
|
+
contents.append(child)
|
|
19
|
+
|
|
20
|
+
return sorted(contents)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def rmtree(tree):
|
|
24
|
+
"""
|
|
25
|
+
a wrapper around shutil.rmtree to recursively delete a directory specified
|
|
26
|
+
by a pathlib.Path object
|
|
27
|
+
"""
|
|
28
|
+
shutil.rmtree(str(tree))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_same_file(file1, file2):
|
|
32
|
+
"""
|
|
33
|
+
test if two pathlib.Path() objects are the same file
|
|
34
|
+
|
|
35
|
+
TODO: consider using pathlib.Path.samefile() instead
|
|
36
|
+
NOTE: this can raise exception FileNotFoundError
|
|
37
|
+
"""
|
|
38
|
+
return file1.resolve() == file2.resolve()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_same_files(files1, files2):
|
|
42
|
+
"""
|
|
43
|
+
test if two collection of files are equivalent
|
|
44
|
+
"""
|
|
45
|
+
files1_resolved = [f.resolve() for f in files1]
|
|
46
|
+
files2_resolved = [f.resolve() for f in files2]
|
|
47
|
+
return files1_resolved == files2_resolved
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_absolute_path(file):
|
|
51
|
+
"""
|
|
52
|
+
get the absolute path of a pathlib.Path() object
|
|
53
|
+
"""
|
|
54
|
+
absolute_path = os.path.abspath(os.path.expanduser(str(file)))
|
|
55
|
+
return pathlib.Path(absolute_path)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_relative_path(path, start_at):
|
|
59
|
+
"""
|
|
60
|
+
get the relative path of a pathlib.Path() object
|
|
61
|
+
|
|
62
|
+
TODO: consider using Path.relative_to() with walk_up=True (3.12+)
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
relative_path = os.path.relpath(str(path), str(start_at))
|
|
66
|
+
except ValueError: # when a relative path does not exist
|
|
67
|
+
return get_absolute_path(path)
|
|
68
|
+
|
|
69
|
+
return pathlib.Path(relative_path)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_file_readable(a_file):
|
|
73
|
+
"""
|
|
74
|
+
check if a pathlib.Path() file is readable
|
|
75
|
+
"""
|
|
76
|
+
return os.access(str(a_file), os.R_OK)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def is_file_writable(a_file):
|
|
80
|
+
"""
|
|
81
|
+
check if a pathlib.Path() file is writable
|
|
82
|
+
"""
|
|
83
|
+
return os.access(str(a_file), os.W_OK)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def is_directory_readable(directory):
|
|
87
|
+
"""
|
|
88
|
+
check if a pathlib.Path() directory is readable
|
|
89
|
+
"""
|
|
90
|
+
return os.access(str(directory), os.R_OK)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def is_directory_writable(directory):
|
|
94
|
+
"""
|
|
95
|
+
check if a pathlib.Path() directory is writable
|
|
96
|
+
"""
|
|
97
|
+
return os.access(str(directory), os.W_OK)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def is_directory_executable(directory):
|
|
101
|
+
"""
|
|
102
|
+
check if a pathlib.Path() directory is executable
|
|
103
|
+
"""
|
|
104
|
+
return os.access(str(directory), os.X_OK)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def readlink(path, absolute_target=False):
|
|
108
|
+
"""
|
|
109
|
+
get the target of a symbolic link passed as a pathlib.Path object and
|
|
110
|
+
provide the option to return an absolute path even if the link target is
|
|
111
|
+
relative.
|
|
112
|
+
|
|
113
|
+
Note: we can't use pathlib.Path.resolve because it doesn't work for broken
|
|
114
|
+
links (it resolves the target, which may not exist)
|
|
115
|
+
|
|
116
|
+
"""
|
|
117
|
+
link_target = os.readlink(str(path))
|
|
118
|
+
path_dir = os.path.dirname(str(path))
|
|
119
|
+
if absolute_target:
|
|
120
|
+
if not os.path.isabs(link_target):
|
|
121
|
+
link_target = os.path.join(path_dir, link_target)
|
|
122
|
+
return pathlib.Path(link_target)
|
|
123
|
+
return pathlib.Path(link_target)
|