Imported Upstream version 0.9.9~rc3
Gianfranco Costamagna
8 years ago
0 | s3cmd 0.9.9-rc3 - 2009-02-02 | |
1 | =============== | |
2 | * Fixed crash in S3Error().__str__() (typically Amazon's Internal | |
3 | errors, etc). | |
4 | ||
5 | s3cmd 0.9.9-rc2 - 2009-01-30 | |
6 | =============== | |
7 | * Fixed s3cmd crash when put / get / sync found | |
8 | zero files to transfer. | |
9 | * In --dry-run output files to be deleted only | |
10 | with --delete-removed, otherwise users get scared. | |
11 | ||
12 | s3cmd 0.9.9-rc1 - 2009-01-27 | |
13 | =============== | |
14 | * CloudFront support through cfcreate, cfdelete, | |
15 | cfmodify and cfinfo commands. | |
16 | * Added --include / --rinclude / --(r)include-from | |
17 | options to override --exclude exclusions. | |
18 | * Enabled --dry-run for [put] and [get]. | |
19 | * Fixed GPG (--encrypt) compatibility with Python 2.6. | |
20 | ||
21 | s3cmd 0.9.9-pre5 - 2009-01-22 | |
22 | ================ | |
23 | * New command 'setacl' for setting ACL on existing objects. | |
24 | * Recursive [put] with a slightly different semantic. | |
25 | * Multiple sources for [sync] and slightly different semantics. | |
26 | * Support for --dry-run with [sync] | |
27 | ||
28 | s3cmd 0.9.9-pre4 - 2008-12-30 | |
29 | ================ | |
30 | * Support for non-recursive [ls] | |
31 | * Support for multiple sources and recursive [get]. | |
32 | * Improved wildcard [get]. | |
33 | * New option --skip-existing for [get] and [sync]. | |
34 | * Improved Progress class (fixes Mac OS X) | |
35 | * Fixed installation on Windows and Mac OS X. | |
36 | * Don't print nasty backtrace on KeyboardInterrupt. | |
37 | * Should work fine on non-UTF8 systems, provided all | |
38 | the files are in current system encoding. | |
39 | * System encoding can be overriden using --encoding. | |
40 | ||
41 | s3cmd 0.9.9-pre3 - 2008-12-01 | |
42 | ================ | |
43 | * Bugfixes only | |
44 | - Fixed sync from S3 to local | |
45 | - Fixed progress meter with Unicode chars | |
46 | ||
47 | s3cmd 0.9.9-pre2 - 2008-11-24 | |
48 | ================ | |
49 | * Implemented progress meter (--progress / --no-progress) | |
50 | * Removing of non-empty buckets with --force | |
51 | * Recursively remove objects from buckets with a given | |
52 | prefix with --recursive (-r) | |
53 | * Copying and moving objects, within or between buckets. | |
54 | (Andrew Ryan) | |
55 | * Continue getting partially downloaded files with --continue | |
56 | * Improved resistance to communication errors (Connection | |
57 | reset by peer, etc.) | |
58 | ||
0 | 59 | s3cmd 0.9.8.4 - 2008-11-07 |
1 | 60 | ============= |
2 | 61 | * Stabilisation / bugfix release: |
0 | 0 | Metadata-Version: 1.0 |
1 | 1 | Name: s3cmd |
2 | Version: 0.9.8.4 | |
3 | Summary: S3cmd is a tool for managing Amazon S3 storage space. | |
2 | Version: 0.9.9-rc3 | |
3 | Summary: Command line tool for managing Amazon S3 and CloudFront services | |
4 | 4 | Home-page: http://s3tools.logix.cz |
5 | 5 | Author: Michal Ludvig |
6 | 6 | Author-email: michal@logix.cz |
9 | 9 | |
10 | 10 | S3cmd lets you copy files from/to Amazon S3 |
11 | 11 | (Simple Storage Service) using a simple to use |
12 | command line client. | |
12 | command line client. Supports rsync-like backup, | |
13 | GPG encryption, and more. Also supports management | |
14 | of Amazon's CloudFront content delivery network. | |
13 | 15 | |
14 | 16 | |
15 | 17 | Authors: |
0 | ## Amazon S3 - Access Control List representation | |
1 | ## Author: Michal Ludvig <michal@logix.cz> | |
2 | ## http://www.logix.cz/michal | |
3 | ## License: GPL Version 2 | |
4 | ||
5 | from Utils import * | |
6 | ||
7 | try: | |
8 | import xml.etree.ElementTree as ET | |
9 | except ImportError: | |
10 | import elementtree.ElementTree as ET | |
11 | ||
12 | class Grantee(object): | |
13 | ALL_USERS_URI = "http://acs.amazonaws.com/groups/global/AllUsers" | |
14 | ||
15 | def __init__(self): | |
16 | self.xsi_type = None | |
17 | self.tag = None | |
18 | self.name = None | |
19 | self.display_name = None | |
20 | self.permission = None | |
21 | ||
22 | def __repr__(self): | |
23 | return 'Grantee("%(tag)s", "%(name)s", "%(permission)s")' % { | |
24 | "tag" : self.tag, | |
25 | "name" : self.name, | |
26 | "permission" : self.permission | |
27 | } | |
28 | ||
29 | def isAllUsers(self): | |
30 | return self.tag == "URI" and self.name == Grantee.ALL_USERS_URI | |
31 | ||
32 | def isAnonRead(self): | |
33 | return self.isAllUsers and self.permission == "READ" | |
34 | ||
35 | def getElement(self): | |
36 | el = ET.Element("Grant") | |
37 | grantee = ET.SubElement(el, "Grantee", { | |
38 | 'xmlns:xsi' : 'http://www.w3.org/2001/XMLSchema-instance', | |
39 | 'xsi:type' : self.xsi_type | |
40 | }) | |
41 | name = ET.SubElement(grantee, self.tag) | |
42 | name.text = self.name | |
43 | permission = ET.SubElement(el, "Permission") | |
44 | permission.text = self.permission | |
45 | return el | |
46 | ||
47 | class GranteeAnonRead(Grantee): | |
48 | def __init__(self): | |
49 | Grantee.__init__(self) | |
50 | self.xsi_type = "Group" | |
51 | self.tag = "URI" | |
52 | self.name = Grantee.ALL_USERS_URI | |
53 | self.permission = "READ" | |
54 | ||
55 | class ACL(object): | |
56 | EMPTY_ACL = "<AccessControlPolicy><Owner><ID></ID></Owner><AccessControlList></AccessControlList></AccessControlPolicy>" | |
57 | ||
58 | def __init__(self, xml = None): | |
59 | if not xml: | |
60 | xml = ACL.EMPTY_ACL | |
61 | ||
62 | self.grantees = [] | |
63 | self.owner_id = "" | |
64 | self.owner_nick = "" | |
65 | ||
66 | tree = getTreeFromXml(xml) | |
67 | self.parseOwner(tree) | |
68 | self.parseGrants(tree) | |
69 | ||
70 | def parseOwner(self, tree): | |
71 | self.owner_id = tree.findtext(".//Owner//ID") | |
72 | self.owner_nick = tree.findtext(".//Owner//DisplayName") | |
73 | ||
74 | def parseGrants(self, tree): | |
75 | for grant in tree.findall(".//Grant"): | |
76 | grantee = Grantee() | |
77 | g = grant.find(".//Grantee") | |
78 | grantee.xsi_type = g.attrib['{http://www.w3.org/2001/XMLSchema-instance}type'] | |
79 | grantee.permission = grant.find('Permission').text | |
80 | for el in g: | |
81 | if el.tag == "DisplayName": | |
82 | grantee.display_name = el.text | |
83 | else: | |
84 | grantee.tag = el.tag | |
85 | grantee.name = el.text | |
86 | self.grantees.append(grantee) | |
87 | ||
88 | def getGrantList(self): | |
89 | acl = {} | |
90 | for grantee in self.grantees: | |
91 | if grantee.display_name: | |
92 | user = grantee.display_name | |
93 | elif grantee.isAllUsers(): | |
94 | user = "*anon*" | |
95 | else: | |
96 | user = grantee.name | |
97 | acl[user] = grantee.permission | |
98 | return acl | |
99 | ||
100 | def getOwner(self): | |
101 | return { 'id' : self.owner_id, 'nick' : self.owner_nick } | |
102 | ||
103 | def isAnonRead(self): | |
104 | for grantee in self.grantees: | |
105 | if grantee.isAnonRead(): | |
106 | return True | |
107 | return False | |
108 | ||
109 | def grantAnonRead(self): | |
110 | if not self.isAnonRead(): | |
111 | self.grantees.append(GranteeAnonRead()) | |
112 | ||
113 | def revokeAnonRead(self): | |
114 | self.grantees = [g for g in self.grantees if not g.isAnonRead()] | |
115 | ||
116 | def __str__(self): | |
117 | tree = getTreeFromXml(ACL.EMPTY_ACL) | |
118 | tree.attrib['xmlns'] = "http://s3.amazonaws.com/doc/2006-03-01/" | |
119 | owner = tree.find(".//Owner//ID") | |
120 | owner.text = self.owner_id | |
121 | acl = tree.find(".//AccessControlList") | |
122 | for grantee in self.grantees: | |
123 | acl.append(grantee.getElement()) | |
124 | return ET.tostring(tree) | |
125 | ||
126 | if __name__ == "__main__": | |
127 | xml = """<?xml version="1.0" encoding="UTF-8"?> | |
128 | <AccessControlPolicy xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> | |
129 | <Owner> | |
130 | <ID>12345678901234567890</ID> | |
131 | <DisplayName>owner-nickname</DisplayName> | |
132 | </Owner> | |
133 | <AccessControlList> | |
134 | <Grant> | |
135 | <Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="CanonicalUser"> | |
136 | <ID>12345678901234567890</ID> | |
137 | <DisplayName>owner-nickname</DisplayName> | |
138 | </Grantee> | |
139 | <Permission>FULL_CONTROL</Permission> | |
140 | </Grant> | |
141 | <Grant> | |
142 | <Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Group"> | |
143 | <URI>http://acs.amazonaws.com/groups/global/AllUsers</URI> | |
144 | </Grantee> | |
145 | <Permission>READ</Permission> | |
146 | </Grant> | |
147 | </AccessControlList> | |
148 | </AccessControlPolicy> | |
149 | """ | |
150 | acl = ACL(xml) | |
151 | print "Grants:", acl.getGrantList() | |
152 | acl.revokeAnonRead() | |
153 | print "Grants:", acl.getGrantList() | |
154 | acl.grantAnonRead() | |
155 | print "Grants:", acl.getGrantList() | |
156 | print acl |
0 | ## Amazon CloudFront support | |
1 | ## Author: Michal Ludvig <michal@logix.cz> | |
2 | ## http://www.logix.cz/michal | |
3 | ## License: GPL Version 2 | |
4 | ||
5 | import sys | |
6 | import base64 | |
7 | import time | |
8 | import httplib | |
9 | from logging import debug, info, warning, error | |
10 | ||
11 | try: | |
12 | from hashlib import md5, sha1 | |
13 | except ImportError: | |
14 | from md5 import md5 | |
15 | import sha as sha1 | |
16 | import hmac | |
17 | ||
18 | try: | |
19 | import xml.etree.ElementTree as ET | |
20 | except ImportError: | |
21 | import elementtree.ElementTree as ET | |
22 | ||
23 | from Config import Config | |
24 | from Exceptions import * | |
25 | from Utils import getTreeFromXml, appendXmlTextNode, getDictFromTree, dateS3toPython | |
26 | from S3Uri import S3Uri, S3UriS3 | |
27 | ||
28 | def output(message): | |
29 | sys.stdout.write(message + "\n") | |
30 | ||
31 | def pretty_output(label, message): | |
32 | #label = ("%s " % label).ljust(20, ".") | |
33 | label = ("%s:" % label).ljust(15) | |
34 | output("%s %s" % (label, message)) | |
35 | ||
36 | class DistributionSummary(object): | |
37 | ## Example: | |
38 | ## | |
39 | ## <DistributionSummary> | |
40 | ## <Id>1234567890ABC</Id> | |
41 | ## <Status>Deployed</Status> | |
42 | ## <LastModifiedTime>2009-01-16T11:49:02.189Z</LastModifiedTime> | |
43 | ## <DomainName>blahblahblah.cloudfront.net</DomainName> | |
44 | ## <Origin>example.bucket.s3.amazonaws.com</Origin> | |
45 | ## <Enabled>true</Enabled> | |
46 | ## </DistributionSummary> | |
47 | ||
48 | def __init__(self, tree): | |
49 | if tree.tag != "DistributionSummary": | |
50 | raise ValueError("Expected <DistributionSummary /> xml, got: <%s />" % tree.tag) | |
51 | self.parse(tree) | |
52 | ||
53 | def parse(self, tree): | |
54 | self.info = getDictFromTree(tree) | |
55 | self.info['Enabled'] = (self.info['Enabled'].lower() == "true") | |
56 | ||
57 | def uri(self): | |
58 | return S3Uri("cf://%s" % self.info['Id']) | |
59 | ||
60 | class DistributionList(object): | |
61 | ## Example: | |
62 | ## | |
63 | ## <DistributionList xmlns="http://cloudfront.amazonaws.com/doc/2008-06-30/"> | |
64 | ## <Marker /> | |
65 | ## <MaxItems>100</MaxItems> | |
66 | ## <IsTruncated>false</IsTruncated> | |
67 | ## <DistributionSummary> | |
68 | ## ... handled by DistributionSummary() class ... | |
69 | ## </DistributionSummary> | |
70 | ## </DistributionList> | |
71 | ||
72 | def __init__(self, xml): | |
73 | tree = getTreeFromXml(xml) | |
74 | if tree.tag != "DistributionList": | |
75 | raise ValueError("Expected <DistributionList /> xml, got: <%s />" % tree.tag) | |
76 | self.parse(tree) | |
77 | ||
78 | def parse(self, tree): | |
79 | self.info = getDictFromTree(tree) | |
80 | ## Normalise some items | |
81 | self.info['IsTruncated'] = (self.info['IsTruncated'].lower() == "true") | |
82 | ||
83 | self.dist_summs = [] | |
84 | for dist_summ in tree.findall(".//DistributionSummary"): | |
85 | self.dist_summs.append(DistributionSummary(dist_summ)) | |
86 | ||
87 | class Distribution(object): | |
88 | ## Example: | |
89 | ## | |
90 | ## <Distribution xmlns="http://cloudfront.amazonaws.com/doc/2008-06-30/"> | |
91 | ## <Id>1234567890ABC</Id> | |
92 | ## <Status>InProgress</Status> | |
93 | ## <LastModifiedTime>2009-01-16T13:07:11.319Z</LastModifiedTime> | |
94 | ## <DomainName>blahblahblah.cloudfront.net</DomainName> | |
95 | ## <DistributionConfig> | |
96 | ## ... handled by DistributionConfig() class ... | |
97 | ## </DistributionConfig> | |
98 | ## </Distribution> | |
99 | ||
100 | def __init__(self, xml): | |
101 | tree = getTreeFromXml(xml) | |
102 | if tree.tag != "Distribution": | |
103 | raise ValueError("Expected <Distribution /> xml, got: <%s />" % tree.tag) | |
104 | self.parse(tree) | |
105 | ||
106 | def parse(self, tree): | |
107 | self.info = getDictFromTree(tree) | |
108 | ## Normalise some items | |
109 | self.info['LastModifiedTime'] = dateS3toPython(self.info['LastModifiedTime']) | |
110 | ||
111 | self.info['DistributionConfig'] = DistributionConfig(tree = tree.find(".//DistributionConfig")) | |
112 | ||
113 | def uri(self): | |
114 | return S3Uri("cf://%s" % self.info['Id']) | |
115 | ||
116 | class DistributionConfig(object): | |
117 | ## Example: | |
118 | ## | |
119 | ## <DistributionConfig> | |
120 | ## <Origin>somebucket.s3.amazonaws.com</Origin> | |
121 | ## <CallerReference>s3://somebucket/</CallerReference> | |
122 | ## <Comment>http://somebucket.s3.amazonaws.com/</Comment> | |
123 | ## <Enabled>true</Enabled> | |
124 | ## </DistributionConfig> | |
125 | ||
126 | EMPTY_CONFIG = "<DistributionConfig><Origin/><CallerReference/><Enabled>true</Enabled></DistributionConfig>" | |
127 | xmlns = "http://cloudfront.amazonaws.com/doc/2008-06-30/" | |
128 | def __init__(self, xml = None, tree = None): | |
129 | if not xml: | |
130 | xml = DistributionConfig.EMPTY_CONFIG | |
131 | ||
132 | if not tree: | |
133 | tree = getTreeFromXml(xml) | |
134 | ||
135 | if tree.tag != "DistributionConfig": | |
136 | raise ValueError("Expected <DistributionConfig /> xml, got: <%s />" % tree.tag) | |
137 | self.parse(tree) | |
138 | ||
139 | def parse(self, tree): | |
140 | self.info = getDictFromTree(tree) | |
141 | self.info['Enabled'] = (self.info['Enabled'].lower() == "true") | |
142 | if not self.info.has_key("CNAME"): | |
143 | self.info['CNAME'] = [] | |
144 | if type(self.info['CNAME']) != list: | |
145 | self.info['CNAME'] = [self.info['CNAME']] | |
146 | self.info['CNAME'] = [cname.lower() for cname in self.info['CNAME']] | |
147 | if not self.info.has_key("Comment"): | |
148 | self.info['Comment'] = "" | |
149 | ||
150 | def __str__(self): | |
151 | tree = ET.Element("DistributionConfig") | |
152 | tree.attrib['xmlns'] = DistributionConfig.xmlns | |
153 | ||
154 | ## Retain the order of the following calls! | |
155 | appendXmlTextNode("Origin", self.info['Origin'], tree) | |
156 | appendXmlTextNode("CallerReference", self.info['CallerReference'], tree) | |
157 | for cname in self.info['CNAME']: | |
158 | appendXmlTextNode("CNAME", cname.lower(), tree) | |
159 | if self.info['Comment']: | |
160 | appendXmlTextNode("Comment", self.info['Comment'], tree) | |
161 | appendXmlTextNode("Enabled", str(self.info['Enabled']).lower(), tree) | |
162 | ||
163 | return ET.tostring(tree) | |
164 | ||
165 | class CloudFront(object): | |
166 | operations = { | |
167 | "CreateDist" : { 'method' : "POST", 'resource' : "" }, | |
168 | "DeleteDist" : { 'method' : "DELETE", 'resource' : "/%(dist_id)s" }, | |
169 | "GetList" : { 'method' : "GET", 'resource' : "" }, | |
170 | "GetDistInfo" : { 'method' : "GET", 'resource' : "/%(dist_id)s" }, | |
171 | "GetDistConfig" : { 'method' : "GET", 'resource' : "/%(dist_id)s/config" }, | |
172 | "SetDistConfig" : { 'method' : "PUT", 'resource' : "/%(dist_id)s/config" }, | |
173 | } | |
174 | ||
175 | ## Maximum attempts of re-issuing failed requests | |
176 | _max_retries = 5 | |
177 | ||
178 | def __init__(self, config): | |
179 | self.config = config | |
180 | ||
181 | ## -------------------------------------------------- | |
182 | ## Methods implementing CloudFront API | |
183 | ## -------------------------------------------------- | |
184 | ||
185 | def GetList(self): | |
186 | response = self.send_request("GetList") | |
187 | response['dist_list'] = DistributionList(response['data']) | |
188 | if response['dist_list'].info['IsTruncated']: | |
189 | raise NotImplementedError("List is truncated. Ask s3cmd author to add support.") | |
190 | ## TODO: handle Truncated | |
191 | return response | |
192 | ||
193 | def CreateDistribution(self, uri, cnames_add = [], comment = None): | |
194 | dist_config = DistributionConfig() | |
195 | dist_config.info['Enabled'] = True | |
196 | dist_config.info['Origin'] = uri.host_name() | |
197 | dist_config.info['CallerReference'] = str(uri) | |
198 | if comment == None: | |
199 | dist_config.info['Comment'] = uri.public_url() | |
200 | else: | |
201 | dist_config.info['Comment'] = comment | |
202 | for cname in cnames_add: | |
203 | if dist_config.info['CNAME'].count(cname) == 0: | |
204 | dist_config.info['CNAME'].append(cname) | |
205 | request_body = str(dist_config) | |
206 | debug("CreateDistribution(): request_body: %s" % request_body) | |
207 | response = self.send_request("CreateDist", body = request_body) | |
208 | response['distribution'] = Distribution(response['data']) | |
209 | return response | |
210 | ||
211 | def ModifyDistribution(self, cfuri, cnames_add = [], cnames_remove = [], | |
212 | comment = None, enabled = None): | |
213 | if cfuri.type != "cf": | |
214 | raise ValueError("Expected CFUri instead of: %s" % cfuri) | |
215 | # Get current dist status (enabled/disabled) and Etag | |
216 | info("Checking current status of %s" % cfuri) | |
217 | response = self.GetDistConfig(cfuri) | |
218 | dc = response['dist_config'] | |
219 | if enabled != None: | |
220 | dc.info['Enabled'] = enabled | |
221 | if comment != None: | |
222 | dc.info['Comment'] = comment | |
223 | for cname in cnames_add: | |
224 | if dc.info['CNAME'].count(cname) == 0: | |
225 | dc.info['CNAME'].append(cname) | |
226 | for cname in cnames_remove: | |
227 | while dc.info['CNAME'].count(cname) > 0: | |
228 | dc.info['CNAME'].remove(cname) | |
229 | response = self.SetDistConfig(cfuri, dc, response['headers']['etag']) | |
230 | return response | |
231 | ||
232 | def DeleteDistribution(self, cfuri): | |
233 | if cfuri.type != "cf": | |
234 | raise ValueError("Expected CFUri instead of: %s" % cfuri) | |
235 | # Get current dist status (enabled/disabled) and Etag | |
236 | info("Checking current status of %s" % cfuri) | |
237 | response = self.GetDistConfig(cfuri) | |
238 | if response['dist_config'].info['Enabled']: | |
239 | info("Distribution is ENABLED. Disabling first.") | |
240 | response['dist_config'].info['Enabled'] = False | |
241 | response = self.SetDistConfig(cfuri, response['dist_config'], | |
242 | response['headers']['etag']) | |
243 | warning("Waiting for Distribution to become disabled.") | |
244 | warning("This may take several minutes, please wait.") | |
245 | while True: | |
246 | response = self.GetDistInfo(cfuri) | |
247 | d = response['distribution'] | |
248 | if d.info['Status'] == "Deployed" and d.info['Enabled'] == False: | |
249 | info("Distribution is now disabled") | |
250 | break | |
251 | warning("Still waiting...") | |
252 | time.sleep(10) | |
253 | headers = {} | |
254 | headers['if-match'] = response['headers']['etag'] | |
255 | response = self.send_request("DeleteDist", dist_id = cfuri.dist_id(), | |
256 | headers = headers) | |
257 | return response | |
258 | ||
259 | def GetDistInfo(self, cfuri): | |
260 | if cfuri.type != "cf": | |
261 | raise ValueError("Expected CFUri instead of: %s" % cfuri) | |
262 | response = self.send_request("GetDistInfo", dist_id = cfuri.dist_id()) | |
263 | response['distribution'] = Distribution(response['data']) | |
264 | return response | |
265 | ||
266 | def GetDistConfig(self, cfuri): | |
267 | if cfuri.type != "cf": | |
268 | raise ValueError("Expected CFUri instead of: %s" % cfuri) | |
269 | response = self.send_request("GetDistConfig", dist_id = cfuri.dist_id()) | |
270 | response['dist_config'] = DistributionConfig(response['data']) | |
271 | return response | |
272 | ||
273 | def SetDistConfig(self, cfuri, dist_config, etag = None): | |
274 | if etag == None: | |
275 | debug("SetDistConfig(): Etag not set. Fetching it first.") | |
276 | etag = self.GetDistConfig(cfuri)['headers']['etag'] | |
277 | debug("SetDistConfig(): Etag = %s" % etag) | |
278 | request_body = str(dist_config) | |
279 | debug("SetDistConfig(): request_body: %s" % request_body) | |
280 | headers = {} | |
281 | headers['if-match'] = etag | |
282 | response = self.send_request("SetDistConfig", dist_id = cfuri.dist_id(), | |
283 | body = request_body, headers = headers) | |
284 | return response | |
285 | ||
286 | ## -------------------------------------------------- | |
287 | ## Low-level methods for handling CloudFront requests | |
288 | ## -------------------------------------------------- | |
289 | ||
290 | def send_request(self, op_name, dist_id = None, body = None, headers = {}, retries = _max_retries): | |
291 | operation = self.operations[op_name] | |
292 | if body: | |
293 | headers['content-type'] = 'text/plain' | |
294 | request = self.create_request(operation, dist_id, headers) | |
295 | conn = self.get_connection() | |
296 | debug("send_request(): %s %s" % (request['method'], request['resource'])) | |
297 | conn.request(request['method'], request['resource'], body, request['headers']) | |
298 | http_response = conn.getresponse() | |
299 | response = {} | |
300 | response["status"] = http_response.status | |
301 | response["reason"] = http_response.reason | |
302 | response["headers"] = dict(http_response.getheaders()) | |
303 | response["data"] = http_response.read() | |
304 | conn.close() | |
305 | ||
306 | debug("CloudFront: response: %r" % response) | |
307 | ||
308 | if response["status"] >= 500: | |
309 | e = CloudFrontError(response) | |
310 | if retries: | |
311 | warning(u"Retrying failed request: %s" % op_name) | |
312 | warning(unicode(e)) | |
313 | warning("Waiting %d sec..." % self._fail_wait(retries)) | |
314 | time.sleep(self._fail_wait(retries)) | |
315 | return self.send_request(op_name, dist_id, body, retries - 1) | |
316 | else: | |
317 | raise e | |
318 | ||
319 | if response["status"] < 200 or response["status"] > 299: | |
320 | raise CloudFrontError(response) | |
321 | ||
322 | return response | |
323 | ||
324 | def create_request(self, operation, dist_id = None, headers = None): | |
325 | resource = self.config.cloudfront_resource + ( | |
326 | operation['resource'] % { 'dist_id' : dist_id }) | |
327 | ||
328 | if not headers: | |
329 | headers = {} | |
330 | ||
331 | if headers.has_key("date"): | |
332 | if not headers.has_key("x-amz-date"): | |
333 | headers["x-amz-date"] = headers["date"] | |
334 | del(headers["date"]) | |
335 | ||
336 | if not headers.has_key("x-amz-date"): | |
337 | headers["x-amz-date"] = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime()) | |
338 | ||
339 | signature = self.sign_request(headers) | |
340 | headers["Authorization"] = "AWS "+self.config.access_key+":"+signature | |
341 | ||
342 | request = {} | |
343 | request['resource'] = resource | |
344 | request['headers'] = headers | |
345 | request['method'] = operation['method'] | |
346 | ||
347 | return request | |
348 | ||
349 | def sign_request(self, headers): | |
350 | string_to_sign = headers['x-amz-date'] | |
351 | signature = base64.encodestring(hmac.new(self.config.secret_key, string_to_sign, sha1).digest()).strip() | |
352 | debug(u"CloudFront.sign_request('%s') = %s" % (string_to_sign, signature)) | |
353 | return signature | |
354 | ||
355 | def get_connection(self): | |
356 | if self.config.proxy_host != "": | |
357 | raise ParameterError("CloudFront commands don't work from behind a HTTP proxy") | |
358 | return httplib.HTTPSConnection(self.config.cloudfront_host) | |
359 | ||
360 | def _fail_wait(self, retries): | |
361 | # Wait a few seconds. The more it fails the more we wait. | |
362 | return (self._max_retries - retries + 1) * 3 | |
363 | ||
364 | class Cmd(object): | |
365 | """ | |
366 | Class that implements CloudFront commands | |
367 | """ | |
368 | ||
369 | class Options(object): | |
370 | cf_cnames_add = [] | |
371 | cf_cnames_remove = [] | |
372 | cf_comment = None | |
373 | cf_enable = None | |
374 | ||
375 | def option_list(self): | |
376 | return [opt for opt in dir(self) if opt.startswith("cf_")] | |
377 | ||
378 | def update_option(self, option, value): | |
379 | setattr(Cmd.options, option, value) | |
380 | ||
381 | options = Options() | |
382 | ||
383 | @staticmethod | |
384 | def info(args): | |
385 | cf = CloudFront(Config()) | |
386 | if not args: | |
387 | response = cf.GetList() | |
388 | for d in response['dist_list'].dist_summs: | |
389 | pretty_output("Origin", S3UriS3.httpurl_to_s3uri(d.info['Origin'])) | |
390 | pretty_output("DistId", d.uri()) | |
391 | pretty_output("DomainName", d.info['DomainName']) | |
392 | pretty_output("Status", d.info['Status']) | |
393 | pretty_output("Enabled", d.info['Enabled']) | |
394 | output("") | |
395 | else: | |
396 | cfuris = [] | |
397 | for arg in args: | |
398 | cfuris.append(S3Uri(arg)) | |
399 | if cfuris[-1].type != 'cf': | |
400 | raise ParameterError("CloudFront URI required instead of: %s" % arg) | |
401 | for cfuri in cfuris: | |
402 | response = cf.GetDistInfo(cfuri) | |
403 | d = response['distribution'] | |
404 | dc = d.info['DistributionConfig'] | |
405 | pretty_output("Origin", S3UriS3.httpurl_to_s3uri(dc.info['Origin'])) | |
406 | pretty_output("DistId", d.uri()) | |
407 | pretty_output("DomainName", d.info['DomainName']) | |
408 | pretty_output("Status", d.info['Status']) | |
409 | pretty_output("CNAMEs", ", ".join(dc.info['CNAME'])) | |
410 | pretty_output("Comment", dc.info['Comment']) | |
411 | pretty_output("Enabled", dc.info['Enabled']) | |
412 | pretty_output("Etag", response['headers']['etag']) | |
413 | ||
414 | @staticmethod | |
415 | def create(args): | |
416 | cf = CloudFront(Config()) | |
417 | buckets = [] | |
418 | for arg in args: | |
419 | uri = S3Uri(arg) | |
420 | if uri.type != "s3": | |
421 | raise ParameterError("Bucket can only be created from a s3:// URI instead of: %s" % arg) | |
422 | if uri.object(): | |
423 | raise ParameterError("Use s3:// URI with a bucket name only instead of: %s" % arg) | |
424 | if not uri.is_dns_compatible(): | |
425 | raise ParameterError("CloudFront can only handle lowercase-named buckets.") | |
426 | buckets.append(uri) | |
427 | if not buckets: | |
428 | raise ParameterError("No valid bucket names found") | |
429 | for uri in buckets: | |
430 | info("Creating distribution from: %s" % uri) | |
431 | response = cf.CreateDistribution(uri, cnames_add = Cmd.options.cf_cnames_add, | |
432 | comment = Cmd.options.cf_comment) | |
433 | d = response['distribution'] | |
434 | dc = d.info['DistributionConfig'] | |
435 | output("Distribution created:") | |
436 | pretty_output("Origin", S3UriS3.httpurl_to_s3uri(dc.info['Origin'])) | |
437 | pretty_output("DistId", d.uri()) | |
438 | pretty_output("DomainName", d.info['DomainName']) | |
439 | pretty_output("CNAMEs", ", ".join(dc.info['CNAME'])) | |
440 | pretty_output("Comment", dc.info['Comment']) | |
441 | pretty_output("Status", d.info['Status']) | |
442 | pretty_output("Enabled", dc.info['Enabled']) | |
443 | pretty_output("Etag", response['headers']['etag']) | |
444 | ||
445 | @staticmethod | |
446 | def delete(args): | |
447 | cf = CloudFront(Config()) | |
448 | cfuris = [] | |
449 | for arg in args: | |
450 | cfuris.append(S3Uri(arg)) | |
451 | if cfuris[-1].type != 'cf': | |
452 | raise ParameterError("CloudFront URI required instead of: %s" % arg) | |
453 | for cfuri in cfuris: | |
454 | response = cf.DeleteDistribution(cfuri) | |
455 | if response['status'] >= 400: | |
456 | error("Distribution %s could not be deleted: %s" % (cfuri, response['reason'])) | |
457 | output("Distribution %s deleted" % cfuri) | |
458 | ||
459 | @staticmethod | |
460 | def modify(args): | |
461 | cf = CloudFront(Config()) | |
462 | cfuri = S3Uri(args.pop(0)) | |
463 | if cfuri.type != 'cf': | |
464 | raise ParameterError("CloudFront URI required instead of: %s" % arg) | |
465 | if len(args): | |
466 | raise ParameterError("Too many parameters. Modify one Distribution at a time.") | |
467 | ||
468 | response = cf.ModifyDistribution(cfuri, | |
469 | cnames_add = Cmd.options.cf_cnames_add, | |
470 | cnames_remove = Cmd.options.cf_cnames_remove, | |
471 | comment = Cmd.options.cf_comment, | |
472 | enabled = Cmd.options.cf_enable) | |
473 | if response['status'] >= 400: | |
474 | error("Distribution %s could not be modified: %s" % (cfuri, response['reason'])) | |
475 | output("Distribution modified: %s" % cfuri) | |
476 | response = cf.GetDistInfo(cfuri) | |
477 | d = response['distribution'] | |
478 | dc = d.info['DistributionConfig'] | |
479 | pretty_output("Origin", S3UriS3.httpurl_to_s3uri(dc.info['Origin'])) | |
480 | pretty_output("DistId", d.uri()) | |
481 | pretty_output("DomainName", d.info['DomainName']) | |
482 | pretty_output("Status", d.info['Status']) | |
483 | pretty_output("CNAMEs", ", ".join(dc.info['CNAME'])) | |
484 | pretty_output("Comment", dc.info['Comment']) | |
485 | pretty_output("Enabled", dc.info['Enabled']) | |
486 | pretty_output("Etag", response['headers']['etag']) |
5 | 5 | import logging |
6 | 6 | from logging import debug, info, warning, error |
7 | 7 | import re |
8 | import Progress | |
8 | 9 | |
9 | 10 | class Config(object): |
10 | 11 | _instance = None |
15 | 16 | host_base = "s3.amazonaws.com" |
16 | 17 | host_bucket = "%(bucket)s.s3.amazonaws.com" |
17 | 18 | simpledb_host = "sdb.amazonaws.com" |
19 | cloudfront_host = "cloudfront.amazonaws.com" | |
20 | cloudfront_resource = "/2008-06-30/distribution" | |
18 | 21 | verbosity = logging.WARNING |
22 | progress_meter = True | |
23 | progress_class = Progress.ProgressCR | |
19 | 24 | send_chunk = 4096 |
20 | 25 | recv_chunk = 4096 |
21 | 26 | human_readable_sizes = False |
22 | 27 | force = False |
28 | get_continue = False | |
29 | skip_existing = False | |
30 | recursive = False | |
23 | 31 | acl_public = False |
24 | 32 | proxy_host = "" |
25 | 33 | proxy_port = 3128 |
46 | 54 | use_https = False |
47 | 55 | bucket_location = "US" |
48 | 56 | default_mime_type = "binary/octet-stream" |
49 | guess_mime_type = False | |
57 | guess_mime_type = True | |
50 | 58 | debug_syncmatch = False |
59 | # List of checks to be performed for 'sync' | |
60 | sync_checks = ['size', 'md5'] # 'weak-timestamp' | |
51 | 61 | # List of compiled REGEXPs |
52 | 62 | exclude = [] |
63 | include = [] | |
53 | 64 | # Dict mapping compiled REGEXPs back to their textual form |
54 | 65 | debug_exclude = {} |
66 | debug_include = {} | |
67 | encoding = "utf-8" | |
55 | 68 | |
56 | 69 | ## Creating a singleton |
57 | 70 | def __new__(self, configfile = None): |
118 | 131 | self.parse_file(file, sections) |
119 | 132 | |
120 | 133 | def parse_file(self, file, sections = []): |
121 | info("ConfigParser: Reading file '%s'" % file) | |
134 | debug("ConfigParser: Reading file '%s'" % file) | |
122 | 135 | if type(sections) != type([]): |
123 | 136 | sections = [sections] |
124 | 137 | in_our_section = True |
2 | 2 | ## http://www.logix.cz/michal |
3 | 3 | ## License: GPL Version 2 |
4 | 4 | |
5 | from Utils import getTreeFromXml, unicodise, deunicodise | |
5 | 6 | from logging import debug, info, warning, error |
6 | 7 | |
7 | 8 | try: |
10 | 11 | import elementtree.ElementTree as ET |
11 | 12 | |
12 | 13 | class S3Exception(Exception): |
14 | def __init__(self, message = ""): | |
15 | self.message = unicodise(message) | |
16 | ||
13 | 17 | def __str__(self): |
14 | ## Is this legal? | |
15 | return unicode(self) | |
18 | ## Call unicode(self) instead of self.message because | |
19 | ## __unicode__() method could be overriden in subclasses! | |
20 | return deunicodise(unicode(self)) | |
16 | 21 | |
17 | 22 | def __unicode__(self): |
18 | 23 | return self.message |
24 | ||
25 | ## (Base)Exception.message has been deprecated in Python 2.6 | |
26 | def _get_message(self): | |
27 | return self._message | |
28 | def _set_message(self, message): | |
29 | self._message = message | |
30 | message = property(_get_message, _set_message) | |
31 | ||
19 | 32 | |
20 | 33 | class S3Error (S3Exception): |
21 | 34 | def __init__(self, response): |
27 | 40 | for header in response["headers"]: |
28 | 41 | debug("HttpHeader: %s: %s" % (header, response["headers"][header])) |
29 | 42 | if response.has_key("data"): |
30 | tree = ET.fromstring(response["data"]) | |
31 | for child in tree.getchildren(): | |
43 | tree = getTreeFromXml(response["data"]) | |
44 | error_node = tree | |
45 | if not error_node.tag == "Error": | |
46 | error_node = tree.find(".//Error") | |
47 | for child in error_node.getchildren(): | |
32 | 48 | if child.text != "": |
33 | 49 | debug("ErrorXML: " + child.tag + ": " + repr(child.text)) |
34 | 50 | self.info[child.tag] = child.text |
35 | 51 | |
36 | 52 | def __unicode__(self): |
37 | retval = "%d (%s)" % (self.status, self.reason) | |
38 | try: | |
39 | retval += (": %s" % self.info["Code"]) | |
40 | except (AttributeError, KeyError): | |
41 | pass | |
53 | retval = u"%d " % (self.status) | |
54 | retval += (u"(%s)" % (self.info.has_key("Code") and self.info["Code"] or self.reason)) | |
55 | if self.info.has_key("Message"): | |
56 | retval += (u": %s" % self.info["Message"]) | |
42 | 57 | return retval |
43 | 58 | |
59 | class CloudFrontError(S3Error): | |
60 | pass | |
61 | ||
44 | 62 | class S3UploadError(S3Exception): |
45 | 63 | pass |
46 | 64 |
0 | 0 | package = "s3cmd" |
1 | version = "0.9.8.4" | |
1 | version = "0.9.9-rc3" | |
2 | 2 | url = "http://s3tools.logix.cz" |
3 | 3 | license = "GPL version 2" |
4 | short_description = "S3cmd is a tool for managing Amazon S3 storage space." | |
4 | short_description = "Command line tool for managing Amazon S3 and CloudFront services" | |
5 | 5 | long_description = """ |
6 | 6 | S3cmd lets you copy files from/to Amazon S3 |
7 | 7 | (Simple Storage Service) using a simple to use |
8 | command line client. | |
8 | command line client. Supports rsync-like backup, | |
9 | GPG encryption, and more. Also supports management | |
10 | of Amazon's CloudFront content delivery network. | |
9 | 11 | """ |
10 | 12 |
0 | ## Amazon S3 manager | |
1 | ## Author: Michal Ludvig <michal@logix.cz> | |
2 | ## http://www.logix.cz/michal | |
3 | ## License: GPL Version 2 | |
4 | ||
5 | import sys | |
6 | import datetime | |
7 | import Utils | |
8 | ||
9 | class Progress(object): | |
10 | _stdout = sys.stdout | |
11 | ||
12 | def __init__(self, labels, total_size): | |
13 | self._stdout = sys.stdout | |
14 | self.new_file(labels, total_size) | |
15 | ||
16 | def new_file(self, labels, total_size): | |
17 | self.labels = labels | |
18 | self.total_size = total_size | |
19 | # Set initial_position to something in the | |
20 | # case we're not counting from 0. For instance | |
21 | # when appending to a partially downloaded file. | |
22 | # Setting initial_position will let the speed | |
23 | # be computed right. | |
24 | self.initial_position = 0 | |
25 | self.current_position = self.initial_position | |
26 | self.time_start = datetime.datetime.now() | |
27 | self.time_last = self.time_start | |
28 | self.time_current = self.time_start | |
29 | ||
30 | self.display(new_file = True) | |
31 | ||
32 | def update(self, current_position = -1, delta_position = -1): | |
33 | self.time_last = self.time_current | |
34 | self.time_current = datetime.datetime.now() | |
35 | if current_position > -1: | |
36 | self.current_position = current_position | |
37 | elif delta_position > -1: | |
38 | self.current_position += delta_position | |
39 | #else: | |
40 | # no update, just call display() | |
41 | self.display() | |
42 | ||
43 | def done(self, message): | |
44 | self.display(done_message = message) | |
45 | ||
46 | def output_labels(self): | |
47 | self._stdout.write(u"%(source)s -> %(destination)s %(extra)s\n" % self.labels) | |
48 | self._stdout.flush() | |
49 | ||
50 | def display(self, new_file = False, done_message = None): | |
51 | """ | |
52 | display(new_file = False[/True], done = False[/True]) | |
53 | ||
54 | Override this method to provide a nicer output. | |
55 | """ | |
56 | if new_file: | |
57 | self.output_labels() | |
58 | self.last_milestone = 0 | |
59 | return | |
60 | ||
61 | if self.current_position == self.total_size: | |
62 | print_size = Utils.formatSize(self.current_position, True) | |
63 | if print_size[1] != "": print_size[1] += "B" | |
64 | timedelta = self.time_current - self.time_start | |
65 | sec_elapsed = timedelta.days * 86400 + timedelta.seconds + float(timedelta.microseconds)/1000000.0 | |
66 | print_speed = Utils.formatSize((self.current_position - self.initial_position) / sec_elapsed, True, True) | |
67 | self._stdout.write("100%% %s%s in %.2fs (%.2f %sB/s)\n" % | |
68 | (print_size[0], print_size[1], sec_elapsed, print_speed[0], print_speed[1])) | |
69 | self._stdout.flush() | |
70 | return | |
71 | ||
72 | rel_position = selfself.current_position * 100 / self.total_size | |
73 | if rel_position >= self.last_milestone: | |
74 | self.last_milestone = (int(rel_position) / 5) * 5 | |
75 | self._stdout.write("%d%% ", self.last_milestone) | |
76 | self._stdout.flush() | |
77 | return | |
78 | ||
79 | class ProgressANSI(Progress): | |
80 | ## http://en.wikipedia.org/wiki/ANSI_escape_code | |
81 | SCI = '\x1b[' | |
82 | ANSI_hide_cursor = SCI + "?25l" | |
83 | ANSI_show_cursor = SCI + "?25h" | |
84 | ANSI_save_cursor_pos = SCI + "s" | |
85 | ANSI_restore_cursor_pos = SCI + "u" | |
86 | ANSI_move_cursor_to_column = SCI + "%uG" | |
87 | ANSI_erase_to_eol = SCI + "0K" | |
88 | ANSI_erase_current_line = SCI + "2K" | |
89 | ||
90 | def display(self, new_file = False, done_message = None): | |
91 | """ | |
92 | display(new_file = False[/True], done_message = None) | |
93 | """ | |
94 | if new_file: | |
95 | self.output_labels() | |
96 | self._stdout.write(self.ANSI_save_cursor_pos) | |
97 | self._stdout.flush() | |
98 | return | |
99 | ||
100 | timedelta = self.time_current - self.time_start | |
101 | sec_elapsed = timedelta.days * 86400 + timedelta.seconds + float(timedelta.microseconds)/1000000.0 | |
102 | if (sec_elapsed > 0): | |
103 | print_speed = Utils.formatSize((self.current_position - self.initial_position) / sec_elapsed, True, True) | |
104 | else: | |
105 | print_speed = (0, "") | |
106 | self._stdout.write(self.ANSI_restore_cursor_pos) | |
107 | self._stdout.write(self.ANSI_erase_to_eol) | |
108 | self._stdout.write("%(current)s of %(total)s %(percent)3d%% in %(elapsed)ds %(speed).2f %(speed_coeff)sB/s" % { | |
109 | "current" : str(self.current_position).rjust(len(str(self.total_size))), | |
110 | "total" : self.total_size, | |
111 | "percent" : self.total_size and (self.current_position * 100 / self.total_size) or 0, | |
112 | "elapsed" : sec_elapsed, | |
113 | "speed" : print_speed[0], | |
114 | "speed_coeff" : print_speed[1] | |
115 | }) | |
116 | ||
117 | if done_message: | |
118 | self._stdout.write(" %s\n" % done_message) | |
119 | ||
120 | self._stdout.flush() | |
121 | ||
122 | class ProgressCR(Progress): | |
123 | ## Uses CR char (Carriage Return) just like other progress bars do. | |
124 | CR_char = chr(13) | |
125 | ||
126 | def display(self, new_file = False, done_message = None): | |
127 | """ | |
128 | display(new_file = False[/True], done_message = None) | |
129 | """ | |
130 | if new_file: | |
131 | self.output_labels() | |
132 | return | |
133 | ||
134 | timedelta = self.time_current - self.time_start | |
135 | sec_elapsed = timedelta.days * 86400 + timedelta.seconds + float(timedelta.microseconds)/1000000.0 | |
136 | if (sec_elapsed > 0): | |
137 | print_speed = Utils.formatSize((self.current_position - self.initial_position) / sec_elapsed, True, True) | |
138 | else: | |
139 | print_speed = (0, "") | |
140 | self._stdout.write(self.CR_char) | |
141 | output = " %(current)s of %(total)s %(percent)3d%% in %(elapsed)4ds %(speed)7.2f %(speed_coeff)sB/s" % { | |
142 | "current" : str(self.current_position).rjust(len(str(self.total_size))), | |
143 | "total" : self.total_size, | |
144 | "percent" : self.total_size and (self.current_position * 100 / self.total_size) or 0, | |
145 | "elapsed" : sec_elapsed, | |
146 | "speed" : print_speed[0], | |
147 | "speed_coeff" : print_speed[1] | |
148 | } | |
149 | self._stdout.write(output) | |
150 | if done_message: | |
151 | self._stdout.write(" %s\n" % done_message) | |
152 | ||
153 | self._stdout.flush() |
5 | 5 | import sys |
6 | 6 | import os, os.path |
7 | 7 | import base64 |
8 | import md5 | |
9 | import sha | |
10 | import hmac | |
8 | import time | |
11 | 9 | import httplib |
12 | 10 | import logging |
13 | 11 | import mimetypes |
14 | 12 | from logging import debug, info, warning, error |
15 | 13 | from stat import ST_SIZE |
16 | 14 | |
15 | try: | |
16 | from hashlib import md5, sha1 | |
17 | except ImportError: | |
18 | from md5 import md5 | |
19 | import sha as sha1 | |
20 | import hmac | |
21 | ||
17 | 22 | from Utils import * |
18 | 23 | from SortedDict import SortedDict |
19 | 24 | from BidirMap import BidirMap |
20 | 25 | from Config import Config |
21 | 26 | from Exceptions import * |
27 | from ACL import ACL | |
22 | 28 | |
23 | 29 | class S3(object): |
24 | 30 | http_methods = BidirMap( |
57 | 63 | ## S3 sometimes sends HTTP-307 response |
58 | 64 | redir_map = {} |
59 | 65 | |
66 | ## Maximum attempts of re-issuing failed requests | |
67 | _max_retries = 5 | |
68 | ||
60 | 69 | def __init__(self, config): |
61 | 70 | self.config = config |
62 | 71 | |
100 | 109 | response["list"] = getListFromXml(response["data"], "Bucket") |
101 | 110 | return response |
102 | 111 | |
103 | def bucket_list(self, bucket, prefix = None): | |
112 | def bucket_list(self, bucket, prefix = None, recursive = None): | |
104 | 113 | def _list_truncated(data): |
105 | 114 | ## <IsTruncated> can either be "true" or "false" or be missing completely |
106 | 115 | is_truncated = getTextFromXml(data, ".//IsTruncated") or "false" |
109 | 118 | def _get_contents(data): |
110 | 119 | return getListFromXml(data, "Contents") |
111 | 120 | |
112 | prefix = self.urlencode_string(prefix) | |
113 | request = self.create_request("BUCKET_LIST", bucket = bucket, prefix = prefix) | |
121 | def _get_common_prefixes(data): | |
122 | return getListFromXml(data, "CommonPrefixes") | |
123 | ||
124 | uri_params = {} | |
125 | if prefix: | |
126 | uri_params['prefix'] = self.urlencode_string(prefix) | |
127 | if not self.config.recursive and not recursive: | |
128 | uri_params['delimiter'] = "/" | |
129 | request = self.create_request("BUCKET_LIST", bucket = bucket, **uri_params) | |
114 | 130 | response = self.send_request(request) |
115 | 131 | #debug(response) |
116 | 132 | list = _get_contents(response["data"]) |
133 | prefixes = _get_common_prefixes(response["data"]) | |
117 | 134 | while _list_truncated(response["data"]): |
118 | marker = list[-1]["Key"] | |
119 | info("Listing continues after '%s'" % marker) | |
120 | request = self.create_request("BUCKET_LIST", bucket = bucket, | |
121 | prefix = prefix, | |
122 | marker = self.urlencode_string(marker)) | |
135 | uri_params['marker'] = self.urlencode_string(list[-1]["Key"]) | |
136 | debug("Listing continues after '%s'" % uri_params['marker']) | |
137 | request = self.create_request("BUCKET_LIST", bucket = bucket, **uri_params) | |
123 | 138 | response = self.send_request(request) |
124 | 139 | list += _get_contents(response["data"]) |
140 | prefixes += _get_common_prefixes(response["data"]) | |
125 | 141 | response['list'] = list |
142 | response['common_prefixes'] = prefixes | |
126 | 143 | return response |
127 | 144 | |
128 | 145 | def bucket_create(self, bucket, bucket_location = None): |
154 | 171 | response['bucket-location'] = getTextFromXml(response['data'], "LocationConstraint") or "any" |
155 | 172 | return response |
156 | 173 | |
157 | def object_put(self, filename, bucket, object, extra_headers = None): | |
174 | def object_put(self, filename, uri, extra_headers = None, extra_label = ""): | |
175 | # TODO TODO | |
176 | # Make it consistent with stream-oriented object_get() | |
177 | if uri.type != "s3": | |
178 | raise ValueError("Expected URI type 's3', got '%s'" % uri.type) | |
179 | ||
158 | 180 | if not os.path.isfile(filename): |
159 | raise InvalidFileError("%s is not a regular file" % filename) | |
181 | raise InvalidFileError(u"%s is not a regular file" % unicodise(filename)) | |
160 | 182 | try: |
161 | 183 | file = open(filename, "rb") |
162 | 184 | size = os.stat(filename)[ST_SIZE] |
163 | 185 | except IOError, e: |
164 | raise InvalidFileError("%s: %s" % (filename, e.strerror)) | |
186 | raise InvalidFileError(u"%s: %s" % (unicodise(filename), e.strerror)) | |
165 | 187 | headers = SortedDict() |
166 | 188 | if extra_headers: |
167 | 189 | headers.update(extra_headers) |
175 | 197 | headers["content-type"] = content_type |
176 | 198 | if self.config.acl_public: |
177 | 199 | headers["x-amz-acl"] = "public-read" |
178 | request = self.create_request("OBJECT_PUT", bucket = bucket, object = object, headers = headers) | |
179 | response = self.send_file(request, file) | |
180 | return response | |
181 | ||
182 | def object_get_uri(self, uri, stream): | |
200 | request = self.create_request("OBJECT_PUT", uri = uri, headers = headers) | |
201 | labels = { 'source' : unicodise(filename), 'destination' : unicodise(uri.uri()), 'extra' : extra_label } | |
202 | response = self.send_file(request, file, labels) | |
203 | return response | |
204 | ||
205 | def object_get(self, uri, stream, start_position = 0, extra_label = ""): | |
183 | 206 | if uri.type != "s3": |
184 | 207 | raise ValueError("Expected URI type 's3', got '%s'" % uri.type) |
185 | request = self.create_request("OBJECT_GET", bucket = uri.bucket(), object = uri.object()) | |
186 | response = self.recv_file(request, stream) | |
187 | return response | |
188 | ||
189 | def object_delete(self, bucket, object): | |
190 | request = self.create_request("OBJECT_DELETE", bucket = bucket, object = object) | |
191 | response = self.send_request(request) | |
192 | return response | |
193 | ||
194 | def object_put_uri(self, filename, uri, extra_headers = None): | |
195 | # TODO TODO | |
196 | # Make it consistent with stream-oriented object_get_uri() | |
208 | request = self.create_request("OBJECT_GET", uri = uri) | |
209 | labels = { 'source' : unicodise(uri.uri()), 'destination' : unicodise(stream.name), 'extra' : extra_label } | |
210 | response = self.recv_file(request, stream, labels, start_position) | |
211 | return response | |
212 | ||
213 | def object_delete(self, uri): | |
197 | 214 | if uri.type != "s3": |
198 | 215 | raise ValueError("Expected URI type 's3', got '%s'" % uri.type) |
199 | return self.object_put(filename, uri.bucket(), uri.object(), extra_headers) | |
200 | ||
201 | def object_delete_uri(self, uri): | |
202 | if uri.type != "s3": | |
203 | raise ValueError("Expected URI type 's3', got '%s'" % uri.type) | |
204 | return self.object_delete(uri.bucket(), uri.object()) | |
216 | request = self.create_request("OBJECT_DELETE", uri = uri) | |
217 | response = self.send_request(request) | |
218 | return response | |
219 | ||
220 | def object_copy(self, src_uri, dst_uri, extra_headers = None): | |
221 | if src_uri.type != "s3": | |
222 | raise ValueError("Expected URI type 's3', got '%s'" % src_uri.type) | |
223 | if dst_uri.type != "s3": | |
224 | raise ValueError("Expected URI type 's3', got '%s'" % dst_uri.type) | |
225 | headers = SortedDict() | |
226 | headers['x-amz-copy-source'] = "/%s/%s" % (src_uri.bucket(), self.urlencode_string(src_uri.object())) | |
227 | if self.config.acl_public: | |
228 | headers["x-amz-acl"] = "public-read" | |
229 | if extra_headers: | |
230 | headers.update(extra_headers) | |
231 | request = self.create_request("OBJECT_PUT", uri = dst_uri, headers = headers) | |
232 | response = self.send_request(request) | |
233 | return response | |
234 | ||
235 | def object_move(self, src_uri, dst_uri, extra_headers = None): | |
236 | response_copy = self.object_copy(src_uri, dst_uri, extra_headers) | |
237 | debug("Object %s copied to %s" % (src_uri, dst_uri)) | |
238 | if getRootTagName(response_copy["data"]) == "CopyObjectResult": | |
239 | response_delete = self.object_delete(src_uri) | |
240 | debug("Object %s deleted" % src_uri) | |
241 | return response_copy | |
205 | 242 | |
206 | 243 | def object_info(self, uri): |
207 | request = self.create_request("OBJECT_HEAD", bucket = uri.bucket(), object = uri.object()) | |
244 | request = self.create_request("OBJECT_HEAD", uri = uri) | |
208 | 245 | response = self.send_request(request) |
209 | 246 | return response |
210 | 247 | |
211 | 248 | def get_acl(self, uri): |
212 | 249 | if uri.has_object(): |
213 | request = self.create_request("OBJECT_GET", bucket = uri.bucket(), object = uri.object(), extra = "?acl") | |
250 | request = self.create_request("OBJECT_GET", uri = uri, extra = "?acl") | |
214 | 251 | else: |
215 | 252 | request = self.create_request("BUCKET_LIST", bucket = uri.bucket(), extra = "?acl") |
216 | acl = {} | |
217 | response = self.send_request(request) | |
218 | grants = getListFromXml(response['data'], "Grant") | |
219 | for grant in grants: | |
220 | if grant['Grantee'][0].has_key('DisplayName'): | |
221 | user = grant['Grantee'][0]['DisplayName'] | |
222 | if grant['Grantee'][0].has_key('URI'): | |
223 | user = grant['Grantee'][0]['URI'] | |
224 | if user == 'http://acs.amazonaws.com/groups/global/AllUsers': | |
225 | user = "*anon*" | |
226 | perm = grant['Permission'] | |
227 | acl[user] = perm | |
253 | ||
254 | response = self.send_request(request) | |
255 | acl = ACL(response['data']) | |
228 | 256 | return acl |
257 | ||
258 | def set_acl(self, uri, acl): | |
259 | if uri.has_object(): | |
260 | request = self.create_request("OBJECT_PUT", uri = uri, extra = "?acl") | |
261 | else: | |
262 | request = self.create_request("BUCKET_CREATE", bucket = uri.bucket(), extra = "?acl") | |
263 | ||
264 | body = str(acl) | |
265 | debug(u"set_acl(%s): acl-xml: %s" % (uri, body)) | |
266 | response = self.send_request(request, body) | |
267 | return response | |
229 | 268 | |
230 | 269 | ## Low level methods |
231 | 270 | def urlencode_string(self, string): |
267 | 306 | debug("String '%s' encoded to '%s'" % (string, encoded)) |
268 | 307 | return encoded |
269 | 308 | |
270 | def create_request(self, operation, bucket = None, object = None, headers = None, extra = None, **params): | |
309 | def create_request(self, operation, uri = None, bucket = None, object = None, headers = None, extra = None, **params): | |
271 | 310 | resource = { 'bucket' : None, 'uri' : "/" } |
311 | ||
312 | if uri and (bucket or object): | |
313 | raise ValueError("Both 'uri' and either 'bucket' or 'object' parameters supplied") | |
314 | ## If URI is given use that instead of bucket/object parameters | |
315 | if uri: | |
316 | bucket = uri.bucket() | |
317 | object = uri.has_object() and uri.object() or None | |
318 | ||
272 | 319 | if bucket: |
273 | 320 | resource['bucket'] = str(bucket) |
274 | 321 | if object: |
301 | 348 | debug("CreateRequest: resource[uri]=" + resource['uri']) |
302 | 349 | return (method_string, resource, headers) |
303 | 350 | |
304 | def send_request(self, request, body = None, retries = 5): | |
351 | def _fail_wait(self, retries): | |
352 | # Wait a few seconds. The more it fails the more we wait. | |
353 | return (self._max_retries - retries + 1) * 3 | |
354 | ||
355 | def send_request(self, request, body = None, retries = _max_retries): | |
305 | 356 | method_string, resource, headers = request |
306 | info("Processing request, please wait...") | |
357 | debug("Processing request, please wait...") | |
307 | 358 | try: |
308 | 359 | conn = self.get_connection(resource['bucket']) |
309 | 360 | conn.request(method_string, self.format_uri(resource), body, headers) |
318 | 369 | except Exception, e: |
319 | 370 | if retries: |
320 | 371 | warning("Retrying failed request: %s (%s)" % (resource['uri'], e)) |
372 | warning("Waiting %d sec..." % self._fail_wait(retries)) | |
373 | time.sleep(self._fail_wait(retries)) | |
321 | 374 | return self.send_request(request, body, retries - 1) |
322 | 375 | else: |
323 | 376 | raise S3RequestError("Request failed for: %s" % resource['uri']) |
327 | 380 | redir_bucket = getTextFromXml(response['data'], ".//Bucket") |
328 | 381 | redir_hostname = getTextFromXml(response['data'], ".//Endpoint") |
329 | 382 | self.set_hostname(redir_bucket, redir_hostname) |
330 | info("Redirected to: %s" % (redir_hostname)) | |
383 | warning("Redirected to: %s" % (redir_hostname)) | |
331 | 384 | return self.send_request(request, body) |
332 | 385 | |
333 | 386 | if response["status"] >= 500: |
335 | 388 | if retries: |
336 | 389 | warning(u"Retrying failed request: %s" % resource['uri']) |
337 | 390 | warning(unicode(e)) |
391 | warning("Waiting %d sec..." % self._fail_wait(retries)) | |
392 | time.sleep(self._fail_wait(retries)) | |
338 | 393 | return self.send_request(request, body, retries - 1) |
339 | 394 | else: |
340 | 395 | raise e |
344 | 399 | |
345 | 400 | return response |
346 | 401 | |
347 | def send_file(self, request, file, throttle = 0, retries = 3): | |
402 | def send_file(self, request, file, labels, throttle = 0, retries = _max_retries): | |
348 | 403 | method_string, resource, headers = request |
349 | info("Sending file '%s', please wait..." % file.name) | |
350 | conn = self.get_connection(resource['bucket']) | |
351 | conn.connect() | |
352 | conn.putrequest(method_string, self.format_uri(resource)) | |
353 | for header in headers.keys(): | |
354 | conn.putheader(header, str(headers[header])) | |
355 | conn.endheaders() | |
404 | size_left = size_total = headers.get("content-length") | |
405 | if self.config.progress_meter: | |
406 | progress = self.config.progress_class(labels, size_total) | |
407 | else: | |
408 | info("Sending file '%s', please wait..." % file.name) | |
409 | timestamp_start = time.time() | |
410 | try: | |
411 | conn = self.get_connection(resource['bucket']) | |
412 | conn.connect() | |
413 | conn.putrequest(method_string, self.format_uri(resource)) | |
414 | for header in headers.keys(): | |
415 | conn.putheader(header, str(headers[header])) | |
416 | conn.endheaders() | |
417 | except Exception, e: | |
418 | if self.config.progress_meter: | |
419 | progress.done("failed") | |
420 | if retries: | |
421 | warning("Retrying failed request: %s (%s)" % (resource['uri'], e)) | |
422 | warning("Waiting %d sec..." % self._fail_wait(retries)) | |
423 | time.sleep(self._fail_wait(retries)) | |
424 | # Connection error -> same throttle value | |
425 | return self.send_file(request, file, labels, throttle, retries - 1) | |
426 | else: | |
427 | raise S3UploadError("Upload failed for: %s" % resource['uri']) | |
356 | 428 | file.seek(0) |
357 | timestamp_start = time.time() | |
358 | md5_hash = md5.new() | |
359 | size_left = size_total = headers.get("content-length") | |
360 | while (size_left > 0): | |
361 | debug("SendFile: Reading up to %d bytes from '%s'" % (self.config.send_chunk, file.name)) | |
362 | data = file.read(self.config.send_chunk) | |
363 | md5_hash.update(data) | |
364 | debug("SendFile: Sending %d bytes to the server" % len(data)) | |
365 | try: | |
429 | md5_hash = md5() | |
430 | try: | |
431 | while (size_left > 0): | |
432 | #debug("SendFile: Reading up to %d bytes from '%s'" % (self.config.send_chunk, file.name)) | |
433 | data = file.read(self.config.send_chunk) | |
434 | md5_hash.update(data) | |
366 | 435 | conn.send(data) |
367 | except Exception, e: | |
368 | ## When an exception occurs insert a | |
369 | if retries: | |
370 | conn.close() | |
371 | warning("Upload of '%s' failed %s " % (file.name, e)) | |
372 | throttle = throttle and throttle * 5 or 0.01 | |
373 | warning("Retrying on lower speed (throttle=%0.2f)" % throttle) | |
374 | return self.send_file(request, file, throttle, retries - 1) | |
375 | else: | |
376 | debug("Giving up on '%s' %s" % (file.name, e)) | |
377 | raise S3UploadError | |
378 | ||
379 | size_left -= len(data) | |
380 | if throttle: | |
381 | time.sleep(throttle) | |
382 | debug("Sent %d bytes (%d %% of %d)" % ( | |
383 | (size_total - size_left), | |
384 | (size_total - size_left) * 100 / size_total, | |
385 | size_total)) | |
436 | if self.config.progress_meter: | |
437 | progress.update(delta_position = len(data)) | |
438 | size_left -= len(data) | |
439 | if throttle: | |
440 | time.sleep(throttle) | |
441 | md5_computed = md5_hash.hexdigest() | |
442 | response = {} | |
443 | http_response = conn.getresponse() | |
444 | response["status"] = http_response.status | |
445 | response["reason"] = http_response.reason | |
446 | response["headers"] = convertTupleListToDict(http_response.getheaders()) | |
447 | response["data"] = http_response.read() | |
448 | response["size"] = size_total | |
449 | conn.close() | |
450 | debug(u"Response: %s" % response) | |
451 | except Exception, e: | |
452 | if self.config.progress_meter: | |
453 | progress.done("failed") | |
454 | if retries: | |
455 | throttle = throttle and throttle * 5 or 0.01 | |
456 | warning("Upload failed: %s (%s)" % (resource['uri'], e)) | |
457 | warning("Retrying on lower speed (throttle=%0.2f)" % throttle) | |
458 | warning("Waiting %d sec..." % self._fail_wait(retries)) | |
459 | time.sleep(self._fail_wait(retries)) | |
460 | # Connection error -> same throttle value | |
461 | return self.send_file(request, file, labels, throttle, retries - 1) | |
462 | else: | |
463 | debug("Giving up on '%s' %s" % (file.name, e)) | |
464 | raise S3UploadError("Upload failed for: %s" % resource['uri']) | |
465 | ||
386 | 466 | timestamp_end = time.time() |
387 | md5_computed = md5_hash.hexdigest() | |
388 | response = {} | |
389 | http_response = conn.getresponse() | |
390 | response["status"] = http_response.status | |
391 | response["reason"] = http_response.reason | |
392 | response["headers"] = convertTupleListToDict(http_response.getheaders()) | |
393 | response["data"] = http_response.read() | |
394 | 467 | response["elapsed"] = timestamp_end - timestamp_start |
395 | response["size"] = size_total | |
396 | 468 | response["speed"] = response["elapsed"] and float(response["size"]) / response["elapsed"] or float(-1) |
397 | conn.close() | |
469 | ||
470 | if self.config.progress_meter: | |
471 | ## The above conn.close() takes some time -> update() progress meter | |
472 | ## to correct the average speed. Otherwise people will complain that | |
473 | ## 'progress' and response["speed"] are inconsistent ;-) | |
474 | progress.update() | |
475 | progress.done("done") | |
398 | 476 | |
399 | 477 | if response["status"] == 307: |
400 | 478 | ## RedirectPermanent |
401 | 479 | redir_bucket = getTextFromXml(response['data'], ".//Bucket") |
402 | 480 | redir_hostname = getTextFromXml(response['data'], ".//Endpoint") |
403 | 481 | self.set_hostname(redir_bucket, redir_hostname) |
404 | info("Redirected to: %s" % (redir_hostname)) | |
405 | return self.send_file(request, file) | |
482 | warning("Redirected to: %s" % (redir_hostname)) | |
483 | return self.send_file(request, file, labels) | |
406 | 484 | |
407 | 485 | # S3 from time to time doesn't send ETag back in a response :-( |
408 | 486 | # Force re-upload here. |
409 | 487 | if not response['headers'].has_key('etag'): |
410 | 488 | response['headers']['etag'] = '' |
411 | 489 | |
490 | if response["status"] < 200 or response["status"] > 299: | |
491 | if response["status"] >= 500: | |
492 | ## AWS internal error - retry | |
493 | if retries: | |
494 | warning("Upload failed: %s (%s)" % (resource['uri'], S3Error(response))) | |
495 | warning("Waiting %d sec..." % self._fail_wait(retries)) | |
496 | time.sleep(self._fail_wait(retries)) | |
497 | return self.send_file(request, file, labels, throttle, retries - 1) | |
498 | else: | |
499 | warning("Too many failures. Giving up on '%s'" % (file.name)) | |
500 | raise S3UploadError | |
501 | ## Non-recoverable error | |
502 | raise S3Error(response) | |
503 | ||
412 | 504 | debug("MD5 sums: computed=%s, received=%s" % (md5_computed, response["headers"]["etag"])) |
413 | 505 | if response["headers"]["etag"].strip('"\'') != md5_hash.hexdigest(): |
414 | 506 | warning("MD5 Sums don't match!") |
415 | 507 | if retries: |
416 | info("Retrying upload.") | |
417 | return self.send_file(request, file, throttle, retries - 1) | |
418 | else: | |
419 | debug("Too many failures. Giving up on '%s'" % (file.name)) | |
508 | warning("Retrying upload of %s" % (file.name)) | |
509 | return self.send_file(request, file, labels, throttle, retries - 1) | |
510 | else: | |
511 | warning("Too many failures. Giving up on '%s'" % (file.name)) | |
420 | 512 | raise S3UploadError |
421 | 513 | |
422 | if response["status"] < 200 or response["status"] > 299: | |
423 | raise S3Error(response) | |
424 | return response | |
425 | ||
426 | def recv_file(self, request, stream): | |
514 | return response | |
515 | ||
516 | def recv_file(self, request, stream, labels, start_position = 0, retries = _max_retries): | |
427 | 517 | method_string, resource, headers = request |
428 | info("Receiving file '%s', please wait..." % stream.name) | |
429 | conn = self.get_connection(resource['bucket']) | |
430 | conn.connect() | |
431 | conn.putrequest(method_string, self.format_uri(resource)) | |
432 | for header in headers.keys(): | |
433 | conn.putheader(header, str(headers[header])) | |
434 | conn.endheaders() | |
435 | response = {} | |
436 | http_response = conn.getresponse() | |
437 | response["status"] = http_response.status | |
438 | response["reason"] = http_response.reason | |
439 | response["headers"] = convertTupleListToDict(http_response.getheaders()) | |
518 | if self.config.progress_meter: | |
519 | progress = self.config.progress_class(labels, 0) | |
520 | else: | |
521 | info("Receiving file '%s', please wait..." % stream.name) | |
522 | timestamp_start = time.time() | |
523 | try: | |
524 | conn = self.get_connection(resource['bucket']) | |
525 | conn.connect() | |
526 | conn.putrequest(method_string, self.format_uri(resource)) | |
527 | for header in headers.keys(): | |
528 | conn.putheader(header, str(headers[header])) | |
529 | if start_position > 0: | |
530 | debug("Requesting Range: %d .. end" % start_position) | |
531 | conn.putheader("Range", "bytes=%d-" % start_position) | |
532 | conn.endheaders() | |
533 | response = {} | |
534 | http_response = conn.getresponse() | |
535 | response["status"] = http_response.status | |
536 | response["reason"] = http_response.reason | |
537 | response["headers"] = convertTupleListToDict(http_response.getheaders()) | |
538 | debug("Response: %s" % response) | |
539 | except Exception, e: | |
540 | if self.config.progress_meter: | |
541 | progress.done("failed") | |
542 | if retries: | |
543 | warning("Retrying failed request: %s (%s)" % (resource['uri'], e)) | |
544 | warning("Waiting %d sec..." % self._fail_wait(retries)) | |
545 | time.sleep(self._fail_wait(retries)) | |
546 | # Connection error -> same throttle value | |
547 | return self.recv_file(request, stream, labels, start_position, retries - 1) | |
548 | else: | |
549 | raise S3DownloadError("Download failed for: %s" % resource['uri']) | |
440 | 550 | |
441 | 551 | if response["status"] == 307: |
442 | 552 | ## RedirectPermanent |
444 | 554 | redir_bucket = getTextFromXml(response['data'], ".//Bucket") |
445 | 555 | redir_hostname = getTextFromXml(response['data'], ".//Endpoint") |
446 | 556 | self.set_hostname(redir_bucket, redir_hostname) |
447 | info("Redirected to: %s" % (redir_hostname)) | |
448 | return self.recv_file(request, stream) | |
557 | warning("Redirected to: %s" % (redir_hostname)) | |
558 | return self.recv_file(request, stream, labels) | |
449 | 559 | |
450 | 560 | if response["status"] < 200 or response["status"] > 299: |
451 | 561 | raise S3Error(response) |
452 | 562 | |
453 | md5_hash = md5.new() | |
454 | size_left = size_total = int(response["headers"]["content-length"]) | |
455 | size_recvd = 0 | |
456 | timestamp_start = time.time() | |
457 | while (size_recvd < size_total): | |
458 | this_chunk = size_left > self.config.recv_chunk and self.config.recv_chunk or size_left | |
459 | debug("ReceiveFile: Receiving up to %d bytes from the server" % this_chunk) | |
460 | data = http_response.read(this_chunk) | |
461 | debug("ReceiveFile: Writing %d bytes to file '%s'" % (len(data), stream.name)) | |
462 | stream.write(data) | |
463 | md5_hash.update(data) | |
464 | size_recvd += len(data) | |
465 | debug("Received %d bytes (%d %% of %d)" % ( | |
466 | size_recvd, | |
467 | size_recvd * 100 / size_total, | |
468 | size_total)) | |
469 | conn.close() | |
563 | if start_position == 0: | |
564 | # Only compute MD5 on the fly if we're downloading from beginning | |
565 | # Otherwise we'd get a nonsense. | |
566 | md5_hash = md5() | |
567 | size_left = int(response["headers"]["content-length"]) | |
568 | size_total = start_position + size_left | |
569 | current_position = start_position | |
570 | ||
571 | if self.config.progress_meter: | |
572 | progress.total_size = size_total | |
573 | progress.initial_position = current_position | |
574 | progress.current_position = current_position | |
575 | ||
576 | try: | |
577 | while (current_position < size_total): | |
578 | this_chunk = size_left > self.config.recv_chunk and self.config.recv_chunk or size_left | |
579 | data = http_response.read(this_chunk) | |
580 | stream.write(data) | |
581 | if start_position == 0: | |
582 | md5_hash.update(data) | |
583 | current_position += len(data) | |
584 | ## Call progress meter from here... | |
585 | if self.config.progress_meter: | |
586 | progress.update(delta_position = len(data)) | |
587 | conn.close() | |
588 | except Exception, e: | |
589 | if self.config.progress_meter: | |
590 | progress.done("failed") | |
591 | if retries: | |
592 | warning("Retrying failed request: %s (%s)" % (resource['uri'], e)) | |
593 | warning("Waiting %d sec..." % self._fail_wait(retries)) | |
594 | time.sleep(self._fail_wait(retries)) | |
595 | # Connection error -> same throttle value | |
596 | return self.recv_file(request, stream, labels, current_position, retries - 1) | |
597 | else: | |
598 | raise S3DownloadError("Download failed for: %s" % resource['uri']) | |
599 | ||
600 | stream.flush() | |
470 | 601 | timestamp_end = time.time() |
471 | response["md5"] = md5_hash.hexdigest() | |
602 | ||
603 | if self.config.progress_meter: | |
604 | ## The above stream.flush() may take some time -> update() progress meter | |
605 | ## to correct the average speed. Otherwise people will complain that | |
606 | ## 'progress' and response["speed"] are inconsistent ;-) | |
607 | progress.update() | |
608 | progress.done("done") | |
609 | ||
610 | if start_position == 0: | |
611 | # Only compute MD5 on the fly if we were downloading from the beginning | |
612 | response["md5"] = md5_hash.hexdigest() | |
613 | else: | |
614 | # Otherwise try to compute MD5 of the output file | |
615 | try: | |
616 | response["md5"] = hash_file_md5(stream.name) | |
617 | except IOError, e: | |
618 | if e.errno != errno.ENOENT: | |
619 | warning("Unable to open file: %s: %s" % (stream.name, e)) | |
620 | warning("Unable to verify MD5. Assume it matches.") | |
621 | response["md5"] = response["headers"]["etag"] | |
622 | ||
472 | 623 | response["md5match"] = response["headers"]["etag"].find(response["md5"]) >= 0 |
473 | 624 | response["elapsed"] = timestamp_end - timestamp_start |
474 | response["size"] = size_recvd | |
625 | response["size"] = current_position | |
475 | 626 | response["speed"] = response["elapsed"] and float(response["size"]) / response["elapsed"] or float(-1) |
476 | if response["size"] != long(response["headers"]["content-length"]): | |
627 | if response["size"] != start_position + long(response["headers"]["content-length"]): | |
477 | 628 | warning("Reported size (%s) does not match received size (%s)" % ( |
478 | response["headers"]["content-length"], response["size"])) | |
629 | start_position + response["headers"]["content-length"], response["size"])) | |
479 | 630 | debug("ReceiveFile: Computed MD5 = %s" % response["md5"]) |
480 | 631 | if not response["md5match"]: |
481 | 632 | warning("MD5 signatures do not match: computed=%s, received=%s" % ( |
494 | 645 | h += "/" + resource['bucket'] |
495 | 646 | h += resource['uri'] |
496 | 647 | debug("SignHeaders: " + repr(h)) |
497 | return base64.encodestring(hmac.new(self.config.secret_key, h, sha).digest()).strip() | |
648 | return base64.encodestring(hmac.new(self.config.secret_key, h, sha1).digest()).strip() | |
498 | 649 | |
499 | 650 | @staticmethod |
500 | 651 | def check_bucket_name(bucket, dns_strict = True): |
2 | 2 | ## http://www.logix.cz/michal |
3 | 3 | ## License: GPL Version 2 |
4 | 4 | |
5 | import os | |
5 | 6 | import re |
6 | 7 | import sys |
7 | 8 | from BidirMap import BidirMap |
41 | 42 | def public_url(self): |
42 | 43 | raise ValueError("This S3 URI does not have Anonymous URL representation") |
43 | 44 | |
45 | def basename(self): | |
46 | return self.__unicode__().split("/")[-1] | |
47 | ||
44 | 48 | class S3UriS3(S3Uri): |
45 | 49 | type = "s3" |
46 | 50 | _re = re.compile("^s3://([^/]+)/?(.*)", re.IGNORECASE) |
67 | 71 | def uri(self): |
68 | 72 | return "/".join(["s3:/", self._bucket, self._object]) |
69 | 73 | |
74 | def is_dns_compatible(self): | |
75 | return S3.check_bucket_name_dns_conformity(self._bucket) | |
76 | ||
70 | 77 | def public_url(self): |
71 | if S3.check_bucket_name_dns_conformity(self._bucket): | |
78 | if self.is_dns_compatible(): | |
72 | 79 | return "http://%s.s3.amazonaws.com/%s" % (self._bucket, self._object) |
73 | 80 | else: |
74 | 81 | return "http://s3.amazonaws.com/%s/%s" % (self._bucket, self._object) |
75 | 82 | |
83 | def host_name(self): | |
84 | if self.is_dns_compatible(): | |
85 | return "%s.s3.amazonaws.com" % (self._bucket) | |
86 | else: | |
87 | return "s3.amazonaws.com" | |
88 | ||
76 | 89 | @staticmethod |
77 | 90 | def compose_uri(bucket, object = ""): |
78 | 91 | return "s3://%s/%s" % (bucket, object) |
79 | 92 | |
93 | @staticmethod | |
94 | def httpurl_to_s3uri(http_url): | |
95 | m=re.match("(https?://)?([^/]+)/?(.*)", http_url, re.IGNORECASE) | |
96 | hostname, object = m.groups()[1:] | |
97 | hostname = hostname.lower() | |
98 | if hostname == "s3.amazonaws.com": | |
99 | ## old-style url: http://s3.amazonaws.com/bucket/object | |
100 | if object.count("/") == 0: | |
101 | ## no object given | |
102 | bucket = object | |
103 | object = "" | |
104 | else: | |
105 | ## bucket/object | |
106 | bucket, object = object.split("/", 1) | |
107 | elif hostname.endswith(".s3.amazonaws.com"): | |
108 | ## new-style url: http://bucket.s3.amazonaws.com/object | |
109 | bucket = hostname[:-(len(".s3.amazonaws.com"))] | |
110 | else: | |
111 | raise ValueError("Unable to parse URL: %s" % http_url) | |
112 | return S3Uri("s3://%(bucket)s/%(object)s" % { | |
113 | 'bucket' : bucket, | |
114 | 'object' : object }) | |
115 | ||
80 | 116 | class S3UriS3FS(S3Uri): |
81 | 117 | type = "s3fs" |
82 | 118 | _re = re.compile("^s3fs://([^/]*)/?(.*)", re.IGNORECASE) |
113 | 149 | def uri(self): |
114 | 150 | return "/".join(["file:/", self.path()]) |
115 | 151 | |
152 | def isdir(self): | |
153 | return os.path.isdir(self.path()) | |
154 | ||
155 | def dirname(self): | |
156 | return os.path.dirname(self.path()) | |
157 | ||
158 | class S3UriCloudFront(S3Uri): | |
159 | type = "cf" | |
160 | _re = re.compile("^cf://([^/]*)/?", re.IGNORECASE) | |
161 | def __init__(self, string): | |
162 | match = self._re.match(string) | |
163 | if not match: | |
164 | raise ValueError("%s: not a CloudFront URI" % string) | |
165 | groups = match.groups() | |
166 | self._dist_id = groups[0] | |
167 | ||
168 | def dist_id(self): | |
169 | return self._dist_id | |
170 | ||
171 | def uri(self): | |
172 | return "/".join(["cf:/", self.dist_id()]) | |
173 | ||
116 | 174 | if __name__ == "__main__": |
117 | 175 | uri = S3Uri("s3://bucket/object") |
118 | 176 | print "type() =", type(uri) |
142 | 200 | print "uri.type=", uri.type |
143 | 201 | print "path =", uri.path() |
144 | 202 | |
203 | ||
204 | uri = S3Uri("cf://1234567890ABCD/") | |
205 | print "type() =", type(uri) | |
206 | print "uri =", uri | |
207 | print "uri.type=", uri.type | |
208 | print "dist_id =", uri.dist_id() | |
209 | ||
210 |
7 | 7 | import re |
8 | 8 | import string |
9 | 9 | import random |
10 | import md5 | |
10 | import rfc822 | |
11 | try: | |
12 | from hashlib import md5 | |
13 | except ImportError: | |
14 | from md5 import md5 | |
11 | 15 | import errno |
12 | 16 | |
13 | 17 | from logging import debug, info, warning, error |
18 | ||
19 | import Config | |
14 | 20 | |
15 | 21 | try: |
16 | 22 | import xml.etree.ElementTree as ET |
17 | 23 | except ImportError: |
18 | 24 | import elementtree.ElementTree as ET |
19 | 25 | |
20 | def stripTagXmlns(xmlns, tag): | |
21 | """ | |
22 | Returns a function that, given a tag name argument, removes | |
23 | eventual ElementTree xmlns from it. | |
24 | ||
25 | Example: | |
26 | stripTagXmlns("{myXmlNS}tag") -> "tag" | |
27 | """ | |
28 | if not xmlns: | |
29 | return tag | |
30 | return re.sub(xmlns, "", tag) | |
31 | ||
32 | def fixupXPath(xmlns, xpath, max = 0): | |
33 | if not xmlns: | |
34 | return xpath | |
35 | retval = re.subn("//", "//%s" % xmlns, xpath, max)[0] | |
36 | return retval | |
37 | ||
38 | def parseNodes(nodes, xmlns = ""): | |
26 | def parseNodes(nodes): | |
39 | 27 | ## WARNING: Ignores text nodes from mixed xml/text. |
40 | 28 | ## For instance <tag1>some text<tag2>other text</tag2></tag1> |
41 | 29 | ## will be ignore "some text" node |
43 | 31 | for node in nodes: |
44 | 32 | retval_item = {} |
45 | 33 | for child in node.getchildren(): |
46 | name = stripTagXmlns(xmlns, child.tag) | |
34 | name = child.tag | |
47 | 35 | if child.getchildren(): |
48 | retval_item[name] = parseNodes([child], xmlns) | |
36 | retval_item[name] = parseNodes([child]) | |
49 | 37 | else: |
50 | 38 | retval_item[name] = node.findtext(".//%s" % child.tag) |
51 | 39 | retval.append(retval_item) |
52 | 40 | return retval |
53 | 41 | |
54 | def getNameSpace(element): | |
55 | if not element.tag.startswith("{"): | |
56 | return "" | |
57 | return re.compile("^(\{[^}]+\})").match(element.tag).groups()[0] | |
58 | ||
42 | def stripNameSpace(xml): | |
43 | """ | |
44 | removeNameSpace(xml) -- remove top-level AWS namespace | |
45 | """ | |
46 | r = re.compile('^(<?[^>]+?>\s?)(<\w+) xmlns=[\'"](http://[^\'"]+)[\'"](.*)', re.MULTILINE) | |
47 | if r.match(xml): | |
48 | xmlns = r.match(xml).groups()[2] | |
49 | xml = r.sub("\\1\\2\\4", xml) | |
50 | else: | |
51 | xmlns = None | |
52 | return xml, xmlns | |
53 | ||
54 | def getTreeFromXml(xml): | |
55 | xml, xmlns = stripNameSpace(xml) | |
56 | tree = ET.fromstring(xml) | |
57 | if xmlns: | |
58 | tree.attrib['xmlns'] = xmlns | |
59 | return tree | |
60 | ||
59 | 61 | def getListFromXml(xml, node): |
60 | tree = ET.fromstring(xml) | |
61 | xmlns = getNameSpace(tree) | |
62 | nodes = tree.findall('.//%s%s' % (xmlns, node)) | |
63 | return parseNodes(nodes, xmlns) | |
64 | ||
62 | tree = getTreeFromXml(xml) | |
63 | nodes = tree.findall('.//%s' % (node)) | |
64 | return parseNodes(nodes) | |
65 | ||
66 | def getDictFromTree(tree): | |
67 | ret_dict = {} | |
68 | for child in tree.getchildren(): | |
69 | if child.getchildren(): | |
70 | ## Complex-type child. We're not interested | |
71 | continue | |
72 | if ret_dict.has_key(child.tag): | |
73 | if not type(ret_dict[child.tag]) == list: | |
74 | ret_dict[child.tag] = [ret_dict[child.tag]] | |
75 | ret_dict[child.tag].append(child.text or "") | |
76 | else: | |
77 | ret_dict[child.tag] = child.text or "" | |
78 | return ret_dict | |
79 | ||
65 | 80 | def getTextFromXml(xml, xpath): |
66 | tree = ET.fromstring(xml) | |
67 | xmlns = getNameSpace(tree) | |
81 | tree = getTreeFromXml(xml) | |
68 | 82 | if tree.tag.endswith(xpath): |
69 | 83 | return tree.text |
70 | 84 | else: |
71 | return tree.findtext(fixupXPath(xmlns, xpath)) | |
85 | return tree.findtext(xpath) | |
86 | ||
87 | def getRootTagName(xml): | |
88 | tree = getTreeFromXml(xml) | |
89 | return tree.tag | |
90 | ||
91 | def xmlTextNode(tag_name, text): | |
92 | el = ET.Element(tag_name) | |
93 | el.text = unicode(text) | |
94 | return el | |
95 | ||
96 | def appendXmlTextNode(tag_name, text, parent): | |
97 | """ | |
98 | Creates a new <tag_name> Node and sets | |
99 | its content to 'text'. Then appends the | |
100 | created Node to 'parent' element if given. | |
101 | Returns the newly created Node. | |
102 | """ | |
103 | parent.append(xmlTextNode(tag_name, text)) | |
72 | 104 | |
73 | 105 | def dateS3toPython(date): |
74 | 106 | date = re.compile("\.\d\d\dZ").sub(".000Z", date) |
79 | 111 | ## Currently the argument to strptime() is GMT but mktime() |
80 | 112 | ## treats it as "localtime". Anyway... |
81 | 113 | return time.mktime(dateS3toPython(date)) |
114 | ||
115 | def dateRFC822toPython(date): | |
116 | return rfc822.parsedate(date) | |
117 | ||
118 | def dateRFC822toUnix(date): | |
119 | return time.mktime(dateRFC822toPython(date)) | |
82 | 120 | |
83 | 121 | def formatSize(size, human_readable = False, floating_point = False): |
84 | 122 | size = floating_point and float(size) or int(size) |
136 | 174 | return mktmpsomething(prefix, randchars, createfunc) |
137 | 175 | |
138 | 176 | def hash_file_md5(filename): |
139 | h = md5.new() | |
177 | h = md5() | |
140 | 178 | f = open(filename, "rb") |
141 | 179 | while True: |
142 | 180 | # Hash 32kB chunks |
173 | 211 | return False |
174 | 212 | return True |
175 | 213 | |
176 | def unicodise(string): | |
214 | def unicodise(string, encoding = None, errors = "replace"): | |
177 | 215 | """ |
178 | 216 | Convert 'string' to Unicode or raise an exception. |
179 | 217 | """ |
180 | debug("Unicodising %r" % string) | |
218 | ||
219 | if not encoding: | |
220 | encoding = Config.Config().encoding | |
221 | ||
181 | 222 | if type(string) == unicode: |
182 | 223 | return string |
224 | debug("Unicodising %r using %s" % (string, encoding)) | |
183 | 225 | try: |
184 | return string.decode("utf-8") | |
226 | return string.decode(encoding, errors) | |
185 | 227 | except UnicodeDecodeError: |
186 | 228 | raise UnicodeDecodeError("Conversion to unicode failed: %r" % string) |
187 | 229 | |
188 | def try_unicodise(string): | |
230 | def deunicodise(string, encoding = None, errors = "replace"): | |
231 | """ | |
232 | Convert unicode 'string' to <type str>, by default replacing | |
233 | all invalid characters with '?' or raise an exception. | |
234 | """ | |
235 | ||
236 | if not encoding: | |
237 | encoding = Config.Config().encoding | |
238 | ||
239 | if type(string) != unicode: | |
240 | return str(string) | |
241 | debug("DeUnicodising %r using %s" % (string, encoding)) | |
189 | 242 | try: |
190 | return unicodise(string) | |
191 | except UnicodeDecodeError: | |
192 | return string | |
193 | ||
243 | return string.encode(encoding, errors) | |
244 | except UnicodeEncodeError: | |
245 | raise UnicodeEncodeError("Conversion from unicode failed: %r" % string) | |
246 | ||
247 | def unicodise_safe(string, encoding = None): | |
248 | """ | |
249 | Convert 'string' to Unicode according to current encoding | |
250 | and replace all invalid characters with '?' | |
251 | """ | |
252 | ||
253 | return unicodise(deunicodise(string, encoding), encoding).replace(u'\ufffd', '?') | |
254 |
10 | 10 | import os |
11 | 11 | import re |
12 | 12 | import errno |
13 | import pwd, grp | |
14 | 13 | import glob |
15 | 14 | import traceback |
16 | 15 | import codecs |
16 | import locale | |
17 | import subprocess | |
17 | 18 | |
18 | 19 | from copy import copy |
19 | 20 | from optparse import OptionParser, Option, OptionValueError, IndentedHelpFormatter |
20 | 21 | from logging import debug, info, warning, error |
21 | 22 | from distutils.spawn import find_executable |
22 | 23 | |
23 | ## Output native on TTY, UTF-8 otherwise (redirects) | |
24 | #_stdout = sys.stdout.isatty() and sys.stdout or codecs.getwriter("utf-8")(sys.stdout) | |
25 | #_stderr = sys.stderr.isatty() and sys.stderr or codecs.getwriter("utf-8")(sys.stderr) | |
26 | ## Output UTF-8 in all cases | |
27 | _stdout = codecs.getwriter("utf-8")(sys.stdout) | |
28 | _stderr = codecs.getwriter("utf-8")(sys.stderr) | |
29 | ## Leave it to the terminal | |
30 | #_stdout = sys.stdout | |
31 | #_stderr = sys.stderr | |
32 | ||
33 | 24 | def output(message): |
34 | _stdout.write(message + "\n") | |
25 | sys.stdout.write(message + "\n") | |
35 | 26 | |
36 | 27 | def check_args_type(args, type, verbose_type): |
37 | 28 | for arg in args: |
57 | 48 | buckets_size += size |
58 | 49 | total_size, size_coeff = formatSize(buckets_size, Config().human_readable_sizes) |
59 | 50 | total_size_str = str(total_size) + size_coeff |
60 | output("".rjust(8, "-")) | |
61 | output("%s Total" % (total_size_str.ljust(8))) | |
51 | output(u"".rjust(8, "-")) | |
52 | output(u"%s Total" % (total_size_str.ljust(8))) | |
62 | 53 | |
63 | 54 | def subcmd_bucket_usage(s3, uri): |
64 | 55 | bucket = uri.bucket() |
67 | 58 | if object.endswith('*'): |
68 | 59 | object = object[:-1] |
69 | 60 | try: |
70 | response = s3.bucket_list(bucket, prefix = object) | |
61 | response = s3.bucket_list(bucket, prefix = object, recursive = True) | |
71 | 62 | except S3Error, e: |
72 | 63 | if S3.codes.has_key(e.Code): |
73 | 64 | error(S3.codes[e.Code] % bucket) |
80 | 71 | bucket_size += size |
81 | 72 | total_size, size_coeff = formatSize(bucket_size, Config().human_readable_sizes) |
82 | 73 | total_size_str = str(total_size) + size_coeff |
83 | output("%s %s" % (total_size_str.ljust(8), uri)) | |
74 | output(u"%s %s" % (total_size_str.ljust(8), uri)) | |
84 | 75 | return bucket_size |
85 | 76 | |
86 | 77 | def cmd_ls(args): |
99 | 90 | |
100 | 91 | for bucket in response["list"]: |
101 | 92 | subcmd_bucket_list(s3, S3Uri("s3://" + bucket["Name"])) |
102 | output("") | |
93 | output(u"") | |
103 | 94 | |
104 | 95 | |
105 | 96 | def subcmd_buckets_list_all(s3): |
106 | 97 | response = s3.list_all_buckets() |
107 | 98 | for bucket in response["list"]: |
108 | output("%s s3://%s" % ( | |
99 | output(u"%s s3://%s" % ( | |
109 | 100 | formatDateTime(bucket["CreationDate"]), |
110 | 101 | bucket["Name"], |
111 | 102 | )) |
112 | 103 | |
113 | 104 | def subcmd_bucket_list(s3, uri): |
114 | 105 | bucket = uri.bucket() |
115 | object = uri.object() | |
116 | ||
117 | output("Bucket 's3://%s':" % bucket) | |
118 | if object.endswith('*'): | |
119 | object = object[:-1] | |
106 | prefix = uri.object() | |
107 | ||
108 | debug(u"Bucket 's3://%s':" % bucket) | |
109 | if prefix.endswith('*'): | |
110 | prefix = prefix[:-1] | |
120 | 111 | try: |
121 | response = s3.bucket_list(bucket, prefix = object) | |
112 | response = s3.bucket_list(bucket, prefix = prefix) | |
122 | 113 | except S3Error, e: |
123 | 114 | if S3.codes.has_key(e.info["Code"]): |
124 | 115 | error(S3.codes[e.info["Code"]] % bucket) |
125 | 116 | return |
126 | 117 | else: |
127 | 118 | raise |
119 | ||
120 | for prefix in response['common_prefixes']: | |
121 | output(u"%s %s" % ( | |
122 | "DIR".rjust(26), | |
123 | uri.compose_uri(bucket, prefix["Prefix"]))) | |
124 | ||
128 | 125 | for object in response["list"]: |
129 | 126 | size, size_coeff = formatSize(object["Size"], Config().human_readable_sizes) |
130 | output("%s %s%s %s" % ( | |
127 | output(u"%s %s%s %s" % ( | |
131 | 128 | formatDateTime(object["LastModified"]), |
132 | 129 | str(size).rjust(8), size_coeff.ljust(1), |
133 | 130 | uri.compose_uri(bucket, object["Key"]), |
134 | 131 | )) |
135 | 132 | |
136 | 133 | def cmd_bucket_create(args): |
137 | uri = S3Uri(args[0]) | |
138 | if not uri.type == "s3" or not uri.has_bucket() or uri.has_object(): | |
139 | raise ParameterError("Expecting S3 URI with just the bucket name set instead of '%s'" % args[0]) | |
140 | try: | |
141 | s3 = S3(Config()) | |
142 | response = s3.bucket_create(uri.bucket(), cfg.bucket_location) | |
143 | except S3Error, e: | |
144 | if S3.codes.has_key(e.info["Code"]): | |
145 | error(S3.codes[e.info["Code"]] % uri.bucket()) | |
146 | return | |
147 | else: | |
148 | raise | |
149 | output("Bucket '%s' created" % uri.bucket()) | |
150 | ||
151 | def cmd_bucket_delete(args): | |
152 | uri = S3Uri(args[0]) | |
153 | if not uri.type == "s3" or not uri.has_bucket() or uri.has_object(): | |
154 | raise ParameterError("Expecting S3 URI with just the bucket name set instead of '%s'" % args[0]) | |
155 | try: | |
156 | s3 = S3(Config()) | |
157 | response = s3.bucket_delete(uri.bucket()) | |
158 | except S3Error, e: | |
159 | if S3.codes.has_key(e.info["Code"]): | |
160 | error(S3.codes[e.info["Code"]] % uri.bucket()) | |
161 | return | |
162 | else: | |
163 | raise | |
164 | output("Bucket '%s' removed" % uri.bucket()) | |
165 | ||
166 | def cmd_object_put(args): | |
167 | 134 | s3 = S3(Config()) |
168 | ||
169 | uri_arg = args.pop() | |
170 | check_args_type(args, 'file', 'filename') | |
171 | ||
172 | uri = S3Uri(uri_arg) | |
173 | if uri.type != "s3": | |
174 | raise ParameterError("Expecting S3 URI instead of '%s'" % uri_arg) | |
175 | ||
176 | if len(args) > 1 and uri.object() != "" and not Config().force: | |
177 | error("When uploading multiple files the last argument must") | |
178 | error("be a S3 URI specifying just the bucket name") | |
179 | error("WITHOUT object name!") | |
180 | error("Alternatively use --force argument and the specified") | |
181 | error("object name will be prefixed to all stored filenames.") | |
182 | sys.exit(1) | |
183 | ||
184 | seq = 0 | |
185 | total = len(args) | |
186 | for file in args: | |
187 | seq += 1 | |
188 | uri_arg_final = str(uri) | |
189 | if len(args) > 1 or uri.object() == "": | |
190 | uri_arg_final += os.path.basename(file) | |
191 | ||
192 | uri_final = S3Uri(uri_arg_final) | |
193 | extra_headers = {} | |
194 | real_filename = file | |
195 | if Config().encrypt: | |
196 | exitcode, real_filename, extra_headers["x-amz-meta-s3tools-gpgenc"] = gpg_encrypt(file) | |
135 | for arg in args: | |
136 | uri = S3Uri(arg) | |
137 | if not uri.type == "s3" or not uri.has_bucket() or uri.has_object(): | |
138 | raise ParameterError("Expecting S3 URI with just the bucket name set instead of '%s'" % arg) | |
197 | 139 | try: |
198 | response = s3.object_put_uri(real_filename, uri_final, extra_headers) | |
199 | except S3UploadError, e: | |
200 | error("Upload of '%s' failed too many times. Skipping that file." % real_filename) | |
201 | continue | |
202 | except InvalidFileError, e: | |
203 | warning("File can not be uploaded: %s" % e) | |
204 | continue | |
205 | speed_fmt = formatSize(response["speed"], human_readable = True, floating_point = True) | |
206 | output("File '%s' stored as %s (%d bytes in %0.1f seconds, %0.2f %sB/s) [%d of %d]" % | |
207 | (file, uri_final, response["size"], response["elapsed"], speed_fmt[0], speed_fmt[1], | |
208 | seq, total)) | |
209 | if Config().acl_public: | |
210 | output("Public URL of the object is: %s" % | |
211 | (uri_final.public_url())) | |
212 | if Config().encrypt and real_filename != file: | |
213 | debug("Removing temporary encrypted file: %s" % real_filename) | |
214 | os.remove(real_filename) | |
215 | ||
216 | def cmd_object_get(args): | |
217 | s3 = S3(Config()) | |
218 | ||
219 | if not S3Uri(args[0]).type == 's3': | |
220 | raise ParameterError("Expecting S3 URI instead of '%s'" % args[0]) | |
221 | ||
222 | destination_dir = None | |
223 | destination_file = None | |
224 | if len(args) > 1: | |
225 | if S3Uri(args[-1]).type == 's3': | |
226 | # all S3, use object names to local dir | |
227 | check_args_type(args, type="s3", verbose_type="S3 URI") # May raise ParameterError | |
228 | else: | |
229 | if (len(args) > 2): | |
230 | # last must be dir, all preceding S3 | |
231 | if not os.path.isdir(args[-1]): | |
232 | raise ParameterError("Last parameter must be a directory") | |
233 | destination_dir = args.pop() | |
234 | check_args_type(args, type="s3", verbose_type="S3 URI") # May raise ParameterError | |
235 | else: | |
236 | # last must be a dir or a filename | |
237 | if os.path.isdir(args[-1]): | |
238 | destination_dir = args.pop() | |
239 | else: | |
240 | destination_file = args.pop() | |
241 | ||
242 | while (len(args)): | |
243 | uri_arg = args.pop(0) | |
244 | uri = S3Uri(uri_arg) | |
245 | ||
246 | if destination_file: | |
247 | destination = destination_file | |
248 | elif destination_dir: | |
249 | destination = destination_dir + "/" + uri.object() | |
250 | else: | |
251 | # By default the destination filename is the object name | |
252 | destination = uri.object() | |
253 | if destination == "-": | |
254 | ## stdout | |
255 | dst_stream = sys.stdout | |
256 | else: | |
257 | ## File | |
258 | if not Config().force and os.path.exists(destination): | |
259 | raise ParameterError("File %s already exists. Use --force to overwrite it" % destination) | |
260 | try: | |
261 | dst_stream = open(destination, "wb") | |
262 | except IOError, e: | |
263 | error("Skipping %s: %s" % (destination, e.strerror)) | |
264 | continue | |
265 | response = s3.object_get_uri(uri, dst_stream) | |
266 | if response["headers"].has_key("x-amz-meta-s3tools-gpgenc"): | |
267 | gpg_decrypt(destination, response["headers"]["x-amz-meta-s3tools-gpgenc"]) | |
268 | response["size"] = os.stat(destination)[6] | |
269 | if destination != "-": | |
270 | speed_fmt = formatSize(response["speed"], human_readable = True, floating_point = True) | |
271 | output("Object %s saved as '%s' (%d bytes in %0.1f seconds, %0.2f %sB/s)" % | |
272 | (uri, destination, response["size"], response["elapsed"], speed_fmt[0], speed_fmt[1])) | |
273 | ||
274 | def cmd_object_del(args): | |
275 | s3 = S3(Config()) | |
276 | ||
277 | while (len(args)): | |
278 | uri_arg = args.pop(0) | |
279 | uri = S3Uri(uri_arg) | |
280 | if uri.type != "s3" or not uri.has_object(): | |
281 | raise ParameterError("Expecting S3 URI instead of '%s'" % uri_arg) | |
282 | ||
283 | response = s3.object_delete_uri(uri) | |
284 | output("Object %s deleted" % uri) | |
285 | ||
286 | def cmd_info(args): | |
287 | s3 = S3(Config()) | |
288 | ||
289 | while (len(args)): | |
290 | uri_arg = args.pop(0) | |
291 | uri = S3Uri(uri_arg) | |
292 | if uri.type != "s3" or not uri.has_bucket(): | |
293 | raise ParameterError("Expecting S3 URI instead of '%s'" % uri_arg) | |
294 | ||
295 | try: | |
296 | if uri.has_object(): | |
297 | info = s3.object_info(uri) | |
298 | output("%s (object):" % uri.uri()) | |
299 | output(" File size: %s" % info['headers']['content-length']) | |
300 | output(" Last mod: %s" % info['headers']['last-modified']) | |
301 | output(" MIME type: %s" % info['headers']['content-type']) | |
302 | output(" MD5 sum: %s" % info['headers']['etag'].strip('"')) | |
303 | else: | |
304 | info = s3.bucket_info(uri) | |
305 | output("%s (bucket):" % uri.uri()) | |
306 | output(" Location: %s" % info['bucket-location']) | |
307 | acl = s3.get_acl(uri) | |
308 | for user in acl.keys(): | |
309 | output(" ACL: %s: %s" % (user, acl[user])) | |
140 | response = s3.bucket_create(uri.bucket(), cfg.bucket_location) | |
141 | output(u"Bucket '%s' created" % uri.uri()) | |
310 | 142 | except S3Error, e: |
311 | 143 | if S3.codes.has_key(e.info["Code"]): |
312 | 144 | error(S3.codes[e.info["Code"]] % uri.bucket()) |
314 | 146 | else: |
315 | 147 | raise |
316 | 148 | |
149 | def cmd_bucket_delete(args): | |
150 | def _bucket_delete_one(uri): | |
151 | try: | |
152 | response = s3.bucket_delete(uri.bucket()) | |
153 | except S3Error, e: | |
154 | if e.info['Code'] == 'BucketNotEmpty' and (cfg.force or cfg.recursive): | |
155 | warning(u"Bucket is not empty. Removing all the objects from it first. This may take some time...") | |
156 | subcmd_object_del_uri(uri, recursive = True) | |
157 | return _bucket_delete_one(uri) | |
158 | elif S3.codes.has_key(e.info["Code"]): | |
159 | error(S3.codes[e.info["Code"]] % uri.bucket()) | |
160 | return | |
161 | else: | |
162 | raise | |
163 | ||
164 | s3 = S3(Config()) | |
165 | for arg in args: | |
166 | uri = S3Uri(arg) | |
167 | if not uri.type == "s3" or not uri.has_bucket() or uri.has_object(): | |
168 | raise ParameterError("Expecting S3 URI with just the bucket name set instead of '%s'" % arg) | |
169 | _bucket_delete_one(uri) | |
170 | output(u"Bucket '%s' removed" % uri.uri()) | |
171 | ||
172 | def fetch_local_list(args, recursive = None): | |
173 | local_uris = [] | |
174 | local_list = SortedDict() | |
175 | single_file = False | |
176 | ||
177 | if type(args) not in (list, tuple): | |
178 | args = [args] | |
179 | ||
180 | if recursive == None: | |
181 | recursive = cfg.recursive | |
182 | ||
183 | for arg in args: | |
184 | uri = S3Uri(arg) | |
185 | if not uri.type == 'file': | |
186 | raise ParameterError("Expecting filename or directory instead of: %s" % arg) | |
187 | if uri.isdir() and not recursive: | |
188 | raise ParameterError("Use --recursive to upload a directory: %s" % arg) | |
189 | local_uris.append(uri) | |
190 | ||
191 | for uri in local_uris: | |
192 | list_for_uri, single_file = _get_filelist_local(uri) | |
193 | local_list.update(list_for_uri) | |
194 | ||
195 | ## Single file is True if and only if the user | |
196 | ## specified one local URI and that URI represents | |
197 | ## a FILE. Ie it is False if the URI was of a DIR | |
198 | ## and that dir contained only one FILE. That's not | |
199 | ## a case of single_file==True. | |
200 | if len(local_list) > 1: | |
201 | single_file = False | |
202 | ||
203 | return local_list, single_file | |
204 | ||
205 | def fetch_remote_list(args, require_attribs = False, recursive = None): | |
206 | remote_uris = [] | |
207 | remote_list = SortedDict() | |
208 | ||
209 | if type(args) not in (list, tuple): | |
210 | args = [args] | |
211 | ||
212 | if recursive == None: | |
213 | recursive = cfg.recursive | |
214 | ||
215 | for arg in args: | |
216 | uri = S3Uri(arg) | |
217 | if not uri.type == 's3': | |
218 | raise ParameterError("Expecting S3 URI instead of '%s'" % arg) | |
219 | remote_uris.append(uri) | |
220 | ||
221 | if recursive: | |
222 | for uri in remote_uris: | |
223 | objectlist = _get_filelist_remote(uri) | |
224 | for key in objectlist: | |
225 | remote_list[key] = objectlist[key] | |
226 | else: | |
227 | for uri in remote_uris: | |
228 | uri_str = str(uri) | |
229 | ## Wildcards used in remote URI? | |
230 | ## If yes we'll need a bucket listing... | |
231 | if uri_str.find('*') > -1 or uri_str.find('?') > -1: | |
232 | first_wildcard = uri_str.find('*') | |
233 | first_questionmark = uri_str.find('?') | |
234 | if first_questionmark > -1 and first_questionmark < first_wildcard: | |
235 | first_wildcard = first_questionmark | |
236 | prefix = uri_str[:first_wildcard] | |
237 | rest = uri_str[first_wildcard+1:] | |
238 | ## Only request recursive listing if the 'rest' of the URI, | |
239 | ## i.e. the part after first wildcard, contains '/' | |
240 | need_recursion = rest.find('/') > -1 | |
241 | objectlist = _get_filelist_remote(S3Uri(prefix), recursive = need_recursion) | |
242 | for key in objectlist: | |
243 | ## Check whether the 'key' matches the requested wildcards | |
244 | if glob.fnmatch.fnmatch(objectlist[key]['object_uri_str'], uri_str): | |
245 | remote_list[key] = objectlist[key] | |
246 | else: | |
247 | ## No wildcards - simply append the given URI to the list | |
248 | key = os.path.basename(uri.object()) | |
249 | if not key: | |
250 | raise ParameterError(u"Expecting S3 URI with a filename or --recursive: %s" % uri.uri()) | |
251 | remote_item = { | |
252 | 'base_uri': uri, | |
253 | 'object_uri_str': unicode(uri), | |
254 | 'object_key': uri.object() | |
255 | } | |
256 | if require_attribs: | |
257 | response = S3(cfg).object_info(uri) | |
258 | remote_item.update({ | |
259 | 'size': int(response['headers']['content-length']), | |
260 | 'md5': response['headers']['etag'].strip('"\''), | |
261 | 'timestamp' : Utils.dateRFC822toUnix(response['headers']['date']) | |
262 | }) | |
263 | remote_list[key] = remote_item | |
264 | return remote_list | |
265 | ||
266 | def cmd_object_put(args): | |
267 | cfg = Config() | |
268 | s3 = S3(cfg) | |
269 | ||
270 | if len(args) == 0: | |
271 | raise ParameterError("Nothing to upload. Expecting a local file or directory and a S3 URI destination.") | |
272 | ||
273 | destination_base = args.pop() | |
274 | if S3Uri(destination_base).type != 's3': | |
275 | raise ParameterError("Destination must be S3Uri. Got: %s" % destination_base) | |
276 | ||
277 | if len(args) == 0: | |
278 | raise ParameterError("Nothing to upload. Expecting a local file or directory.") | |
279 | ||
280 | local_list, single_file_local = fetch_local_list(args) | |
281 | ||
282 | local_list, exclude_list = _filelist_filter_exclude_include(local_list) | |
283 | ||
284 | local_count = len(local_list) | |
285 | ||
286 | info(u"Summary: %d local files to upload" % local_count) | |
287 | ||
288 | if local_count > 0: | |
289 | if not destination_base.endswith("/"): | |
290 | if not single_file_local: | |
291 | raise ParameterError("Destination S3 URI must end with '/' (ie must refer to a directory on the remote side).") | |
292 | local_list[local_list.keys()[0]]['remote_uri'] = unicodise(destination_base) | |
293 | else: | |
294 | for key in local_list: | |
295 | local_list[key]['remote_uri'] = unicodise(destination_base + key) | |
296 | ||
297 | if cfg.dry_run: | |
298 | for key in exclude_list: | |
299 | output(u"exclude: %s" % unicodise(key)) | |
300 | for key in local_list: | |
301 | output(u"upload: %s -> %s" % (local_list[key]['full_name_unicode'], local_list[key]['remote_uri'])) | |
302 | ||
303 | warning(u"Exitting now because of --dry-run") | |
304 | return | |
305 | ||
306 | seq = 0 | |
307 | for key in local_list: | |
308 | seq += 1 | |
309 | ||
310 | uri_final = S3Uri(local_list[key]['remote_uri']) | |
311 | ||
312 | extra_headers = {} | |
313 | full_name_orig = local_list[key]['full_name'] | |
314 | full_name = full_name_orig | |
315 | seq_label = "[%d of %d]" % (seq, local_count) | |
316 | if Config().encrypt: | |
317 | exitcode, full_name, extra_headers["x-amz-meta-s3tools-gpgenc"] = gpg_encrypt(full_name_orig) | |
318 | try: | |
319 | response = s3.object_put(full_name, uri_final, extra_headers, extra_label = seq_label) | |
320 | except S3UploadError, e: | |
321 | error(u"Upload of '%s' failed too many times. Skipping that file." % real_filename) | |
322 | continue | |
323 | except InvalidFileError, e: | |
324 | warning(u"File can not be uploaded: %s" % e) | |
325 | continue | |
326 | speed_fmt = formatSize(response["speed"], human_readable = True, floating_point = True) | |
327 | if not Config().progress_meter: | |
328 | output(u"File '%s' stored as '%s' (%d bytes in %0.1f seconds, %0.2f %sB/s) %s" % | |
329 | (unicodise(full_name_orig), uri_final, response["size"], response["elapsed"], | |
330 | speed_fmt[0], speed_fmt[1], seq_label)) | |
331 | if Config().acl_public: | |
332 | output(u"Public URL of the object is: %s" % | |
333 | (uri_final.public_url())) | |
334 | if Config().encrypt and full_name != full_name_orig: | |
335 | debug(u"Removing temporary encrypted file: %s" % unicodise(full_name)) | |
336 | os.remove(full_name) | |
337 | ||
338 | def cmd_object_get(args): | |
339 | cfg = Config() | |
340 | s3 = S3(cfg) | |
341 | ||
342 | ## Check arguments: | |
343 | ## if not --recursive: | |
344 | ## - first N arguments must be S3Uri | |
345 | ## - if the last one is S3 make current dir the destination_base | |
346 | ## - if the last one is a directory: | |
347 | ## - take all 'basenames' of the remote objects and | |
348 | ## make the destination name be 'destination_base'+'basename' | |
349 | ## - if the last one is a file or not existing: | |
350 | ## - if the number of sources (N, above) == 1 treat it | |
351 | ## as a filename and save the object there. | |
352 | ## - if there's more sources -> Error | |
353 | ## if --recursive: | |
354 | ## - first N arguments must be S3Uri | |
355 | ## - for each Uri get a list of remote objects with that Uri as a prefix | |
356 | ## - apply exclude/include rules | |
357 | ## - each list item will have MD5sum, Timestamp and pointer to S3Uri | |
358 | ## used as a prefix. | |
359 | ## - the last arg may be a local directory - destination_base | |
360 | ## - if the last one is S3 make current dir the destination_base | |
361 | ## - if the last one doesn't exist check remote list: | |
362 | ## - if there is only one item and its_prefix==its_name | |
363 | ## download that item to the name given in last arg. | |
364 | ## - if there are more remote items use the last arg as a destination_base | |
365 | ## and try to create the directory (incl. all parents). | |
366 | ## | |
367 | ## In both cases we end up with a list mapping remote object names (keys) to local file names. | |
368 | ||
369 | ## Each item will be a dict with the following attributes | |
370 | # {'remote_uri', 'local_filename'} | |
371 | download_list = [] | |
372 | ||
373 | if len(args) == 0: | |
374 | raise ParameterError("Nothing to download. Expecting S3 URI.") | |
375 | ||
376 | if S3Uri(args[-1]).type == 'file': | |
377 | destination_base = args.pop() | |
378 | else: | |
379 | destination_base = "." | |
380 | ||
381 | if len(args) == 0: | |
382 | raise ParameterError("Nothing to download. Expecting S3 URI.") | |
383 | ||
384 | remote_list = fetch_remote_list(args, require_attribs = False) | |
385 | remote_list, exclude_list = _filelist_filter_exclude_include(remote_list) | |
386 | ||
387 | remote_count = len(remote_list) | |
388 | ||
389 | info(u"Summary: %d remote files to download" % remote_count) | |
390 | ||
391 | if remote_count > 0: | |
392 | if not os.path.isdir(destination_base) or destination_base == '-': | |
393 | ## We were either given a file name (existing or not) or want STDOUT | |
394 | if remote_count > 1: | |
395 | raise ParameterError("Destination must be a directory when downloading multiple sources.") | |
396 | remote_list[remote_list.keys()[0]]['local_filename'] = deunicodise(destination_base) | |
397 | elif os.path.isdir(destination_base): | |
398 | if destination_base[-1] != os.path.sep: | |
399 | destination_base += os.path.sep | |
400 | for key in remote_list: | |
401 | remote_list[key]['local_filename'] = destination_base + key | |
402 | else: | |
403 | raise InternalError("WTF? Is it a dir or not? -- %s" % destination_base) | |
404 | ||
405 | if cfg.dry_run: | |
406 | for key in exclude_list: | |
407 | output(u"exclude: %s" % unicodise(key)) | |
408 | for key in remote_list: | |
409 | output(u"download: %s -> %s" % (remote_list[key]['object_uri_str'], remote_list[key]['local_filename'])) | |
410 | ||
411 | warning(u"Exitting now because of --dry-run") | |
412 | return | |
413 | ||
414 | seq = 0 | |
415 | for key in remote_list: | |
416 | seq += 1 | |
417 | item = remote_list[key] | |
418 | uri = S3Uri(item['object_uri_str']) | |
419 | ## Encode / Decode destination with "replace" to make sure it's compatible with current encoding | |
420 | destination = unicodise_safe(item['local_filename']) | |
421 | seq_label = "[%d of %d]" % (seq, remote_count) | |
422 | ||
423 | start_position = 0 | |
424 | ||
425 | if destination == "-": | |
426 | ## stdout | |
427 | dst_stream = sys.__stdout__ | |
428 | else: | |
429 | ## File | |
430 | try: | |
431 | file_exists = os.path.exists(destination) | |
432 | try: | |
433 | dst_stream = open(destination, "ab") | |
434 | except IOError, e: | |
435 | if e.errno == errno.ENOENT: | |
436 | basename = destination[:destination.rindex(os.path.sep)] | |
437 | info(u"Creating directory: %s" % basename) | |
438 | os.makedirs(basename) | |
439 | dst_stream = open(destination, "ab") | |
440 | else: | |
441 | raise | |
442 | if file_exists: | |
443 | if Config().get_continue: | |
444 | start_position = dst_stream.tell() | |
445 | elif Config().force: | |
446 | start_position = 0L | |
447 | dst_stream.seek(0L) | |
448 | dst_stream.truncate() | |
449 | elif Config().skip_existing: | |
450 | info(u"Skipping over existing file: %s" % (destination)) | |
451 | continue | |
452 | else: | |
453 | dst_stream.close() | |
454 | raise ParameterError(u"File %s already exists. Use either of --force / --continue / --skip-existing or give it a new name." % destination) | |
455 | except IOError, e: | |
456 | error(u"Skipping %s: %s" % (destination, e.strerror)) | |
457 | continue | |
458 | response = s3.object_get(uri, dst_stream, start_position = start_position, extra_label = seq_label) | |
459 | if response["headers"].has_key("x-amz-meta-s3tools-gpgenc"): | |
460 | gpg_decrypt(destination, response["headers"]["x-amz-meta-s3tools-gpgenc"]) | |
461 | response["size"] = os.stat(destination)[6] | |
462 | if not Config().progress_meter and destination != "-": | |
463 | speed_fmt = formatSize(response["speed"], human_readable = True, floating_point = True) | |
464 | output(u"File %s saved as '%s' (%d bytes in %0.1f seconds, %0.2f %sB/s)" % | |
465 | (uri, destination, response["size"], response["elapsed"], speed_fmt[0], speed_fmt[1])) | |
466 | ||
467 | def cmd_object_del(args): | |
468 | while (len(args)): | |
469 | uri_arg = args.pop(0) | |
470 | uri = S3Uri(uri_arg) | |
471 | if uri.type != "s3": | |
472 | raise ParameterError("Expecting S3 URI instead of '%s'" % uri_arg) | |
473 | if not uri.has_object(): | |
474 | if Config().recursive and not Config().force: | |
475 | raise ParameterError("Please use --force to delete ALL contents of %s" % uri) | |
476 | elif not Config().recursive: | |
477 | raise ParameterError("Object name required, not only the bucket name") | |
478 | subcmd_object_del_uri(uri) | |
479 | ||
480 | def subcmd_object_del_uri(uri, recursive = None): | |
481 | s3 = S3(Config()) | |
482 | if recursive is None: | |
483 | recursive = cfg.recursive | |
484 | uri_list = [] | |
485 | if recursive: | |
486 | filelist = _get_filelist_remote(uri) | |
487 | uri_base = 's3://' + uri.bucket() + "/" | |
488 | for idx in filelist: | |
489 | object = filelist[idx] | |
490 | debug(u"Adding URI " + uri_base + object['object_key']) | |
491 | uri_list.append(S3Uri(uri_base + object['object_key'])) | |
492 | else: | |
493 | uri_list.append(uri) | |
494 | for _uri in uri_list: | |
495 | response = s3.object_delete(_uri) | |
496 | output(u"Object %s deleted" % _uri) | |
497 | ||
498 | def subcmd_cp_mv(args, process_fce, message): | |
499 | src_uri = S3Uri(args.pop(0)) | |
500 | dst_uri = S3Uri(args.pop(0)) | |
501 | ||
502 | if len(args): | |
503 | raise ParameterError("Too many parameters! Expected: %s" % commands['cp']['param']) | |
504 | ||
505 | if src_uri.type != "s3" or dst_uri.type != "s3": | |
506 | raise ParameterError("Parameters are not URIs! Expected: %s" % commands['cp']['param']) | |
507 | ||
508 | if dst_uri.object() == "": | |
509 | dst_uri = S3Uri(dst_uri.uri() + src_uri.object()) | |
510 | ||
511 | response = process_fce(src_uri, dst_uri) | |
512 | output(message % { "src" : src_uri, "dst" : dst_uri}) | |
513 | if Config().acl_public: | |
514 | output(u"Public URL is: %s" % dst_uri.public_url()) | |
515 | ||
516 | def cmd_cp(args): | |
517 | s3 = S3(Config()) | |
518 | subcmd_cp_mv(args, s3.object_copy, "Object %(src)s copied to %(dst)s") | |
519 | ||
520 | def cmd_mv(args): | |
521 | s3 = S3(Config()) | |
522 | subcmd_cp_mv(args, s3.object_move, "Object %(src)s moved to %(dst)s") | |
523 | ||
524 | def cmd_info(args): | |
525 | s3 = S3(Config()) | |
526 | ||
527 | while (len(args)): | |
528 | uri_arg = args.pop(0) | |
529 | uri = S3Uri(uri_arg) | |
530 | if uri.type != "s3" or not uri.has_bucket(): | |
531 | raise ParameterError("Expecting S3 URI instead of '%s'" % uri_arg) | |
532 | ||
533 | try: | |
534 | if uri.has_object(): | |
535 | info = s3.object_info(uri) | |
536 | output(u"%s (object):" % uri.uri()) | |
537 | output(u" File size: %s" % info['headers']['content-length']) | |
538 | output(u" Last mod: %s" % info['headers']['last-modified']) | |
539 | output(u" MIME type: %s" % info['headers']['content-type']) | |
540 | output(u" MD5 sum: %s" % info['headers']['etag'].strip('"')) | |
541 | else: | |
542 | info = s3.bucket_info(uri) | |
543 | output(u"%s (bucket):" % uri.uri()) | |
544 | output(u" Location: %s" % info['bucket-location']) | |
545 | acl = s3.get_acl(uri) | |
546 | acl_list = acl.getGrantList() | |
547 | for user in acl_list: | |
548 | output(u" ACL: %s: %s" % (user, acl_list[user])) | |
549 | if acl.isAnonRead(): | |
550 | output(u" URL: %s" % uri.public_url()) | |
551 | except S3Error, e: | |
552 | if S3.codes.has_key(e.info["Code"]): | |
553 | error(S3.codes[e.info["Code"]] % uri.bucket()) | |
554 | return | |
555 | else: | |
556 | raise | |
557 | ||
317 | 558 | def _get_filelist_local(local_uri): |
318 | output("Compiling list of local files...") | |
319 | local_path = local_uri.path() | |
320 | if os.path.isdir(local_path): | |
321 | loc_base = os.path.join(local_path, "") | |
559 | info(u"Compiling list of local files...") | |
560 | if local_uri.isdir(): | |
561 | local_base = deunicodise(local_uri.basename()) | |
562 | local_path = deunicodise(local_uri.path()) | |
322 | 563 | filelist = os.walk(local_path) |
564 | single_file = False | |
323 | 565 | else: |
324 | loc_base = "./" | |
325 | filelist = [( '.', [], [local_path] )] | |
326 | loc_base_len = len(loc_base) | |
327 | loc_list = {} | |
566 | local_base = "" | |
567 | local_path = deunicodise(local_uri.dirname()) | |
568 | filelist = [( local_path, [], [deunicodise(local_uri.basename())] )] | |
569 | single_file = True | |
570 | loc_list = SortedDict() | |
328 | 571 | for root, dirs, files in filelist: |
329 | ## TODO: implement explicit exclude | |
572 | rel_root = root.replace(local_path, local_base, 1) | |
330 | 573 | for f in files: |
331 | 574 | full_name = os.path.join(root, f) |
332 | 575 | if not os.path.isfile(full_name): |
335 | 578 | ## Synchronize symlinks... one day |
336 | 579 | ## for now skip over |
337 | 580 | continue |
338 | file = full_name[loc_base_len:] | |
581 | relative_file = unicodise(os.path.join(rel_root, f)) | |
582 | if relative_file.startswith('./'): | |
583 | relative_file = relative_file[2:] | |
339 | 584 | sr = os.stat_result(os.lstat(full_name)) |
340 | loc_list[file] = { | |
585 | loc_list[relative_file] = { | |
586 | 'full_name_unicode' : unicodise(full_name), | |
341 | 587 | 'full_name' : full_name, |
342 | 588 | 'size' : sr.st_size, |
343 | 589 | 'mtime' : sr.st_mtime, |
344 | 590 | ## TODO: Possibly more to save here... |
345 | 591 | } |
346 | return loc_list | |
347 | ||
348 | def _get_filelist_remote(remote_uri): | |
349 | output("Retrieving list of remote files...") | |
592 | return loc_list, single_file | |
593 | ||
594 | def _get_filelist_remote(remote_uri, recursive = True): | |
595 | ## If remote_uri ends with '/' then all remote files will have | |
596 | ## the remote_uri prefix removed in the relative path. | |
597 | ## If, on the other hand, the remote_uri ends with something else | |
598 | ## (probably alphanumeric symbol) we'll use the last path part | |
599 | ## in the relative path. | |
600 | ## | |
601 | ## Complicated, eh? See an example: | |
602 | ## _get_filelist_remote("s3://bckt/abc/def") may yield: | |
603 | ## { 'def/file1.jpg' : {}, 'def/xyz/blah.txt' : {} } | |
604 | ## _get_filelist_remote("s3://bckt/abc/def/") will yield: | |
605 | ## { 'file1.jpg' : {}, 'xyz/blah.txt' : {} } | |
606 | ## Furthermore a prefix-magic can restrict the return list: | |
607 | ## _get_filelist_remote("s3://bckt/abc/def/x") yields: | |
608 | ## { 'xyz/blah.txt' : {} } | |
609 | ||
610 | info(u"Retrieving list of remote files for %s ..." % remote_uri) | |
350 | 611 | |
351 | 612 | s3 = S3(Config()) |
352 | response = s3.bucket_list(remote_uri.bucket(), prefix = remote_uri.object()) | |
353 | ||
354 | rem_base = remote_uri.object() | |
613 | response = s3.bucket_list(remote_uri.bucket(), prefix = remote_uri.object(), recursive = recursive) | |
614 | ||
615 | rem_base_original = rem_base = remote_uri.object() | |
616 | remote_uri_original = remote_uri | |
617 | if rem_base != '' and rem_base[-1] != '/': | |
618 | rem_base = rem_base[:rem_base.rfind('/')+1] | |
619 | remote_uri = S3Uri("s3://%s/%s" % (remote_uri.bucket(), rem_base)) | |
355 | 620 | rem_base_len = len(rem_base) |
356 | rem_list = {} | |
621 | rem_list = SortedDict() | |
622 | break_now = False | |
357 | 623 | for object in response['list']: |
358 | key = object['Key'][rem_base_len:] | |
624 | if object['Key'] == rem_base_original and object['Key'][-1] != os.path.sep: | |
625 | ## We asked for one file and we got that file :-) | |
626 | key = os.path.basename(object['Key']) | |
627 | object_uri_str = remote_uri_original.uri() | |
628 | break_now = True | |
629 | rem_list = {} ## Remove whatever has already been put to rem_list | |
630 | else: | |
631 | key = object['Key'][rem_base_len:] ## Beware - this may be '' if object['Key']==rem_base !! | |
632 | object_uri_str = remote_uri.uri() + key | |
359 | 633 | rem_list[key] = { |
360 | 634 | 'size' : int(object['Size']), |
361 | # 'mtime' : dateS3toUnix(object['LastModified']), ## That's upload time, not our lastmod time :-( | |
635 | 'timestamp' : dateS3toUnix(object['LastModified']), ## Sadly it's upload time, not our lastmod time :-( | |
362 | 636 | 'md5' : object['ETag'][1:-1], |
363 | 'object_key' : object['Key'] | |
637 | 'object_key' : object['Key'], | |
638 | 'object_uri_str' : object_uri_str, | |
639 | 'base_uri' : remote_uri, | |
364 | 640 | } |
641 | if break_now: | |
642 | break | |
365 | 643 | return rem_list |
366 | ||
367 | def _compare_filelists(src_list, dst_list, src_is_local_and_dst_is_remote): | |
368 | output("Verifying checksums...") | |
644 | ||
645 | def _filelist_filter_exclude_include(src_list): | |
646 | info(u"Applying --exclude/--include") | |
369 | 647 | cfg = Config() |
370 | exists_list = {} | |
371 | exclude_list = {} | |
372 | if cfg.debug_syncmatch: | |
373 | logging.root.setLevel(logging.DEBUG) | |
648 | exclude_list = SortedDict() | |
374 | 649 | for file in src_list.keys(): |
375 | if not cfg.debug_syncmatch: | |
376 | debug("CHECK: %s" % (os.sep + file)) | |
650 | debug(u"CHECK: %s" % file) | |
377 | 651 | excluded = False |
378 | 652 | for r in cfg.exclude: |
379 | ## all paths start with '/' from the base dir | |
380 | if r.search(os.sep + file): | |
381 | ## Can't directly 'continue' to the outer loop | |
382 | ## therefore this awkward excluded switch :-( | |
653 | if r.search(file): | |
383 | 654 | excluded = True |
384 | if cfg.debug_syncmatch: | |
385 | debug("EXCL: %s" % (os.sep + file)) | |
386 | debug("RULE: '%s'" % (cfg.debug_exclude[r])) | |
387 | else: | |
388 | info("%s: excluded" % file) | |
655 | debug(u"EXCL-MATCH: '%s'" % (cfg.debug_exclude[r])) | |
389 | 656 | break |
390 | 657 | if excluded: |
391 | exclude_list = src_list[file] | |
658 | ## No need to check for --include if not excluded | |
659 | for r in cfg.include: | |
660 | if r.search(file): | |
661 | excluded = False | |
662 | debug(u"INCL-MATCH: '%s'" % (cfg.debug_include[r])) | |
663 | break | |
664 | if excluded: | |
665 | ## Still excluded - ok, action it | |
666 | debug(u"EXCLUDE: %s" % file) | |
667 | exclude_list[file] = src_list[file] | |
392 | 668 | del(src_list[file]) |
393 | 669 | continue |
394 | 670 | else: |
395 | debug("PASS: %s" % (os.sep + file)) | |
671 | debug(u"PASS: %s" % (file)) | |
672 | return src_list, exclude_list | |
673 | ||
674 | def _compare_filelists(src_list, dst_list, src_is_local_and_dst_is_remote): | |
675 | info(u"Verifying attributes...") | |
676 | cfg = Config() | |
677 | exists_list = SortedDict() | |
678 | if cfg.debug_syncmatch: | |
679 | logging.root.setLevel(logging.DEBUG) | |
680 | ||
681 | for file in src_list.keys(): | |
682 | if not cfg.debug_syncmatch: | |
683 | debug(u"CHECK: %s" % file) | |
396 | 684 | if dst_list.has_key(file): |
685 | ## Was --skip-existing requested? | |
686 | if cfg.skip_existing: | |
687 | debug(u"IGNR: %s (used --skip-existing)" % (file)) | |
688 | exists_list[file] = src_list[file] | |
689 | del(src_list[file]) | |
690 | ## Remove from destination-list, all that is left there will be deleted | |
691 | del(dst_list[file]) | |
692 | continue | |
693 | ||
694 | attribs_match = True | |
397 | 695 | ## Check size first |
398 | if dst_list[file]['size'] == src_list[file]['size']: | |
399 | #debug("%s same size: %s" % (file, dst_list[file]['size'])) | |
696 | if 'size' in cfg.sync_checks and dst_list[file]['size'] != src_list[file]['size']: | |
697 | debug(u"XFER: %s (size mismatch: src=%s dst=%s)" % (file, src_list[file]['size'], dst_list[file]['size'])) | |
698 | attribs_match = False | |
699 | ||
700 | if attribs_match and 'md5' in cfg.sync_checks: | |
400 | 701 | ## ... same size, check MD5 |
401 | 702 | if src_is_local_and_dst_is_remote: |
402 | 703 | src_md5 = Utils.hash_file_md5(src_list[file]['full_name']) |
404 | 705 | else: |
405 | 706 | src_md5 = src_list[file]['md5'] |
406 | 707 | dst_md5 = Utils.hash_file_md5(dst_list[file]['full_name']) |
407 | if src_md5 == dst_md5: | |
408 | #debug("%s md5 matches: %s" % (file, dst_md5)) | |
409 | ## Checksums are the same. | |
410 | ## Remove from source-list, all that is left there will be transferred | |
411 | debug("IGNR: %s (transfer not needed: MD5 OK, Size OK)" % file) | |
412 | exists_list[file] = src_list[file] | |
413 | del(src_list[file]) | |
414 | else: | |
415 | debug("XFER: %s (md5 mismatch: src=%s dst=%s)" % (file, src_md5, dst_md5)) | |
416 | else: | |
417 | debug("XFER: %s (size mismatch: src=%s dst=%s)" % (file, src_list[file]['size'], dst_list[file]['size'])) | |
418 | ||
708 | if src_md5 != dst_md5: | |
709 | ## Checksums are different. | |
710 | attribs_match = False | |
711 | debug(u"XFER: %s (md5 mismatch: src=%s dst=%s)" % (file, src_md5, dst_md5)) | |
712 | ||
713 | if attribs_match: | |
714 | ## Remove from source-list, all that is left there will be transferred | |
715 | debug(u"IGNR: %s (transfer not needed)" % file) | |
716 | exists_list[file] = src_list[file] | |
717 | del(src_list[file]) | |
718 | ||
419 | 719 | ## Remove from destination-list, all that is left there will be deleted |
420 | #debug("%s removed from destination list" % file) | |
421 | 720 | del(dst_list[file]) |
721 | ||
422 | 722 | if cfg.debug_syncmatch: |
423 | warning("Exiting because of --debug-syncmatch") | |
424 | sys.exit(0) | |
425 | ||
426 | return src_list, dst_list, exists_list, exclude_list | |
427 | ||
428 | def cmd_sync_remote2local(src, dst): | |
723 | warning(u"Exiting because of --debug-syncmatch") | |
724 | sys.exit(1) | |
725 | ||
726 | return src_list, dst_list, exists_list | |
727 | ||
728 | def cmd_sync_remote2local(args): | |
429 | 729 | def _parse_attrs_header(attrs_header): |
430 | 730 | attrs = {} |
431 | 731 | for attr in attrs_header.split("/"): |
435 | 735 | |
436 | 736 | s3 = S3(Config()) |
437 | 737 | |
438 | src_uri = S3Uri(src) | |
439 | dst_uri = S3Uri(dst) | |
440 | ||
441 | rem_list = _get_filelist_remote(src_uri) | |
442 | rem_count = len(rem_list) | |
443 | ||
444 | loc_list = _get_filelist_local(dst_uri) | |
445 | loc_count = len(loc_list) | |
446 | ||
447 | output("Found %d remote files, %d local files" % (rem_count, loc_count)) | |
448 | ||
449 | _compare_filelists(rem_list, loc_list, False) | |
450 | ||
451 | output("Summary: %d remote files to download, %d local files to delete" % (len(rem_list), len(loc_list))) | |
452 | ||
453 | for file in loc_list: | |
738 | destination_base = args[-1] | |
739 | local_list, single_file_local = fetch_local_list(destination_base, recursive = True) | |
740 | remote_list = fetch_remote_list(args[:-1], recursive = True, require_attribs = True) | |
741 | ||
742 | local_count = len(local_list) | |
743 | remote_count = len(remote_list) | |
744 | ||
745 | info(u"Found %d remote files, %d local files" % (remote_count, local_count)) | |
746 | ||
747 | remote_list, exclude_list = _filelist_filter_exclude_include(remote_list) | |
748 | ||
749 | remote_list, local_list, existing_list = _compare_filelists(remote_list, local_list, False) | |
750 | ||
751 | local_count = len(local_list) | |
752 | remote_count = len(remote_list) | |
753 | ||
754 | info(u"Summary: %d remote files to download, %d local files to delete" % (remote_count, local_count)) | |
755 | ||
756 | if not os.path.isdir(destination_base): | |
757 | ## We were either given a file name (existing or not) or want STDOUT | |
758 | if remote_count > 1: | |
759 | raise ParameterError("Destination must be a directory when downloading multiple sources.") | |
760 | remote_list[remote_list.keys()[0]]['local_filename'] = deunicodise(destination_base) | |
761 | else: | |
762 | if destination_base[-1] != os.path.sep: | |
763 | destination_base += os.path.sep | |
764 | for key in remote_list: | |
765 | remote_list[key]['local_filename'] = deunicodise(destination_base + key) | |
766 | ||
767 | if cfg.dry_run: | |
768 | for key in exclude_list: | |
769 | output(u"exclude: %s" % unicodise(key)) | |
454 | 770 | if cfg.delete_removed: |
455 | # os.unlink(file) | |
456 | output("deleted '%s'" % file) | |
457 | else: | |
458 | output("not-deleted '%s'" % file) | |
771 | for key in local_list: | |
772 | output(u"delete: %s" % local_list[key]['full_name_unicode']) | |
773 | for key in remote_list: | |
774 | output(u"download: %s -> %s" % (remote_list[key]['object_uri_str'], remote_list[key]['local_filename'])) | |
775 | ||
776 | warning(u"Exitting now because of --dry-run") | |
777 | return | |
778 | ||
779 | if cfg.delete_removed: | |
780 | for key in local_list: | |
781 | os.unlink(local_list[key]['full_name']) | |
782 | output(u"deleted: %s" % local_list[key]['full_name_unicode']) | |
459 | 783 | |
460 | 784 | total_size = 0 |
461 | total_count = len(rem_list) | |
462 | 785 | total_elapsed = 0.0 |
463 | 786 | timestamp_start = time.time() |
464 | 787 | seq = 0 |
465 | 788 | dir_cache = {} |
466 | src_base = src_uri.uri() | |
467 | dst_base = dst_uri.path() | |
468 | if not src_base[-1] == "/": src_base += "/" | |
469 | file_list = rem_list.keys() | |
789 | file_list = remote_list.keys() | |
470 | 790 | file_list.sort() |
471 | 791 | for file in file_list: |
472 | 792 | seq += 1 |
473 | uri = S3Uri(src_base + file) | |
474 | dst_file = dst_base + file | |
793 | item = remote_list[file] | |
794 | uri = S3Uri(item['object_uri_str']) | |
795 | dst_file = item['local_filename'] | |
796 | seq_label = "[%d of %d]" % (seq, remote_count) | |
475 | 797 | try: |
476 | 798 | dst_dir = os.path.dirname(dst_file) |
477 | 799 | if not dir_cache.has_key(dst_dir): |
478 | 800 | dir_cache[dst_dir] = Utils.mkdir_with_parents(dst_dir) |
479 | 801 | if dir_cache[dst_dir] == False: |
480 | warning("%s: destination directory not writable: %s" % (file, dst_dir)) | |
802 | warning(u"%s: destination directory not writable: %s" % (file, dst_dir)) | |
481 | 803 | continue |
482 | 804 | try: |
483 | 805 | open_flags = os.O_CREAT |
484 | if cfg.force: | |
485 | open_flags |= os.O_TRUNC | |
486 | else: | |
487 | open_flags |= os.O_EXCL | |
488 | ||
489 | debug("dst_file=%s" % dst_file) | |
806 | open_flags |= os.O_TRUNC | |
807 | # open_flags |= os.O_EXCL | |
808 | ||
809 | debug(u"dst_file=%s" % unicodise(dst_file)) | |
490 | 810 | # This will have failed should the file exist |
491 | 811 | os.close(os.open(dst_file, open_flags)) |
492 | 812 | # Yeah I know there is a race condition here. Sadly I don't know how to open() in exclusive mode. |
493 | 813 | dst_stream = open(dst_file, "wb") |
494 | response = s3.object_get_uri(uri, dst_stream) | |
814 | response = s3.object_get(uri, dst_stream, extra_label = seq_label) | |
495 | 815 | dst_stream.close() |
496 | 816 | if response['headers'].has_key('x-amz-meta-s3cmd-attrs') and cfg.preserve_attrs: |
497 | 817 | attrs = _parse_attrs_header(response['headers']['x-amz-meta-s3cmd-attrs']) |
506 | 826 | try: dst_stream.close() |
507 | 827 | except: pass |
508 | 828 | if e.errno == errno.EEXIST: |
509 | warning("%s exists - not overwriting" % (dst_file)) | |
829 | warning(u"%s exists - not overwriting" % (dst_file)) | |
510 | 830 | continue |
511 | 831 | if e.errno in (errno.EPERM, errno.EACCES): |
512 | warning("%s not writable: %s" % (dst_file, e.strerror)) | |
832 | warning(u"%s not writable: %s" % (dst_file, e.strerror)) | |
513 | 833 | continue |
514 | 834 | raise e |
515 | 835 | except KeyboardInterrupt: |
516 | 836 | try: dst_stream.close() |
517 | 837 | except: pass |
518 | warning("Exiting after keyboard interrupt") | |
838 | warning(u"Exiting after keyboard interrupt") | |
519 | 839 | return |
520 | 840 | except Exception, e: |
521 | 841 | try: dst_stream.close() |
522 | 842 | except: pass |
523 | error("%s: %s" % (file, e)) | |
843 | error(u"%s: %s" % (file, e)) | |
524 | 844 | continue |
525 | 845 | # We have to keep repeating this call because |
526 | 846 | # Python 2.4 doesn't support try/except/finally |
528 | 848 | try: dst_stream.close() |
529 | 849 | except: pass |
530 | 850 | except S3DownloadError, e: |
531 | error("%s: download failed too many times. Skipping that file." % file) | |
851 | error(u"%s: download failed too many times. Skipping that file." % file) | |
532 | 852 | continue |
533 | 853 | speed_fmt = formatSize(response["speed"], human_readable = True, floating_point = True) |
534 | output("File '%s' stored as %s (%d bytes in %0.1f seconds, %0.2f %sB/s) [%d of %d]" % | |
535 | (uri, dst_file, response["size"], response["elapsed"], speed_fmt[0], speed_fmt[1], | |
536 | seq, total_count)) | |
854 | if not Config().progress_meter: | |
855 | output(u"File '%s' stored as '%s' (%d bytes in %0.1f seconds, %0.2f %sB/s) %s" % | |
856 | (uri, unicodise(dst_file), response["size"], response["elapsed"], speed_fmt[0], speed_fmt[1], | |
857 | seq_label)) | |
537 | 858 | total_size += response["size"] |
538 | 859 | |
539 | 860 | total_elapsed = time.time() - timestamp_start |
540 | 861 | speed_fmt = formatSize(total_size/total_elapsed, human_readable = True, floating_point = True) |
541 | output("Done. Downloaded %d bytes in %0.1f seconds, %0.2f %sB/s" % | |
542 | (total_size, total_elapsed, speed_fmt[0], speed_fmt[1])) | |
543 | ||
544 | def cmd_sync_local2remote(src, dst): | |
862 | ||
863 | # Only print out the result if any work has been done or | |
864 | # if the user asked for verbose output | |
865 | outstr = "Done. Downloaded %d bytes in %0.1f seconds, %0.2f %sB/s" % (total_size, total_elapsed, speed_fmt[0], speed_fmt[1]) | |
866 | if total_size > 0: | |
867 | output(outstr) | |
868 | else: | |
869 | info(outstr) | |
870 | ||
871 | def cmd_sync_local2remote(args): | |
545 | 872 | def _build_attr_header(src): |
873 | import pwd, grp | |
546 | 874 | attrs = {} |
875 | src = deunicodise(src) | |
547 | 876 | st = os.stat_result(os.stat(src)) |
548 | 877 | for attr in cfg.preserve_attrs_list: |
549 | 878 | if attr == 'uname': |
552 | 881 | except KeyError: |
553 | 882 | attr = "uid" |
554 | 883 | val = st.st_uid |
555 | warning("%s: Owner username not known. Storing UID=%d instead." % (src, val)) | |
884 | warning(u"%s: Owner username not known. Storing UID=%d instead." % (unicodise(src), val)) | |
556 | 885 | elif attr == 'gname': |
557 | 886 | try: |
558 | 887 | val = grp.getgrgid(st.st_gid).gr_name |
559 | 888 | except KeyError: |
560 | 889 | attr = "gid" |
561 | 890 | val = st.st_gid |
562 | warning("%s: Owner groupname not known. Storing GID=%d instead." % (src, val)) | |
891 | warning(u"%s: Owner groupname not known. Storing GID=%d instead." % (unicodise(src), val)) | |
563 | 892 | else: |
564 | 893 | val = getattr(st, 'st_' + attr) |
565 | 894 | attrs[attr] = val |
570 | 899 | s3 = S3(cfg) |
571 | 900 | |
572 | 901 | if cfg.encrypt: |
573 | error("S3cmd 'sync' doesn't support GPG encryption, sorry.") | |
574 | error("Either use unconditional 's3cmd put --recursive'") | |
575 | error("or disable encryption with --no-encryption parameter.") | |
902 | error(u"S3cmd 'sync' doesn't yet support GPG encryption, sorry.") | |
903 | error(u"Either use unconditional 's3cmd put --recursive'") | |
904 | error(u"or disable encryption with --no-encrypt parameter.") | |
576 | 905 | sys.exit(1) |
577 | 906 | |
578 | ||
579 | src_uri = S3Uri(src) | |
580 | dst_uri = S3Uri(dst) | |
581 | ||
582 | loc_list = _get_filelist_local(src_uri) | |
583 | loc_count = len(loc_list) | |
584 | ||
585 | rem_list = _get_filelist_remote(dst_uri) | |
586 | rem_count = len(rem_list) | |
587 | ||
588 | output("Found %d local files, %d remote files" % (loc_count, rem_count)) | |
589 | ||
590 | _compare_filelists(loc_list, rem_list, True) | |
591 | ||
592 | output("Summary: %d local files to upload, %d remote files to delete" % (len(loc_list), len(rem_list))) | |
593 | ||
594 | for file in rem_list: | |
595 | uri = S3Uri("s3://" + dst_uri.bucket()+"/"+rem_list[file]['object_key']) | |
907 | destination_base = args[-1] | |
908 | local_list, single_file_local = fetch_local_list(args[:-1], recursive = True) | |
909 | remote_list = fetch_remote_list(destination_base, recursive = True, require_attribs = True) | |
910 | ||
911 | local_count = len(local_list) | |
912 | remote_count = len(remote_list) | |
913 | ||
914 | info(u"Found %d local files, %d remote files" % (local_count, remote_count)) | |
915 | ||
916 | local_list, exclude_list = _filelist_filter_exclude_include(local_list) | |
917 | ||
918 | if single_file_local and len(local_list) == 1 and len(remote_list) == 1: | |
919 | ## Make remote_key same as local_key for comparison if we're dealing with only one file | |
920 | remote_list_entry = remote_list[remote_list.keys()[0]] | |
921 | # Flush remote_list, by the way | |
922 | remote_list = { local_list.keys()[0] : remote_list_entry } | |
923 | ||
924 | local_list, remote_list, existing_list = _compare_filelists(local_list, remote_list, True) | |
925 | ||
926 | local_count = len(local_list) | |
927 | remote_count = len(remote_list) | |
928 | ||
929 | info(u"Summary: %d local files to upload, %d remote files to delete" % (local_count, remote_count)) | |
930 | ||
931 | if local_count > 0: | |
932 | ## Populate 'remote_uri' only if we've got something to upload | |
933 | if not destination_base.endswith("/"): | |
934 | if not single_file_local: | |
935 | raise ParameterError("Destination S3 URI must end with '/' (ie must refer to a directory on the remote side).") | |
936 | local_list[local_list.keys()[0]]['remote_uri'] = unicodise(destination_base) | |
937 | else: | |
938 | for key in local_list: | |
939 | local_list[key]['remote_uri'] = unicodise(destination_base + key) | |
940 | ||
941 | if cfg.dry_run: | |
942 | for key in exclude_list: | |
943 | output(u"exclude: %s" % unicodise(key)) | |
596 | 944 | if cfg.delete_removed: |
597 | response = s3.object_delete_uri(uri) | |
598 | output("deleted '%s'" % uri) | |
599 | else: | |
600 | output("not-deleted '%s'" % uri) | |
945 | for key in remote_list: | |
946 | output(u"delete: %s" % remote_list[key]['object_uri_str']) | |
947 | for key in local_list: | |
948 | output(u"upload: %s -> %s" % (local_list[key]['full_name_unicode'], local_list[key]['remote_uri'])) | |
949 | ||
950 | warning(u"Exitting now because of --dry-run") | |
951 | return | |
952 | ||
953 | if cfg.delete_removed: | |
954 | for key in remote_list: | |
955 | uri = S3Uri(remote_list[key]['object_uri_str']) | |
956 | s3.object_delete(uri) | |
957 | output(u"deleted: '%s'" % uri) | |
601 | 958 | |
602 | 959 | total_size = 0 |
603 | total_count = len(loc_list) | |
604 | 960 | total_elapsed = 0.0 |
605 | 961 | timestamp_start = time.time() |
606 | 962 | seq = 0 |
607 | dst_base = dst_uri.uri() | |
608 | if not dst_base[-1] == "/": dst_base += "/" | |
609 | file_list = loc_list.keys() | |
963 | file_list = local_list.keys() | |
610 | 964 | file_list.sort() |
611 | 965 | for file in file_list: |
612 | 966 | seq += 1 |
613 | src = loc_list[file]['full_name'] | |
614 | uri = S3Uri(dst_base + file) | |
967 | item = local_list[file] | |
968 | src = item['full_name'] | |
969 | uri = S3Uri(item['remote_uri']) | |
970 | seq_label = "[%d of %d]" % (seq, local_count) | |
971 | attr_header = None | |
615 | 972 | if cfg.preserve_attrs: |
616 | 973 | attr_header = _build_attr_header(src) |
617 | 974 | debug(attr_header) |
618 | 975 | try: |
619 | response = s3.object_put_uri(src, uri, attr_header) | |
976 | response = s3.object_put(src, uri, attr_header, extra_label = seq_label) | |
620 | 977 | except S3UploadError, e: |
621 | error("%s: upload failed too many times. Skipping that file." % src) | |
978 | error(u"%s: upload failed too many times. Skipping that file." % item['full_name_unicode']) | |
622 | 979 | continue |
623 | 980 | except InvalidFileError, e: |
624 | warning("File can not be uploaded: %s" % e) | |
981 | warning(u"File can not be uploaded: %s" % e) | |
625 | 982 | continue |
626 | 983 | speed_fmt = formatSize(response["speed"], human_readable = True, floating_point = True) |
627 | output("File '%s' stored as %s (%d bytes in %0.1f seconds, %0.2f %sB/s) [%d of %d]" % | |
628 | (src, uri, response["size"], response["elapsed"], speed_fmt[0], speed_fmt[1], | |
629 | seq, total_count)) | |
984 | if not cfg.progress_meter: | |
985 | output(u"File '%s' stored as '%s' (%d bytes in %0.1f seconds, %0.2f %sB/s) %s" % | |
986 | (item['full_name_unicode'], uri, response["size"], response["elapsed"], | |
987 | speed_fmt[0], speed_fmt[1], seq_label)) | |
630 | 988 | total_size += response["size"] |
631 | 989 | |
632 | 990 | total_elapsed = time.time() - timestamp_start |
633 | speed_fmt = formatSize(total_size/total_elapsed, human_readable = True, floating_point = True) | |
634 | output("Done. Uploaded %d bytes in %0.1f seconds, %0.2f %sB/s" % | |
635 | (total_size, total_elapsed, speed_fmt[0], speed_fmt[1])) | |
991 | total_speed = total_elapsed and total_size/total_elapsed or 0.0 | |
992 | speed_fmt = formatSize(total_speed, human_readable = True, floating_point = True) | |
993 | ||
994 | # Only print out the result if any work has been done or | |
995 | # if the user asked for verbose output | |
996 | outstr = "Done. Uploaded %d bytes in %0.1f seconds, %0.2f %sB/s" % (total_size, total_elapsed, speed_fmt[0], speed_fmt[1]) | |
997 | if total_size > 0: | |
998 | output(outstr) | |
999 | else: | |
1000 | info(outstr) | |
636 | 1001 | |
637 | 1002 | def cmd_sync(args): |
638 | src = args.pop(0) | |
639 | dst = args.pop(0) | |
640 | if (len(args)): | |
641 | raise ParameterError("Too many parameters! Expected: %s" % commands['sync']['param']) | |
642 | ||
643 | if S3Uri(src).type == "s3" and not src.endswith('/'): | |
644 | src += "/" | |
645 | ||
646 | if not dst.endswith('/'): | |
647 | dst += "/" | |
648 | ||
649 | if S3Uri(src).type == "file" and S3Uri(dst).type == "s3": | |
650 | return cmd_sync_local2remote(src, dst) | |
651 | if S3Uri(src).type == "s3" and S3Uri(dst).type == "file": | |
652 | return cmd_sync_remote2local(src, dst) | |
653 | ||
1003 | if (len(args) < 2): | |
1004 | raise ParameterError("Too few parameters! Expected: %s" % commands['sync']['param']) | |
1005 | ||
1006 | if S3Uri(args[0]).type == "file" and S3Uri(args[-1]).type == "s3": | |
1007 | return cmd_sync_local2remote(args) | |
1008 | if S3Uri(args[0]).type == "s3" and S3Uri(args[-1]).type == "file": | |
1009 | return cmd_sync_remote2local(args) | |
1010 | raise ParameterError("Invalid source/destination: '%s'" % "' '".join(args)) | |
1011 | ||
1012 | def cmd_setacl(args): | |
1013 | s3 = S3(cfg) | |
1014 | ||
1015 | set_to_acl = cfg.acl_public and "Public" or "Private" | |
1016 | ||
1017 | remote_list = fetch_remote_list(args) | |
1018 | remote_count = len(remote_list) | |
1019 | seq = 0 | |
1020 | for key in remote_list: | |
1021 | seq += 1 | |
1022 | seq_label = "[%d of %d]" % (seq, remote_count) | |
1023 | uri = S3Uri(remote_list[key]['object_uri_str']) | |
1024 | acl = s3.get_acl(uri) | |
1025 | debug(u"acl: %s - %r" % (uri, acl.grantees)) | |
1026 | if cfg.acl_public: | |
1027 | if acl.isAnonRead(): | |
1028 | info(u"%s: already Public, skipping %s" % (uri, seq_label)) | |
1029 | continue | |
1030 | acl.grantAnonRead() | |
1031 | else: | |
1032 | if not acl.isAnonRead(): | |
1033 | info(u"%s: already Private, skipping %s" % (uri, seq_label)) | |
1034 | continue | |
1035 | acl.revokeAnonRead() | |
1036 | retsponse = s3.set_acl(uri, acl) | |
1037 | if retsponse['status'] == 200: | |
1038 | output(u"%s: ACL set to %s %s" % (uri, set_to_acl, seq_label)) | |
1039 | ||
654 | 1040 | def resolve_list(lst, args): |
655 | 1041 | retval = [] |
656 | 1042 | for item in lst: |
658 | 1044 | return retval |
659 | 1045 | |
660 | 1046 | def gpg_command(command, passphrase = ""): |
661 | p_in, p_out = os.popen4(command) | |
662 | if command.count("--passphrase-fd"): | |
663 | p_in.write(passphrase+"\n") | |
664 | p_in.flush() | |
665 | for line in p_out: | |
666 | info(line.strip()) | |
667 | p_pid, p_exitcode = os.wait() | |
1047 | debug("GPG command: " + " ".join(command)) | |
1048 | p = subprocess.Popen(command, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.STDOUT) | |
1049 | p_stdout, p_stderr = p.communicate(passphrase + "\n") | |
1050 | debug("GPG output:") | |
1051 | for line in p_stdout.split("\n"): | |
1052 | debug("GPG: " + line) | |
1053 | p_exitcode = p.wait() | |
668 | 1054 | return p_exitcode |
669 | 1055 | |
670 | 1056 | def gpg_encrypt(filename): |
675 | 1061 | "input_file" : filename, |
676 | 1062 | "output_file" : tmp_filename, |
677 | 1063 | } |
678 | info("Encrypting file %(input_file)s to %(output_file)s..." % args) | |
1064 | info(u"Encrypting file %(input_file)s to %(output_file)s..." % args) | |
679 | 1065 | command = resolve_list(cfg.gpg_encrypt.split(" "), args) |
680 | 1066 | code = gpg_command(command, cfg.gpg_passphrase) |
681 | 1067 | return (code, tmp_filename, "gpg") |
688 | 1074 | "input_file" : filename, |
689 | 1075 | "output_file" : tmp_filename, |
690 | 1076 | } |
691 | info("Decrypting file %(input_file)s to %(output_file)s..." % args) | |
1077 | info(u"Decrypting file %(input_file)s to %(output_file)s..." % args) | |
692 | 1078 | command = resolve_list(cfg.gpg_decrypt.split(" "), args) |
693 | 1079 | code = gpg_command(command, cfg.gpg_passphrase) |
694 | 1080 | if code == 0 and in_place: |
695 | debug("Renaming %s to %s" % (tmp_filename, filename)) | |
1081 | debug(u"Renaming %s to %s" % (tmp_filename, filename)) | |
696 | 1082 | os.unlink(filename) |
697 | 1083 | os.rename(tmp_filename, filename) |
698 | 1084 | tmp_filename = filename |
721 | 1107 | |
722 | 1108 | try: |
723 | 1109 | while 1: |
724 | output("\nEnter new values or accept defaults in brackets with Enter.") | |
725 | output("Refer to user manual for detailed description of all options.") | |
1110 | output(u"\nEnter new values or accept defaults in brackets with Enter.") | |
1111 | output(u"Refer to user manual for detailed description of all options.") | |
726 | 1112 | for option in options: |
727 | 1113 | prompt = option[1] |
728 | 1114 | ## Option-specific handling |
743 | 1129 | pass |
744 | 1130 | |
745 | 1131 | if len(option) >= 3: |
746 | output("\n%s" % option[2]) | |
1132 | output(u"\n%s" % option[2]) | |
747 | 1133 | |
748 | 1134 | val = raw_input(prompt + ": ") |
749 | 1135 | if val != "": |
751 | 1137 | # Turn 'Yes' into True, everything else into False |
752 | 1138 | val = val.lower().startswith('y') |
753 | 1139 | setattr(cfg, option[0], val) |
754 | output("\nNew settings:") | |
1140 | output(u"\nNew settings:") | |
755 | 1141 | for option in options: |
756 | output(" %s: %s" % (option[1], getattr(cfg, option[0]))) | |
1142 | output(u" %s: %s" % (option[1], getattr(cfg, option[0]))) | |
757 | 1143 | val = raw_input("\nTest access with supplied credentials? [Y/n] ") |
758 | 1144 | if val.lower().startswith("y") or val == "": |
759 | 1145 | try: |
760 | output("Please wait...") | |
1146 | output(u"Please wait...") | |
761 | 1147 | S3(Config()).bucket_list("", "") |
762 | output("Success. Your access key and secret key worked fine :-)") | |
763 | ||
764 | output("\nNow verifying that encryption works...") | |
1148 | output(u"Success. Your access key and secret key worked fine :-)") | |
1149 | ||
1150 | output(u"\nNow verifying that encryption works...") | |
765 | 1151 | if not getattr(cfg, "gpg_command") or not getattr(cfg, "gpg_passphrase"): |
766 | output("Not configured. Never mind.") | |
1152 | output(u"Not configured. Never mind.") | |
767 | 1153 | else: |
768 | 1154 | if not getattr(cfg, "gpg_command"): |
769 | 1155 | raise Exception("Path to GPG program not set") |
789 | 1175 | raise Exception("Encryption verification error.") |
790 | 1176 | |
791 | 1177 | except Exception, e: |
792 | error("Test failed: %s" % (e)) | |
1178 | error(u"Test failed: %s" % (e)) | |
793 | 1179 | val = raw_input("\nRetry configuration? [Y/n] ") |
794 | 1180 | if val.lower().startswith("y") or val == "": |
795 | 1181 | continue |
813 | 1199 | os.umask(old_mask) |
814 | 1200 | cfg.dump_config(f) |
815 | 1201 | f.close() |
816 | output("Configuration saved to '%s'" % config_file) | |
1202 | output(u"Configuration saved to '%s'" % config_file) | |
817 | 1203 | |
818 | 1204 | except (EOFError, KeyboardInterrupt): |
819 | output("\nConfiguration aborted. Changes were NOT saved.") | |
1205 | output(u"\nConfiguration aborted. Changes were NOT saved.") | |
820 | 1206 | return |
821 | 1207 | |
822 | 1208 | except IOError, e: |
823 | error("Writing config file failed: %s: %s" % (config_file, e.strerror)) | |
1209 | error(u"Writing config file failed: %s: %s" % (config_file, e.strerror)) | |
824 | 1210 | sys.exit(1) |
825 | 1211 | |
826 | def process_exclude_from_file(exf, exclude_array): | |
827 | exfi = open(exf, "rt") | |
828 | for ex in exfi: | |
829 | ex = ex.strip() | |
830 | if re.match("^#", ex) or re.match("^\s*$", ex): | |
1212 | def process_patterns_from_file(fname, patterns_list): | |
1213 | try: | |
1214 | fn = open(fname, "rt") | |
1215 | except IOError, e: | |
1216 | error(e) | |
1217 | sys.exit(1) | |
1218 | for pattern in fn: | |
1219 | pattern = pattern.strip() | |
1220 | if re.match("^#", pattern) or re.match("^\s*$", pattern): | |
831 | 1221 | continue |
832 | debug("adding rule: %s" % ex) | |
833 | exclude_array.append(ex) | |
834 | ||
835 | commands = {} | |
836 | commands_list = [ | |
1222 | debug(u"%s: adding rule: %s" % (fname, pattern)) | |
1223 | patterns_list.append(pattern) | |
1224 | ||
1225 | return patterns_list | |
1226 | ||
1227 | def process_patterns(patterns_list, patterns_from, is_glob, option_txt = ""): | |
1228 | """ | |
1229 | process_patterns(patterns, patterns_from, is_glob, option_txt = "") | |
1230 | Process --exclude / --include GLOB and REGEXP patterns. | |
1231 | 'option_txt' is 'exclude' / 'include' / 'rexclude' / 'rinclude' | |
1232 | Returns: patterns_compiled, patterns_text | |
1233 | """ | |
1234 | ||
1235 | patterns_compiled = [] | |
1236 | patterns_textual = {} | |
1237 | ||
1238 | if patterns_list is None: | |
1239 | patterns_list = [] | |
1240 | ||
1241 | if patterns_from: | |
1242 | ## Append patterns from glob_from | |
1243 | for fname in patterns_from: | |
1244 | debug(u"processing --%s-from %s" % (option_txt, fname)) | |
1245 | patterns_list = process_patterns_from_file(fname, patterns_list) | |
1246 | ||
1247 | for pattern in patterns_list: | |
1248 | debug(u"processing %s rule: %s" % (option_txt, patterns_list)) | |
1249 | if is_glob: | |
1250 | pattern = glob.fnmatch.translate(pattern) | |
1251 | r = re.compile(pattern) | |
1252 | patterns_compiled.append(r) | |
1253 | patterns_textual[r] = pattern | |
1254 | ||
1255 | return patterns_compiled, patterns_textual | |
1256 | ||
1257 | def get_commands_list(): | |
1258 | return [ | |
837 | 1259 | {"cmd":"mb", "label":"Make bucket", "param":"s3://BUCKET", "func":cmd_bucket_create, "argc":1}, |
838 | 1260 | {"cmd":"rb", "label":"Remove bucket", "param":"s3://BUCKET", "func":cmd_bucket_delete, "argc":1}, |
839 | 1261 | {"cmd":"ls", "label":"List objects or buckets", "param":"[s3://BUCKET[/PREFIX]]", "func":cmd_ls, "argc":0}, |
845 | 1267 | {"cmd":"sync", "label":"Synchronize a directory tree to S3", "param":"LOCAL_DIR s3://BUCKET[/PREFIX] or s3://BUCKET[/PREFIX] LOCAL_DIR", "func":cmd_sync, "argc":2}, |
846 | 1268 | {"cmd":"du", "label":"Disk usage by buckets", "param":"[s3://BUCKET[/PREFIX]]", "func":cmd_du, "argc":0}, |
847 | 1269 | {"cmd":"info", "label":"Get various information about Buckets or Objects", "param":"s3://BUCKET[/OBJECT]", "func":cmd_info, "argc":1}, |
848 | #{"cmd":"setacl", "label":"Modify Access control list for Bucket or Object", "param":"s3://BUCKET[/OBJECT]", "func":cmd_setacl, "argc":1}, | |
1270 | {"cmd":"cp", "label":"Copy object", "param":"s3://BUCKET1/OBJECT1 s3://BUCKET2[/OBJECT2]", "func":cmd_cp, "argc":2}, | |
1271 | {"cmd":"mv", "label":"Move object", "param":"s3://BUCKET1/OBJECT1 s3://BUCKET2[/OBJECT2]", "func":cmd_mv, "argc":2}, | |
1272 | {"cmd":"setacl", "label":"Modify Access control list for Bucket or Object", "param":"s3://BUCKET[/OBJECT]", "func":cmd_setacl, "argc":1}, | |
1273 | ## CloudFront commands | |
1274 | {"cmd":"cflist", "label":"List CloudFront distribution points", "param":"", "func":CfCmd.info, "argc":0}, | |
1275 | {"cmd":"cfinfo", "label":"Display CloudFront distribution point parameters", "param":"[cf://DIST_ID]", "func":CfCmd.info, "argc":0}, | |
1276 | {"cmd":"cfcreate", "label":"Create CloudFront distribution point", "param":"s3://BUCKET", "func":CfCmd.create, "argc":1}, | |
1277 | {"cmd":"cfdelete", "label":"Delete CloudFront distribution point", "param":"cf://DIST_ID", "func":CfCmd.delete, "argc":1}, | |
1278 | {"cmd":"cfmodify", "label":"Change CloudFront distribution point parameters", "param":"cf://DIST_ID", "func":CfCmd.modify, "argc":1}, | |
849 | 1279 | ] |
850 | 1280 | |
851 | def format_commands(progname): | |
1281 | def format_commands(progname, commands_list): | |
852 | 1282 | help = "Commands:\n" |
853 | 1283 | for cmd in commands_list: |
854 | 1284 | help += " %s\n %s %s %s\n" % (cmd["label"], progname, cmd["cmd"], cmd["param"]) |
877 | 1307 | sys.stderr.write("ERROR: Python 2.4 or higher required, sorry.\n") |
878 | 1308 | sys.exit(1) |
879 | 1309 | |
1310 | commands_list = get_commands_list() | |
1311 | commands = {} | |
1312 | ||
880 | 1313 | ## Populate "commands" from "commands_list" |
881 | 1314 | for cmd in commands_list: |
882 | 1315 | if cmd.has_key("cmd"): |
886 | 1319 | optparser = OptionParser(option_class=OptionMimeType, formatter=MyHelpFormatter()) |
887 | 1320 | #optparser.disable_interspersed_args() |
888 | 1321 | |
1322 | config_file = None | |
889 | 1323 | if os.getenv("HOME"): |
890 | optparser.set_defaults(config=os.getenv("HOME")+"/.s3cfg") | |
891 | ||
1324 | config_file = os.path.join(os.getenv("HOME"), ".s3cfg") | |
1325 | elif os.name == "nt" and os.getenv("USERPROFILE"): | |
1326 | config_file = os.path.join(os.getenv("USERPROFILE"), "Application Data", "s3cmd.ini") | |
1327 | ||
1328 | preferred_encoding = locale.getpreferredencoding() or "UTF-8" | |
1329 | ||
1330 | optparser.set_defaults(encoding = preferred_encoding) | |
1331 | optparser.set_defaults(config = config_file) | |
892 | 1332 | optparser.set_defaults(verbosity = default_verbosity) |
893 | 1333 | |
894 | 1334 | optparser.add_option( "--configure", dest="run_configure", action="store_true", help="Invoke interactive (re)configuration tool.") |
895 | 1335 | optparser.add_option("-c", "--config", dest="config", metavar="FILE", help="Config file name. Defaults to %default") |
896 | 1336 | optparser.add_option( "--dump-config", dest="dump_config", action="store_true", help="Dump current configuration after parsing config files and command line options and exit.") |
897 | 1337 | |
898 | #optparser.add_option("-n", "--dry-run", dest="dry_run", action="store_true", help="Only show what should be uploaded or downloaded but don't actually do it. May still perform S3 requests to get bucket listings and other information though.") | |
1338 | optparser.add_option("-n", "--dry-run", dest="dry_run", action="store_true", help="Only show what should be uploaded or downloaded but don't actually do it. May still perform S3 requests to get bucket listings and other information though (only for [sync] command)") | |
899 | 1339 | |
900 | 1340 | optparser.add_option("-e", "--encrypt", dest="encrypt", action="store_true", help="Encrypt files before uploading to S3.") |
901 | 1341 | optparser.add_option( "--no-encrypt", dest="encrypt", action="store_false", help="Don't encrypt files.") |
902 | 1342 | optparser.add_option("-f", "--force", dest="force", action="store_true", help="Force overwrite and other dangerous operations.") |
1343 | optparser.add_option( "--continue", dest="get_continue", action="store_true", help="Continue getting a partially downloaded file (only for [get] command).") | |
1344 | optparser.add_option( "--skip-existing", dest="skip_existing", action="store_true", help="Skip over files that exist at the destination (only for [get] and [sync] commands).") | |
1345 | optparser.add_option("-r", "--recursive", dest="recursive", action="store_true", help="Recursive upload, download or removal.") | |
903 | 1346 | optparser.add_option("-P", "--acl-public", dest="acl_public", action="store_true", help="Store objects with ACL allowing read for anyone.") |
904 | 1347 | optparser.add_option( "--acl-private", dest="acl_public", action="store_false", help="Store objects with default ACL allowing access for you only.") |
905 | 1348 | optparser.add_option( "--delete-removed", dest="delete_removed", action="store_true", help="Delete remote objects with no corresponding local file [sync]") |
910 | 1353 | optparser.add_option( "--exclude-from", dest="exclude_from", action="append", metavar="FILE", help="Read --exclude GLOBs from FILE") |
911 | 1354 | optparser.add_option( "--rexclude", dest="rexclude", action="append", metavar="REGEXP", help="Filenames and paths matching REGEXP (regular expression) will be excluded from sync") |
912 | 1355 | optparser.add_option( "--rexclude-from", dest="rexclude_from", action="append", metavar="FILE", help="Read --rexclude REGEXPs from FILE") |
1356 | optparser.add_option( "--include", dest="include", action="append", metavar="GLOB", help="Filenames and paths matching GLOB will be included even if previously excluded by one of --(r)exclude(-from) patterns") | |
1357 | optparser.add_option( "--include-from", dest="include_from", action="append", metavar="FILE", help="Read --include GLOBs from FILE") | |
1358 | optparser.add_option( "--rinclude", dest="rinclude", action="append", metavar="REGEXP", help="Same as --include but uses REGEXP (regular expression) instead of GLOB") | |
1359 | optparser.add_option( "--rinclude-from", dest="rinclude_from", action="append", metavar="FILE", help="Read --rinclude REGEXPs from FILE") | |
913 | 1360 | optparser.add_option( "--debug-syncmatch", "--debug-exclude", dest="debug_syncmatch", action="store_true", help="Output detailed information about remote vs. local filelist matching and --exclude processing and then exit") |
914 | 1361 | |
915 | 1362 | optparser.add_option( "--bucket-location", dest="bucket_location", help="Datacentre to create bucket in. Either EU or US (default)") |
917 | 1364 | optparser.add_option("-m", "--mime-type", dest="default_mime_type", type="mimetype", metavar="MIME/TYPE", help="Default MIME-type to be set for objects stored.") |
918 | 1365 | optparser.add_option("-M", "--guess-mime-type", dest="guess_mime_type", action="store_true", help="Guess MIME-type of files by their extension. Falls back to default MIME-Type as specified by --mime-type option") |
919 | 1366 | |
1367 | optparser.add_option( "--encoding", dest="encoding", metavar="ENCODING", help="Override autodetected terminal and filesystem encoding (character set). Autodetected: %s" % preferred_encoding) | |
1368 | ||
920 | 1369 | optparser.add_option("-H", "--human-readable-sizes", dest="human_readable_sizes", action="store_true", help="Print sizes in human readable form.") |
921 | 1370 | |
1371 | optparser.add_option( "--progress", dest="progress_meter", action="store_true", help="Display progress meter (default on TTY).") | |
1372 | optparser.add_option( "--no-progress", dest="progress_meter", action="store_false", help="Don't display progress meter (default on non-TTY).") | |
1373 | optparser.add_option( "--enable", dest="cf_enable", action="store_true", help="Enable given CloudFront distribution (only for [cfmodify] command)") | |
1374 | optparser.add_option( "--disable", dest="cf_enable", action="store_false", help="Enable given CloudFront distribution (only for [cfmodify] command)") | |
1375 | optparser.add_option( "--cf-add-cname", dest="cf_cnames_add", action="append", metavar="CNAME", help="Add given CNAME to a CloudFront distribution (only for [cfcreate] and [cfmodify] commands)") | |
1376 | optparser.add_option( "--cf-remove-cname", dest="cf_cnames_remove", action="append", metavar="CNAME", help="Remove given CNAME from a CloudFront distribution (only for [cfmodify] command)") | |
1377 | optparser.add_option( "--cf-comment", dest="cf_comment", action="store", metavar="COMMENT", help="Set COMMENT for a given CloudFront distribution (only for [cfcreate] and [cfmodify] commands)") | |
922 | 1378 | optparser.add_option("-v", "--verbose", dest="verbosity", action="store_const", const=logging.INFO, help="Enable verbose output.") |
923 | 1379 | optparser.add_option("-d", "--debug", dest="verbosity", action="store_const", const=logging.DEBUG, help="Enable debug output.") |
924 | 1380 | optparser.add_option( "--version", dest="show_version", action="store_true", help="Show s3cmd version (%s) and exit." % (PkgInfo.version)) |
928 | 1384 | 'Amazon S3 storage. It allows for making and removing '+ |
929 | 1385 | '"buckets" and uploading, downloading and removing '+ |
930 | 1386 | '"objects" from these buckets.') |
931 | optparser.epilog = format_commands(optparser.get_prog_name()) | |
1387 | optparser.epilog = format_commands(optparser.get_prog_name(), commands_list) | |
932 | 1388 | optparser.epilog += ("\nSee program homepage for more information at\n%s\n" % PkgInfo.url) |
933 | 1389 | |
934 | 1390 | (options, args) = optparser.parse_args() |
937 | 1393 | ## debugging/verbose output for config file parser on request |
938 | 1394 | logging.basicConfig(level=options.verbosity, |
939 | 1395 | format='%(levelname)s: %(message)s', |
940 | stream = _stderr) | |
1396 | stream = sys.stderr) | |
941 | 1397 | |
942 | 1398 | if options.show_version: |
943 | output("s3cmd version %s" % PkgInfo.version) | |
1399 | output(u"s3cmd version %s" % PkgInfo.version) | |
944 | 1400 | sys.exit(0) |
945 | 1401 | |
946 | 1402 | ## Now finally parse the config file |
947 | 1403 | if not options.config: |
948 | error("Can't find a config file. Please use --config option.") | |
1404 | error(u"Can't find a config file. Please use --config option.") | |
949 | 1405 | sys.exit(1) |
950 | 1406 | |
951 | 1407 | try: |
954 | 1410 | if options.run_configure: |
955 | 1411 | cfg = Config() |
956 | 1412 | else: |
957 | error("%s: %s" % (options.config, e.strerror)) | |
958 | error("Configuration file not available.") | |
959 | error("Consider using --configure parameter to create one.") | |
1413 | error(u"%s: %s" % (options.config, e.strerror)) | |
1414 | error(u"Configuration file not available.") | |
1415 | error(u"Consider using --configure parameter to create one.") | |
960 | 1416 | sys.exit(1) |
961 | 1417 | |
962 | 1418 | ## And again some logging level adjustments |
965 | 1421 | cfg.verbosity = options.verbosity |
966 | 1422 | logging.root.setLevel(cfg.verbosity) |
967 | 1423 | |
1424 | ## Default to --progress on TTY devices, --no-progress elsewhere | |
1425 | ## Can be overriden by actual --(no-)progress parameter | |
1426 | cfg.update_option('progress_meter', sys.stdout.isatty()) | |
1427 | ||
1428 | ## Unsupported features on Win32 platform | |
1429 | if os.name == "nt": | |
1430 | if cfg.preserve_attrs: | |
1431 | error(u"Option --preserve is not yet supported on MS Windows platform. Assuming --no-preserve.") | |
1432 | cfg.preserve_attrs = False | |
1433 | if cfg.progress_meter: | |
1434 | error(u"Option --progress is not yet supported on MS Windows platform. Assuming --no-progress.") | |
1435 | cfg.progress_meter = False | |
1436 | ||
968 | 1437 | ## Update Config with other parameters |
969 | 1438 | for option in cfg.option_list(): |
970 | 1439 | try: |
971 | 1440 | if getattr(options, option) != None: |
972 | debug("Updating %s -> %s" % (option, getattr(options, option))) | |
1441 | debug(u"Updating Config.Config %s -> %s" % (option, getattr(options, option))) | |
973 | 1442 | cfg.update_option(option, getattr(options, option)) |
974 | 1443 | except AttributeError: |
975 | 1444 | ## Some Config() options are not settable from command line |
976 | 1445 | pass |
977 | ||
978 | ## Process GLOB (shell wildcard style) excludes | |
979 | if options.exclude is None: | |
980 | options.exclude = [] | |
981 | ||
982 | if options.exclude_from: | |
983 | for exf in options.exclude_from: | |
984 | debug("processing --exclude-from %s" % exf) | |
985 | process_exclude_from_file(exf, options.exclude) | |
986 | ||
987 | if options.exclude: | |
988 | for ex in options.exclude: | |
989 | debug("processing rule: %s" % ex) | |
990 | exc = re.compile(glob.fnmatch.translate(ex)) | |
991 | cfg.exclude.append(exc) | |
992 | if options.debug_syncmatch: | |
993 | cfg.debug_exclude[exc] = ex | |
994 | ||
995 | ## Process REGEXP style excludes | |
996 | if options.rexclude is None: | |
997 | options.rexclude = [] | |
998 | ||
999 | if options.rexclude_from: | |
1000 | for exf in options.rexclude_from: | |
1001 | debug("processing --rexclude-from %s" % exf) | |
1002 | process_exclude_from_file(exf, options.rexclude) | |
1003 | ||
1004 | if options.rexclude: | |
1005 | for ex in options.rexclude: | |
1006 | debug("processing rule: %s" % ex) | |
1007 | exc = re.compile(ex) | |
1008 | cfg.exclude.append(exc) | |
1009 | if options.debug_syncmatch: | |
1010 | cfg.debug_exclude[exc] = ex | |
1446 | ||
1447 | ## Update CloudFront options if some were set | |
1448 | for option in CfCmd.options.option_list(): | |
1449 | try: | |
1450 | if getattr(options, option) != None: | |
1451 | debug(u"Updating CloudFront.Cmd %s -> %s" % (option, getattr(options, option))) | |
1452 | CfCmd.options.update_option(option, getattr(options, option)) | |
1453 | except AttributeError: | |
1454 | ## Some CloudFront.Cmd.Options() options are not settable from command line | |
1455 | pass | |
1456 | ||
1457 | ## Set output and filesystem encoding for printing out filenames. | |
1458 | sys.stdout = codecs.getwriter(cfg.encoding)(sys.stdout, "replace") | |
1459 | sys.stderr = codecs.getwriter(cfg.encoding)(sys.stderr, "replace") | |
1460 | ||
1461 | ## Process --exclude and --exclude-from | |
1462 | patterns_list, patterns_textual = process_patterns(options.exclude, options.exclude_from, is_glob = True, option_txt = "exclude") | |
1463 | cfg.exclude.extend(patterns_list) | |
1464 | cfg.debug_exclude.update(patterns_textual) | |
1465 | ||
1466 | ## Process --rexclude and --rexclude-from | |
1467 | patterns_list, patterns_textual = process_patterns(options.rexclude, options.rexclude_from, is_glob = False, option_txt = "rexclude") | |
1468 | cfg.exclude.extend(patterns_list) | |
1469 | cfg.debug_exclude.update(patterns_textual) | |
1470 | ||
1471 | ## Process --include and --include-from | |
1472 | patterns_list, patterns_textual = process_patterns(options.include, options.include_from, is_glob = True, option_txt = "include") | |
1473 | cfg.include.extend(patterns_list) | |
1474 | cfg.debug_include.update(patterns_textual) | |
1475 | ||
1476 | ## Process --rinclude and --rinclude-from | |
1477 | patterns_list, patterns_textual = process_patterns(options.rinclude, options.rinclude_from, is_glob = False, option_txt = "rinclude") | |
1478 | cfg.include.extend(patterns_list) | |
1479 | cfg.debug_include.update(patterns_textual) | |
1011 | 1480 | |
1012 | 1481 | if cfg.encrypt and cfg.gpg_passphrase == "": |
1013 | error("Encryption requested but no passphrase set in config file.") | |
1014 | error("Please re-run 's3cmd --configure' and supply it.") | |
1482 | error(u"Encryption requested but no passphrase set in config file.") | |
1483 | error(u"Please re-run 's3cmd --configure' and supply it.") | |
1015 | 1484 | sys.exit(1) |
1016 | 1485 | |
1017 | 1486 | if options.dump_config: |
1023 | 1492 | sys.exit(0) |
1024 | 1493 | |
1025 | 1494 | if len(args) < 1: |
1026 | error("Missing command. Please run with --help for more information.") | |
1495 | error(u"Missing command. Please run with --help for more information.") | |
1027 | 1496 | sys.exit(1) |
1028 | 1497 | |
1029 | 1498 | ## Unicodise all remaining arguments: |
1031 | 1500 | |
1032 | 1501 | command = args.pop(0) |
1033 | 1502 | try: |
1034 | debug("Command: %s" % commands[command]["cmd"]) | |
1503 | debug(u"Command: %s" % commands[command]["cmd"]) | |
1035 | 1504 | ## We must do this lookup in extra step to |
1036 | 1505 | ## avoid catching all KeyError exceptions |
1037 | 1506 | ## from inner functions. |
1038 | 1507 | cmd_func = commands[command]["func"] |
1039 | 1508 | except KeyError, e: |
1040 | error("Invalid command: %s" % e) | |
1509 | error(u"Invalid command: %s" % e) | |
1041 | 1510 | sys.exit(1) |
1042 | 1511 | |
1043 | 1512 | if len(args) < commands[command]["argc"]: |
1044 | error("Not enough paramters for command '%s'" % command) | |
1513 | error(u"Not enough paramters for command '%s'" % command) | |
1045 | 1514 | sys.exit(1) |
1046 | 1515 | |
1047 | 1516 | try: |
1048 | 1517 | cmd_func(args) |
1049 | 1518 | except S3Error, e: |
1050 | error("S3 error: %s" % e) | |
1519 | error(u"S3 error: %s" % e) | |
1051 | 1520 | sys.exit(1) |
1052 | 1521 | except ParameterError, e: |
1053 | error("Parameter problem: %s" % e) | |
1522 | error(u"Parameter problem: %s" % e) | |
1054 | 1523 | sys.exit(1) |
1055 | 1524 | |
1056 | 1525 | if __name__ == '__main__': |
1065 | 1534 | from S3 import Utils |
1066 | 1535 | from S3.Exceptions import * |
1067 | 1536 | from S3.Utils import unicodise |
1537 | from S3.Progress import Progress | |
1538 | from S3.CloudFront import Cmd as CfCmd | |
1068 | 1539 | |
1069 | 1540 | main() |
1070 | 1541 | sys.exit(0) |
1542 | ||
1071 | 1543 | except SystemExit, e: |
1072 | 1544 | sys.exit(e.code) |
1545 | ||
1546 | except KeyboardInterrupt: | |
1547 | sys.stderr.write("See ya!\n") | |
1548 | sys.exit(1) | |
1073 | 1549 | |
1074 | 1550 | except Exception, e: |
1075 | 1551 | sys.stderr.write(""" |
1076 | 1552 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! |
1077 | 1553 | An unexpected error has occurred. |
1078 | 1554 | Please report the following lines to: |
1079 | s3tools-general@lists.sourceforge.net | |
1555 | s3tools-bugs@lists.sourceforge.net | |
1080 | 1556 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! |
1081 | 1557 | |
1082 | 1558 | """) |
1083 | sys.stderr.write("S3cmd: %s\n" % PkgInfo.version) | |
1084 | sys.stderr.write("Python: %s\n" % sys.version.replace('\n', ' ')) | |
1559 | tb = traceback.format_exc(sys.exc_info()) | |
1560 | e_class = str(e.__class__) | |
1561 | e_class = e_class[e_class.rfind(".")+1 : -2] | |
1562 | sys.stderr.write(u"Problem: %s: %s\n" % (e_class, e)) | |
1563 | try: | |
1564 | sys.stderr.write("S3cmd: %s\n" % PkgInfo.version) | |
1565 | except NameError: | |
1566 | sys.stderr.write("S3cmd: unknown version. Module import problem?\n") | |
1567 | sys.stderr.write("Python: %s\n" % sys.version.replace('\n', ' ')) | |
1085 | 1568 | sys.stderr.write("\n") |
1086 | sys.stderr.write(traceback.format_exc(sys.exc_info())+"\n") | |
1569 | sys.stderr.write(unicode(tb, errors="replace")) | |
1570 | if type(e) == ImportError: | |
1571 | sys.stderr.write("\n") | |
1572 | sys.stderr.write("Your sys.path contains these entries:\n") | |
1573 | for path in sys.path: | |
1574 | sys.stderr.write(u"\t%s\n" % path) | |
1575 | sys.stderr.write("Now the question is where has S3/S3.py been installed?\n") | |
1087 | 1576 | sys.stderr.write(""" |
1088 | 1577 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! |
1089 | 1578 | An unexpected error has occurred. |
1090 | 1579 | Please report the above lines to: |
1091 | s3tools-general@lists.sourceforge.net | |
1580 | s3tools-bugs@lists.sourceforge.net | |
1092 | 1581 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! |
1093 | 1582 | """) |
1094 | 1583 | sys.exit(1) |
41 | 41 | \fBsync\fR \fIs3://BUCKET[/PREFIX] LOCAL_DIR\fR |
42 | 42 | Restore a tree from S3 to local directory |
43 | 43 | .TP |
44 | \fBcp\fR \fIs3://BUCKET1/OBJECT1 s3://BUCKET2[/OBJECT2]\fR | |
45 | \fBmv\fR \fIs3://BUCKET1/OBJECT1 s3://BUCKET2[/OBJECT2]\fR | |
46 | Make a copy of a file (\fIcp\fR) or move a file (\fImv\fR). | |
47 | Destination can be in the same bucket with a different name | |
48 | or in another bucket with the same or different name. | |
49 | Adding \fI\-\-acl\-public\fR will make the destination object | |
50 | publicly accessible (see below). | |
51 | .TP | |
44 | 52 | \fBinfo\fR \fIs3://BUCKET[/OBJECT]\fR |
45 | 53 | Get various information about a Bucket or Object |
46 | 54 | .TP |
108 | 116 | \fB\-f\fR, \fB\-\-force\fR |
109 | 117 | Force overwrite and other dangerous operations. |
110 | 118 | .TP |
119 | \fB\-\-continue\fR | |
120 | Continue getting a partially downloaded file (only for \fIget\fR command). This comes handy once download of a large file, say an ISO image, from a S3 bucket fails and a partially downloaded file is left on the disk. Unfortunately \fIput\fR command doesn't support restarting of failed upload due to Amazon S3 limitation. | |
121 | .TP | |
111 | 122 | \fB\-P\fR, \fB\-\-acl\-public\fR |
112 | 123 | Store objects with permissions allowing read for anyone. |
113 | 124 | .TP |
133 | 144 | .\".TP |
134 | 145 | .\"\fB\-u\fR, \fB\-\-show\-uri\fR |
135 | 146 | .\"Show complete S3 URI in listings. |
147 | .TP | |
148 | \fB\-\-progress\fR, \fB\-\-no\-progress\fR | |
149 | Display or don't display progress meter. When running on TTY (e.g. console or xterm) the default is to display progress meter. If not on TTY (e.g. output is redirected somewhere or running from cron) the default is to not display progress meter. | |
136 | 150 | .TP |
137 | 151 | \fB\-v\fR, \fB\-\-verbose\fR |
138 | 152 | Enable verbose output. |