PageRenderTime 56ms CodeModel.GetById 26ms RepoModel.GetById 1ms app.codeStats 0ms

/fabric/contrib/files.py

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