comparison piecrust/publishing/sftp.py @ 759:dd03385adb62

publish: Add SFTP publisher.
author Ludovic Chabant <ludovic@chabant.com>
date Sat, 25 Jun 2016 17:03:43 -0700
parents
children 3b33d9fb007c
comparison
equal deleted inserted replaced
758:6abb436fea5b 759:dd03385adb62
1 import os
2 import os.path
3 import socket
4 import urllib.parse
5 import getpass
6 import logging
7 import paramiko
8 from piecrust.publishing.base import Publisher, PublisherConfigurationError
9
10
11 logger = logging.getLogger(__name__)
12
13
14 class SftpPublisher(Publisher):
15 PUBLISHER_NAME = 'sftp'
16 PUBLISHER_SCHEME = 'sftp'
17
18 def setupPublishParser(self, parser, app):
19 parser.add_argument(
20 '--force',
21 action='store_true',
22 help=("Upload the entire bake directory instead of only "
23 "the files changed by the last bake."))
24
25 def run(self, ctx):
26 remote = self.config
27 if not self.has_url_config:
28 host = self.getConfigValue('host')
29 if not host:
30 raise PublisherConfigurationError(
31 "Publish target '%s' doesn't specify a 'host'." %
32 self.target)
33 remote = urllib.parse.urlparse(host)
34
35 hostname = remote.hostname
36 port = remote.port or 22
37 path = remote.path
38 if not hostname:
39 hostname = path
40 path = ''
41
42 username = remote.username
43 pkey_path = None
44
45 if not self.has_url_config:
46 if not username:
47 username = self.getConfigValue('username')
48 if not path:
49 path = self.getConfigValue('path')
50
51 pkey_path = self.getConfigValue('key')
52
53 password = None
54 if username:
55 password = getpass.getpass("Password for '%s': " % username)
56
57 logger.debug("Connecting to %s:%s..." % (hostname, port))
58 lfk = (not username and not pkey_path)
59 sshc = paramiko.SSHClient()
60 sshc.load_system_host_keys()
61 sshc.set_missing_host_key_policy(paramiko.WarningPolicy())
62 sshc.connect(
63 hostname, port=port,
64 username=username, password=password,
65 key_filename=pkey_path,
66 look_for_keys=lfk)
67 try:
68 logger.info("Connected as %s" %
69 sshc.get_transport().get_username())
70 client = sshc.open_sftp()
71 try:
72 self._upload(sshc, client, ctx, path)
73 finally:
74 client.close()
75 finally:
76 sshc.close()
77
78 def _upload(self, session, client, ctx, dest_dir):
79 if dest_dir:
80 if dest_dir.startswith('~/'):
81 _, out_chan, _ = session.exec_command("echo $HOME")
82 home_dir = out_chan.read().decode('utf8').strip()
83 dest_dir = home_dir + dest_dir[1:]
84 logger.debug("CHDIR %s" % dest_dir)
85 try:
86 client.chdir(dest_dir)
87 except IOError:
88 client.mkdir(dest_dir)
89 client.chdir(dest_dir)
90
91 known_dirs = {}
92 if ctx.was_baked and not ctx.args.force:
93 to_upload = list(self.getBakedFiles(ctx))
94 to_delete = list(self.getDeletedFiles(ctx))
95 if to_upload or to_delete:
96 logger.info("Uploading new/changed files...")
97 for path in self.getBakedFiles(ctx):
98 rel_path = os.path.relpath(path, ctx.bake_out_dir)
99 logger.info(rel_path)
100 if not ctx.preview:
101 self._putFile(client, path, rel_path, known_dirs)
102 logger.info("Deleting removed files...")
103 for path in self.getDeletedFiles(ctx):
104 rel_path = os.path.relpath(path, ctx.bake_out_dir)
105 logger.info("%s [DELETE]" % rel_path)
106 if not ctx.preview:
107 try:
108 client.remove(rel_path)
109 except OSError:
110 pass
111 else:
112 logger.info("Nothing to upload or delete on the remote server.")
113 logger.info("If you want to force uploading the entire website, "
114 "use the `--force` flag.")
115 else:
116 logger.info("Uploading entire website...")
117 for dirpath, dirnames, filenames in os.walk(ctx.bake_out_dir):
118 for f in filenames:
119 abs_f = os.path.join(dirpath, f)
120 rel_f = os.path.relpath(abs_f, ctx.bake_out_dir)
121 logger.info(rel_f)
122 if not ctx.preview:
123 self._putFile(client, abs_f, rel_f, known_dirs)
124
125 def _putFile(self, client, local_path, remote_path, known_dirs):
126 # Split the remote path in bits.
127 remote_path = os.path.normpath(remote_path)
128 if os.sep != '/':
129 remote_path = remote_path.sub(os.sep, '/')
130
131 # Make sure each directory in the remote path exists... to prevent
132 # testing the same directories several times, we keep a cache of
133 # `known_dirs` which we know exist.
134 remote_bits = remote_path.split('/')
135 cur = ''
136 for b in remote_bits[:-1]:
137 cur = os.path.join(cur, b)
138 if cur not in known_dirs:
139 try:
140 info = client.stat(cur)
141 except FileNotFoundError:
142 logger.debug("Creating remote dir: %s" % cur)
143 client.mkdir(cur)
144 known_dirs[cur] = True
145
146 # Should be all good! Upload the file.
147 client.put(local_path, remote_path)
148