sftp.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. from __future__ import with_statement
  2. import os
  3. import posixpath
  4. import stat
  5. import re
  6. import uuid
  7. from fnmatch import filter as fnfilter
  8. from fabric.state import output, connections, env
  9. from fabric.utils import warn
  10. from fabric.context_managers import settings
  11. # TODO: use self.sftp.listdir_iter on Paramiko 1.15+
  12. def _format_local(local_path, local_is_path):
  13. """Format a path for log output"""
  14. if local_is_path:
  15. return local_path
  16. else:
  17. # This allows users to set a name attr on their StringIO objects
  18. # just like an open file object would have
  19. return getattr(local_path, 'name', '<file obj>')
  20. class SFTP(object):
  21. """
  22. SFTP helper class, which is also a facade for ssh.SFTPClient.
  23. """
  24. def __init__(self, host_string):
  25. self.ftp = connections[host_string].open_sftp()
  26. # Recall that __getattr__ is the "fallback" attribute getter, and is thus
  27. # pretty safe to use for facade-like behavior as we're doing here.
  28. def __getattr__(self, attr):
  29. return getattr(self.ftp, attr)
  30. def isdir(self, path):
  31. try:
  32. return stat.S_ISDIR(self.ftp.stat(path).st_mode)
  33. except IOError:
  34. return False
  35. def islink(self, path):
  36. try:
  37. return stat.S_ISLNK(self.ftp.lstat(path).st_mode)
  38. except IOError:
  39. return False
  40. def exists(self, path):
  41. try:
  42. self.ftp.lstat(path).st_mode
  43. except IOError:
  44. return False
  45. return True
  46. def glob(self, path):
  47. from fabric.state import win32
  48. dirpart, pattern = os.path.split(path)
  49. rlist = self.ftp.listdir(dirpart)
  50. names = fnfilter([f for f in rlist if not f[0] == '.'], pattern)
  51. ret = []
  52. if len(names):
  53. s = '/'
  54. ret = [dirpart.rstrip(s) + s + name.lstrip(s) for name in names]
  55. if not win32:
  56. ret = [posixpath.join(dirpart, name) for name in names]
  57. return ret
  58. def walk(self, top, topdown=True, onerror=None, followlinks=False):
  59. from os.path import join
  60. # We may not have read permission for top, in which case we can't get a
  61. # list of the files the directory contains. os.path.walk always
  62. # suppressed the exception then, rather than blow up for a minor reason
  63. # when (say) a thousand readable directories are still left to visit.
  64. # That logic is copied here.
  65. try:
  66. # Note that listdir and error are globals in this module due to
  67. # earlier import-*.
  68. names = self.ftp.listdir(top)
  69. except Exception, err:
  70. if onerror is not None:
  71. onerror(err)
  72. return
  73. dirs, nondirs = [], []
  74. for name in names:
  75. if self.isdir(join(top, name)):
  76. dirs.append(name)
  77. else:
  78. nondirs.append(name)
  79. if topdown:
  80. yield top, dirs, nondirs
  81. for name in dirs:
  82. path = join(top, name)
  83. if followlinks or not self.islink(path):
  84. for x in self.walk(path, topdown, onerror, followlinks):
  85. yield x
  86. if not topdown:
  87. yield top, dirs, nondirs
  88. def mkdir(self, path, use_sudo):
  89. from fabric.api import sudo, hide
  90. if use_sudo:
  91. with hide('everything'):
  92. sudo('mkdir "%s"' % path)
  93. else:
  94. self.ftp.mkdir(path)
  95. def get(self, remote_path, local_path, use_sudo, local_is_path, rremote=None, temp_dir=""):
  96. from fabric.api import sudo, hide
  97. # rremote => relative remote path, so get(/var/log) would result in
  98. # this function being called with
  99. # remote_path=/var/log/apache2/access.log and
  100. # rremote=apache2/access.log
  101. rremote = rremote if rremote is not None else remote_path
  102. # Handle format string interpolation (e.g. %(dirname)s)
  103. path_vars = {
  104. 'host': env.host_string.replace(':', '-'),
  105. 'basename': os.path.basename(rremote),
  106. 'dirname': os.path.dirname(rremote),
  107. 'path': rremote
  108. }
  109. if local_is_path:
  110. # Fix for issue #711 and #1348 - escape %'s as well as possible.
  111. format_re = r'(%%(?!\((?:%s)\)\w))' % '|'.join(path_vars.keys())
  112. escaped_path = re.sub(format_re, r'%\1', local_path)
  113. local_path = os.path.abspath(escaped_path % path_vars)
  114. # Ensure we give ssh.SFTPCLient a file by prepending and/or
  115. # creating local directories as appropriate.
  116. dirpath, filepath = os.path.split(local_path)
  117. if dirpath and not os.path.exists(dirpath):
  118. os.makedirs(dirpath)
  119. if os.path.isdir(local_path):
  120. local_path = os.path.join(local_path, path_vars['basename'])
  121. if output.running:
  122. print("[%s] download: %s <- %s" % (
  123. env.host_string,
  124. _format_local(local_path, local_is_path),
  125. remote_path
  126. ))
  127. # Warn about overwrites, but keep going
  128. if local_is_path and os.path.exists(local_path):
  129. msg = "Local file %s already exists and is being overwritten."
  130. warn(msg % local_path)
  131. # When using sudo, "bounce" the file through a guaranteed-unique file
  132. # path in the default remote CWD (which, typically, the login user will
  133. # have write permissions on) in order to sudo(cp) it.
  134. if use_sudo:
  135. target_path = posixpath.join(temp_dir, uuid.uuid4().hex)
  136. # Temporarily nuke 'cwd' so sudo() doesn't "cd" its mv command.
  137. # (The target path has already been cwd-ified elsewhere.)
  138. with settings(hide('everything'), cwd=""):
  139. sudo('cp -p "%s" "%s"' % (remote_path, target_path))
  140. # The user should always own the copied file.
  141. sudo('chown %s "%s"' % (env.user, target_path))
  142. # Only root and the user has the right to read the file
  143. sudo('chmod %o "%s"' % (0400, target_path))
  144. remote_path = target_path
  145. try:
  146. # File-like objects: reset to file seek 0 (to ensure full overwrite)
  147. # and then use Paramiko's getfo() directly
  148. getter = self.ftp.get
  149. if not local_is_path:
  150. local_path.seek(0)
  151. getter = self.ftp.getfo
  152. getter(remote_path, local_path)
  153. finally:
  154. # try to remove the temporary file after the download
  155. if use_sudo:
  156. with settings(hide('everything'), cwd=""):
  157. sudo('rm -f "%s"' % remote_path)
  158. # Return local_path object for posterity. (If mutated, caller will want
  159. # to know.)
  160. return local_path
  161. def get_dir(self, remote_path, local_path, use_sudo, temp_dir):
  162. # Decide what needs to be stripped from remote paths so they're all
  163. # relative to the given remote_path
  164. if os.path.basename(remote_path):
  165. strip = os.path.dirname(remote_path)
  166. else:
  167. strip = os.path.dirname(os.path.dirname(remote_path))
  168. # Store all paths gotten so we can return them when done
  169. result = []
  170. # Use our facsimile of os.walk to find all files within remote_path
  171. for context, dirs, files in self.walk(remote_path):
  172. # Normalize current directory to be relative
  173. # E.g. remote_path of /var/log and current dir of /var/log/apache2
  174. # would be turned into just 'apache2'
  175. lcontext = rcontext = context.replace(strip, '', 1).lstrip('/')
  176. # Prepend local path to that to arrive at the local mirrored
  177. # version of this directory. So if local_path was 'mylogs', we'd
  178. # end up with 'mylogs/apache2'
  179. lcontext = os.path.join(local_path, lcontext)
  180. # Download any files in current directory
  181. for f in files:
  182. # Construct full and relative remote paths to this file
  183. rpath = posixpath.join(context, f)
  184. rremote = posixpath.join(rcontext, f)
  185. # If local_path isn't using a format string that expands to
  186. # include its remote path, we need to add it here.
  187. if "%(path)s" not in local_path \
  188. and "%(dirname)s" not in local_path:
  189. lpath = os.path.join(lcontext, f)
  190. # Otherwise, just passthrough local_path to self.get()
  191. else:
  192. lpath = local_path
  193. # Now we can make a call to self.get() with specific file paths
  194. # on both ends.
  195. result.append(self.get(rpath, lpath, use_sudo, True, rremote, temp_dir))
  196. return result
  197. def put(self, local_path, remote_path, use_sudo, mirror_local_mode, mode,
  198. local_is_path, temp_dir):
  199. from fabric.api import sudo, hide
  200. pre = self.ftp.getcwd()
  201. pre = pre if pre else ''
  202. if local_is_path and self.isdir(remote_path):
  203. basename = os.path.basename(local_path)
  204. remote_path = posixpath.join(remote_path, basename)
  205. if output.running:
  206. print("[%s] put: %s -> %s" % (
  207. env.host_string,
  208. _format_local(local_path, local_is_path),
  209. posixpath.join(pre, remote_path)
  210. ))
  211. # When using sudo, "bounce" the file through a guaranteed-unique file
  212. # path in the default remote CWD (which, typically, the login user will
  213. # have write permissions on) in order to sudo(mv) it later.
  214. if use_sudo:
  215. target_path = remote_path
  216. remote_path = posixpath.join(temp_dir, uuid.uuid4().hex)
  217. # Read, ensuring we handle file-like objects correct re: seek pointer
  218. putter = self.ftp.put
  219. if not local_is_path:
  220. old_pointer = local_path.tell()
  221. local_path.seek(0)
  222. putter = self.ftp.putfo
  223. rattrs = putter(local_path, remote_path)
  224. if not local_is_path:
  225. local_path.seek(old_pointer)
  226. # Handle modes if necessary
  227. if (local_is_path and mirror_local_mode) or (mode is not None):
  228. lmode = os.stat(local_path).st_mode if mirror_local_mode else mode
  229. # Cast to octal integer in case of string
  230. if isinstance(lmode, basestring):
  231. lmode = int(lmode, 8)
  232. lmode = lmode & 07777
  233. rmode = rattrs.st_mode
  234. # Only bitshift if we actually got an rmode
  235. if rmode is not None:
  236. rmode = (rmode & 07777)
  237. if lmode != rmode:
  238. if use_sudo:
  239. # Temporarily nuke 'cwd' so sudo() doesn't "cd" its mv
  240. # command. (The target path has already been cwd-ified
  241. # elsewhere.)
  242. with settings(hide('everything'), cwd=""):
  243. sudo('chmod %o \"%s\"' % (lmode, remote_path))
  244. else:
  245. self.ftp.chmod(remote_path, lmode)
  246. if use_sudo:
  247. # Temporarily nuke 'cwd' so sudo() doesn't "cd" its mv command.
  248. # (The target path has already been cwd-ified elsewhere.)
  249. with settings(hide('everything'), cwd=""):
  250. sudo("mv \"%s\" \"%s\"" % (remote_path, target_path))
  251. # Revert to original remote_path for return value's sake
  252. remote_path = target_path
  253. return remote_path
  254. def put_dir(self, local_path, remote_path, use_sudo, mirror_local_mode,
  255. mode, temp_dir):
  256. if os.path.basename(local_path):
  257. strip = os.path.dirname(local_path)
  258. else:
  259. strip = os.path.dirname(os.path.dirname(local_path))
  260. remote_paths = []
  261. for context, dirs, files in os.walk(local_path):
  262. rcontext = context.replace(strip, '', 1)
  263. # normalize pathname separators with POSIX separator
  264. rcontext = rcontext.replace(os.sep, '/')
  265. rcontext = rcontext.lstrip('/')
  266. rcontext = posixpath.join(remote_path, rcontext)
  267. if not self.exists(rcontext):
  268. self.mkdir(rcontext, use_sudo)
  269. for d in dirs:
  270. n = posixpath.join(rcontext, d)
  271. if not self.exists(n):
  272. self.mkdir(n, use_sudo)
  273. for f in files:
  274. local_path = os.path.join(context, f)
  275. n = posixpath.join(rcontext, f)
  276. p = self.put(local_path, n, use_sudo, mirror_local_mode, mode,
  277. True, temp_dir)
  278. remote_paths.append(p)
  279. return remote_paths