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/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dploy script is an attempt at creating a clone of GNU stow that will work on
|
|
3
|
+
Windows as well as *nix
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dploy import linkcmd
|
|
7
|
+
from dploy import stowcmd
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def stow(sources, dest, is_silent=True, is_dry_run=False, ignore_patterns=None):
|
|
11
|
+
"""
|
|
12
|
+
sub command stow
|
|
13
|
+
"""
|
|
14
|
+
stowcmd.Stow(sources, dest, is_silent, is_dry_run, ignore_patterns)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def unstow(sources, dest, is_silent=True, is_dry_run=False, ignore_patterns=None):
|
|
18
|
+
"""
|
|
19
|
+
sub command unstow
|
|
20
|
+
"""
|
|
21
|
+
stowcmd.UnStow(sources, dest, is_silent, is_dry_run, ignore_patterns)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def clean(sources, dest, is_silent=True, is_dry_run=False, ignore_patterns=None):
|
|
25
|
+
"""
|
|
26
|
+
sub command clean
|
|
27
|
+
"""
|
|
28
|
+
stowcmd.Clean(sources, dest, is_silent, is_dry_run, ignore_patterns)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def link(source, dest, is_silent=True, is_dry_run=False, ignore_patterns=None):
|
|
32
|
+
"""
|
|
33
|
+
sub command link
|
|
34
|
+
"""
|
|
35
|
+
linkcmd.Link(source, dest, is_silent, is_dry_run, ignore_patterns)
|
dploy/__main__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
The entry point when dploy is called as a module
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dploy import cli
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
"""
|
|
11
|
+
main entry point when using dploy from the command line
|
|
12
|
+
"""
|
|
13
|
+
cli.run()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if __name__ == "__main__":
|
|
17
|
+
main()
|
dploy/actions.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains the actions that are combined to perform dploy's sub
|
|
3
|
+
commands
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from dploy import utils
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Actions:
|
|
11
|
+
"""
|
|
12
|
+
A class that collects and executes action objects
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, is_silent, is_dry_run):
|
|
16
|
+
self.actions = []
|
|
17
|
+
self.is_silent = is_silent
|
|
18
|
+
self.is_dry_run = is_dry_run
|
|
19
|
+
|
|
20
|
+
def add(self, action):
|
|
21
|
+
"""
|
|
22
|
+
Adds an action
|
|
23
|
+
"""
|
|
24
|
+
self.actions.append(action)
|
|
25
|
+
|
|
26
|
+
def execute(self):
|
|
27
|
+
"""
|
|
28
|
+
Prints and executes actions
|
|
29
|
+
"""
|
|
30
|
+
for action in self.actions:
|
|
31
|
+
if not self.is_silent:
|
|
32
|
+
print(action)
|
|
33
|
+
if not self.is_dry_run:
|
|
34
|
+
action.execute()
|
|
35
|
+
|
|
36
|
+
def get_unlink_actions(self):
|
|
37
|
+
"""
|
|
38
|
+
get the current Unlink() actions from the self.actions
|
|
39
|
+
"""
|
|
40
|
+
return [a for a in self.actions if isinstance(a, UnLink)]
|
|
41
|
+
|
|
42
|
+
def get_unlink_target_parents(self):
|
|
43
|
+
"""
|
|
44
|
+
Get list of the parents for the current Unlink() actions from
|
|
45
|
+
self.actions
|
|
46
|
+
"""
|
|
47
|
+
unlink_actions = self.get_unlink_actions()
|
|
48
|
+
# sort for deterministic output
|
|
49
|
+
return sorted(set([a.target.parent for a in unlink_actions]))
|
|
50
|
+
|
|
51
|
+
def get_unlink_targets(self):
|
|
52
|
+
"""
|
|
53
|
+
Get list of the targets for the current Unlink() actions from
|
|
54
|
+
self.actions
|
|
55
|
+
"""
|
|
56
|
+
unlink_actions = self.get_unlink_actions()
|
|
57
|
+
return [a.target for a in unlink_actions]
|
|
58
|
+
|
|
59
|
+
def get_duplicates(self):
|
|
60
|
+
"""
|
|
61
|
+
return a tuple containing tuples with the following structure
|
|
62
|
+
(link destination, [indices of duplicates])
|
|
63
|
+
"""
|
|
64
|
+
tally = defaultdict(list)
|
|
65
|
+
for index, action in enumerate(self.actions):
|
|
66
|
+
if isinstance(action, SymbolicLink):
|
|
67
|
+
tally[action.dest].append(index)
|
|
68
|
+
# sort for deterministic output
|
|
69
|
+
return sorted([indices for _, indices in tally.items() if len(indices) > 1])
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class AbstractBaseAction:
|
|
73
|
+
# pylint: disable=too-few-public-methods
|
|
74
|
+
"""
|
|
75
|
+
An abstract base class that define the interface for actions
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self):
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
def execute(self):
|
|
82
|
+
"""
|
|
83
|
+
function that executes the logic of each concrete action
|
|
84
|
+
"""
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class SymbolicLink(AbstractBaseAction):
|
|
89
|
+
# pylint: disable=too-few-public-methods
|
|
90
|
+
"""
|
|
91
|
+
Action to create a symbolic link relative to the source of the link
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self, subcmd, source, dest):
|
|
95
|
+
super().__init__()
|
|
96
|
+
self.source = source
|
|
97
|
+
self.source_relative = utils.get_relative_path(source, dest.parent)
|
|
98
|
+
self.subcmd = subcmd
|
|
99
|
+
self.dest = dest
|
|
100
|
+
|
|
101
|
+
def execute(self):
|
|
102
|
+
self.dest.symlink_to(self.source_relative)
|
|
103
|
+
|
|
104
|
+
def __repr__(self):
|
|
105
|
+
return "dploy {subcmd}: link {dest} => {source}".format(
|
|
106
|
+
subcmd=self.subcmd, dest=self.dest, source=self.source_relative
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class AlreadyLinked(AbstractBaseAction):
|
|
111
|
+
# pylint: disable=too-few-public-methods
|
|
112
|
+
"""
|
|
113
|
+
Action to used to print an already linked message
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(self, subcmd, source, dest):
|
|
117
|
+
super().__init__()
|
|
118
|
+
self.source = source
|
|
119
|
+
self.source_relative = utils.get_relative_path(source, dest.parent)
|
|
120
|
+
self.dest = dest
|
|
121
|
+
self.subcmd = subcmd
|
|
122
|
+
|
|
123
|
+
def execute(self):
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
def __repr__(self):
|
|
127
|
+
return "dploy {subcmd}: already linked {dest} => {source}".format(
|
|
128
|
+
subcmd=self.subcmd, source=self.source_relative, dest=self.dest
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class AlreadyUnlinked(AbstractBaseAction):
|
|
133
|
+
# pylint: disable=too-few-public-methods
|
|
134
|
+
"""
|
|
135
|
+
Action to used to print an already unlinked message
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def __init__(self, subcmd, source, dest):
|
|
139
|
+
super().__init__()
|
|
140
|
+
self.source = source
|
|
141
|
+
self.source_relative = utils.get_relative_path(source, dest.parent)
|
|
142
|
+
self.dest = dest
|
|
143
|
+
self.subcmd = subcmd
|
|
144
|
+
|
|
145
|
+
def execute(self):
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
def __repr__(self):
|
|
149
|
+
return "dploy {subcmd}: already unlinked {dest} => {source}".format(
|
|
150
|
+
subcmd=self.subcmd, source=self.source_relative, dest=self.dest
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class UnLink(AbstractBaseAction):
|
|
155
|
+
# pylint: disable=too-few-public-methods
|
|
156
|
+
"""
|
|
157
|
+
Action to unlink a symbolic link
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
def __init__(self, subcmd, target):
|
|
161
|
+
super().__init__()
|
|
162
|
+
self.target = target
|
|
163
|
+
self.subcmd = subcmd
|
|
164
|
+
|
|
165
|
+
def execute(self):
|
|
166
|
+
if not self.target.is_symlink():
|
|
167
|
+
# pylint: disable=line-too-long
|
|
168
|
+
raise RuntimeError(
|
|
169
|
+
"dploy detected and aborted an attempt to unlink a non-symlink {target} this is a bug and should be reported".format(
|
|
170
|
+
target=self.target
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
self.target.unlink()
|
|
174
|
+
|
|
175
|
+
def __repr__(self):
|
|
176
|
+
return "dploy {subcmd}: unlink {target} => {source}".format(
|
|
177
|
+
subcmd=self.subcmd, target=self.target, source=utils.readlink(self.target)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class MakeDirectory(AbstractBaseAction):
|
|
182
|
+
# pylint: disable=too-few-public-methods
|
|
183
|
+
"""
|
|
184
|
+
Action to create a directory
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
def __init__(self, subcmd, target):
|
|
188
|
+
super().__init__()
|
|
189
|
+
self.target = target
|
|
190
|
+
self.subcmd = subcmd
|
|
191
|
+
|
|
192
|
+
def execute(self):
|
|
193
|
+
self.target.mkdir()
|
|
194
|
+
|
|
195
|
+
def __repr__(self):
|
|
196
|
+
return "dploy {subcmd}: make directory {target}".format(
|
|
197
|
+
target=self.target, subcmd=self.subcmd
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class RemoveDirectory(AbstractBaseAction):
|
|
202
|
+
# pylint: disable=too-few-public-methods
|
|
203
|
+
"""
|
|
204
|
+
Action to remove a directory
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
def __init__(self, subcmd, target):
|
|
208
|
+
super().__init__()
|
|
209
|
+
self.target = target
|
|
210
|
+
self.subcmd = subcmd
|
|
211
|
+
|
|
212
|
+
def execute(self):
|
|
213
|
+
self.target.rmdir()
|
|
214
|
+
|
|
215
|
+
def __repr__(self):
|
|
216
|
+
msg = "dploy {subcmd}: remove directory {target}"
|
|
217
|
+
return msg.format(target=self.target, subcmd=self.subcmd)
|
dploy/cli.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The command line interface
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import argparse
|
|
7
|
+
from dploy import linkcmd
|
|
8
|
+
from dploy import stowcmd
|
|
9
|
+
from dploy import version
|
|
10
|
+
from dploy.error import DployError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def add_ignore_argument(parser):
|
|
14
|
+
"""
|
|
15
|
+
adds the ignore argument to a subcmd parser
|
|
16
|
+
"""
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"--ignore",
|
|
19
|
+
dest="ignore_patterns",
|
|
20
|
+
action="append",
|
|
21
|
+
default=None,
|
|
22
|
+
help="glob pattern used to ignore directories",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def create_parser():
|
|
27
|
+
"""
|
|
28
|
+
create the CLI argument parser
|
|
29
|
+
"""
|
|
30
|
+
parser = argparse.ArgumentParser(prog="dploy")
|
|
31
|
+
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--version",
|
|
34
|
+
action="version",
|
|
35
|
+
version="%(prog)s {version}".format(version=version.__version__),
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--silent", dest="is_silent", action="store_true", help="suppress all output"
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--dry-run",
|
|
42
|
+
dest="is_dry_run",
|
|
43
|
+
action="store_true",
|
|
44
|
+
help="show what would be done without doing it",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
sub_parsers = parser.add_subparsers(dest="subcmd")
|
|
48
|
+
|
|
49
|
+
stow_parser = sub_parsers.add_parser("stow")
|
|
50
|
+
stow_parser.add_argument("source", nargs="+", help="source directory to stow")
|
|
51
|
+
stow_parser.add_argument("dest", help="destination path to stow into")
|
|
52
|
+
add_ignore_argument(stow_parser)
|
|
53
|
+
|
|
54
|
+
unstow_parser = sub_parsers.add_parser("unstow")
|
|
55
|
+
unstow_parser.add_argument(
|
|
56
|
+
"source", nargs="+", help="source directory to unstow from"
|
|
57
|
+
)
|
|
58
|
+
unstow_parser.add_argument("dest", help="destination path to unstow")
|
|
59
|
+
add_ignore_argument(unstow_parser)
|
|
60
|
+
|
|
61
|
+
clean_parser = sub_parsers.add_parser("clean")
|
|
62
|
+
clean_parser.add_argument(
|
|
63
|
+
"source", nargs="+", help="source directory to clean from"
|
|
64
|
+
)
|
|
65
|
+
clean_parser.add_argument("dest", help="destination path to clean")
|
|
66
|
+
add_ignore_argument(clean_parser)
|
|
67
|
+
|
|
68
|
+
link_parser = sub_parsers.add_parser("link")
|
|
69
|
+
link_parser.add_argument("source", help="source file or directory to link")
|
|
70
|
+
link_parser.add_argument("dest", help="destination path to link")
|
|
71
|
+
add_ignore_argument(link_parser)
|
|
72
|
+
return parser
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def run(arguments=None):
|
|
76
|
+
"""
|
|
77
|
+
interpret the parser arguments and execute the corresponding commands
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
subcmd_map = {
|
|
81
|
+
"stow": stowcmd.Stow,
|
|
82
|
+
"unstow": stowcmd.UnStow,
|
|
83
|
+
"clean": stowcmd.Clean,
|
|
84
|
+
"link": linkcmd.Link,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
parser = create_parser()
|
|
89
|
+
|
|
90
|
+
if arguments is None:
|
|
91
|
+
args = parser.parse_args()
|
|
92
|
+
else:
|
|
93
|
+
args = parser.parse_args(arguments)
|
|
94
|
+
|
|
95
|
+
if args.subcmd in subcmd_map:
|
|
96
|
+
subcmd = subcmd_map[args.subcmd]
|
|
97
|
+
else:
|
|
98
|
+
parser.print_help()
|
|
99
|
+
sys.exit(0)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
subcmd(
|
|
103
|
+
args.source,
|
|
104
|
+
args.dest,
|
|
105
|
+
is_silent=args.is_silent,
|
|
106
|
+
is_dry_run=args.is_dry_run,
|
|
107
|
+
ignore_patterns=args.ignore_patterns,
|
|
108
|
+
)
|
|
109
|
+
except DployError:
|
|
110
|
+
sys.exit(1)
|
|
111
|
+
|
|
112
|
+
except (KeyboardInterrupt) as error:
|
|
113
|
+
print(error, file=sys.stderr)
|
|
114
|
+
sys.exit(130)
|
dploy/error.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""
|
|
2
|
+
All the exceptions and their messages used by the program
|
|
3
|
+
"""
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
ERROR_HEAD = "dploy {subcmd}: can not {subcmd} "
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Errors:
|
|
10
|
+
"""
|
|
11
|
+
A class that collects and executes action objects
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, is_silent):
|
|
15
|
+
self.exceptions = []
|
|
16
|
+
self.is_silent = is_silent
|
|
17
|
+
|
|
18
|
+
def add(self, error):
|
|
19
|
+
"""
|
|
20
|
+
Adds an error
|
|
21
|
+
"""
|
|
22
|
+
self.exceptions.append(error)
|
|
23
|
+
|
|
24
|
+
def handle(self):
|
|
25
|
+
"""
|
|
26
|
+
Prints and handles errors
|
|
27
|
+
"""
|
|
28
|
+
if len(self.exceptions) > 0:
|
|
29
|
+
if not self.is_silent:
|
|
30
|
+
for exception in self.exceptions:
|
|
31
|
+
print(exception, file=sys.stderr)
|
|
32
|
+
raise self.exceptions[0]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# pylint: disable=missing-docstring
|
|
36
|
+
# pylint: disable=too-few-public-methods
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DployError(Exception):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SourceIsSameAsDest(DployError):
|
|
44
|
+
def __init__(self, subcmd, file):
|
|
45
|
+
self.msg = (
|
|
46
|
+
ERROR_HEAD + "'{file}': A source argument is the same as the dest argument"
|
|
47
|
+
)
|
|
48
|
+
self.msg = self.msg.format(subcmd=subcmd, file=file)
|
|
49
|
+
# self.exception = ValueError(self.msg)
|
|
50
|
+
|
|
51
|
+
def __str__(self):
|
|
52
|
+
return self.msg
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ConflictsWithAnotherSource(DployError):
|
|
56
|
+
def __init__(self, subcmd, files):
|
|
57
|
+
self.msg = ERROR_HEAD + "the following: Conflicts with other source {files}"
|
|
58
|
+
files_list = "\n " + "\n ".join(files)
|
|
59
|
+
self.msg = self.msg.format(subcmd=subcmd, files=files_list)
|
|
60
|
+
# self.exception = ValueError(self.msg)
|
|
61
|
+
|
|
62
|
+
def __str__(self):
|
|
63
|
+
return self.msg
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ConflictsWithExistingFile(DployError):
|
|
67
|
+
def __init__(self, subcmd, source, dest):
|
|
68
|
+
self.msg = ERROR_HEAD + "'{source}': Conflicts with existing file '{dest}'"
|
|
69
|
+
self.msg = self.msg.format(subcmd=subcmd, source=source, dest=dest)
|
|
70
|
+
# self.exception = ValueError(self.msg)
|
|
71
|
+
|
|
72
|
+
def __str__(self):
|
|
73
|
+
return self.msg
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ConflictsWithExistingLink(DployError):
|
|
77
|
+
def __init__(self, subcmd, source, dest):
|
|
78
|
+
self.msg = ERROR_HEAD + "'{source}': Conflicts with existing symlink '{dest}'"
|
|
79
|
+
self.msg = self.msg.format(subcmd=subcmd, source=source, dest=dest)
|
|
80
|
+
# self.exception = ValueError(self.msg)
|
|
81
|
+
|
|
82
|
+
def __str__(self):
|
|
83
|
+
return self.msg
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class InsufficientPermissions(DployError):
|
|
87
|
+
def __init__(self, subcmd, file):
|
|
88
|
+
self.msg = ERROR_HEAD + "'{file}': Insufficient permissions"
|
|
89
|
+
self.msg = self.msg.format(subcmd=subcmd, file=file)
|
|
90
|
+
# self.exception = PermissionError(self.msg)
|
|
91
|
+
|
|
92
|
+
def __str__(self):
|
|
93
|
+
return self.msg
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class NoSuchDirectory(DployError):
|
|
97
|
+
def __init__(self, subcmd, file):
|
|
98
|
+
self.msg = ERROR_HEAD + "'{file}': No such directory"
|
|
99
|
+
self.msg = self.msg.format(subcmd=subcmd, file=file)
|
|
100
|
+
# self.exception = NotADirectoryError(self.msg)
|
|
101
|
+
|
|
102
|
+
def __str__(self):
|
|
103
|
+
return self.msg
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class PermissionDenied(DployError):
|
|
107
|
+
def __init__(self, subcmd, file):
|
|
108
|
+
self.msg = ERROR_HEAD + "'{file}': Permission denied"
|
|
109
|
+
self.msg = self.msg.format(subcmd=subcmd, file=file)
|
|
110
|
+
# self.exception = PermissionError(self.msg)
|
|
111
|
+
|
|
112
|
+
def __str__(self):
|
|
113
|
+
return self.msg
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class InsufficientPermissionsToSubcmdFrom(DployError):
|
|
117
|
+
def __init__(self, subcmd, file):
|
|
118
|
+
self.msg = ERROR_HEAD + "from '{file}': Insufficient permissions"
|
|
119
|
+
self.msg = self.msg.format(subcmd=subcmd, file=file)
|
|
120
|
+
# self.exception = PermissionError(self.msg)
|
|
121
|
+
|
|
122
|
+
def __str__(self):
|
|
123
|
+
return self.msg
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class NoSuchDirectoryToSubcmdInto(DployError):
|
|
127
|
+
def __init__(self, subcmd, file):
|
|
128
|
+
self.msg = ERROR_HEAD + "into '{file}': No such directory"
|
|
129
|
+
self.msg = self.msg.format(subcmd=subcmd, file=file)
|
|
130
|
+
# self.exception = NotADirectoryError(self.msg)
|
|
131
|
+
|
|
132
|
+
def __str__(self):
|
|
133
|
+
return self.msg
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class InsufficientPermissionsToSubcmdTo(DployError):
|
|
137
|
+
def __init__(self, subcmd, file):
|
|
138
|
+
self.msg = ERROR_HEAD + "to '{file}': Insufficient permissions"
|
|
139
|
+
self.msg = self.msg.format(subcmd=subcmd, file=file)
|
|
140
|
+
# self.exception = PermissionError(self.msg)
|
|
141
|
+
|
|
142
|
+
def __str__(self):
|
|
143
|
+
return self.msg
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class NoSuchFileOrDirectory(DployError):
|
|
147
|
+
def __init__(self, subcmd, file):
|
|
148
|
+
self.msg = ERROR_HEAD + "'{file}': No such file or directory"
|
|
149
|
+
self.msg = self.msg.format(subcmd=subcmd, file=file)
|
|
150
|
+
# self.exception = FileNotFoundError(self.msg)
|
|
151
|
+
|
|
152
|
+
def __str__(self):
|
|
153
|
+
return self.msg
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class DuplicateSource(DployError):
|
|
157
|
+
def __init__(self, subcmd, file):
|
|
158
|
+
self.msg = ERROR_HEAD + "'{file}': Duplicate source argument"
|
|
159
|
+
self.msg = self.msg.format(subcmd=subcmd, file=file)
|
|
160
|
+
# self.exception = ValueError(self.msg)
|
|
161
|
+
|
|
162
|
+
def __str__(self):
|
|
163
|
+
return self.msg
|
dploy/ignore.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for the --ignore IGNORE_PATTERN flag and .dploystowignore file
|
|
3
|
+
"""
|
|
4
|
+
import pathlib
|
|
5
|
+
from dploy import utils
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Ignore:
|
|
9
|
+
"""
|
|
10
|
+
Handles ignoring of files via glob patterns either passed in directly in or
|
|
11
|
+
in a specified ignore file.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, patterns, source):
|
|
15
|
+
if patterns is None:
|
|
16
|
+
input_patterns = []
|
|
17
|
+
else:
|
|
18
|
+
input_patterns = patterns
|
|
19
|
+
self.ignored_files = []
|
|
20
|
+
|
|
21
|
+
file = source.parent / pathlib.Path(".dploystowignore")
|
|
22
|
+
|
|
23
|
+
self.patterns = [str(file.name)] # ignore the ignore file
|
|
24
|
+
self.patterns.extend(input_patterns)
|
|
25
|
+
self._read_ignore_file_patterns(file)
|
|
26
|
+
|
|
27
|
+
def _read_ignore_file_patterns(self, file):
|
|
28
|
+
"""
|
|
29
|
+
read ignore patterns from a specified file
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
with open(str(file)) as afile:
|
|
33
|
+
file_patterns = afile.read().splitlines()
|
|
34
|
+
self.patterns.extend(file_patterns)
|
|
35
|
+
except FileNotFoundError:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
def should_ignore(self, source):
|
|
39
|
+
"""
|
|
40
|
+
check if a source should be ignored, based on the ignore patterns in
|
|
41
|
+
self.patterns
|
|
42
|
+
|
|
43
|
+
This checks if the ignore patterns match either the file exactly or
|
|
44
|
+
its parents
|
|
45
|
+
"""
|
|
46
|
+
for pattern in self.patterns:
|
|
47
|
+
try:
|
|
48
|
+
files = sorted(source.parent.glob(pattern))
|
|
49
|
+
except IndexError: # the glob result was empty
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
for file in files:
|
|
53
|
+
if utils.is_same_file(file, source) or source in file.parents:
|
|
54
|
+
return True
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
def ignore(self, file):
|
|
58
|
+
"""
|
|
59
|
+
add a file to be ignored
|
|
60
|
+
"""
|
|
61
|
+
self.ignored_files.append(file)
|
|
62
|
+
|
|
63
|
+
def get_ignored_files(self):
|
|
64
|
+
"""
|
|
65
|
+
get a list of the files that have been ignored
|
|
66
|
+
"""
|
|
67
|
+
return self.ignored_files
|
dploy/linkcmd.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The logic and workings behind the link sub-commands
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dploy import actions
|
|
6
|
+
from dploy import utils
|
|
7
|
+
from dploy import error
|
|
8
|
+
from dploy import main
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# pylint: disable=too-few-public-methods
|
|
12
|
+
class Link(main.AbstractBaseSubCommand):
|
|
13
|
+
"""
|
|
14
|
+
Concrete class implementation of the link sub-command
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# pylint: disable=too-many-arguments
|
|
18
|
+
def __init__(
|
|
19
|
+
self, source, dest, is_silent=True, is_dry_run=False, ignore_patterns=None
|
|
20
|
+
):
|
|
21
|
+
super().__init__("link", [source], dest, is_silent, is_dry_run, ignore_patterns)
|
|
22
|
+
|
|
23
|
+
def _is_valid_input(self, sources, dest):
|
|
24
|
+
"""
|
|
25
|
+
Check to see if the input is valid
|
|
26
|
+
"""
|
|
27
|
+
return LinkInput(self.errors, self.subcmd).is_valid(sources, dest)
|
|
28
|
+
|
|
29
|
+
def _collect_actions(self, source, dest):
|
|
30
|
+
"""
|
|
31
|
+
Concrete method to collect required actions to perform a link
|
|
32
|
+
sub-command
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
if dest.exists():
|
|
36
|
+
if utils.is_same_file(dest, source):
|
|
37
|
+
self.actions.add(actions.AlreadyLinked(self.subcmd, source, dest))
|
|
38
|
+
else:
|
|
39
|
+
self.errors.add(
|
|
40
|
+
error.ConflictsWithExistingFile(self.subcmd, source, dest)
|
|
41
|
+
)
|
|
42
|
+
elif dest.is_symlink():
|
|
43
|
+
self.errors.add(error.ConflictsWithExistingLink(self.subcmd, source, dest))
|
|
44
|
+
|
|
45
|
+
elif not dest.parent.exists():
|
|
46
|
+
self.errors.add(error.NoSuchDirectoryToSubcmdInto(self.subcmd, dest.parent))
|
|
47
|
+
|
|
48
|
+
else:
|
|
49
|
+
self.actions.add(actions.SymbolicLink(self.subcmd, source, dest))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class LinkInput(main.Input):
|
|
53
|
+
"""
|
|
54
|
+
Input validator for the link command
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def _is_valid_dest(self, dest):
|
|
58
|
+
if not dest.parent.exists():
|
|
59
|
+
self.errors.add(error.NoSuchFileOrDirectory(self.subcmd, dest.parent))
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
elif not utils.is_file_writable(dest.parent) or not utils.is_directory_writable(
|
|
63
|
+
dest.parent
|
|
64
|
+
):
|
|
65
|
+
self.errors.add(error.InsufficientPermissionsToSubcmdTo(self.subcmd, dest))
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
def _is_valid_source(self, source):
|
|
71
|
+
if not source.exists():
|
|
72
|
+
self.errors.add(error.NoSuchFileOrDirectory(self.subcmd, source))
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
elif not utils.is_file_readable(source) or not utils.is_directory_readable(
|
|
76
|
+
source
|
|
77
|
+
):
|
|
78
|
+
self.errors.add(error.InsufficientPermissions(self.subcmd, source))
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
return True
|