PageRenderTime 43ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/fabric/contrib/files.py

https://github.com/dfs-sh/fabric
Python | 414 lines | 348 code | 21 blank | 45 comment | 25 complexity | cc3e1751d404bffa7ed26dd0a3da0b85 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. """
  2. Module providing easy API for working with remote files and folders.
  3. """
  4. from __future__ import with_statement
  5. import hashlib
  6. import tempfile
  7. import re
  8. import os
  9. from StringIO import StringIO
  10. from fabric.api import *
  11. from fabric.utils import apply_lcwd
  12. def exists(path, use_sudo=False, verbose=False):
  13. """
  14. Return True if given path exists on the current remote host.
  15. If ``use_sudo`` is True, will use `sudo` instead of `run`.
  16. `exists` will, by default, hide all output (including the run line, stdout,
  17. stderr and any warning resulting from the file not existing) in order to
  18. avoid cluttering output. You may specify ``verbose=True`` to change this
  19. behavior.
  20. """
  21. func = use_sudo and sudo or run
  22. cmd = 'test -e %s' % _expand_path(path)
  23. # If verbose, run normally
  24. if verbose:
  25. with settings(warn_only=True):
  26. return not func(cmd).failed
  27. # Otherwise, be quiet
  28. with settings(hide('everything'), warn_only=True):
  29. return not func(cmd).failed
  30. def is_link(path, use_sudo=False, verbose=False):
  31. """
  32. Return True if the given path is a symlink on the current remote host.
  33. If ``use_sudo`` is True, will use `.sudo` instead of `.run`.
  34. `.is_link` will, by default, hide all output. Give ``verbose=True`` to change this.
  35. """
  36. func = sudo if use_sudo else run
  37. cmd = 'test -L "$(echo %s)"' % path
  38. args, kwargs = [], {'warn_only': True}
  39. if not verbose:
  40. opts = [hide('everything')]
  41. with settings(*args, **kwargs):
  42. return func(cmd).succeeded
  43. def first(*args, **kwargs):
  44. """
  45. Given one or more file paths, returns first one found, or None if none
  46. exist. May specify ``use_sudo`` and ``verbose`` which are passed to `exists`.
  47. """
  48. for directory in args:
  49. if exists(directory, **kwargs):
  50. return directory
  51. def upload_template(filename, destination, context=None, use_jinja=False,
  52. template_dir=None, use_sudo=False, backup=True, mirror_local_mode=False,
  53. mode=None):
  54. """
  55. Render and upload a template text file to a remote host.
  56. Returns the result of the inner call to `~fabric.operations.put` -- see its
  57. documentation for details.
  58. ``filename`` should be the path to a text file, which may contain `Python
  59. string interpolation formatting
  60. <http://docs.python.org/library/stdtypes.html#string-formatting>`_ and will
  61. be rendered with the given context dictionary ``context`` (if given.)
  62. Alternately, if ``use_jinja`` is set to True and you have the Jinja2
  63. templating library available, Jinja will be used to render the template
  64. instead. Templates will be loaded from the invoking user's current working
  65. directory by default, or from ``template_dir`` if given.
  66. The resulting rendered file will be uploaded to the remote file path
  67. ``destination``. If the destination file already exists, it will be
  68. renamed with a ``.bak`` extension unless ``backup=False`` is specified.
  69. By default, the file will be copied to ``destination`` as the logged-in
  70. user; specify ``use_sudo=True`` to use `sudo` instead.
  71. The ``mirror_local_mode`` and ``mode`` kwargs are passed directly to an
  72. internal `~fabric.operations.put` call; please see its documentation for
  73. details on these two options.
  74. .. versionchanged:: 1.1
  75. Added the ``backup``, ``mirror_local_mode`` and ``mode`` kwargs.
  76. """
  77. func = use_sudo and sudo or run
  78. # Normalize destination to be an actual filename, due to using StringIO
  79. with settings(hide('everything'), warn_only=True):
  80. if func('test -d %s' % _expand_path(destination)).succeeded:
  81. sep = "" if destination.endswith('/') else "/"
  82. destination += sep + os.path.basename(filename)
  83. # Use mode kwarg to implement mirror_local_mode, again due to using
  84. # StringIO
  85. if mirror_local_mode and mode is None:
  86. mode = os.stat(filename).st_mode
  87. # To prevent put() from trying to do this
  88. # logic itself
  89. mirror_local_mode = False
  90. # Process template
  91. text = None
  92. if use_jinja:
  93. try:
  94. template_dir = template_dir or os.getcwd()
  95. template_dir = apply_lcwd(template_dir, env)
  96. from jinja2 import Environment, FileSystemLoader
  97. jenv = Environment(loader=FileSystemLoader(template_dir))
  98. text = jenv.get_template(filename).render(**context or {})
  99. # Force to a byte representation of Unicode, or str()ification
  100. # within Paramiko's SFTP machinery may cause decode issues for
  101. # truly non-ASCII characters.
  102. text = text.encode('utf-8')
  103. except ImportError:
  104. import traceback
  105. tb = traceback.format_exc()
  106. abort(tb + "\nUnable to import Jinja2 -- see above.")
  107. else:
  108. filename = apply_lcwd(filename, env)
  109. with open(os.path.expanduser(filename)) as inputfile:
  110. text = inputfile.read()
  111. if context:
  112. text = text % context
  113. # Back up original file
  114. if backup and exists(destination):
  115. func("cp %s{,.bak}" % _expand_path(destination))
  116. # Upload the file.
  117. return put(
  118. local_path=StringIO(text),
  119. remote_path=destination,
  120. use_sudo=use_sudo,
  121. mirror_local_mode=mirror_local_mode,
  122. mode=mode
  123. )
  124. def sed(filename, before, after, limit='', use_sudo=False, backup='.bak',
  125. flags='', shell=False):
  126. """
  127. Run a search-and-replace on ``filename`` with given regex patterns.
  128. Equivalent to ``sed -i<backup> -r -e "/<limit>/ s/<before>/<after>/<flags>g"
  129. <filename>``. Setting ``backup`` to an empty string will, disable backup
  130. file creation.
  131. For convenience, ``before`` and ``after`` will automatically escape forward
  132. slashes, single quotes and parentheses for you, so you don't need to
  133. specify e.g. ``http:\/\/foo\.com``, instead just using ``http://foo\.com``
  134. is fine.
  135. If ``use_sudo`` is True, will use `sudo` instead of `run`.
  136. The ``shell`` argument will be eventually passed to `run`/`sudo`. It
  137. defaults to False in order to avoid problems with many nested levels of
  138. quotes and backslashes. However, setting it to True may help when using
  139. ``~fabric.operations.cd`` to wrap explicit or implicit ``sudo`` calls.
  140. (``cd`` by it's nature is a shell built-in, not a standalone command, so it
  141. should be called within a shell.)
  142. Other options may be specified with sed-compatible regex flags -- for
  143. example, to make the search and replace case insensitive, specify
  144. ``flags="i"``. The ``g`` flag is always specified regardless, so you do not
  145. need to remember to include it when overriding this parameter.
  146. .. versionadded:: 1.1
  147. The ``flags`` parameter.
  148. .. versionadded:: 1.6
  149. Added the ``shell`` keyword argument.
  150. """
  151. func = use_sudo and sudo or run
  152. # Characters to be escaped in both
  153. for char in "/'":
  154. before = before.replace(char, r'\%s' % char)
  155. after = after.replace(char, r'\%s' % char)
  156. # Characters to be escaped in replacement only (they're useful in regexen
  157. # in the 'before' part)
  158. for char in "()":
  159. after = after.replace(char, r'\%s' % char)
  160. if limit:
  161. limit = r'/%s/ ' % limit
  162. context = {
  163. 'script': r"'%ss/%s/%s/%sg'" % (limit, before, after, flags),
  164. 'filename': _expand_path(filename),
  165. 'backup': backup
  166. }
  167. # Test the OS because of differences between sed versions
  168. with hide('running', 'stdout'):
  169. platform = run("uname")
  170. if platform in ('NetBSD', 'OpenBSD', 'QNX'):
  171. # Attempt to protect against failures/collisions
  172. hasher = hashlib.sha1()
  173. hasher.update(env.host_string)
  174. hasher.update(filename)
  175. context['tmp'] = "/tmp/%s" % hasher.hexdigest()
  176. # Use temp file to work around lack of -i
  177. expr = r"""cp -p %(filename)s %(tmp)s \
  178. && sed -r -e %(script)s %(filename)s > %(tmp)s \
  179. && cp -p %(filename)s %(filename)s%(backup)s \
  180. && mv %(tmp)s %(filename)s"""
  181. else:
  182. context['extended_regex'] = '-E' if platform == 'Darwin' else '-r'
  183. expr = r"sed -i%(backup)s %(extended_regex)s -e %(script)s %(filename)s"
  184. command = expr % context
  185. return func(command, shell=shell)
  186. def uncomment(filename, regex, use_sudo=False, char='#', backup='.bak',
  187. shell=False):
  188. """
  189. Attempt to uncomment all lines in ``filename`` matching ``regex``.
  190. The default comment delimiter is `#` and may be overridden by the ``char``
  191. argument.
  192. This function uses the `sed` function, and will accept the same
  193. ``use_sudo``, ``shell`` and ``backup`` keyword arguments that `sed` does.
  194. `uncomment` will remove a single whitespace character following the comment
  195. character, if it exists, but will preserve all preceding whitespace. For
  196. example, ``# foo`` would become ``foo`` (the single space is stripped) but
  197. `` # foo`` would become `` foo`` (the single space is still stripped,
  198. but the preceding 4 spaces are not.)
  199. .. versionchanged:: 1.6
  200. Added the ``shell`` keyword argument.
  201. """
  202. return sed(
  203. filename,
  204. before=r'^([[:space:]]*)%s[[:space:]]?' % char,
  205. after=r'\1',
  206. limit=regex,
  207. use_sudo=use_sudo,
  208. backup=backup,
  209. shell=shell
  210. )
  211. def comment(filename, regex, use_sudo=False, char='#', backup='.bak',
  212. shell=False):
  213. """
  214. Attempt to comment out all lines in ``filename`` matching ``regex``.
  215. The default commenting character is `#` and may be overridden by the
  216. ``char`` argument.
  217. This function uses the `sed` function, and will accept the same
  218. ``use_sudo``, ``shell`` and ``backup`` keyword arguments that `sed` does.
  219. `comment` will prepend the comment character to the beginning of the line,
  220. so that lines end up looking like so::
  221. this line is uncommented
  222. #this line is commented
  223. # this line is indented and commented
  224. In other words, comment characters will not "follow" indentation as they
  225. sometimes do when inserted by hand. Neither will they have a trailing space
  226. unless you specify e.g. ``char='# '``.
  227. .. note::
  228. In order to preserve the line being commented out, this function will
  229. wrap your ``regex`` argument in parentheses, so you don't need to. It
  230. will ensure that any preceding/trailing ``^`` or ``$`` characters are
  231. correctly moved outside the parentheses. For example, calling
  232. ``comment(filename, r'^foo$')`` will result in a `sed` call with the
  233. "before" regex of ``r'^(foo)$'`` (and the "after" regex, naturally, of
  234. ``r'#\\1'``.)
  235. .. versionadded:: 1.5
  236. Added the ``shell`` keyword argument.
  237. """
  238. carot, dollar = '', ''
  239. if regex.startswith('^'):
  240. carot = '^'
  241. regex = regex[1:]
  242. if regex.endswith('$'):
  243. dollar = '$'
  244. regex = regex[:-1]
  245. regex = "%s(%s)%s" % (carot, regex, dollar)
  246. return sed(
  247. filename,
  248. before=regex,
  249. after=r'%s\1' % char,
  250. use_sudo=use_sudo,
  251. backup=backup,
  252. shell=shell
  253. )
  254. def contains(filename, text, exact=False, use_sudo=False, escape=True,
  255. shell=False):
  256. """
  257. Return True if ``filename`` contains ``text`` (which may be a regex.)
  258. By default, this function will consider a partial line match (i.e. where
  259. ``text`` only makes up part of the line it's on). Specify ``exact=True`` to
  260. change this behavior so that only a line containing exactly ``text``
  261. results in a True return value.
  262. This function leverages ``egrep`` on the remote end (so it may not follow
  263. Python regular expression syntax perfectly), and skips ``env.shell``
  264. wrapper by default.
  265. If ``use_sudo`` is True, will use `sudo` instead of `run`.
  266. If ``escape`` is False, no extra regular expression related escaping is
  267. performed (this includes overriding ``exact`` so that no ``^``/``$`` is
  268. added.)
  269. The ``shell`` argument will be eventually passed to ``run/sudo``. See
  270. description of the same argumnet in ``~fabric.contrib.sed`` for details.
  271. .. versionchanged:: 1.0
  272. Swapped the order of the ``filename`` and ``text`` arguments to be
  273. consistent with other functions in this module.
  274. .. versionchanged:: 1.4
  275. Updated the regular expression related escaping to try and solve
  276. various corner cases.
  277. .. versionchanged:: 1.4
  278. Added ``escape`` keyword argument.
  279. .. versionadded:: 1.6
  280. Added the ``shell`` keyword argument.
  281. """
  282. func = use_sudo and sudo or run
  283. if escape:
  284. text = _escape_for_regex(text)
  285. if exact:
  286. text = "^%s$" % text
  287. with settings(hide('everything'), warn_only=True):
  288. egrep_cmd = 'egrep "%s" %s' % (text, _expand_path(filename))
  289. return func(egrep_cmd, shell=shell).succeeded
  290. def append(filename, text, use_sudo=False, partial=False, escape=True,
  291. shell=False):
  292. """
  293. Append string (or list of strings) ``text`` to ``filename``.
  294. When a list is given, each string inside is handled independently (but in
  295. the order given.)
  296. If ``text`` is already found in ``filename``, the append is not run, and
  297. None is returned immediately. Otherwise, the given text is appended to the
  298. end of the given ``filename`` via e.g. ``echo '$text' >> $filename``.
  299. The test for whether ``text`` already exists defaults to a full line match,
  300. e.g. ``^<text>$``, as this seems to be the most sensible approach for the
  301. "append lines to a file" use case. You may override this and force partial
  302. searching (e.g. ``^<text>``) by specifying ``partial=True``.
  303. Because ``text`` is single-quoted, single quotes will be transparently
  304. backslash-escaped. This can be disabled with ``escape=False``.
  305. If ``use_sudo`` is True, will use `sudo` instead of `run`.
  306. The ``shell`` argument will be eventually passed to ``run/sudo``. See
  307. description of the same argumnet in ``~fabric.contrib.sed`` for details.
  308. .. versionchanged:: 0.9.1
  309. Added the ``partial`` keyword argument.
  310. .. versionchanged:: 1.0
  311. Swapped the order of the ``filename`` and ``text`` arguments to be
  312. consistent with other functions in this module.
  313. .. versionchanged:: 1.0
  314. Changed default value of ``partial`` kwarg to be ``False``.
  315. .. versionchanged:: 1.4
  316. Updated the regular expression related escaping to try and solve
  317. various corner cases.
  318. .. versionadded:: 1.6
  319. Added the ``shell`` keyword argument.
  320. """
  321. func = use_sudo and sudo or run
  322. # Normalize non-list input to be a list
  323. if isinstance(text, basestring):
  324. text = [text]
  325. for line in text:
  326. regex = '^' + _escape_for_regex(line) + ('' if partial else '$')
  327. if (exists(filename, use_sudo=use_sudo) and line
  328. and contains(filename, regex, use_sudo=use_sudo, escape=False,
  329. shell=shell)):
  330. continue
  331. line = line.replace("'", r"'\\''") if escape else line
  332. func("echo '%s' >> %s" % (line, _expand_path(filename)))
  333. def _escape_for_regex(text):
  334. """Escape ``text`` to allow literal matching using egrep"""
  335. regex = re.escape(text)
  336. # Seems like double escaping is needed for \
  337. regex = regex.replace('\\\\', '\\\\\\')
  338. # Triple-escaping seems to be required for $ signs
  339. regex = regex.replace(r'\$', r'\\\$')
  340. # Whereas single quotes should not be escaped
  341. regex = regex.replace(r"\'", "'")
  342. return regex
  343. def _expand_path(path):
  344. return '"$(echo %s)"' % path