PageRenderTime 55ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/fabric/contrib/files.py

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