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.
... | ... |
@@ -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 |