PageRenderTime 59ms CodeModel.GetById 34ms RepoModel.GetById 0ms app.codeStats 0ms

/fabric/contrib/files.py

https://bitbucket.org/pythonos/pythonos
Python | 328 lines | 271 code | 18 blank | 39 comment | 23 complexity | 7b33fe60b56800ddc996b813ec0fa401 MD5 | raw file
Possible License(s): LGPL-2.0, MPL-2.0-no-copyleft-exception, GPL-3.0, AGPL-1.0, MIT, ISC, LGPL-2.1, GPL-2.0, BSD-3-Clause, 0BSD
  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 "%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`` which is passed to `exists`.
  33. """
  34. for directory in args:
  35. if not kwargs.get('use_sudo'):
  36. if exists(directory, sudo=False):
  37. return directory
  38. else:
  39. if exists(directory):
  40. return directory
  41. def upload_template(filename, destination, context=None, use_jinja=False,
  42. template_dir=None, use_sudo=False, backup=True, mirror_local_mode=False,
  43. mode=None):
  44. """
  45. Render and upload a template text file to a remote host.
  46. ``filename`` should be the path to a text file, which may contain `Python
  47. string interpolation formatting
  48. <http://docs.python.org/release/2.5.4/lib/typesseq-strings.html>`_ and will
  49. be rendered with the given context dictionary ``context`` (if given.)
  50. Alternately, if ``use_jinja`` is set to True and you have the Jinja2
  51. templating library available, Jinja will be used to render the template
  52. instead. Templates will be loaded from the invoking user's current working
  53. directory by default, or from ``template_dir`` if given.
  54. The resulting rendered file will be uploaded to the remote file path
  55. ``destination``. If the destination file already exists, it will be
  56. renamed with a ``.bak`` extension unless ``backup=False`` is specified.
  57. By default, the file will be copied to ``destination`` as the logged-in
  58. user; specify ``use_sudo=True`` to use `sudo` instead.
  59. The ``mirror_local_mode`` and ``mode`` kwargs are passed directly to an
  60. internal `~fabric.operations.put` call; please see its documentation for
  61. details on these two options.
  62. .. versionchanged:: 1.1
  63. Added the ``backup``, ``mirror_local_mode`` and ``mode`` kwargs.
  64. """
  65. func = use_sudo and sudo or run
  66. # Normalize destination to be an actual filename, due to using StringIO
  67. with settings(hide('everything'), warn_only=True):
  68. if func('test -d %s' % destination).succeeded:
  69. sep = "" if destination.endswith('/') else "/"
  70. destination += sep + os.path.basename(filename)
  71. # Use mode kwarg to implement mirror_local_mode, again due to using
  72. # StringIO
  73. if mirror_local_mode and mode is None:
  74. mode = os.stat(filename).st_mode
  75. # To prevent put() from trying to do this
  76. # logic itself
  77. mirror_local_mode = False
  78. # Process template
  79. text = None
  80. if use_jinja:
  81. try:
  82. from jinja2 import Environment, FileSystemLoader
  83. jenv = Environment(loader=FileSystemLoader(template_dir or '.'))
  84. text = jenv.get_template(filename).render(**context or {})
  85. except ImportError, e:
  86. abort("tried to use Jinja2 but was unable to import: %s" % e)
  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. 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>"``.
  109. For convenience, ``before`` and ``after`` will automatically escape forward
  110. slashes, single quotes and parentheses for you, so you don't need to
  111. specify e.g. ``http:\/\/foo\.com``, instead just using ``http://foo\.com``
  112. is fine.
  113. If ``use_sudo`` is True, will use `sudo` instead of `run`.
  114. `sed` will pass ``shell=False`` to `run`/`sudo`, in order to avoid problems
  115. with many nested levels of quotes and backslashes.
  116. Other options may be specified with sed-compatible regex flags -- for
  117. example, to make the search and replace case insensitive, specify
  118. ``flags="i"``. The ``g`` flag is always specified regardless, so you do not
  119. need to remember to include it when overriding this parameter.
  120. .. versionadded:: 1.1
  121. The ``flags`` parameter.
  122. """
  123. func = use_sudo and sudo or run
  124. # Characters to be escaped in both
  125. for char in "/'":
  126. before = before.replace(char, r'\%s' % char)
  127. after = after.replace(char, r'\%s' % char)
  128. # Characters to be escaped in replacement only (they're useful in regexen
  129. # in the 'before' part)
  130. for char in "()":
  131. after = after.replace(char, r'\%s' % char)
  132. if limit:
  133. limit = r'/%s/ ' % limit
  134. # Test the OS because of differences between sed versions
  135. with hide('running', 'stdout'):
  136. platform = run("uname")
  137. if platform in ('NetBSD', 'OpenBSD'):
  138. # Attempt to protect against failures/collisions
  139. hasher = hashlib.sha1()
  140. hasher.update(env.host_string)
  141. hasher.update(filename)
  142. tmp = "/tmp/%s" % hasher.hexdigest()
  143. # Use temp file to work around lack of -i
  144. expr = r"""cp -p %(filename)s %(tmp)s \
  145. && sed -r -e '%(limit)ss/%(before)s/%(after)s/%(flags)sg' %(filename)s > %(tmp)s \
  146. && cp -p %(filename)s %(filename)s%(backup)s \
  147. && mv %(tmp)s %(filename)s"""
  148. command = expr % locals()
  149. else:
  150. expr = r"sed -i%s -r -e '%ss/%s/%s/%sg' %s"
  151. command = expr % (backup, limit, before, after, flags, filename)
  152. return func(command, shell=False)
  153. def uncomment(filename, regex, use_sudo=False, char='#', backup='.bak'):
  154. """
  155. Attempt to uncomment all lines in ``filename`` matching ``regex``.
  156. The default comment delimiter is `#` and may be overridden by the ``char``
  157. argument.
  158. This function uses the `sed` function, and will accept the same
  159. ``use_sudo`` and ``backup`` keyword arguments that `sed` does.
  160. `uncomment` will remove a single whitespace character following the comment
  161. character, if it exists, but will preserve all preceding whitespace. For
  162. example, ``# foo`` would become ``foo`` (the single space is stripped) but
  163. `` # foo`` would become `` foo`` (the single space is still stripped,
  164. but the preceding 4 spaces are not.)
  165. """
  166. return sed(
  167. filename,
  168. before=r'^([[:space:]]*)%s[[:space:]]?' % char,
  169. after=r'\1',
  170. limit=regex,
  171. use_sudo=use_sudo,
  172. backup=backup
  173. )
  174. def comment(filename, regex, use_sudo=False, char='#', backup='.bak'):
  175. """
  176. Attempt to comment out all lines in ``filename`` matching ``regex``.
  177. The default commenting character is `#` and may be overridden by the
  178. ``char`` argument.
  179. This function uses the `sed` function, and will accept the same
  180. ``use_sudo`` and ``backup`` keyword arguments that `sed` does.
  181. `comment` will prepend the comment character to the beginning of the line,
  182. so that lines end up looking like so::
  183. this line is uncommented
  184. #this line is commented
  185. # this line is indented and commented
  186. In other words, comment characters will not "follow" indentation as they
  187. sometimes do when inserted by hand. Neither will they have a trailing space
  188. unless you specify e.g. ``char='# '``.
  189. .. note::
  190. In order to preserve the line being commented out, this function will
  191. wrap your ``regex`` argument in parentheses, so you don't need to. It
  192. will ensure that any preceding/trailing ``^`` or ``$`` characters are
  193. correctly moved outside the parentheses. For example, calling
  194. ``comment(filename, r'^foo$')`` will result in a `sed` call with the
  195. "before" regex of ``r'^(foo)$'`` (and the "after" regex, naturally, of
  196. ``r'#\\1'``.)
  197. """
  198. carot, dollar = '', ''
  199. if regex.startswith('^'):
  200. carot = '^'
  201. regex = regex[1:]
  202. if regex.endswith('$'):
  203. dollar = '$'
  204. regex = regex[:-1]
  205. regex = "%s(%s)%s" % (carot, regex, dollar)
  206. return sed(
  207. filename,
  208. before=regex,
  209. after=r'%s\1' % char,
  210. use_sudo=use_sudo,
  211. backup=backup
  212. )
  213. def contains(filename, text, exact=False, use_sudo=False):
  214. """
  215. Return True if ``filename`` contains ``text``.
  216. By default, this function will consider a partial line match (i.e. where
  217. the given text only makes up part of the line it's on). Specify
  218. ``exact=True`` to change this behavior so that only a line containing
  219. exactly ``text`` results in a True return value.
  220. Double-quotes in either ``text`` or ``filename`` will be automatically
  221. backslash-escaped in order to behave correctly during the remote shell
  222. invocation.
  223. If ``use_sudo`` is True, will use `sudo` instead of `run`.
  224. .. versionchanged:: 1.0
  225. Swapped the order of the ``filename`` and ``text`` arguments to be
  226. consistent with other functions in this module.
  227. """
  228. func = use_sudo and sudo or run
  229. if exact:
  230. text = "^%s$" % text
  231. with settings(hide('everything'), warn_only=True):
  232. return func('egrep "%s" "%s"' % (
  233. text.replace('"', r'\"'),
  234. filename.replace('"', r'\"')
  235. )).succeeded
  236. def append(filename, text, use_sudo=False, partial=False, escape=True):
  237. """
  238. Append string (or list of strings) ``text`` to ``filename``.
  239. When a list is given, each string inside is handled independently (but in
  240. the order given.)
  241. If ``text`` is already found in ``filename``, the append is not run, and
  242. None is returned immediately. Otherwise, the given text is appended to the
  243. end of the given ``filename`` via e.g. ``echo '$text' >> $filename``.
  244. The test for whether ``text`` already exists defaults to a full line match,
  245. e.g. ``^<text>$``, as this seems to be the most sensible approach for the
  246. "append lines to a file" use case. You may override this and force partial
  247. searching (e.g. ``^<text>``) by specifying ``partial=True``.
  248. Because ``text`` is single-quoted, single quotes will be transparently
  249. backslash-escaped. This can be disabled with ``escape=False``.
  250. If ``use_sudo`` is True, will use `sudo` instead of `run`.
  251. .. versionchanged:: 0.9.1
  252. Added the ``partial`` keyword argument.
  253. .. versionchanged:: 1.0
  254. Swapped the order of the ``filename`` and ``text`` arguments to be
  255. consistent with other functions in this module.
  256. .. versionchanged:: 1.0
  257. Changed default value of ``partial`` kwarg to be ``False``.
  258. """
  259. func = use_sudo and sudo or run
  260. # Normalize non-list input to be a list
  261. if isinstance(text, basestring):
  262. text = [text]
  263. for line in text:
  264. regex = '^' + re.escape(line) + ('' if partial else '$')
  265. if (exists(filename) and line
  266. and contains(filename, regex, use_sudo=use_sudo)):
  267. continue
  268. line = line.replace("'", r'\'') if escape else line
  269. func("echo '%s' >> %s" % (line, filename))