swift 2.23.3__py3-none-any.whl → 2.35.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. swift/__init__.py +29 -50
  2. swift/account/auditor.py +21 -118
  3. swift/account/backend.py +33 -28
  4. swift/account/reaper.py +37 -28
  5. swift/account/replicator.py +22 -0
  6. swift/account/server.py +60 -26
  7. swift/account/utils.py +28 -11
  8. swift-2.23.3.data/scripts/swift-account-audit → swift/cli/account_audit.py +23 -13
  9. swift-2.23.3.data/scripts/swift-config → swift/cli/config.py +2 -2
  10. swift/cli/container_deleter.py +5 -11
  11. swift-2.23.3.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +8 -7
  12. swift/cli/dispersion_report.py +10 -9
  13. swift-2.23.3.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +63 -21
  14. swift/cli/form_signature.py +3 -7
  15. swift-2.23.3.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +8 -2
  16. swift/cli/info.py +154 -14
  17. swift/cli/manage_shard_ranges.py +705 -37
  18. swift-2.23.3.data/scripts/swift-oldies → swift/cli/oldies.py +25 -14
  19. swift-2.23.3.data/scripts/swift-orphans → swift/cli/orphans.py +7 -3
  20. swift/cli/recon.py +196 -67
  21. swift-2.23.3.data/scripts/swift-recon-cron → swift/cli/recon_cron.py +17 -20
  22. swift-2.23.3.data/scripts/swift-reconciler-enqueue → swift/cli/reconciler_enqueue.py +2 -3
  23. swift/cli/relinker.py +807 -126
  24. swift/cli/reload.py +135 -0
  25. swift/cli/ringbuilder.py +217 -20
  26. swift/cli/ringcomposer.py +0 -1
  27. swift/cli/shard-info.py +4 -3
  28. swift/common/base_storage_server.py +9 -20
  29. swift/common/bufferedhttp.py +48 -74
  30. swift/common/constraints.py +20 -15
  31. swift/common/container_sync_realms.py +9 -11
  32. swift/common/daemon.py +25 -8
  33. swift/common/db.py +195 -128
  34. swift/common/db_auditor.py +168 -0
  35. swift/common/db_replicator.py +95 -55
  36. swift/common/digest.py +141 -0
  37. swift/common/direct_client.py +144 -33
  38. swift/common/error_limiter.py +93 -0
  39. swift/common/exceptions.py +25 -1
  40. swift/common/header_key_dict.py +2 -9
  41. swift/common/http_protocol.py +373 -0
  42. swift/common/internal_client.py +129 -59
  43. swift/common/linkat.py +3 -4
  44. swift/common/manager.py +284 -67
  45. swift/common/memcached.py +390 -145
  46. swift/common/middleware/__init__.py +4 -0
  47. swift/common/middleware/account_quotas.py +211 -46
  48. swift/common/middleware/acl.py +3 -8
  49. swift/common/middleware/backend_ratelimit.py +230 -0
  50. swift/common/middleware/bulk.py +22 -34
  51. swift/common/middleware/catch_errors.py +1 -3
  52. swift/common/middleware/cname_lookup.py +6 -11
  53. swift/common/middleware/container_quotas.py +1 -1
  54. swift/common/middleware/container_sync.py +39 -17
  55. swift/common/middleware/copy.py +12 -0
  56. swift/common/middleware/crossdomain.py +22 -9
  57. swift/common/middleware/crypto/__init__.py +2 -1
  58. swift/common/middleware/crypto/crypto_utils.py +11 -15
  59. swift/common/middleware/crypto/decrypter.py +28 -11
  60. swift/common/middleware/crypto/encrypter.py +12 -17
  61. swift/common/middleware/crypto/keymaster.py +8 -15
  62. swift/common/middleware/crypto/kms_keymaster.py +2 -1
  63. swift/common/middleware/dlo.py +15 -11
  64. swift/common/middleware/domain_remap.py +5 -4
  65. swift/common/middleware/etag_quoter.py +128 -0
  66. swift/common/middleware/formpost.py +73 -70
  67. swift/common/middleware/gatekeeper.py +8 -1
  68. swift/common/middleware/keystoneauth.py +33 -3
  69. swift/common/middleware/list_endpoints.py +4 -4
  70. swift/common/middleware/listing_formats.py +85 -49
  71. swift/common/middleware/memcache.py +4 -95
  72. swift/common/middleware/name_check.py +3 -2
  73. swift/common/middleware/proxy_logging.py +160 -92
  74. swift/common/middleware/ratelimit.py +17 -10
  75. swift/common/middleware/read_only.py +6 -4
  76. swift/common/middleware/recon.py +59 -22
  77. swift/common/middleware/s3api/acl_handlers.py +25 -3
  78. swift/common/middleware/s3api/acl_utils.py +6 -1
  79. swift/common/middleware/s3api/controllers/__init__.py +6 -0
  80. swift/common/middleware/s3api/controllers/acl.py +3 -2
  81. swift/common/middleware/s3api/controllers/bucket.py +242 -137
  82. swift/common/middleware/s3api/controllers/logging.py +2 -2
  83. swift/common/middleware/s3api/controllers/multi_delete.py +43 -20
  84. swift/common/middleware/s3api/controllers/multi_upload.py +219 -133
  85. swift/common/middleware/s3api/controllers/obj.py +112 -8
  86. swift/common/middleware/s3api/controllers/object_lock.py +44 -0
  87. swift/common/middleware/s3api/controllers/s3_acl.py +2 -2
  88. swift/common/middleware/s3api/controllers/tagging.py +57 -0
  89. swift/common/middleware/s3api/controllers/versioning.py +36 -7
  90. swift/common/middleware/s3api/etree.py +22 -9
  91. swift/common/middleware/s3api/exception.py +0 -4
  92. swift/common/middleware/s3api/s3api.py +113 -41
  93. swift/common/middleware/s3api/s3request.py +384 -218
  94. swift/common/middleware/s3api/s3response.py +126 -23
  95. swift/common/middleware/s3api/s3token.py +16 -17
  96. swift/common/middleware/s3api/schema/delete.rng +1 -1
  97. swift/common/middleware/s3api/subresource.py +7 -10
  98. swift/common/middleware/s3api/utils.py +27 -10
  99. swift/common/middleware/slo.py +665 -358
  100. swift/common/middleware/staticweb.py +64 -37
  101. swift/common/middleware/symlink.py +51 -18
  102. swift/common/middleware/tempauth.py +76 -58
  103. swift/common/middleware/tempurl.py +191 -173
  104. swift/common/middleware/versioned_writes/__init__.py +51 -0
  105. swift/common/middleware/{versioned_writes.py → versioned_writes/legacy.py} +27 -26
  106. swift/common/middleware/versioned_writes/object_versioning.py +1482 -0
  107. swift/common/middleware/x_profile/exceptions.py +1 -4
  108. swift/common/middleware/x_profile/html_viewer.py +18 -19
  109. swift/common/middleware/x_profile/profile_model.py +1 -2
  110. swift/common/middleware/xprofile.py +10 -10
  111. swift-2.23.3.data/scripts/swift-container-server → swift/common/recon.py +13 -8
  112. swift/common/registry.py +147 -0
  113. swift/common/request_helpers.py +324 -57
  114. swift/common/ring/builder.py +67 -25
  115. swift/common/ring/composite_builder.py +1 -1
  116. swift/common/ring/ring.py +177 -51
  117. swift/common/ring/utils.py +1 -1
  118. swift/common/splice.py +10 -6
  119. swift/common/statsd_client.py +205 -0
  120. swift/common/storage_policy.py +49 -44
  121. swift/common/swob.py +86 -102
  122. swift/common/{utils.py → utils/__init__.py} +2163 -2772
  123. swift/common/utils/base.py +131 -0
  124. swift/common/utils/config.py +433 -0
  125. swift/common/utils/ipaddrs.py +256 -0
  126. swift/common/utils/libc.py +345 -0
  127. swift/common/utils/logs.py +859 -0
  128. swift/common/utils/timestamp.py +412 -0
  129. swift/common/wsgi.py +553 -535
  130. swift/container/auditor.py +14 -100
  131. swift/container/backend.py +490 -231
  132. swift/container/reconciler.py +126 -37
  133. swift/container/replicator.py +96 -22
  134. swift/container/server.py +358 -165
  135. swift/container/sharder.py +1540 -684
  136. swift/container/sync.py +94 -88
  137. swift/container/updater.py +53 -32
  138. swift/obj/auditor.py +153 -35
  139. swift/obj/diskfile.py +466 -217
  140. swift/obj/expirer.py +406 -124
  141. swift/obj/mem_diskfile.py +7 -4
  142. swift/obj/mem_server.py +1 -0
  143. swift/obj/reconstructor.py +523 -262
  144. swift/obj/replicator.py +249 -188
  145. swift/obj/server.py +207 -122
  146. swift/obj/ssync_receiver.py +145 -85
  147. swift/obj/ssync_sender.py +113 -54
  148. swift/obj/updater.py +652 -139
  149. swift/obj/watchers/__init__.py +0 -0
  150. swift/obj/watchers/dark_data.py +213 -0
  151. swift/proxy/controllers/account.py +11 -11
  152. swift/proxy/controllers/base.py +848 -604
  153. swift/proxy/controllers/container.py +433 -92
  154. swift/proxy/controllers/info.py +3 -2
  155. swift/proxy/controllers/obj.py +1000 -489
  156. swift/proxy/server.py +185 -112
  157. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/AUTHORS +58 -11
  158. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/METADATA +51 -56
  159. swift-2.35.0.dist-info/RECORD +201 -0
  160. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/WHEEL +1 -1
  161. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/entry_points.txt +43 -0
  162. swift-2.35.0.dist-info/pbr.json +1 -0
  163. swift/locale/de/LC_MESSAGES/swift.po +0 -1216
  164. swift/locale/en_GB/LC_MESSAGES/swift.po +0 -1207
  165. swift/locale/es/LC_MESSAGES/swift.po +0 -1085
  166. swift/locale/fr/LC_MESSAGES/swift.po +0 -909
  167. swift/locale/it/LC_MESSAGES/swift.po +0 -894
  168. swift/locale/ja/LC_MESSAGES/swift.po +0 -965
  169. swift/locale/ko_KR/LC_MESSAGES/swift.po +0 -964
  170. swift/locale/pt_BR/LC_MESSAGES/swift.po +0 -881
  171. swift/locale/ru/LC_MESSAGES/swift.po +0 -891
  172. swift/locale/tr_TR/LC_MESSAGES/swift.po +0 -832
  173. swift/locale/zh_CN/LC_MESSAGES/swift.po +0 -833
  174. swift/locale/zh_TW/LC_MESSAGES/swift.po +0 -838
  175. swift-2.23.3.data/scripts/swift-account-auditor +0 -23
  176. swift-2.23.3.data/scripts/swift-account-info +0 -51
  177. swift-2.23.3.data/scripts/swift-account-reaper +0 -23
  178. swift-2.23.3.data/scripts/swift-account-replicator +0 -34
  179. swift-2.23.3.data/scripts/swift-account-server +0 -23
  180. swift-2.23.3.data/scripts/swift-container-auditor +0 -23
  181. swift-2.23.3.data/scripts/swift-container-info +0 -55
  182. swift-2.23.3.data/scripts/swift-container-reconciler +0 -21
  183. swift-2.23.3.data/scripts/swift-container-replicator +0 -34
  184. swift-2.23.3.data/scripts/swift-container-sharder +0 -37
  185. swift-2.23.3.data/scripts/swift-container-sync +0 -23
  186. swift-2.23.3.data/scripts/swift-container-updater +0 -23
  187. swift-2.23.3.data/scripts/swift-dispersion-report +0 -24
  188. swift-2.23.3.data/scripts/swift-form-signature +0 -20
  189. swift-2.23.3.data/scripts/swift-init +0 -119
  190. swift-2.23.3.data/scripts/swift-object-auditor +0 -29
  191. swift-2.23.3.data/scripts/swift-object-expirer +0 -33
  192. swift-2.23.3.data/scripts/swift-object-info +0 -60
  193. swift-2.23.3.data/scripts/swift-object-reconstructor +0 -33
  194. swift-2.23.3.data/scripts/swift-object-relinker +0 -41
  195. swift-2.23.3.data/scripts/swift-object-replicator +0 -37
  196. swift-2.23.3.data/scripts/swift-object-server +0 -27
  197. swift-2.23.3.data/scripts/swift-object-updater +0 -23
  198. swift-2.23.3.data/scripts/swift-proxy-server +0 -23
  199. swift-2.23.3.data/scripts/swift-recon +0 -24
  200. swift-2.23.3.data/scripts/swift-ring-builder +0 -24
  201. swift-2.23.3.data/scripts/swift-ring-builder-analyzer +0 -22
  202. swift-2.23.3.data/scripts/swift-ring-composer +0 -22
  203. swift-2.23.3.dist-info/RECORD +0 -220
  204. swift-2.23.3.dist-info/pbr.json +0 -1
  205. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/LICENSE +0 -0
  206. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/top_level.txt +0 -0
swift/common/db.py CHANGED
@@ -17,17 +17,13 @@
17
17
 
18
18
  from contextlib import contextmanager, closing
19
19
  import base64
20
- import hashlib
21
20
  import json
22
21
  import logging
23
22
  import os
24
23
  from uuid import uuid4
25
- import sys
26
24
  import time
27
25
  import errno
28
- import six
29
- import six.moves.cPickle as pickle
30
- from swift import gettext_ as _
26
+ import pickle # nosec: B403
31
27
  from tempfile import mkstemp
32
28
 
33
29
  from eventlet import sleep, Timeout
@@ -36,13 +32,15 @@ import sqlite3
36
32
  from swift.common.constraints import MAX_META_COUNT, MAX_META_OVERALL_SIZE, \
37
33
  check_utf8
38
34
  from swift.common.utils import Timestamp, renamer, \
39
- mkdirs, lock_parent_directory, fallocate
35
+ mkdirs, lock_parent_directory, fallocate, md5
40
36
  from swift.common.exceptions import LockTimeout
41
37
  from swift.common.swob import HTTPBadRequest
42
38
 
43
39
 
44
40
  #: Whether calls will be made to preallocate disk space for database files.
45
41
  DB_PREALLOCATION = False
42
+ #: Whether calls will be made to log queries (py3 only)
43
+ QUERY_LOGGING = False
46
44
  #: Timeout for trying to connect to a DB
47
45
  BROKER_TIMEOUT = 25
48
46
  #: Pickle protocol to use
@@ -55,28 +53,14 @@ SQLITE_ARG_LIMIT = 999
55
53
  RECLAIM_PAGE_SIZE = 10000
56
54
 
57
55
 
58
- def utf8encode(*args):
59
- return [(s.encode('utf8') if isinstance(s, six.text_type) else s)
60
- for s in args]
61
-
62
-
63
56
  def native_str_keys_and_values(metadata):
64
- if six.PY2:
65
- uni_keys = [k for k in metadata if isinstance(k, six.text_type)]
66
- for k in uni_keys:
67
- sv = metadata[k]
68
- del metadata[k]
69
- metadata[k.encode('utf-8')] = [
70
- x.encode('utf-8') if isinstance(x, six.text_type) else x
71
- for x in sv]
72
- else:
73
- bin_keys = [k for k in metadata if isinstance(k, six.binary_type)]
74
- for k in bin_keys:
75
- sv = metadata[k]
76
- del metadata[k]
77
- metadata[k.decode('utf-8')] = [
78
- x.decode('utf-8') if isinstance(x, six.binary_type) else x
79
- for x in sv]
57
+ bin_keys = [k for k in metadata if isinstance(k, bytes)]
58
+ for k in bin_keys:
59
+ sv = metadata[k]
60
+ del metadata[k]
61
+ metadata[k.decode('utf-8')] = [
62
+ x.decode('utf-8') if isinstance(x, bytes) else x
63
+ for x in sv]
80
64
 
81
65
 
82
66
  ZERO_LIKE_VALUES = {None, '', 0, '0'}
@@ -129,19 +113,30 @@ class DatabaseAlreadyExists(sqlite3.DatabaseError):
129
113
 
130
114
  class GreenDBConnection(sqlite3.Connection):
131
115
  """SQLite DB Connection handler that plays well with eventlet."""
116
+ # slots are needed for python 3.11.0 (there's an issue fixed in 3.11.1,
117
+ # see https://github.com/python/cpython/issues/99886)
118
+ __slots__ = ('timeout', 'db_file')
132
119
 
133
120
  def __init__(self, database, timeout=None, *args, **kwargs):
134
121
  if timeout is None:
135
122
  timeout = BROKER_TIMEOUT
136
123
  self.timeout = timeout
137
124
  self.db_file = database
138
- super(GreenDBConnection, self).__init__(database, 0, *args, **kwargs)
125
+ super(GreenDBConnection, self).__init__(
126
+ database, timeout=0, *args, **kwargs)
139
127
 
140
128
  def cursor(self, cls=None):
141
129
  if cls is None:
142
130
  cls = GreenDBCursor
143
131
  return sqlite3.Connection.cursor(self, cls)
144
132
 
133
+ def execute(self, *args, **kwargs):
134
+ # py311 stopped calling self.cursor() to get the cursor;
135
+ # see https://github.com/python/cpython/pull/31351
136
+ curs = self.cursor()
137
+ curs.execute(*args, **kwargs)
138
+ return curs
139
+
145
140
  def commit(self):
146
141
  return _db_timeout(
147
142
  self.timeout, self.db_file,
@@ -150,6 +145,9 @@ class GreenDBConnection(sqlite3.Connection):
150
145
 
151
146
  class GreenDBCursor(sqlite3.Cursor):
152
147
  """SQLite Cursor handler that plays well with eventlet."""
148
+ # slots are needed for python 3.11.0 (there's an issue fixed in 3.11.1,
149
+ # see https://github.com/python/cpython/issues/99886)
150
+ __slots__ = ('timeout', 'db_file')
153
151
 
154
152
  def __init__(self, *args, **kwargs):
155
153
  self.timeout = args[0].timeout
@@ -161,6 +159,9 @@ class GreenDBCursor(sqlite3.Cursor):
161
159
  self.timeout, self.db_file, lambda: sqlite3.Cursor.execute(
162
160
  self, *args, **kwargs))
163
161
 
162
+ # NB: executemany and executescript are *not* greened, and never have been
163
+ # (as far as I can tell)
164
+
164
165
 
165
166
  def dict_factory(crs, row):
166
167
  """
@@ -184,11 +185,12 @@ def chexor(old, name, timestamp):
184
185
  """
185
186
  if name is None:
186
187
  raise Exception('name is None!')
187
- new = hashlib.md5(('%s-%s' % (name, timestamp)).encode('utf8')).hexdigest()
188
+ new = md5(('%s-%s' % (name, timestamp)).encode('utf8'),
189
+ usedforsecurity=False).hexdigest()
188
190
  return '%032x' % (int(old, 16) ^ int(new, 16))
189
191
 
190
192
 
191
- def get_db_connection(path, timeout=30, okay_to_create=False):
193
+ def get_db_connection(path, timeout=30, logger=None, okay_to_create=False):
192
194
  """
193
195
  Returns a properly configured SQLite database connection.
194
196
 
@@ -201,7 +203,9 @@ def get_db_connection(path, timeout=30, okay_to_create=False):
201
203
  connect_time = time.time()
202
204
  conn = sqlite3.connect(path, check_same_thread=False,
203
205
  factory=GreenDBConnection, timeout=timeout)
204
- if path != ':memory:' and not okay_to_create:
206
+ if QUERY_LOGGING and logger:
207
+ conn.set_trace_callback(logger.debug)
208
+ if not okay_to_create:
205
209
  # attempt to detect and fail when connect creates the db file
206
210
  stat = os.stat(path)
207
211
  if stat.st_size == 0 and stat.st_ctime >= connect_time:
@@ -223,9 +227,87 @@ def get_db_connection(path, timeout=30, okay_to_create=False):
223
227
  return conn
224
228
 
225
229
 
230
+ class TombstoneReclaimer(object):
231
+ """Encapsulates reclamation of deleted rows in a database."""
232
+ def __init__(self, broker, age_timestamp):
233
+ """
234
+ Encapsulates reclamation of deleted rows in a database.
235
+
236
+ :param broker: an instance of :class:`~swift.common.db.DatabaseBroker`.
237
+ :param age_timestamp: a float timestamp: tombstones older than this
238
+ time will be deleted.
239
+ """
240
+ self.broker = broker
241
+ self.age_timestamp = age_timestamp
242
+ self.marker = ''
243
+ self.remaining_tombstones = self.reclaimed = 0
244
+ self.finished = False
245
+ # limit 1 offset N gives back the N+1th matching row; that row is used
246
+ # as an exclusive end_marker for a batch of deletes, so a batch
247
+ # comprises rows satisfying self.marker <= name < end_marker.
248
+ self.batch_query = '''
249
+ SELECT name FROM %s WHERE deleted = 1
250
+ AND name >= ?
251
+ ORDER BY NAME LIMIT 1 OFFSET ?
252
+ ''' % self.broker.db_contains_type
253
+ self.clean_batch_query = '''
254
+ DELETE FROM %s WHERE deleted = 1
255
+ AND name >= ? AND %s < %s
256
+ ''' % (self.broker.db_contains_type, self.broker.db_reclaim_timestamp,
257
+ self.age_timestamp)
258
+
259
+ def _reclaim(self, conn):
260
+ curs = conn.execute(self.batch_query, (self.marker, RECLAIM_PAGE_SIZE))
261
+ row = curs.fetchone()
262
+ end_marker = row[0] if row else ''
263
+ if end_marker:
264
+ # do a single book-ended DELETE and bounce out
265
+ curs = conn.execute(self.clean_batch_query + ' AND name < ?',
266
+ (self.marker, end_marker))
267
+ self.marker = end_marker
268
+ self.reclaimed += curs.rowcount
269
+ self.remaining_tombstones += RECLAIM_PAGE_SIZE - curs.rowcount
270
+ else:
271
+ # delete off the end
272
+ curs = conn.execute(self.clean_batch_query, (self.marker,))
273
+ self.finished = True
274
+ self.reclaimed += curs.rowcount
275
+
276
+ def reclaim(self):
277
+ """
278
+ Perform reclaim of deleted rows older than ``age_timestamp``.
279
+ """
280
+ while not self.finished:
281
+ with self.broker.get() as conn:
282
+ self._reclaim(conn)
283
+ conn.commit()
284
+
285
+ def get_tombstone_count(self):
286
+ """
287
+ Return the number of remaining tombstones newer than ``age_timestamp``.
288
+ Executes the ``reclaim`` method if it has not already been called on
289
+ this instance.
290
+
291
+ :return: The number of tombstones in the ``broker`` that are newer than
292
+ ``age_timestamp``.
293
+ """
294
+ if not self.finished:
295
+ self.reclaim()
296
+ with self.broker.get() as conn:
297
+ curs = conn.execute('''
298
+ SELECT COUNT(*) FROM %s WHERE deleted = 1
299
+ AND name >= ?
300
+ ''' % (self.broker.db_contains_type,), (self.marker,))
301
+ tombstones = curs.fetchone()[0]
302
+ self.remaining_tombstones += tombstones
303
+ return self.remaining_tombstones
304
+
305
+
226
306
  class DatabaseBroker(object):
227
307
  """Encapsulates working with a database."""
228
308
 
309
+ delete_meta_whitelist = []
310
+
229
311
  def __init__(self, db_file, timeout=BROKER_TIMEOUT, logger=None,
230
312
  account=None, container=None, pending_timeout=None,
231
313
  stale_reads_ok=False, skip_commits=False):
@@ -277,15 +359,13 @@ class DatabaseBroker(object):
277
359
  :param put_timestamp: internalized timestamp of initial PUT request
278
360
  :param storage_policy_index: only required for containers
279
361
  """
280
- if self._db_file == ':memory:':
281
- tmp_db_file = None
282
- conn = get_db_connection(self._db_file, self.timeout)
283
- else:
284
- mkdirs(self.db_dir)
285
- fd, tmp_db_file = mkstemp(suffix='.tmp', dir=self.db_dir)
286
- os.close(fd)
287
- conn = sqlite3.connect(tmp_db_file, check_same_thread=False,
288
- factory=GreenDBConnection, timeout=0)
362
+ mkdirs(self.db_dir)
363
+ fd, tmp_db_file = mkstemp(suffix='.tmp', dir=self.db_dir)
364
+ os.close(fd)
365
+ conn = sqlite3.connect(tmp_db_file, check_same_thread=False,
366
+ factory=GreenDBConnection, timeout=0)
367
+ if QUERY_LOGGING:
368
+ conn.set_trace_callback(self.logger.debug)
289
369
  # creating dbs implicitly does a lot of transactions, so we
290
370
  # pick fast, unsafe options here and do a big fsync at the end.
291
371
  with closing(conn.cursor()) as cur:
@@ -346,7 +426,8 @@ class DatabaseBroker(object):
346
426
  # of the system were "racing" each other.
347
427
  raise DatabaseAlreadyExists(self.db_file)
348
428
  renamer(tmp_db_file, self.db_file)
349
- self.conn = get_db_connection(self.db_file, self.timeout)
429
+ self.conn = get_db_connection(self.db_file, self.timeout,
430
+ self.logger)
350
431
  else:
351
432
  self.conn = conn
352
433
 
@@ -359,11 +440,20 @@ class DatabaseBroker(object):
359
440
  # first, clear the metadata
360
441
  cleared_meta = {}
361
442
  for k in self.metadata:
443
+ if k.lower() in self.delete_meta_whitelist:
444
+ continue
362
445
  cleared_meta[k] = ('', timestamp)
363
446
  self.update_metadata(cleared_meta)
364
447
  # then mark the db as deleted
365
448
  with self.get() as conn:
366
- self._delete_db(conn, timestamp)
449
+ conn.execute(
450
+ """
451
+ UPDATE %s_stat
452
+ SET delete_timestamp = ?,
453
+ status = 'DELETED',
454
+ status_changed_at = ?
455
+ WHERE delete_timestamp < ? """ % self.db_type,
456
+ (timestamp, timestamp, timestamp))
367
457
  conn.commit()
368
458
 
369
459
  @property
@@ -392,32 +482,32 @@ class DatabaseBroker(object):
392
482
  raise
393
483
  quar_path = "%s-%s" % (quar_path, uuid4().hex)
394
484
  renamer(self.db_dir, quar_path, fsync=False)
395
- detail = _('Quarantined %(db_dir)s to %(quar_path)s due to '
396
- '%(reason)s') % {'db_dir': self.db_dir,
397
- 'quar_path': quar_path,
398
- 'reason': reason}
485
+ detail = ('Quarantined %(db_dir)s to %(quar_path)s due to '
486
+ '%(reason)s') % {'db_dir': self.db_dir,
487
+ 'quar_path': quar_path,
488
+ 'reason': reason}
399
489
  self.logger.error(detail)
400
490
  raise sqlite3.DatabaseError(detail)
401
491
 
402
- def possibly_quarantine(self, exc_type, exc_value, exc_traceback):
492
+ def possibly_quarantine(self, err):
403
493
  """
404
494
  Checks the exception info to see if it indicates a quarantine situation
405
495
  (malformed or corrupted database). If not, the original exception will
406
496
  be reraised. If so, the database will be quarantined and a new
407
497
  sqlite3.DatabaseError will be raised indicating the action taken.
408
498
  """
409
- if 'database disk image is malformed' in str(exc_value):
499
+ if 'database disk image is malformed' in str(err):
410
500
  exc_hint = 'malformed database'
411
- elif 'malformed database schema' in str(exc_value):
501
+ elif 'malformed database schema' in str(err):
412
502
  exc_hint = 'malformed database'
413
- elif ' is not a database' in str(exc_value):
503
+ elif ' is not a database' in str(err):
414
504
  # older versions said 'file is not a database'
415
505
  # now 'file is encrypted or is not a database'
416
506
  exc_hint = 'corrupted database'
417
- elif 'disk I/O error' in str(exc_value):
507
+ elif 'disk I/O error' in str(err):
418
508
  exc_hint = 'disk error while accessing database'
419
509
  else:
420
- six.reraise(exc_type, exc_value, exc_traceback)
510
+ raise err
421
511
 
422
512
  self.quarantine(exc_hint)
423
513
 
@@ -447,11 +537,12 @@ class DatabaseBroker(object):
447
537
  def get(self):
448
538
  """Use with the "with" statement; returns a database connection."""
449
539
  if not self.conn:
450
- if self.db_file != ':memory:' and os.path.exists(self.db_file):
540
+ if os.path.exists(self.db_file):
451
541
  try:
452
- self.conn = get_db_connection(self.db_file, self.timeout)
453
- except (sqlite3.DatabaseError, DatabaseConnectionError):
454
- self.possibly_quarantine(*sys.exc_info())
542
+ self.conn = get_db_connection(self.db_file, self.timeout,
543
+ self.logger)
544
+ except (sqlite3.DatabaseError, DatabaseConnectionError) as e:
545
+ self.possibly_quarantine(e)
455
546
  else:
456
547
  raise DatabaseConnectionError(self.db_file, "DB doesn't exist")
457
548
  conn = self.conn
@@ -460,12 +551,12 @@ class DatabaseBroker(object):
460
551
  yield conn
461
552
  conn.rollback()
462
553
  self.conn = conn
463
- except sqlite3.DatabaseError:
554
+ except sqlite3.DatabaseError as e:
464
555
  try:
465
556
  conn.close()
466
557
  except Exception:
467
558
  pass
468
- self.possibly_quarantine(*sys.exc_info())
559
+ self.possibly_quarantine(e)
469
560
  except (Exception, Timeout):
470
561
  conn.close()
471
562
  raise
@@ -474,8 +565,9 @@ class DatabaseBroker(object):
474
565
  def lock(self):
475
566
  """Use with the "with" statement; locks a database."""
476
567
  if not self.conn:
477
- if self.db_file != ':memory:' and os.path.exists(self.db_file):
478
- self.conn = get_db_connection(self.db_file, self.timeout)
568
+ if os.path.exists(self.db_file):
569
+ self.conn = get_db_connection(self.db_file, self.timeout,
570
+ self.logger)
479
571
  else:
480
572
  raise DatabaseConnectionError(self.db_file, "DB doesn't exist")
481
573
  conn = self.conn
@@ -485,16 +577,19 @@ class DatabaseBroker(object):
485
577
  conn.execute('BEGIN IMMEDIATE')
486
578
  try:
487
579
  yield True
488
- except (Exception, Timeout):
489
- pass
490
- try:
491
- conn.execute('ROLLBACK')
492
- conn.isolation_level = orig_isolation_level
493
- self.conn = conn
494
- except (Exception, Timeout):
495
- logging.exception(
496
- _('Broker error trying to rollback locked connection'))
497
- conn.close()
580
+ finally:
581
+ try:
582
+ conn.execute('ROLLBACK')
583
+ conn.isolation_level = orig_isolation_level
584
+ self.conn = conn
585
+ except (Exception, Timeout):
586
+ logging.exception(
587
+ 'Broker error trying to rollback locked connection')
588
+ conn.close()
589
+
590
+ def _new_db_id(self):
591
+ device_name = os.path.basename(self.get_device_path())
592
+ return "%s-%s" % (str(uuid4()), device_name)
498
593
 
499
594
  def newid(self, remote_id):
500
595
  """
@@ -505,7 +600,7 @@ class DatabaseBroker(object):
505
600
  with self.get() as conn:
506
601
  row = conn.execute('''
507
602
  UPDATE %s_stat SET id=?
508
- ''' % self.db_type, (str(uuid4()),))
603
+ ''' % self.db_type, (self._new_db_id(),))
509
604
  row = conn.execute('''
510
605
  SELECT ROWID FROM %s ORDER BY ROWID DESC LIMIT 1
511
606
  ''' % self.db_contains_type).fetchone()
@@ -537,7 +632,7 @@ class DatabaseBroker(object):
537
632
 
538
633
  :returns: True if the DB is considered to be deleted, False otherwise
539
634
  """
540
- if self.db_file != ':memory:' and not os.path.exists(self.db_file):
635
+ if not os.path.exists(self.db_file):
541
636
  return True
542
637
  self._commit_puts_stale_ok()
543
638
  with self.get() as conn:
@@ -614,22 +709,26 @@ class DatabaseBroker(object):
614
709
  return -1
615
710
  return row['sync_point']
616
711
 
617
- def get_syncs(self, incoming=True):
712
+ def get_syncs(self, incoming=True, include_timestamp=False):
618
713
  """
619
714
  Get a serialized copy of the sync table.
620
715
 
621
716
  :param incoming: if True, get the last incoming sync, otherwise get
622
717
  the last outgoing sync
623
- :returns: list of {'remote_id', 'sync_point'}
718
+ :param include_timestamp: If True include the updated_at timestamp
719
+ :returns: list of {'remote_id', 'sync_point'} or
720
+ {'remote_id', 'sync_point', 'updated_at'}
721
+ if include_timestamp is True.
624
722
  """
625
723
  with self.get() as conn:
724
+ columns = 'remote_id, sync_point'
725
+ if include_timestamp:
726
+ columns += ', updated_at'
626
727
  curs = conn.execute('''
627
- SELECT remote_id, sync_point FROM %s_sync
628
- ''' % ('incoming' if incoming else 'outgoing'))
629
- result = []
630
- for row in curs:
631
- result.append({'remote_id': row[0], 'sync_point': row[1]})
632
- return result
728
+ SELECT %s FROM %s_sync
729
+ ''' % (columns, 'incoming' if incoming else 'outgoing'))
730
+ curs.row_factory = dict_factory
731
+ return [r for r in curs]
633
732
 
634
733
  def get_max_row(self, table=None):
635
734
  if not table:
@@ -670,8 +769,8 @@ class DatabaseBroker(object):
670
769
  """
671
770
  Put a record into the DB. If the DB has an associated pending file with
672
771
  space then the record is appended to that file and a commit to the DB
673
- is deferred. If the DB is in-memory or its pending file is full then
674
- the record will be committed immediately.
772
+ is deferred. If its pending file is full then the record will be
773
+ committed immediately.
675
774
 
676
775
  :param record: a record to be added to the DB.
677
776
  :raises DatabaseConnectionError: if the DB file does not exist or if
@@ -679,9 +778,6 @@ class DatabaseBroker(object):
679
778
  :raises LockTimeout: if a timeout occurs while waiting to take a lock
680
779
  to write to the pending file.
681
780
  """
682
- if self._db_file == ':memory:':
683
- self.merge_items([record])
684
- return
685
781
  if not os.path.exists(self.db_file):
686
782
  raise DatabaseConnectionError(self.db_file, "DB doesn't exist")
687
783
  if self.skip_commits:
@@ -707,8 +803,7 @@ class DatabaseBroker(object):
707
803
  fp.flush()
708
804
 
709
805
  def _skip_commit_puts(self):
710
- return (self._db_file == ':memory:' or self.skip_commits or not
711
- os.path.exists(self.pending_file))
806
+ return self.skip_commits or not os.path.exists(self.pending_file)
712
807
 
713
808
  def _commit_puts(self, item_list=None):
714
809
  """
@@ -737,15 +832,12 @@ class DatabaseBroker(object):
737
832
  for entry in fp.read().split(b':'):
738
833
  if entry:
739
834
  try:
740
- if six.PY2:
741
- data = pickle.loads(base64.b64decode(entry))
742
- else:
743
- data = pickle.loads(base64.b64decode(entry),
744
- encoding='utf8')
835
+ data = pickle.loads(base64.b64decode(entry),
836
+ encoding='utf8') # nosec: B301
745
837
  self._commit_puts_load(item_list, data)
746
838
  except Exception:
747
839
  self.logger.exception(
748
- _('Invalid pending entry %(file)s: %(entry)s'),
840
+ 'Invalid pending entry %(file)s: %(entry)s',
749
841
  {'file': self.pending_file, 'entry': entry})
750
842
  if item_list:
751
843
  self.merge_items(item_list)
@@ -822,7 +914,7 @@ class DatabaseBroker(object):
822
914
  within 512k of a boundary, it allocates to the next boundary.
823
915
  Boundaries are 2m, 5m, 10m, 25m, 50m, then every 50m after.
824
916
  """
825
- if not DB_PREALLOCATION or self._db_file == ':memory:':
917
+ if not DB_PREALLOCATION:
826
918
  return
827
919
  MB = (1024 * 1024)
828
920
 
@@ -968,47 +1060,22 @@ class DatabaseBroker(object):
968
1060
  with lock_parent_directory(self.pending_file,
969
1061
  self.pending_timeout):
970
1062
  self._commit_puts()
971
- marker = ''
972
- finished = False
973
- while not finished:
974
- with self.get() as conn:
975
- marker = self._reclaim(conn, age_timestamp, marker)
976
- if not marker:
977
- finished = True
978
- self._reclaim_other_stuff(
979
- conn, age_timestamp, sync_timestamp)
980
- conn.commit()
1063
+
1064
+ tombstone_reclaimer = TombstoneReclaimer(self, age_timestamp)
1065
+ tombstone_reclaimer.reclaim()
1066
+ with self.get() as conn:
1067
+ self._reclaim_other_stuff(conn, age_timestamp, sync_timestamp)
1068
+ conn.commit()
1069
+ return tombstone_reclaimer
981
1070
 
982
1071
  def _reclaim_other_stuff(self, conn, age_timestamp, sync_timestamp):
983
1072
  """
984
- This is only called once at the end of reclaim after _reclaim has been
985
- called for each page.
1073
+ This is only called once at the end of reclaim after tombstone reclaim
1074
+ has been completed.
986
1075
  """
987
1076
  self._reclaim_sync(conn, sync_timestamp)
988
1077
  self._reclaim_metadata(conn, age_timestamp)
989
1078
 
990
- def _reclaim(self, conn, age_timestamp, marker):
991
- clean_batch_qry = '''
992
- DELETE FROM %s WHERE deleted = 1
993
- AND name > ? AND %s < ?
994
- ''' % (self.db_contains_type, self.db_reclaim_timestamp)
995
- curs = conn.execute('''
996
- SELECT name FROM %s WHERE deleted = 1
997
- AND name > ?
998
- ORDER BY NAME LIMIT 1 OFFSET ?
999
- ''' % (self.db_contains_type,), (marker, RECLAIM_PAGE_SIZE))
1000
- row = curs.fetchone()
1001
- if row:
1002
- # do a single book-ended DELETE and bounce out
1003
- end_marker = row[0]
1004
- conn.execute(clean_batch_qry + ' AND name <= ?', (
1005
- marker, age_timestamp, end_marker))
1006
- else:
1007
- # delete off the end and reset marker to indicate we're done
1008
- end_marker = ''
1009
- conn.execute(clean_batch_qry, (marker, age_timestamp))
1010
- return end_marker
1011
-
1012
1079
  def _reclaim_sync(self, conn, sync_timestamp):
1013
1080
  try:
1014
1081
  conn.execute('''