Mercurial > piecrust2
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 |