sync.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  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 itertools
  19. import os
  20. import pickle
  21. from hashlib import md5
  22. from radicale.log import logger
  23. class CollectionSyncMixin:
  24. def sync(self, old_token=None):
  25. # The sync token has the form http://radicale.org/ns/sync/TOKEN_NAME
  26. # where TOKEN_NAME is the md5 hash of all history etags of present and
  27. # past items of the collection.
  28. def check_token_name(token_name):
  29. if len(token_name) != 32:
  30. return False
  31. for c in token_name:
  32. if c not in "0123456789abcdef":
  33. return False
  34. return True
  35. old_token_name = None
  36. if old_token:
  37. # Extract the token name from the sync token
  38. if not old_token.startswith("http://radicale.org/ns/sync/"):
  39. raise ValueError("Malformed token: %r" % old_token)
  40. old_token_name = old_token[len("http://radicale.org/ns/sync/"):]
  41. if not check_token_name(old_token_name):
  42. raise ValueError("Malformed token: %r" % old_token)
  43. # Get the current state and sync-token of the collection.
  44. state = {}
  45. token_name_hash = md5()
  46. # Find the history of all existing and deleted items
  47. for href, item in itertools.chain(
  48. ((item.href, item) for item in self.get_all()),
  49. ((href, None) for href in self._get_deleted_history_hrefs())):
  50. history_etag = self._update_history_etag(href, item)
  51. state[href] = history_etag
  52. token_name_hash.update((href + "/" + history_etag).encode("utf-8"))
  53. token_name = token_name_hash.hexdigest()
  54. token = "http://radicale.org/ns/sync/%s" % token_name
  55. if token_name == old_token_name:
  56. # Nothing changed
  57. return token, ()
  58. token_folder = os.path.join(self._filesystem_path,
  59. ".Radicale.cache", "sync-token")
  60. token_path = os.path.join(token_folder, token_name)
  61. old_state = {}
  62. if old_token_name:
  63. # load the old token state
  64. old_token_path = os.path.join(token_folder, old_token_name)
  65. try:
  66. # Race: Another process might have deleted the file.
  67. with open(old_token_path, "rb") as f:
  68. old_state = pickle.load(f)
  69. except (FileNotFoundError, pickle.UnpicklingError,
  70. ValueError) as e:
  71. if isinstance(e, (pickle.UnpicklingError, ValueError)):
  72. logger.warning(
  73. "Failed to load stored sync token %r in %r: %s",
  74. old_token_name, self.path, e, exc_info=True)
  75. # Delete the damaged file
  76. try:
  77. os.remove(old_token_path)
  78. except (FileNotFoundError, PermissionError):
  79. pass
  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._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.configuration.get(
  95. "storage", "max_sync_token_age"))
  96. self._clean_history()
  97. else:
  98. # Try to update the modification time
  99. try:
  100. # Race: Another process might have deleted the file.
  101. os.utime(token_path)
  102. except FileNotFoundError:
  103. pass
  104. changes = []
  105. # Find all new, changed and deleted (that are still in the item cache)
  106. # items
  107. for href, history_etag in state.items():
  108. if history_etag != old_state.get(href):
  109. changes.append(href)
  110. # Find all deleted items that are no longer in the item cache
  111. for href, history_etag in old_state.items():
  112. if href not in state:
  113. changes.append(href)
  114. return token, changes