PageRenderTime 45ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/fabric/contrib/files.py

https://github.com/strogo/pylibs
Python | 267 lines | 222 code | 13 blank | 32 comment | 15 complexity | 279dbfd19d13a40706c7a2f8de5b9778 MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause, BSD-2-Clause, LGPL-2.1
  1. """
  2. Module providing easy API for working with remote files and folders.
  3. """
  4. from __future__ import with_statement
  5. import tempfile
  6. import re
  7. import os
  8. from fabric.api import run, sudo, settings, put, hide, abort
  9. def exists(path, use_sudo=False, verbose=False):
  10. """
  11. Return True if given path exists on the current remote host.
  12. If ``use_sudo`` is True, will use `sudo` instead of `run`.
  13. `exists` will, by default, hide all output (including the run line, stdout,
  14. stderr and any warning resulting from the file not existing) in order to
  15. avoid cluttering output. You may specify ``verbose=True`` to change this
  16. behavior.
  17. """
  18. func = use_sudo and sudo or run
  19. cmd = 'test -e "%s"' % path
  20. # If verbose, run normally
  21. if verbose:
  22. with settings(warn_only=True):
  23. return not func(cmd).failed
  24. # Otherwise, be quiet
  25. with settings(hide('everything'), warn_only=True):
  26. return not func(cmd).failed
  27. def first(*args, **kwargs):
  28. """
  29. Given one or more file paths, returns first one found, or None if none
  30. exist. May specify ``use_sudo`` which is passed to `exists`.
  31. """
  32. for directory in args:
  33. if not kwargs.get('use_sudo'):
  34. if exists(directory, sudo=False):
  35. return directory
  36. else:
  37. if exists(directory):
  38. return directory
  39. def upload_template(filename, destination, context=None, use_jinja=False,
  40. template_dir=None, use_sudo=False):
  41. """
  42. Render and upload a template text file to a remote host.
  43. ``filename`` should be the path to a text file, which may contain Python
  44. string interpolation formatting and will be rendered with the given context
  45. dictionary ``context`` (if given.)
  46. Alternately, if ``use_jinja`` is set to True and you have the Jinja2
  47. templating library available, Jinja will be used to render the template
  48. instead. Templates will be loaded from the invoking user's current working
  49. directory by default, or from ``template_dir`` if given.
  50. The resulting rendered file will be uploaded to the remote file path
  51. ``destination`` (which should include the desired remote filename.) If the
  52. destination file already exists, it will be renamed with a ``.bak``
  53. extension.
  54. By default, the file will be copied to ``destination`` as the logged-in
  55. user; specify ``use_sudo=True`` to use `sudo` instead.
  56. """
  57. basename = os.path.basename(filename)
  58. temp_destination = '/tmp/' + basename
  59. # This temporary file should not be automatically deleted on close, as we
  60. # need it there to upload it (Windows locks the file for reading while open).
  61. tempfile_fd, tempfile_name = tempfile.mkstemp()
  62. output = open(tempfile_name, "w+b")
  63. # Init
  64. text = None
  65. if use_jinja:
  66. try:
  67. from jinja2 import Environment, FileSystemLoader
  68. jenv = Environment(loader=FileSystemLoader(template_dir or '.'))
  69. text = jenv.get_template(filename).render(**context or {})
  70. except ImportError, e:
  71. abort("tried to use Jinja2 but was unable to import: %s" % e)
  72. else:
  73. with open(filename) as inputfile:
  74. text = inputfile.read()
  75. if context:
  76. text = text % context
  77. output.write(text)
  78. output.close()
  79. # Upload the file.
  80. put(tempfile_name, temp_destination)
  81. os.close(tempfile_fd)
  82. os.remove(tempfile_name)
  83. func = use_sudo and sudo or run
  84. # Back up any original file (need to do figure out ultimate destination)
  85. to_backup = destination
  86. with settings(hide('everything'), warn_only=True):
  87. # Is destination a directory?
  88. if func('test -f %s' % to_backup).failed:
  89. # If so, tack on the filename to get "real" destination
  90. to_backup = destination + '/' + basename
  91. if exists(to_backup):
  92. func("cp %s %s.bak" % (to_backup, to_backup))
  93. # Actually move uploaded template to destination
  94. func("mv %s %s" % (temp_destination, destination))
  95. def sed(filename, before, after, limit='', use_sudo=False, backup='.bak'):
  96. """
  97. Run a search-and-replace on ``filename`` with given regex patterns.
  98. Equivalent to ``sed -i<backup> -r -e "/<limit>/ s/<before>/<after>/g
  99. <filename>"``.
  100. For convenience, ``before`` and ``after`` will automatically escape forward
  101. slashes (and **only** forward slashes) for you, so you don't need to
  102. specify e.g. ``http:\/\/foo\.com``, instead just using ``http://foo\.com``
  103. is fine.
  104. If ``use_sudo`` is True, will use `sudo` instead of `run`.
  105. `sed` will pass ``shell=False`` to `run`/`sudo`, in order to avoid problems
  106. with many nested levels of quotes and backslashes.
  107. """
  108. func = use_sudo and sudo or run
  109. expr = r"sed -i%s -r -e '%ss/%s/%s/g' %s"
  110. before = before.replace('/', r'\/')
  111. after = after.replace('/', r'\/')
  112. if limit:
  113. limit = r'/%s/ ' % limit
  114. command = expr % (backup, limit, before, after, filename)
  115. return func(command, shell=False)
  116. def uncomment(filename, regex, use_sudo=False, char='#', backup='.bak'):
  117. """
  118. Attempt to uncomment all lines in ``filename`` matching ``regex``.
  119. The default comment delimiter is `#` and may be overridden by the ``char``
  120. argument.
  121. This function uses the `sed` function, and will accept the same
  122. ``use_sudo`` and ``backup`` keyword arguments that `sed` does.
  123. `uncomment` will remove a single whitespace character following the comment
  124. character, if it exists, but will preserve all preceding whitespace. For
  125. example, ``# foo`` would become ``foo`` (the single space is stripped) but
  126. `` # foo`` would become `` foo`` (the single space is still stripped,
  127. but the preceding 4 spaces are not.)
  128. """
  129. return sed(
  130. filename,
  131. before=r'^([[:space:]]*)%s[[:space:]]?' % char,
  132. after=r'\1',
  133. limit=regex,
  134. use_sudo=use_sudo,
  135. backup=backup
  136. )
  137. def comment(filename, regex, use_sudo=False, char='#', backup='.bak'):
  138. """
  139. Attempt to comment out all lines in ``filename`` matching ``regex``.
  140. The default commenting character is `#` and may be overridden by the
  141. ``char`` argument.
  142. This function uses the `sed` function, and will accept the same
  143. ``use_sudo`` and ``backup`` keyword arguments that `sed` does.
  144. `comment` will prepend the comment character to the beginning of the line,
  145. so that lines end up looking like so::
  146. this line is uncommented
  147. #this line is commented
  148. # this line is indented and commented
  149. In other words, comment characters will not "follow" indentation as they
  150. sometimes do when inserted by hand. Neither will they have a trailing space
  151. unless you specify e.g. ``char='# '``.
  152. .. note::
  153. In order to preserve the line being commented out, this function will
  154. wrap your ``regex`` argument in parentheses, so you don't need to. It
  155. will ensure that any preceding/trailing ``^`` or ``$`` characters are
  156. correctly moved outside the parentheses. For example, calling
  157. ``comment(filename, r'^foo$')`` will result in a `sed` call with the
  158. "before" regex of ``r'^(foo)$'`` (and the "after" regex, naturally, of
  159. ``r'#\\1'``.)
  160. """
  161. carot = ''
  162. dollar = ''
  163. if regex.startswith('^'):
  164. carot = '^'
  165. regex = regex[1:]
  166. if regex.endswith('$'):
  167. dollar = '$'
  168. regex = regex[:1]
  169. regex = "%s(%s)%s" % (carot, regex, dollar)
  170. return sed(
  171. filename,
  172. before=regex,
  173. after=r'%s\1' % char,
  174. use_sudo=use_sudo,
  175. backup=backup
  176. )
  177. def contains(text, filename, exact=False, use_sudo=False):
  178. """
  179. Return True if ``filename`` contains ``text``.
  180. By default, this function will consider a partial line match (i.e. where
  181. the given text only makes up part of the line it's on). Specify
  182. ``exact=True`` to change this behavior so that only a line containing
  183. exactly ``text`` results in a True return value.
  184. Double-quotes in either ``text`` or ``filename`` will be automatically
  185. backslash-escaped in order to behave correctly during the remote shell
  186. invocation.
  187. If ``use_sudo`` is True, will use `sudo` instead of `run`.
  188. """
  189. func = use_sudo and sudo or run
  190. if exact:
  191. text = "^%s$" % text
  192. with settings(hide('everything'), warn_only=True):
  193. return func('egrep "%s" "%s"' % (
  194. text.replace('"', r'\"'),
  195. filename.replace('"', r'\"')
  196. ))
  197. def append(text, filename, use_sudo=False):
  198. """
  199. Append string (or list of strings) ``text`` to ``filename``.
  200. When a list is given, each string inside is handled independently (but in
  201. the order given.)
  202. If ``text`` is already found as a discrete line in ``filename``, the append
  203. is not run, and None is returned immediately. Otherwise, the given text is
  204. appended to the end of the given ``filename`` via e.g. ``echo '$text' >>
  205. $filename``.
  206. Because ``text`` is single-quoted, single quotes will be transparently
  207. backslash-escaped.
  208. If ``use_sudo`` is True, will use `sudo` instead of `run`.
  209. """
  210. func = use_sudo and sudo or run
  211. # Normalize non-list input to be a list
  212. if isinstance(text, str):
  213. text = [text]
  214. for line in text:
  215. if (contains('^' + re.escape(line), filename, use_sudo=use_sudo)
  216. and line):
  217. continue
  218. func("echo '%s' >> %s" % (line.replace("'", r'\''), filename))