Repository: Backblaze/b2-sdk-python

Issue:
Custom sync policy?
Is there a way to create a custom sync policy? I want to keep some files in the bucket unchanged even though the local counterpart has these files with a newer timestamp. I've tried with exclude regexes but that will treat the excluded files as missing and deletes them in the bucket.

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e75521aa..6c6565f8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 * Add pypy-3.8 to test matrix
 * Add support for unverified checksum upload mode
 * Add dedicated exception for unverified email
+* Add a parameter to customize `sync_policy_manager`
 
 ### Fixed
 * Fix downloading files with unverified checksum
diff --git a/b2sdk/sync/sync.py b/b2sdk/sync/sync.py
index eb43eec5..8aa9dc3f 100644
--- a/b2sdk/sync/sync.py
+++ b/b2sdk/sync/sync.py
@@ -135,12 +135,13 @@ class Synchronizer:
         compare_version_mode=CompareVersionMode.MODTIME,
         compare_threshold=None,
         keep_days=None,
+        sync_policy_manager: SyncPolicyManager = POLICY_MANAGER,
     ):
         """
         Initialize synchronizer class and validate arguments
 
         :param int max_workers: max number of workers
-        :param policies_manager: policies manager object
+        :param policies_manager: object which decides which files to process
         :param bool dry_run: test mode, does not actually transfer/delete when enabled
         :param bool allow_empty_source: if True, do not check whether source folder is empty
         :param b2sdk.v2.NewerFileSyncMode newer_file_mode: setting which determines handling for destination files newer than on the source
@@ -148,6 +149,7 @@ class Synchronizer:
         :param b2sdk.v2.CompareVersionMode compare_version_mode: how to compare the source and destination files to find new ones
         :param int compare_threshold: should be greater than 0, default is 0
         :param int keep_days: if keep_days_or_delete is `b2sdk.v2.KeepOrDeleteMode.KEEP_BEFORE_DELETE`, then this should be greater than 0
+        :param SyncPolicyManager sync_policy_manager: object which decides what to do with each file (upload, download, delete, copy, hide etc)
         """
         self.newer_file_mode = newer_file_mode
         self.keep_days_or_delete = keep_days_or_delete
@@ -156,7 +158,8 @@ class Synchronizer:
         self.compare_threshold = compare_threshold or 0
         self.dry_run = dry_run
         self.allow_empty_source = allow_empty_source
-        self.policies_manager = policies_manager
+        self.policies_manager = policies_manager  # actually it should be called scan_policies_manager
+        self.sync_policy_manager = sync_policy_manager
         self.max_workers = max_workers
         self._validate()
 
@@ -280,7 +283,7 @@ class Synchronizer:
         :param b2sdk.v2.AbstractFolder dest_folder: destination folder object
         :param int now_millis: current time in milliseconds
         :param b2sdk.v2.SyncReport reporter: reporter object
-        :param b2sdk.v2.SyncPolicyManager policies_manager: policies manager object
+        :param b2sdk.v2.ScanPolicyManager policies_manager: object which decides which files to process
         :param b2sdk.v2.AbstractSyncEncryptionSettingsProvider encryption_settings_provider: encryption setting provider
         """
         if self.keep_days_or_delete == KeepOrDeleteMode.KEEP_BEFORE_DELETE and dest_folder.folder_type(
@@ -358,7 +361,7 @@ class Synchronizer:
         delete = self.keep_days_or_delete == KeepOrDeleteMode.DELETE
         keep_days = self.keep_days
 
-        policy = POLICY_MANAGER.get_policy(
+        policy = self.sync_policy_manager.get_policy(
             sync_type,
             source_path,
             source_folder,
diff --git a/b2sdk/v1/sync/sync.py b/b2sdk/v1/sync/sync.py
index d7d306c7..75833605 100644
--- a/b2sdk/v1/sync/sync.py
+++ b/b2sdk/v1/sync/sync.py
@@ -37,10 +37,19 @@ class Synchronizer(v2.Synchronizer):
         compare_version_mode=v2.CompareVersionMode.MODTIME,
         compare_threshold=None,
         keep_days=None,
+        sync_policy_manager: v2.SyncPolicyManager = v2.POLICY_MANAGER,
     ):
         super().__init__(
-            max_workers, scan_wrap_if_necessary(policies_manager), dry_run, allow_empty_source,
-            newer_file_mode, keep_days_or_delete, compare_version_mode, compare_threshold, keep_days
+            max_workers,
+            scan_wrap_if_necessary(policies_manager),
+            dry_run,
+            allow_empty_source,
+            newer_file_mode,
+            keep_days_or_delete,
+            compare_version_mode,
+            compare_threshold,
+            keep_days,
+            sync_policy_manager,
         )
 
     def make_folder_sync_actions(


Test patch:
diff --git a/test/unit/sync/fixtures.py b/test/unit/sync/fixtures.py
index 3ff871f0..a4861804 100644
--- a/test/unit/sync/fixtures.py
+++ b/test/unit/sync/fixtures.py
@@ -15,7 +15,7 @@ import pytest
 import apiver_deps
 from apiver_deps import AbstractFolder, B2Folder, LocalFolder, B2SyncPath, LocalSyncPath
 from apiver_deps import CompareVersionMode, NewerFileSyncMode, KeepOrDeleteMode
-from apiver_deps import DEFAULT_SCAN_MANAGER, Synchronizer
+from apiver_deps import DEFAULT_SCAN_MANAGER, POLICY_MANAGER, Synchronizer
 
 if apiver_deps.V <= 1:
     from apiver_deps import FileVersionInfo as VFileVersion
@@ -112,6 +112,7 @@ def synchronizer_factory():
         keep_days=None,
         compare_version_mode=CompareVersionMode.MODTIME,
         compare_threshold=None,
+        sync_policy_manager=POLICY_MANAGER,
     ):
         return Synchronizer(
             1,
@@ -123,6 +124,7 @@ def synchronizer_factory():
             keep_days=keep_days,
             compare_version_mode=compare_version_mode,
             compare_threshold=compare_threshold,
+            sync_policy_manager=sync_policy_manager,
         )
 
     return get_synchronizer
diff --git a/test/unit/sync/test_sync.py b/test/unit/sync/test_sync.py
index 6af85667..887d6152 100644
--- a/test/unit/sync/test_sync.py
+++ b/test/unit/sync/test_sync.py
@@ -12,7 +12,7 @@ from unittest import mock
 from enum import Enum
 from functools import partial
 
-from apiver_deps import B2DownloadAction, B2UploadAction, B2CopyAction, AbstractSyncEncryptionSettingsProvider, UploadSourceLocalFile
+from apiver_deps import UpPolicy, B2DownloadAction, B2UploadAction, B2CopyAction, AbstractSyncEncryptionSettingsProvider, UploadSourceLocalFile, SyncPolicyManager
 from apiver_deps_exception import DestFileNewer, InvalidArgument
 from b2sdk.utils import TempDir
 
@@ -849,6 +849,25 @@ class TestSynchronizer:
             )
         ]
 
+    def test_custom_sync_manager_policy(self, synchronizer_factory):
+        class MySyncPolicyManager(SyncPolicyManager):
+            def get_policy_class(self, sync_type, delete, keep_days):
+                return UpPolicy
+
+        synchronizer = synchronizer_factory(
+            compare_version_mode=CompareVersionMode.SIZE,
+            keep_days_or_delete=KeepOrDeleteMode.DELETE,
+            sync_policy_manager=MySyncPolicyManager(),
+        )
+        src = self.folder_factory('local', ('a.txt', [200], 11))
+        dst = self.folder_factory('b2', ('a.txt', [100], 10))
+        # normally_expected = [
+        #     'b2_upload(/dir/a.txt, folder/a.txt, 200)',
+        #     'b2_delete(folder/a.txt, id_a_100, (old version))'
+        # ]
+        expected = ['b2_upload(/dir/a.txt, folder/a.txt, 200)']
+        self.assert_folder_sync_actions(synchronizer, src, dst, expected)
+
 
 class TstEncryptionSettingsProvider(AbstractSyncEncryptionSettingsProvider):
     def __init__(self, source_encryption_setting, destination_encryption_setting):