Repository: Backblaze/B2_Command_Line_Tool

Issue:
CLI Sync errors
Hi all,
after i finished my first sync to Cloud after 3 weeks, i have now errors while syncing new files to the Cloud.

The following lines occurs after a few seconds when i start my CLI Command
```
C:\Program Files\Python36\Scripts>b2.exe sync --excludeRegex DfsrPrivate --threads 10 --keepDays 30 --replaceNewer \\?\D:\DFS\Daten b2://Nuernberg01/Daten

ERROR:b2.console_tool:ConsoleTool unexpected exception
Traceback (most recent call last):
  File "c:\program files\python36\lib\site-packages\b2\console_tool.py", line 992, in run_command
    return command.run(args)
  File "c:\program files\python36\lib\site-packages\b2\console_tool.py", line 781, in run
    dry_run=args.dryRun,
  File "c:\program files\python36\lib\site-packages\logfury\v0_1\trace_call.py", line 84, in wrapper
    return function(*wrapee_args, **wrapee_kwargs)
  File "c:\program files\python36\lib\site-packages\b2\sync\sync.py", line 251, in sync_folders
    source_folder, dest_folder, args, now_millis, reporter
  File "c:\program files\python36\lib\site-packages\b2\sync\sync.py", line 150, in make_folder_sync_actions
    sync_type, source_file, dest_file, source_folder, dest_folder, args, now_millis
  File "c:\program files\python36\lib\site-packages\b2\sync\sync.py", line 106, in make_file_sync_actions
    for action in policy.get_all_actions():
  File "c:\program files\python36\lib\site-packages\b2\sync\policy.py", line 104, in get_all_actions
    for action in self._get_hide_delete_actions():
  File "c:\program files\python36\lib\site-packages\b2\sync\policy.py", line 177, in _get_hide_delete_actions
    self._keepDays, self._now_millis
  File "c:\program files\python36\lib\site-packages\b2\sync\policy.py", line 254, in make_b2_keep_days_actions
    assert prev_age_days is None or prev_age_days <= age_days
AssertionError
Traceback (most recent call last):
  File "C:\Program Files\Python36\Scripts\b2-script.py", line 11, in <module>
    load_entry_point('b2==0.7.0', 'console_scripts', 'b2')()
  File "c:\program files\python36\lib\site-packages\b2\console_tool.py", line 1104, in main
    exit_status = ct.run_command(decoded_argv)
  File "c:\program files\python36\lib\site-packages\b2\console_tool.py", line 992, in run_command
    return command.run(args)
  File "c:\program files\python36\lib\site-packages\b2\console_tool.py", line 781, in run
    dry_run=args.dryRun,
  File "c:\program files\python36\lib\site-packages\logfury\v0_1\trace_call.py", line 84, in wrapper
    return function(*wrapee_args, **wrapee_kwargs)
  File "c:\program files\python36\lib\site-packages\b2\sync\sync.py", line 251, in sync_folders
    source_folder, dest_folder, args, now_millis, reporter
  File "c:\program files\python36\lib\site-packages\b2\sync\sync.py", line 150, in make_folder_sync_actions
    sync_type, source_file, dest_file, source_folder, dest_folder, args, now_millis
  File "c:\program files\python36\lib\site-packages\b2\sync\sync.py", line 106, in make_file_sync_actions
    for action in policy.get_all_actions():
  File "c:\program files\python36\lib\site-packages\b2\sync\policy.py", line 104, in get_all_actions
    for action in self._get_hide_delete_actions():
  File "c:\program files\python36\lib\site-packages\b2\sync\policy.py", line 177, in _get_hide_delete_actions
    self._keepDays, self._now_millis
  File "c:\program files\python36\lib\site-packages\b2\sync\policy.py", line 254, in make_b2_keep_days_actions
    assert prev_age_days is None or prev_age_days <= age_days
AssertionError
```
I have no Idea what to do?



Discussion:

Patch:
diff --git a/b2/sync/policy.py b/b2/sync/policy.py
index b8e7435..5b74f97 100644
--- a/b2/sync/policy.py
+++ b/b2/sync/policy.py
@@ -240,7 +240,6 @@ def make_b2_keep_days_actions(
     only the 25-day old version can be deleted.  The 15 day-old version
     was visible 10 days ago.
     """
-    prev_age_days = None
     deleting = False
     if dest_file is None:
         # B2 does not really store folders, so there is no need to hide
@@ -250,8 +249,17 @@ def make_b2_keep_days_actions(
         # How old is this version?
         age_days = (now_millis - version.mod_time) / ONE_DAY_IN_MS
 
-        # We assume that the versions are ordered by time, newest first.
-        assert prev_age_days is None or prev_age_days <= age_days
+        # Mostly, the versions are ordered by time, newest first,
+        # BUT NOT ALWAYS.  The mod time we have is the src_last_modified_millis
+        # from the file info (if present), or the upload start time
+        # (if not present).  The user-specified src_last_modified_millis
+        # may not be in order.  Because of that, we no longer
+        # assert that age_days is non-decreasing.
+        #
+        # Note that if there is an out-of-order date that is old enough
+        # to trigger deletions, all of the versions uploaded before that
+        # (the ones after it in the list) will be deleted, even if they
+        # aren't over the age threshold.
 
         # Do we need to hide this version?
         if version_index == 0 and source_file is None and version.action == 'upload':
@@ -275,6 +283,3 @@ def make_b2_keep_days_actions(
         # age of this one?
         if keep_days < age_days:
             deleting = True
-
-        # Remember this age for next time around the loop.
-        prev_age_days = age_days


Test patch:
diff --git a/test/test_policy.py b/test/test_policy.py
new file mode 100644
index 0000000..bcc0ec4
--- /dev/null
+++ b/test/test_policy.py
@@ -0,0 +1,77 @@
+######################################################################
+#
+# File: test_policy
+#
+# Copyright 2017, Backblaze Inc. All Rights Reserved.
+#
+# License https://www.backblaze.com/using_b2_code.html
+#
+######################################################################
+
+from b2.sync.file import File, FileVersion
+from b2.sync.folder import B2Folder
+from b2.sync.policy import make_b2_keep_days_actions
+from .test_base import TestBase
+
+try:
+    from unittest.mock import MagicMock
+except ImportError:
+    from mock import MagicMock
+
+
+class TestMakeB2KeepDaysActions(TestBase):
+    def setUp(self):
+        self.keep_days = 7
+        self.today = 100 * 86400
+        self.one_day_millis = 86400 * 1000
+
+    def test_no_versions(self):
+        self.check_one_answer(True, [], [])
+
+    def test_new_version_no_action(self):
+        self.check_one_answer(True, [(1, -5, 'upload')], [])
+
+    def test_no_source_one_old_version_hides(self):
+        # An upload that is old gets deleted if there is no source file.
+        self.check_one_answer(False, [(1, -10, 'upload')], ['b2_hide(folder/a)'])
+
+    def test_old_hide_causes_delete(self):
+        # A hide marker that is old gets deleted, as do the things after it.
+        self.check_one_answer(
+            True, [(1, -5, 'upload'), (2, -10, 'hide'), (3, -20, 'upload')],
+            ['b2_delete(folder/a, 2, (hide marker))', 'b2_delete(folder/a, 3, (old version))']
+        )
+
+    def test_old_upload_causes_delete(self):
+        # An upload that is old stays if there is a source file, but things
+        # behind it go away.
+        self.check_one_answer(
+            True, [(1, -5, 'upload'), (2, -10, 'upload'), (3, -20, 'upload')],
+            ['b2_delete(folder/a, 3, (old version))']
+        )
+
+    def test_out_of_order_dates(self):
+        # The one at date -3 will get deleted because the one before it is old.
+        self.check_one_answer(
+            True, [(1, -5, 'upload'), (2, -10, 'upload'), (3, -3, 'upload')],
+            ['b2_delete(folder/a, 3, (old version))']
+        )
+
+    def check_one_answer(self, has_source, id_relative_date_action_list, expected_actions):
+        source_file = File('a', []) if has_source else None
+        dest_file_versions = [
+            FileVersion(id_, 'a', self.today + relative_date * self.one_day_millis, action, 100)
+            for (id_, relative_date, action) in id_relative_date_action_list
+        ]
+        dest_file = File('a', dest_file_versions)
+        bucket = MagicMock()
+        api = MagicMock()
+        api.get_bucket_by_name.return_value = bucket
+        dest_folder = B2Folder('bucket-1', 'folder', api)
+        actual_actions = list(
+            make_b2_keep_days_actions(
+                source_file, dest_file, dest_folder, dest_folder, self.keep_days, self.today
+            )
+        )
+        actual_action_strs = [str(a) for a in actual_actions]
+        self.assertEqual(expected_actions, actual_action_strs)