Browse code

* s3cmd: Migrated 'sync' local->remote to the new scheme with fetch_{local,remote}_list(). Enabled --dry-run for 'sync'.

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

Michal Ludvig authored on 2009/01/20 23:24:42
Showing 2 changed files
... ...
@@ -1,3 +1,9 @@
1
+2009-01-21  Michal Ludvig  <michal@logix.cz>
2
+
3
+	* s3cmd: Migrated 'sync' local->remote to the new
4
+	  scheme with fetch_{local,remote}_list().
5
+	  Enabled --dry-run for 'sync'.
6
+
1 7
 2009-01-20  Michal Ludvig  <michal@logix.cz>
2 8
 
3 9
 	* s3cmd: Migrated 'sync' remote->local to the new
... ...
@@ -21,10 +21,6 @@ from optparse import OptionParser, Option, OptionValueError, IndentedHelpFormatt
21 21
 from logging import debug, info, warning, error
22 22
 from distutils.spawn import find_executable
23 23
 
24
-error("This s3cmd from SVN is broken!")
25
-error("Use revision 335 or s3cmd-0.9.9-pre4")
26
-sys.exit(1)
27
-
28 24
 def output(message):
29 25
 	sys.stdout.write(message + "\n")
30 26
 
... ...
@@ -175,7 +171,7 @@ def cmd_bucket_delete(args):
175 175
 
176 176
 def fetch_local_list(args, recursive = None):
177 177
 	local_uris = []
178
-	local_list = {}
178
+	local_list = SortedDict()
179 179
 
180 180
 	if type(args) not in (list, tuple):
181 181
 		args = [args]
... ...
@@ -198,7 +194,7 @@ def fetch_local_list(args, recursive = None):
198 198
 
199 199
 def fetch_remote_list(args, require_attribs = False, recursive = None):
200 200
 	remote_uris = []
201
-	remote_list = {}
201
+	remote_list = SortedDict()
202 202
 
203 203
 	if type(args) not in (list, tuple):
204 204
 		args = [args]
... ...
@@ -531,27 +527,27 @@ def cmd_info(args):
531 531
 def _get_filelist_local(local_uri):
532 532
 	info(u"Compiling list of local files...")
533 533
 	if local_uri.isdir():
534
-		local_base = local_uri.basename()
534
+		local_base = deunicodise(local_uri.basename())
535 535
 		local_path = deunicodise(local_uri.path())
536 536
 		filelist = os.walk(local_path)
537 537
 	else:
538 538
 		local_base = ""
539 539
 		local_path = deunicodise(local_uri.dirname())
540 540
 		filelist = [( local_path, [], [deunicodise(local_uri.basename())] )]
541
-	loc_list = {}
541
+	loc_list = SortedDict()
542 542
 	for root, dirs, files in filelist:
543 543
 		rel_root = root.replace(local_path, local_base, 1)
544
-		## TODO: implement explicit exclude
545 544
 		for f in files:
546 545
 			full_name = os.path.join(root, f)
547
-			rel_name = os.path.join(rel_root, f)
548 546
 			if not os.path.isfile(full_name):
549 547
 				continue
550 548
 			if os.path.islink(full_name):
551 549
 				## Synchronize symlinks... one day
552 550
 				## for now skip over
553 551
 				continue
554
-			relative_file = unicodise(rel_name)
552
+			relative_file = unicodise(os.path.join(rel_root, f))
553
+			if relative_file.startswith('./'):
554
+				relative_file = relative_file[2:]
555 555
 			sr = os.stat_result(os.lstat(full_name))
556 556
 			loc_list[relative_file] = {
557 557
 				'full_name_unicode' : unicodise(full_name),
... ...
@@ -589,7 +585,7 @@ def _get_filelist_remote(remote_uri, recursive = True):
589 589
 		rem_base = rem_base[:rem_base.rfind('/')+1]
590 590
 		remote_uri = S3Uri("s3://%s/%s" % (remote_uri.bucket(), rem_base))
591 591
 	rem_base_len = len(rem_base)
592
-	rem_list = {}
592
+	rem_list = SortedDict()
593 593
 	break_now = False
594 594
 	for object in response['list']:
595 595
 		if object['Key'] == rem_base_original and object['Key'][-1] != os.path.sep:
... ...
@@ -616,7 +612,7 @@ def _get_filelist_remote(remote_uri, recursive = True):
616 616
 def _filelist_filter_exclude_include(src_list):
617 617
 	info(u"Applying --exclude/--include")
618 618
 	cfg = Config()
619
-	exclude_list = {}
619
+	exclude_list = SortedDict()
620 620
 	for file in src_list.keys():
621 621
 		debug(u"CHECK: %s" % file)
622 622
 		excluded = False
... ...
@@ -645,7 +641,7 @@ def _filelist_filter_exclude_include(src_list):
645 645
 def _compare_filelists(src_list, dst_list, src_is_local_and_dst_is_remote):
646 646
 	info(u"Verifying attributes...")
647 647
 	cfg = Config()
648
-	exists_list = {}
648
+	exists_list = SortedDict()
649 649
 	if cfg.debug_syncmatch:
650 650
 		logging.root.setLevel(logging.DEBUG)
651 651
 
... ...
@@ -735,23 +731,22 @@ def cmd_sync_remote2local(args):
735 735
 
736 736
 	info(u"Summary: %d remote files to download, %d local files to delete" % (remote_count, local_count))
737 737
 
738
-	for file in local_list:
739
-		if cfg.delete_removed:
740
-			os.unlink(local_list[file]['full_name'])
741
-			output(u"deleted: %s" % local_list[file]['full_name'])
742
-		else:
743
-			info(u"deleted: %s" % local_list[file]['full_name'])
744
-
745
-	if cfg.verbosity == logging.DEBUG:
738
+	if cfg.dry_run:
746 739
 		for key in exclude_list:
747
-			debug(u"excluded: %s" % unicodise(key))
740
+			output(u"excluded: %s" % unicodise(key))
741
+		for key in local_list:
742
+			output(u"delete: %s" % local_list[key]['full_name_unicode'])
748 743
 		for key in remote_list:
749
-			debug(u"download: %s" % unicodise(key))
744
+			output(u"download: %s -> %s" % (remote_list[key]['object_uri_str'], remote_list[key]['local_filename']))
750 745
 
751
-	if cfg.dry_run:
752 746
 		warning(u"Exitting now because of --dry-run")
753 747
 		return
754 748
 
749
+	if cfg.delete_removed:
750
+		for key in local_list:
751
+			os.unlink(local_list[key]['full_name'])
752
+			output(u"deleted: %s" % local_list[key]['full_name_unicode'])
753
+
755 754
 	total_size = 0
756 755
 	total_elapsed = 0.0
757 756
 	timestamp_start = time.time()
... ...
@@ -839,7 +834,7 @@ def cmd_sync_remote2local(args):
839 839
 	else:
840 840
 		info(outstr)
841 841
 
842
-def cmd_sync_local2remote(src, dst):
842
+def cmd_sync_local2remote(args):
843 843
 	def _build_attr_header(src):
844 844
 		import pwd, grp
845 845
 		attrs = {}
... ...
@@ -870,57 +865,74 @@ def cmd_sync_local2remote(src, dst):
870 870
 	s3 = S3(cfg)
871 871
 
872 872
 	if cfg.encrypt:
873
-		error(u"S3cmd 'sync' doesn't support GPG encryption, sorry.")
873
+		error(u"S3cmd 'sync' doesn't yet support GPG encryption, sorry.")
874 874
 		error(u"Either use unconditional 's3cmd put --recursive'")
875 875
 		error(u"or disable encryption with --no-encrypt parameter.")
876 876
 		sys.exit(1)
877 877
 
878
+	destination_base = args[-1]
879
+	local_list = fetch_local_list(args[:-1], recursive = True)
880
+	remote_list = fetch_remote_list(destination_base, recursive = True, require_attribs = True)
878 881
 
879
-	src_uri = S3Uri(src)
880
-	dst_uri = S3Uri(dst)
882
+	local_count = len(local_list)
883
+	remote_count = len(remote_list)
881 884
 
882
-	loc_list = _get_filelist_local(src_uri)
883
-	loc_count = len(loc_list)
884
-	
885
-	rem_list = _get_filelist_remote(dst_uri)
886
-	rem_count = len(rem_list)
885
+	info(u"Found %d local files, %d remote files" % (local_count, remote_count))
887 886
 
888
-	info(u"Found %d local files, %d remote files" % (loc_count, rem_count))
887
+	local_list, exclude_list = _filelist_filter_exclude_include(local_list)
889 888
 
890
-	_compare_filelists(loc_list, rem_list, True)
889
+	local_list, remote_list, existing_list = _compare_filelists(local_list, remote_list, True)
891 890
 
892
-	info(u"Summary: %d local files to upload, %d remote files to delete" % (len(loc_list), len(rem_list)))
891
+	local_count = len(local_list)
892
+	remote_count = len(remote_list)
893 893
 
894
-	for file in rem_list:
895
-		uri = S3Uri("s3://" + dst_uri.bucket()+"/"+rem_list[file]['object_key'])
896
-		if cfg.delete_removed:
897
-			response = s3.object_delete(uri)
898
-			output(u"deleted '%s'" % uri)
899
-		else:
900
-			output(u"not-deleted '%s'" % uri)
894
+	if not destination_base.endswith("/"):
895
+		if local_count > 1:
896
+			raise ParameterError("Destination S3 URI must end with '/' (ie must refer to a directory on the remote side).")
897
+		local_list[local_list.keys()[0]]['remote_uri'] = unicodise(destination_base)
898
+	else:
899
+		for key in local_list:
900
+			local_list[key]['remote_uri'] = unicodise(destination_base + key)
901
+
902
+	info(u"Summary: %d local files to upload, %d remote files to delete" % (local_count, remote_count))
903
+
904
+	if cfg.dry_run:
905
+		for key in exclude_list:
906
+			output(u"excluded: %s" % unicodise(key))
907
+		for key in remote_list:
908
+			output(u"deleted: %s" % remote_list[key]['object_uri_str'])
909
+		for key in local_list:
910
+			output(u"upload: %s -> %s" % (local_list[key]['full_name_unicode'], local_list[key]['remote_uri']))
911
+
912
+		warning(u"Exitting now because of --dry-run")
913
+		return
914
+
915
+	if cfg.delete_removed:
916
+		for key in remote_list:
917
+			uri = S3Uri(remote_list[key]['object_uri_str'])
918
+			s3.object_delete(uri)
919
+			output(u"deleted: '%s'" % uri)
901 920
 
902 921
 	total_size = 0
903
-	total_count = len(loc_list)
904 922
 	total_elapsed = 0.0
905 923
 	timestamp_start = time.time()
906 924
 	seq = 0
907
-	dst_base = dst_uri.uri()
908
-	if not dst_base[-1] == "/": dst_base += "/"
909
-	file_list = loc_list.keys()
925
+	file_list = local_list.keys()
910 926
 	file_list.sort()
911 927
 	for file in file_list:
912 928
 		seq += 1
913
-		src = loc_list[file]
914
-		uri = S3Uri(dst_base + file)
915
-		seq_label = "[%d of %d]" % (seq, total_count)
929
+		item = local_list[file]
930
+		src = item['full_name']
931
+		uri = S3Uri(item['remote_uri'])
932
+		seq_label = "[%d of %d]" % (seq, local_count)
916 933
 		attr_header = None
917 934
 		if cfg.preserve_attrs:
918
-			attr_header = _build_attr_header(src['full_name'])
935
+			attr_header = _build_attr_header(src)
919 936
 			debug(attr_header)
920 937
 		try:
921
-			response = s3.object_put(src['full_name'], uri, attr_header, extra_label = seq_label)
938
+			response = s3.object_put(src, uri, attr_header, extra_label = seq_label)
922 939
 		except S3UploadError, e:
923
-			error(u"%s: upload failed too many times. Skipping that file." % src['full_name_unicode'])
940
+			error(u"%s: upload failed too many times. Skipping that file." % item['full_name_unicode'])
924 941
 			continue
925 942
 		except InvalidFileError, e:
926 943
 			warning(u"File can not be uploaded: %s" % e)
... ...
@@ -928,8 +940,8 @@ def cmd_sync_local2remote(src, dst):
928 928
 		speed_fmt = formatSize(response["speed"], human_readable = True, floating_point = True)
929 929
 		if not cfg.progress_meter:
930 930
 			output(u"File '%s' stored as %s (%d bytes in %0.1f seconds, %0.2f %sB/s) %s" %
931
-				(src, uri, response["size"], response["elapsed"], speed_fmt[0], speed_fmt[1],
932
-				seq_label))
931
+				(item['full_name_unicode'], uri, response["size"], response["elapsed"], 
932
+				speed_fmt[0], speed_fmt[1], seq_label))
933 933
 		total_size += response["size"]
934 934
 
935 935
 	total_elapsed = time.time() - timestamp_start
... ...
@@ -1239,7 +1251,7 @@ def main():
1239 1239
 	optparser.add_option("-c", "--config", dest="config", metavar="FILE", help="Config file name. Defaults to %default")
1240 1240
 	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.")
1241 1241
 
1242
-	#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.")
1242
+	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)")
1243 1243
 
1244 1244
 	optparser.add_option("-e", "--encrypt", dest="encrypt", action="store_true", help="Encrypt files before uploading to S3.")
1245 1245
 	optparser.add_option(      "--no-encrypt", dest="encrypt", action="store_false", help="Don't encrypt files.")