Browse code

* s3cmd, S3/S3Uri.py, NEWS: Support for recursive 'put'.

git-svn-id: https://s3tools.svn.sourceforge.net/svnroot/s3tools/s3cmd/trunk@337 830e0280-6d2a-0410-9c65-932aecc39d9d

Michal Ludvig authored on 2009/01/15 19:51:30
Showing 4 changed files
... ...
@@ -1,3 +1,7 @@
1
+2009-01-15  Michal Ludvig  <michal@logix.cz>
2
+
3
+	* s3cmd, S3/S3Uri.py, NEWS: Support for recursive 'put'.
4
+
1 5
 2009-01-13  Michal Ludvig  <michal@logix.cz>
2 6
 
3 7
 	* TODO: Updated.
... ...
@@ -1,6 +1,7 @@
1 1
 s3cmd 0.9.9-pre5
2 2
 ================
3 3
 * New command 'setacl' for setting ACL on existing objects.
4
+* Recursive [put] with a slightly different semantic.
4 5
 
5 6
 s3cmd 0.9.9-pre4 - 2008-12-30
6 7
 ================
... ...
@@ -3,6 +3,7 @@
3 3
 ##         http://www.logix.cz/michal
4 4
 ## License: GPL Version 2
5 5
 
6
+import os
6 7
 import re
7 8
 import sys
8 9
 from BidirMap import BidirMap
... ...
@@ -117,6 +118,12 @@ class S3UriFile(S3Uri):
117 117
 	def uri(self):
118 118
 		return "/".join(["file:/", self.path()])
119 119
 
120
+	def isdir(self):
121
+		return os.path.isdir(self.path())
122
+
123
+	def dirname(self):
124
+		return os.path.dirname(self.path())
125
+
120 126
 if __name__ == "__main__":
121 127
 	uri = S3Uri("s3://bucket/object")
122 128
 	print "type()  =", type(uri)
... ...
@@ -169,6 +169,29 @@ def cmd_bucket_delete(args):
169 169
 		_bucket_delete_one(uri)
170 170
 		output(u"Bucket '%s' removed" % uri.uri())
171 171
 
172
+def fetch_local_list(args):
173
+	local_uris = []
174
+	local_list = {}
175
+
176
+	for arg in args:
177
+		uri = S3Uri(arg)
178
+		if not uri.type == 'file':
179
+			raise ParameterError("Expecting filename or directory instead of: %s" % arg)
180
+		if uri.isdir() and not cfg.recursive:
181
+			raise ParameterError("Use --recursive to upload a directory: %s" % arg)
182
+		local_uris.append(uri)
183
+
184
+	for uri in local_uris:
185
+		filelist = _get_filelist_local(uri)
186
+		for key in filelist:
187
+			upload_item = {
188
+				'file_info' : filelist[key],
189
+				'relative_key' : key,
190
+			}
191
+			local_list[key] = filelist[key]
192
+
193
+	return local_list
194
+
172 195
 def fetch_remote_list(args):
173 196
 	remote_uris = []
174 197
 	remote_list = []
... ...
@@ -229,39 +252,48 @@ def fetch_remote_list(args):
229 229
 	return remote_list
230 230
 
231 231
 def cmd_object_put(args):
232
-	s3 = S3(Config())
232
+	cfg = Config()
233
+	s3 = S3(cfg)
234
+
235
+	## Each item will be a dict with the following attributes
236
+	# {'remote_uri', 'local_filename'}
237
+	upload_list = []
233 238
 
234
-	uri_arg = args.pop()
235
-	check_args_type(args, 'file', 'filename')
239
+	if len(args) == 0:
240
+		raise ParameterError("Nothing to upload. Expecting a local file or directory.")
236 241
 
237
-	uri = S3Uri(uri_arg)
238
-	if uri.type != "s3":
239
-		raise ParameterError("Expecting S3 URI instead of '%s'" % uri_arg)
242
+	dst_uri = S3Uri(args.pop())
243
+	if dst_uri.type != 's3':
244
+		raise ParameterError("Destination must be S3Uri. Got: %s" % args[-1])
240 245
 
241
-	if len(args) > 1 and uri.object() != "" and not Config().force:
242
-		error(u"When uploading multiple files the last argument must")
243
-		error(u"be a S3 URI specifying just the bucket name")
244
-		error(u"WITHOUT object name!")
245
-		error(u"Alternatively use --force argument and the specified")
246
-		error(u"object name will be prefixed to all stored filenames.")
247
-		sys.exit(1)
248
-	
246
+	if len(args) == 0:
247
+		raise ParameterError("Nothing to upload. Expecting a local file or directory.")
248
+
249
+	local_list = fetch_local_list(args)
250
+	local_count = len(local_list)
251
+
252
+	if local_count > 1 and not dst_uri.object() == "" and not dst_uri.object().endswith("/"):
253
+		raise ParameterError(u"When uploading multiple files the last argument must be a S3 URI ending with '/' or a bucket name only!")
254
+
255
+	sorted_local_keys = local_list.keys()
256
+	sorted_local_keys.sort()
249 257
 	seq = 0
250
-	total = len(args)
251
-	for file in args:
258
+	for key in sorted_local_keys:
252 259
 		seq += 1
253
-		uri_arg_final = str(uri)
254
-		if len(args) > 1 or uri.object() == "":
255
-			uri_arg_final += os.path.basename(file)
256
-		
257
-		uri_final = S3Uri(uri_arg_final)
260
+
261
+		if local_count > 1 or dst_uri.object() == "":
262
+			uri_final = S3Uri(u"%s%s" % (dst_uri, key))
263
+		else:
264
+			uri_final = dst_uri
265
+
258 266
 		extra_headers = {}
259
-		real_filename = file
260
-		seq_label = "[%d of %d]" % (seq, total)
267
+		full_name_orig = local_list[key]['full_name']
268
+		full_name = full_name_orig
269
+		seq_label = "[%d of %d]" % (seq, local_count)
261 270
 		if Config().encrypt:
262
-			exitcode, real_filename, extra_headers["x-amz-meta-s3tools-gpgenc"] = gpg_encrypt(file)
271
+			exitcode, full_name, extra_headers["x-amz-meta-s3tools-gpgenc"] = gpg_encrypt(full_name_orig)
263 272
 		try:
264
-			response = s3.object_put(real_filename, uri_final, extra_headers, extra_label = seq_label)
273
+			response = s3.object_put(full_name, uri_final, extra_headers, extra_label = seq_label)
265 274
 		except S3UploadError, e:
266 275
 			error(u"Upload of '%s' failed too many times. Skipping that file." % real_filename)
267 276
 			continue
... ...
@@ -271,14 +303,14 @@ def cmd_object_put(args):
271 271
 		speed_fmt = formatSize(response["speed"], human_readable = True, floating_point = True)
272 272
 		if not Config().progress_meter:
273 273
 			output(u"File '%s' stored as %s (%d bytes in %0.1f seconds, %0.2f %sB/s) %s" %
274
-				(file, uri_final, response["size"], response["elapsed"], speed_fmt[0], speed_fmt[1],
275
-				seq_label))
274
+				(unicodise(full_name_orig), uri_final, response["size"], response["elapsed"], 
275
+				speed_fmt[0], speed_fmt[1], seq_label))
276 276
 		if Config().acl_public:
277 277
 			output(u"Public URL of the object is: %s" %
278 278
 				(uri_final.public_url()))
279
-		if Config().encrypt and real_filename != file:
280
-			debug(u"Removing temporary encrypted file: %s" % real_filename)
281
-			os.remove(real_filename)
279
+		if Config().encrypt and full_name != full_name_orig:
280
+			debug(u"Removing temporary encrypted file: %s" % unicodise(full_name))
281
+			os.remove(full_name)
282 282
 
283 283
 def cmd_object_get(args):
284 284
 	cfg = Config()
... ...
@@ -487,29 +519,31 @@ def cmd_info(args):
487 487
 
488 488
 def _get_filelist_local(local_uri):
489 489
 	info(u"Compiling list of local files...")
490
-	local_path = deunicodise(local_uri.path())
491
-	if os.path.isdir(local_path):
492
-		loc_base = os.path.join(local_path, "")
490
+	if local_uri.isdir():
491
+		local_base = local_uri.basename()
492
+		local_path = deunicodise(local_uri.path())
493 493
 		filelist = os.walk(local_path)
494 494
 	else:
495
-		loc_base = "." + os.path.sep
496
-		filelist = [( '.', [], [local_path] )]
497
-	loc_base_len = len(loc_base)
495
+		local_base = ""
496
+		local_path = deunicodise(local_uri.dirname())
497
+		filelist = [( local_path, [], [deunicodise(local_uri.basename())] )]
498 498
 	loc_list = {}
499 499
 	for root, dirs, files in filelist:
500
+		rel_root = root.replace(local_path, local_base, 1)
500 501
 		## TODO: implement explicit exclude
501 502
 		for f in files:
502 503
 			full_name = os.path.join(root, f)
504
+			rel_name = os.path.join(rel_root, f)
503 505
 			if not os.path.isfile(full_name):
504 506
 				continue
505 507
 			if os.path.islink(full_name):
506 508
 				## Synchronize symlinks... one day
507 509
 				## for now skip over
508 510
 				continue
509
-			file = unicodise(full_name[loc_base_len:])
511
+			relative_file = unicodise(rel_name)
510 512
 			sr = os.stat_result(os.lstat(full_name))
511
-			loc_list[file] = {
512
-				'full_name_unicoded' : unicodise(full_name),
513
+			loc_list[relative_file] = {
514
+				'full_name_unicode' : unicodise(full_name),
513 515
 				'full_name' : full_name,
514 516
 				'size' : sr.st_size, 
515 517
 				'mtime' : sr.st_mtime,