sync.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. # This file is part of Radicale Server - Calendar Server
  2. # Copyright © 2014 Jean-Marc Martins
  3. # Copyright © 2012-2017 Guillaume Ayoub
  4. # Copyright © 2017-2019 Unrud <unrud@outlook.com>
  5. #
  6. # This library is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This library is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  18. import contextlib
  19. import itertools
  20. import os
  21. import pickle
  22. from hashlib import sha256
  23. from radicale.log import logger
  24. class CollectionSyncMixin:
  25. def sync(self, old_token=""):
  26. # The sync token has the form http://radicale.org/ns/sync/TOKEN_NAME
  27. # where TOKEN_NAME is the sha256 hash of all history etags of present
  28. # and past items of the collection.
  29. def check_token_name(token_name):
  30. if len(token_name) != 64:
  31. return False
  32. for c in token_name:
  33. if c not in "0123456789abcdef":
  34. return False
  35. return True
  36. old_token_name = ""
  37. if old_token:
  38. # Extract the token name from the sync token
  39. if not old_token.startswith("http://radicale.org/ns/sync/"):
  40. raise ValueError("Malformed token: %r" % old_token)
  41. old_token_name = old_token[len("http://radicale.org/ns/sync/"):]
  42. if not check_token_name(old_token_name):
  43. raise ValueError("Malformed token: %r" % old_token)
  44. # Get the current state and sync-token of the collection.
  45. state = {}
  46. token_name_hash = sha256()
  47. # Find the history of all existing and deleted items
  48. for href, item in itertools.chain(
  49. ((item.href, item) for item in self.get_all()),
  50. ((href, None) for href in self._get_deleted_history_hrefs())):
  51. history_etag = self._update_history_etag(href, item)
  52. state[href] = history_etag
  53. token_name_hash.update((href + "/" + history_etag).encode())
  54. token_name = token_name_hash.hexdigest()
  55. token = "http://radicale.org/ns/sync/%s" % token_name
  56. if token_name == old_token_name:
  57. # Nothing changed
  58. return token, ()
  59. token_folder = os.path.join(self._filesystem_path,
  60. ".Radicale.cache", "sync-token")
  61. token_path = os.path.join(token_folder, token_name)
  62. old_state = {}
  63. if old_token_name:
  64. # load the old token state
  65. old_token_path = os.path.join(token_folder, old_token_name)
  66. try:
  67. # Race: Another process might have deleted the file.
  68. with open(old_token_path, "rb") as f:
  69. old_state = pickle.load(f)
  70. except (FileNotFoundError, pickle.UnpicklingError,
  71. ValueError) as e:
  72. if isinstance(e, (pickle.UnpicklingError, ValueError)):
  73. logger.warning(
  74. "Failed to load stored sync token %r in %r: %s",
  75. old_token_name, self.path, e, exc_info=True)
  76. # Delete the damaged file
  77. with contextlib.suppress(FileNotFoundError,
  78. PermissionError):
  79. os.remove(old_token_path)
  80. raise ValueError("Token not found: %r" % old_token)
  81. # write the new token state or update the modification time of
  82. # existing token state
  83. if not os.path.exists(token_path):
  84. self._storage._makedirs_synced(token_folder)
  85. try:
  86. # Race: Other processes might have created and locked the file.
  87. with self._atomic_write(token_path, "wb") as f:
  88. pickle.dump(state, f)
  89. except PermissionError:
  90. pass
  91. else:
  92. # clean up old sync tokens and item cache
  93. self._clean_cache(token_folder, os.listdir(token_folder),
  94. max_age=self._storage.configuration.get(
  95. "storage", "max_sync_token_age"))
  96. self._clean_history()
  97. else:
  98. # Try to update the modification time
  99. with contextlib.suppress(FileNotFoundError):
  100. # Race: Another process might have deleted the file.
  101. os.utime(token_path)
  102. changes = []
  103. # Find all new, changed and deleted (that are still in the item cache)
  104. # items
  105. for href, history_etag in state.items():
  106. if history_etag != old_state.get(href):
  107. changes.append(href)
  108. # Find all deleted items that are no longer in the item cache
  109. for href, history_etag in old_state.items():
  110. if href not in state:
  111. changes.append(href)
  112. return token, changes