Browse code

add --max-delete for del and sync commands, fixes bug #155

Fixes https://github.com/s3tools/s3cmd/issues/155

Similar to rsync --max-delete, allow the user to specify a maximum
number of objects to delete for the [del] and [sync] commands. If
that number would be exceeded, output a warning message and do not
delete anything. This prevents a simple invocation error
(e.g. launching from the wrong directory) from erasing all the data in
a bucket accidentally.

By default, this is disabled. A user must explicitly specify
--max-delete=NUM for this protection to be available, just as in
rsync.

Matt Domsch authored on 2013/04/23 14:06:00
Showing 4 changed files
... ...
@@ -61,6 +61,7 @@ class Config(object):
61 61
     delete_removed = False
62 62
     delete_after = False
63 63
     delete_after_fetch = False
64
+    max_delete = -1
64 65
     _doc['delete_removed'] = "[sync] Remove remote S3 objects when local file has been deleted"
65 66
     delay_updates = False
66 67
     gpg_passphrase = ""
... ...
@@ -513,6 +513,10 @@ test_s3cmd("Simple delete", ['del', '%s/xyz/etc2/Logo.PNG' % pbucket(1)],
513 513
     must_find = [ "File %s/xyz/etc2/Logo.PNG deleted" % pbucket(1) ])
514 514
 
515 515
 
516
+## ====== Recursive delete maximum exceeed
517
+test_s3cmd("Recursive delete maximum exceeded", ['del', '--recursive', '--max-delete=1', '--exclude', 'Atomic*', '%s/xyz/etc' % pbucket(1)],
518
+    must_not_find = [ "File %s/xyz/etc/TypeRa.ttf deleted" % pbucket(1) ])
519
+
516 520
 ## ====== Recursive delete
517 521
 test_s3cmd("Recursive delete", ['del', '--recursive', '--exclude', 'Atomic*', '%s/xyz/etc' % pbucket(1)],
518 522
     must_find = [ "File %s/xyz/etc/TypeRa.ttf deleted" % pbucket(1) ],
... ...
@@ -505,6 +505,9 @@ def subcmd_object_del_uri(uri_str, recursive = None):
505 505
     remote_count = len(remote_list)
506 506
 
507 507
     info(u"Summary: %d remote files to delete" % remote_count)
508
+    if cfg.max_delete > 0 and remote_count > cfg.max_delete:
509
+        warning(u"delete: maximum requested number of deletes would be exceeded, none performed.")
510
+        return
508 511
 
509 512
     if cfg.dry_run:
510 513
         for key in exclude_list:
... ...
@@ -623,6 +626,9 @@ def cmd_info(args):
623 623
 
624 624
 def cmd_sync_remote2remote(args):
625 625
     def _do_deletes(s3, dst_list):
626
+        if cfg.max_delete > 0 and len(dst_list) > cfg.max_delete:
627
+            warning(u"delete: maximum requested number of deletes would be exceeded, none performed.")
628
+            return
626 629
         # Delete items in destination that are not in source
627 630
         if cfg.dry_run:
628 631
             for key in dst_list:
... ...
@@ -719,6 +725,9 @@ def cmd_sync_remote2remote(args):
719 719
 
720 720
 def cmd_sync_remote2local(args):
721 721
     def _do_deletes(local_list):
722
+        if cfg.max_delete > 0 and len(local_list) > cfg.max_delete:
723
+            warning(u"delete: maximum requested number of deletes would be exceeded, none performed.")
724
+            return
722 725
         for key in local_list:
723 726
             os.unlink(local_list[key]['full_name'])
724 727
             output(u"deleted: %s" % local_list[key]['full_name_unicode'])
... ...
@@ -982,6 +991,9 @@ def cmd_sync_local2remote(args):
982 982
         return { 'x-amz-meta-s3cmd-attrs' : result[:-1] }
983 983
 
984 984
     def _do_deletes(s3, remote_list):
985
+        if cfg.max_delete > 0 and len(remote_list) > cfg.max_delete:
986
+            warning(u"delete: maximum requested number of deletes would be exceeded, none performed.")
987
+            return
985 988
         for key in remote_list:
986 989
             uri = S3Uri(remote_list[key]['object_uri_str'])
987 990
             s3.object_delete(uri)
... ...
@@ -1772,6 +1784,7 @@ def main():
1772 1772
     optparser.add_option(      "--no-delete-removed", dest="delete_removed", action="store_false", help="Don't delete remote objects.")
1773 1773
     optparser.add_option(      "--delete-after", dest="delete_after", action="store_true", help="Perform deletes after new uploads [sync]")
1774 1774
     optparser.add_option(      "--delay-updates", dest="delay_updates", action="store_true", help="Put all updated files into place at end [sync]")
1775
+    optparser.add_option(      "--max-delete", dest="max_delete", action="store", help="Do not delete more than NUM files. [del] and [sync]", metavar="NUM")
1775 1776
     optparser.add_option(      "--add-destination", dest="additional_destinations", action="append", help="Additional destination for parallel uploads, in addition to last arg.  May be repeated.")
1776 1777
     optparser.add_option(      "--delete-after-fetch", dest="delete_after_fetch", action="store_true", help="Delete remote objects after fetching to local file (only for [get] and [sync] commands).")
1777 1778
     optparser.add_option("-p", "--preserve", dest="preserve_attrs", action="store_true", help="Preserve filesystem attributes (mode, ownership, timestamps). Default for [sync] command.")
... ...
@@ -195,6 +195,9 @@ Additional destination for parallel uploads, in addition to last arg.  May be re
195 195
 \fB\-\-delete\-after\-fetch\fR
196 196
 Delete remote objects after fetching to local file (only for [get] and [sync] commands).
197 197
 .TP
198
+\fB\-\-max\-delete\fR=NUM 
199
+Do not delete more than NUM files.  If that limit would be exceeded, a warning is output and none are deleted. [del] and [sync]
200
+.TP
198 201
 \fB\-p\fR, \fB\-\-preserve\fR
199 202
 Preserve filesystem attributes (mode, ownership, timestamps). Default for [sync] command.
200 203
 .TP