1#!/usr/local/bin/python
2# -*- coding: utf-8 -*-
3# cython: language_level=3, always_allow_keywords=True
4
5## Copyright 2007-2018 by LivingLogic AG, Bayreuth/Germany
6## Copyright 2007-2018 by Walter Dörwald
7##
8## All Rights Reserved
9##
10## See ll/xist/__init__.py for the license
11
12
13"""
14Purpose
15=======
16
17:program:`ucp` is a script that copies files or directories. It is an
18URL-enabled version of the :command:`cp` system command. Via :mod:`ll.url` and
19:mod:`ll.orasql` :program:`ucp` supports ``ssh`` and ``oracle`` URLs.
20
21
22Options
23=======
24
25:program:`ucp` supports the following options:
26
27.. program:: ucp
28
29.. option:: urls
30
31	Two or more URLs. If more than two URLs are given or the last URL refers
32	to an existing directory, the last URL is the target directory. All other
33	sources are copied into this target directory. Otherwise one file is
34	copied to another file.
35
36.. option:: -v <flag>, --verbose <flag>
37
38	Give a report during the copy process about the files copied and their
39	sizes?
40	(Valid flag values are ``false``, ``no``, ``0``, ``true``, ``yes`` or ``1``)
41
42.. option:: -c <mode>, --color <mode>
43
44	Should the output be colored? If ``auto`` is specified (the default) then
45	the output is colored if stdout is a terminal. Valid modes are ``yes``,
46	``no`` or ``auto``.
47
48.. option:: -u <user>, --user <user>
49
50	A user id or name. If given :program:`ucp` will change the owner of the
51	target files.
52
53.. option:: -g <group>, --group <group>
54
55	A group id or name. If given :program:`ucp` will change the group of the
56	target files.
57
58.. option:: -r <flag>, --recursive <flag>
59
60	Copies files recursively.
61	(Valid flag values are ``false``, ``no``, ``0``, ``true``, ``yes`` or ``1``)
62
63.. option:: -x <flag>, --ignoreerrors <flag>
64
65	Ignores errors occuring during the copy process (otherwise the copy
66	process is aborted).
67	(Valid flag values are ``false``, ``no``, ``0``, ``true``, ``yes`` or ``1``)
68
69.. option:: -i <pattern(s)>, --include <pattern(s)>
70
71	Only copy files that match one of the specified patterns.
72
73.. option:: -e <pattern(s)>, --exclude <pattern(s)>
74
75	Don't copy files that match one of the specified patterns.
76
77.. option:: --enterdir <pattern(s)>
78
79	Only enter directories that match one of the specified patterns.
80
81.. option:: --skipdir <pattern(s)>
82
83	Skip directories that match one of the specified patterns.
84
85.. option:: --ignorecase <flag>
86
87	Perform case-insensitive pattern matching?
88	(Valid flag values are ``false``, ``no``, ``0``, ``true``, ``yes`` or ``1``)
89
90
91Examples
92========
93
94Copy one file to another:
95
96.. sourcecode:: bash
97
98	$ ucp foo.txt bar.txt
99
100Copy a file into an existing directory:
101
102.. sourcecode:: bash
103
104	$ ucp foo.txt dir/
105
106Copy multiple files into a new or existing directory (and give a progress
107report):
108
109.. sourcecode:: bash
110
111	$ ucp foo.txt bar.txt baz.txt dir/ -v
112	ucp: foo.txt -> dir/foo.txt (1,114 bytes)
113	ucp: bar.txt -> dir/bar.txt (2,916 bytes)
114	ucp: baz.txt -> dir/baz.txt (35,812 bytes)
115
116Recursively copy the schema objects in an Oracle database to a local directory:
117
118.. sourcecode:: bash
119
120	ucp oracle://user:pwd@oracle.example.org/ db/ -r
121
122Recursively copy the schema objects in an Oracle database to a remote directory:
123
124.. sourcecode:: bash
125
126	ucp oracle://user:pwd@oracle.example.org/ ssh://user@www.example.org/~/db/ -r
127"""
128
129
130import sys, re, argparse, contextlib
131
132from ll import misc, url
133
134try:
135	import astyle
136except ImportError:
137	from ll import astyle
138
139try:
140	from ll import orasql # activate the oracle scheme
141except ImportError:
142	pass
143
144
145__docformat__ = "reStructuredText"
146
147
148def main(args=None):
149	def copyone(urlread, urlwrite):
150		strurlread = str(urlread)
151		if urlread.isdir():
152			if args.recursive:
153				for u in urlread.walkfiles(include=args.include, exclude=args.exclude, enterdir=args.enterdir, skipdir=args.skipdir, ignorecase=args.ignorecase):
154					copyone(urlread/u, urlwrite/u)
155			else:
156				if args.verbose:
157					msg = astyle.style_default("ucp: ", astyle.style_url(strurlread), astyle.style_warn(" (directory skipped)"))
158					stderr.writeln(msg)
159		else:
160			if args.verbose:
161				msg = astyle.style_default("ucp: ", astyle.style_url(strurlread), " -> ")
162				stderr.write(msg)
163			try:
164				with contextlib.closing(urlread.open("rb")) as fileread:
165					with contextlib.closing(urlwrite.open("wb")) as filewrite:
166						size = 0
167						while True:
168							data = fileread.read(262144)
169							if data:
170								filewrite.write(data)
171								size += len(data)
172							else:
173								break
174				if user or group:
175					urlwrite.chown(user, group)
176			except Exception as exc:
177				if args.ignoreerrors:
178					if args.verbose:
179						exctype = misc.format_class(exc)
180						excmsg = str(exc).replace("\n", " ").strip()
181						msg = astyle.style_error(f" (failed with {exctype}: {excmsg})")
182						stderr.writeln(msg)
183				else:
184					raise
185			else:
186				if args.verbose:
187					msg = astyle.style_default(astyle.style_url(str(urlwrite)), f" ({size:,} bytes)")
188					stderr.writeln(msg)
189
190	p = argparse.ArgumentParser(description="Copies URLs", epilog="For more info see http://python.livinglogic.de/scripts_ucp.html")
191	p.add_argument("urls", metavar="url", help="either one source and one target file, or multiple source files and one target dir", nargs="*", type=url.URL)
192	p.add_argument("-v", "--verbose", dest="verbose", help="Be verbose? (default: %(default)s)", action=misc.FlagAction, default=False)
193	p.add_argument("-c", "--color", dest="color", help="Color output (default: %(default)s)", default="auto", choices=("yes", "no", "auto"))
194	p.add_argument("-u", "--user", dest="user", help="user id or name for target files")
195	p.add_argument("-g", "--group", dest="group", help="group id or name for target files")
196	p.add_argument("-r", "--recursive", dest="recursive", help="Copy stuff recursively? (default: %(default)s)", action=misc.FlagAction, default=False)
197	p.add_argument("-x", "--ignoreerrors", dest="ignoreerrors", help="Ignore errors? (default: %(default)s)", action=misc.FlagAction, default=False)
198	p.add_argument("-i", "--include", dest="include", metavar="PATTERN", help="Include only URLs matching PATTERN", action="append")
199	p.add_argument("-e", "--exclude", dest="exclude", metavar="PATTERN", help="Exclude URLs matching PATTERN", action="append")
200	p.add_argument(      "--enterdir", dest="enterdir", metavar="PATTERN", help="Only enter directories matching PATTERN", action="append")
201	p.add_argument(      "--skipdir", dest="skipdir", metavar="PATTERN", help="Skip directories matching PATTERN", action="append")
202	p.add_argument(      "--ignorecase", dest="ignorecase", help="Perform case-insensitive name matching? (default: %(default)s)", action=misc.FlagAction, default=False)
203
204	args = p.parse_args(args)
205	if len(args.urls) < 2:
206		p.error("need at least one source url and one target url")
207		return 1
208
209	if args.color == "yes":
210		color = True
211	elif args.color == "no":
212		color = False
213	else:
214		color = None
215	stdout = astyle.Stream(sys.stdout, color)
216	stderr = astyle.Stream(sys.stderr, color)
217
218	user = args.user
219	try:
220		user = int(user)
221	except (TypeError, ValueError):
222		pass
223
224	group = args.group
225	try:
226		group = int(group)
227	except (TypeError, ValueError):
228		pass
229
230	with url.Context():
231		urls = args.urls
232		if len(urls) > 2 or urls[-1].isdir(): # treat target as directory
233			for u in urls[:-1]:
234				copyone(u, urls[-1]/u.file)
235		else:
236			copyone(urls[0], urls[-1])
237
238
239if __name__ == "__main__":
240	sys.exit(main())
241