swift 2.23.2__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 (208) 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.2.data/scripts/swift-account-audit → swift/cli/account_audit.py +23 -13
  9. swift-2.23.2.data/scripts/swift-config → swift/cli/config.py +2 -2
  10. swift/cli/container_deleter.py +5 -11
  11. swift-2.23.2.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +8 -7
  12. swift/cli/dispersion_report.py +10 -9
  13. swift-2.23.2.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +63 -21
  14. swift/cli/form_signature.py +3 -7
  15. swift-2.23.2.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +8 -2
  16. swift/cli/info.py +183 -29
  17. swift/cli/manage_shard_ranges.py +708 -37
  18. swift-2.23.2.data/scripts/swift-oldies → swift/cli/oldies.py +25 -14
  19. swift-2.23.2.data/scripts/swift-orphans → swift/cli/orphans.py +7 -3
  20. swift/cli/recon.py +196 -67
  21. swift-2.23.2.data/scripts/swift-recon-cron → swift/cli/recon_cron.py +17 -20
  22. swift-2.23.2.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 +198 -127
  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 +396 -147
  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 -81
  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 +52 -19
  102. swift/common/middleware/tempauth.py +76 -58
  103. swift/common/middleware/tempurl.py +192 -174
  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.2.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} +2191 -2762
  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 +555 -536
  130. swift/container/auditor.py +14 -100
  131. swift/container/backend.py +552 -227
  132. swift/container/reconciler.py +126 -37
  133. swift/container/replicator.py +96 -22
  134. swift/container/server.py +397 -176
  135. swift/container/sharder.py +1580 -639
  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 +213 -122
  146. swift/obj/ssync_receiver.py +145 -85
  147. swift/obj/ssync_sender.py +113 -54
  148. swift/obj/updater.py +653 -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 +452 -86
  154. swift/proxy/controllers/info.py +3 -2
  155. swift/proxy/controllers/obj.py +1009 -490
  156. swift/proxy/server.py +185 -112
  157. swift-2.35.0.dist-info/AUTHORS +501 -0
  158. swift-2.35.0.dist-info/LICENSE +202 -0
  159. {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/METADATA +52 -61
  160. swift-2.35.0.dist-info/RECORD +201 -0
  161. {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/WHEEL +1 -1
  162. {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/entry_points.txt +43 -0
  163. swift-2.35.0.dist-info/pbr.json +1 -0
  164. swift/locale/de/LC_MESSAGES/swift.po +0 -1216
  165. swift/locale/en_GB/LC_MESSAGES/swift.po +0 -1207
  166. swift/locale/es/LC_MESSAGES/swift.po +0 -1085
  167. swift/locale/fr/LC_MESSAGES/swift.po +0 -909
  168. swift/locale/it/LC_MESSAGES/swift.po +0 -894
  169. swift/locale/ja/LC_MESSAGES/swift.po +0 -965
  170. swift/locale/ko_KR/LC_MESSAGES/swift.po +0 -964
  171. swift/locale/pt_BR/LC_MESSAGES/swift.po +0 -881
  172. swift/locale/ru/LC_MESSAGES/swift.po +0 -891
  173. swift/locale/tr_TR/LC_MESSAGES/swift.po +0 -832
  174. swift/locale/zh_CN/LC_MESSAGES/swift.po +0 -833
  175. swift/locale/zh_TW/LC_MESSAGES/swift.po +0 -838
  176. swift-2.23.2.data/scripts/swift-account-auditor +0 -23
  177. swift-2.23.2.data/scripts/swift-account-info +0 -51
  178. swift-2.23.2.data/scripts/swift-account-reaper +0 -23
  179. swift-2.23.2.data/scripts/swift-account-replicator +0 -34
  180. swift-2.23.2.data/scripts/swift-account-server +0 -23
  181. swift-2.23.2.data/scripts/swift-container-auditor +0 -23
  182. swift-2.23.2.data/scripts/swift-container-info +0 -51
  183. swift-2.23.2.data/scripts/swift-container-reconciler +0 -21
  184. swift-2.23.2.data/scripts/swift-container-replicator +0 -34
  185. swift-2.23.2.data/scripts/swift-container-sharder +0 -33
  186. swift-2.23.2.data/scripts/swift-container-sync +0 -23
  187. swift-2.23.2.data/scripts/swift-container-updater +0 -23
  188. swift-2.23.2.data/scripts/swift-dispersion-report +0 -24
  189. swift-2.23.2.data/scripts/swift-form-signature +0 -20
  190. swift-2.23.2.data/scripts/swift-init +0 -119
  191. swift-2.23.2.data/scripts/swift-object-auditor +0 -29
  192. swift-2.23.2.data/scripts/swift-object-expirer +0 -33
  193. swift-2.23.2.data/scripts/swift-object-info +0 -60
  194. swift-2.23.2.data/scripts/swift-object-reconstructor +0 -33
  195. swift-2.23.2.data/scripts/swift-object-relinker +0 -41
  196. swift-2.23.2.data/scripts/swift-object-replicator +0 -37
  197. swift-2.23.2.data/scripts/swift-object-server +0 -27
  198. swift-2.23.2.data/scripts/swift-object-updater +0 -23
  199. swift-2.23.2.data/scripts/swift-proxy-server +0 -23
  200. swift-2.23.2.data/scripts/swift-recon +0 -24
  201. swift-2.23.2.data/scripts/swift-ring-builder +0 -24
  202. swift-2.23.2.data/scripts/swift-ring-builder-analyzer +0 -22
  203. swift-2.23.2.data/scripts/swift-ring-composer +0 -22
  204. swift-2.23.2.dist-info/DESCRIPTION.rst +0 -166
  205. swift-2.23.2.dist-info/RECORD +0 -220
  206. swift-2.23.2.dist-info/metadata.json +0 -1
  207. swift-2.23.2.dist-info/pbr.json +0 -1
  208. {swift-2.23.2.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,24 +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
- def native_str_keys(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')] = sv
70
- else:
71
- bin_keys = [k for k in metadata if isinstance(k, six.binary_type)]
72
- for k in bin_keys:
73
- sv = metadata[k]
74
- del metadata[k]
75
- metadata[k.decode('utf-8')] = sv
56
+ def native_str_keys_and_values(metadata):
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]
76
64
 
77
65
 
78
66
  ZERO_LIKE_VALUES = {None, '', 0, '0'}
@@ -125,19 +113,30 @@ class DatabaseAlreadyExists(sqlite3.DatabaseError):
125
113
 
126
114
  class GreenDBConnection(sqlite3.Connection):
127
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')
128
119
 
129
120
  def __init__(self, database, timeout=None, *args, **kwargs):
130
121
  if timeout is None:
131
122
  timeout = BROKER_TIMEOUT
132
123
  self.timeout = timeout
133
124
  self.db_file = database
134
- super(GreenDBConnection, self).__init__(database, 0, *args, **kwargs)
125
+ super(GreenDBConnection, self).__init__(
126
+ database, timeout=0, *args, **kwargs)
135
127
 
136
128
  def cursor(self, cls=None):
137
129
  if cls is None:
138
130
  cls = GreenDBCursor
139
131
  return sqlite3.Connection.cursor(self, cls)
140
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
+
141
140
  def commit(self):
142
141
  return _db_timeout(
143
142
  self.timeout, self.db_file,
@@ -146,6 +145,9 @@ class GreenDBConnection(sqlite3.Connection):
146
145
 
147
146
  class GreenDBCursor(sqlite3.Cursor):
148
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')
149
151
 
150
152
  def __init__(self, *args, **kwargs):
151
153
  self.timeout = args[0].timeout
@@ -157,6 +159,9 @@ class GreenDBCursor(sqlite3.Cursor):
157
159
  self.timeout, self.db_file, lambda: sqlite3.Cursor.execute(
158
160
  self, *args, **kwargs))
159
161
 
162
+ # NB: executemany and executescript are *not* greened, and never have been
163
+ # (as far as I can tell)
164
+
160
165
 
161
166
  def dict_factory(crs, row):
162
167
  """
@@ -180,11 +185,12 @@ def chexor(old, name, timestamp):
180
185
  """
181
186
  if name is None:
182
187
  raise Exception('name is None!')
183
- new = hashlib.md5(('%s-%s' % (name, timestamp)).encode('utf8')).hexdigest()
188
+ new = md5(('%s-%s' % (name, timestamp)).encode('utf8'),
189
+ usedforsecurity=False).hexdigest()
184
190
  return '%032x' % (int(old, 16) ^ int(new, 16))
185
191
 
186
192
 
187
- 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):
188
194
  """
189
195
  Returns a properly configured SQLite database connection.
190
196
 
@@ -197,7 +203,9 @@ def get_db_connection(path, timeout=30, okay_to_create=False):
197
203
  connect_time = time.time()
198
204
  conn = sqlite3.connect(path, check_same_thread=False,
199
205
  factory=GreenDBConnection, timeout=timeout)
200
- 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:
201
209
  # attempt to detect and fail when connect creates the db file
202
210
  stat = os.stat(path)
203
211
  if stat.st_size == 0 and stat.st_ctime >= connect_time:
@@ -219,9 +227,87 @@ def get_db_connection(path, timeout=30, okay_to_create=False):
219
227
  return conn
220
228
 
221
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
+
222
306
  class DatabaseBroker(object):
223
307
  """Encapsulates working with a database."""
224
308
 
309
+ delete_meta_whitelist = []
310
+
225
311
  def __init__(self, db_file, timeout=BROKER_TIMEOUT, logger=None,
226
312
  account=None, container=None, pending_timeout=None,
227
313
  stale_reads_ok=False, skip_commits=False):
@@ -273,15 +359,13 @@ class DatabaseBroker(object):
273
359
  :param put_timestamp: internalized timestamp of initial PUT request
274
360
  :param storage_policy_index: only required for containers
275
361
  """
276
- if self._db_file == ':memory:':
277
- tmp_db_file = None
278
- conn = get_db_connection(self._db_file, self.timeout)
279
- else:
280
- mkdirs(self.db_dir)
281
- fd, tmp_db_file = mkstemp(suffix='.tmp', dir=self.db_dir)
282
- os.close(fd)
283
- conn = sqlite3.connect(tmp_db_file, check_same_thread=False,
284
- 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)
285
369
  # creating dbs implicitly does a lot of transactions, so we
286
370
  # pick fast, unsafe options here and do a big fsync at the end.
287
371
  with closing(conn.cursor()) as cur:
@@ -342,7 +426,8 @@ class DatabaseBroker(object):
342
426
  # of the system were "racing" each other.
343
427
  raise DatabaseAlreadyExists(self.db_file)
344
428
  renamer(tmp_db_file, self.db_file)
345
- self.conn = get_db_connection(self.db_file, self.timeout)
429
+ self.conn = get_db_connection(self.db_file, self.timeout,
430
+ self.logger)
346
431
  else:
347
432
  self.conn = conn
348
433
 
@@ -355,11 +440,20 @@ class DatabaseBroker(object):
355
440
  # first, clear the metadata
356
441
  cleared_meta = {}
357
442
  for k in self.metadata:
443
+ if k.lower() in self.delete_meta_whitelist:
444
+ continue
358
445
  cleared_meta[k] = ('', timestamp)
359
446
  self.update_metadata(cleared_meta)
360
447
  # then mark the db as deleted
361
448
  with self.get() as conn:
362
- 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))
363
457
  conn.commit()
364
458
 
365
459
  @property
@@ -388,32 +482,32 @@ class DatabaseBroker(object):
388
482
  raise
389
483
  quar_path = "%s-%s" % (quar_path, uuid4().hex)
390
484
  renamer(self.db_dir, quar_path, fsync=False)
391
- detail = _('Quarantined %(db_dir)s to %(quar_path)s due to '
392
- '%(reason)s') % {'db_dir': self.db_dir,
393
- 'quar_path': quar_path,
394
- '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}
395
489
  self.logger.error(detail)
396
490
  raise sqlite3.DatabaseError(detail)
397
491
 
398
- def possibly_quarantine(self, exc_type, exc_value, exc_traceback):
492
+ def possibly_quarantine(self, err):
399
493
  """
400
494
  Checks the exception info to see if it indicates a quarantine situation
401
495
  (malformed or corrupted database). If not, the original exception will
402
496
  be reraised. If so, the database will be quarantined and a new
403
497
  sqlite3.DatabaseError will be raised indicating the action taken.
404
498
  """
405
- if 'database disk image is malformed' in str(exc_value):
499
+ if 'database disk image is malformed' in str(err):
406
500
  exc_hint = 'malformed database'
407
- elif 'malformed database schema' in str(exc_value):
501
+ elif 'malformed database schema' in str(err):
408
502
  exc_hint = 'malformed database'
409
- elif ' is not a database' in str(exc_value):
503
+ elif ' is not a database' in str(err):
410
504
  # older versions said 'file is not a database'
411
505
  # now 'file is encrypted or is not a database'
412
506
  exc_hint = 'corrupted database'
413
- elif 'disk I/O error' in str(exc_value):
507
+ elif 'disk I/O error' in str(err):
414
508
  exc_hint = 'disk error while accessing database'
415
509
  else:
416
- six.reraise(exc_type, exc_value, exc_traceback)
510
+ raise err
417
511
 
418
512
  self.quarantine(exc_hint)
419
513
 
@@ -443,11 +537,12 @@ class DatabaseBroker(object):
443
537
  def get(self):
444
538
  """Use with the "with" statement; returns a database connection."""
445
539
  if not self.conn:
446
- if self.db_file != ':memory:' and os.path.exists(self.db_file):
540
+ if os.path.exists(self.db_file):
447
541
  try:
448
- self.conn = get_db_connection(self.db_file, self.timeout)
449
- except (sqlite3.DatabaseError, DatabaseConnectionError):
450
- 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)
451
546
  else:
452
547
  raise DatabaseConnectionError(self.db_file, "DB doesn't exist")
453
548
  conn = self.conn
@@ -456,12 +551,12 @@ class DatabaseBroker(object):
456
551
  yield conn
457
552
  conn.rollback()
458
553
  self.conn = conn
459
- except sqlite3.DatabaseError:
554
+ except sqlite3.DatabaseError as e:
460
555
  try:
461
556
  conn.close()
462
557
  except Exception:
463
558
  pass
464
- self.possibly_quarantine(*sys.exc_info())
559
+ self.possibly_quarantine(e)
465
560
  except (Exception, Timeout):
466
561
  conn.close()
467
562
  raise
@@ -470,8 +565,9 @@ class DatabaseBroker(object):
470
565
  def lock(self):
471
566
  """Use with the "with" statement; locks a database."""
472
567
  if not self.conn:
473
- if self.db_file != ':memory:' and os.path.exists(self.db_file):
474
- 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)
475
571
  else:
476
572
  raise DatabaseConnectionError(self.db_file, "DB doesn't exist")
477
573
  conn = self.conn
@@ -481,16 +577,19 @@ class DatabaseBroker(object):
481
577
  conn.execute('BEGIN IMMEDIATE')
482
578
  try:
483
579
  yield True
484
- except (Exception, Timeout):
485
- pass
486
- try:
487
- conn.execute('ROLLBACK')
488
- conn.isolation_level = orig_isolation_level
489
- self.conn = conn
490
- except (Exception, Timeout):
491
- logging.exception(
492
- _('Broker error trying to rollback locked connection'))
493
- 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)
494
593
 
495
594
  def newid(self, remote_id):
496
595
  """
@@ -501,7 +600,7 @@ class DatabaseBroker(object):
501
600
  with self.get() as conn:
502
601
  row = conn.execute('''
503
602
  UPDATE %s_stat SET id=?
504
- ''' % self.db_type, (str(uuid4()),))
603
+ ''' % self.db_type, (self._new_db_id(),))
505
604
  row = conn.execute('''
506
605
  SELECT ROWID FROM %s ORDER BY ROWID DESC LIMIT 1
507
606
  ''' % self.db_contains_type).fetchone()
@@ -533,7 +632,7 @@ class DatabaseBroker(object):
533
632
 
534
633
  :returns: True if the DB is considered to be deleted, False otherwise
535
634
  """
536
- if self.db_file != ':memory:' and not os.path.exists(self.db_file):
635
+ if not os.path.exists(self.db_file):
537
636
  return True
538
637
  self._commit_puts_stale_ok()
539
638
  with self.get() as conn:
@@ -610,22 +709,26 @@ class DatabaseBroker(object):
610
709
  return -1
611
710
  return row['sync_point']
612
711
 
613
- def get_syncs(self, incoming=True):
712
+ def get_syncs(self, incoming=True, include_timestamp=False):
614
713
  """
615
714
  Get a serialized copy of the sync table.
616
715
 
617
716
  :param incoming: if True, get the last incoming sync, otherwise get
618
717
  the last outgoing sync
619
- :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.
620
722
  """
621
723
  with self.get() as conn:
724
+ columns = 'remote_id, sync_point'
725
+ if include_timestamp:
726
+ columns += ', updated_at'
622
727
  curs = conn.execute('''
623
- SELECT remote_id, sync_point FROM %s_sync
624
- ''' % ('incoming' if incoming else 'outgoing'))
625
- result = []
626
- for row in curs:
627
- result.append({'remote_id': row[0], 'sync_point': row[1]})
628
- 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]
629
732
 
630
733
  def get_max_row(self, table=None):
631
734
  if not table:
@@ -666,8 +769,8 @@ class DatabaseBroker(object):
666
769
  """
667
770
  Put a record into the DB. If the DB has an associated pending file with
668
771
  space then the record is appended to that file and a commit to the DB
669
- is deferred. If the DB is in-memory or its pending file is full then
670
- the record will be committed immediately.
772
+ is deferred. If its pending file is full then the record will be
773
+ committed immediately.
671
774
 
672
775
  :param record: a record to be added to the DB.
673
776
  :raises DatabaseConnectionError: if the DB file does not exist or if
@@ -675,9 +778,6 @@ class DatabaseBroker(object):
675
778
  :raises LockTimeout: if a timeout occurs while waiting to take a lock
676
779
  to write to the pending file.
677
780
  """
678
- if self._db_file == ':memory:':
679
- self.merge_items([record])
680
- return
681
781
  if not os.path.exists(self.db_file):
682
782
  raise DatabaseConnectionError(self.db_file, "DB doesn't exist")
683
783
  if self.skip_commits:
@@ -703,8 +803,7 @@ class DatabaseBroker(object):
703
803
  fp.flush()
704
804
 
705
805
  def _skip_commit_puts(self):
706
- return (self._db_file == ':memory:' or self.skip_commits or not
707
- os.path.exists(self.pending_file))
806
+ return self.skip_commits or not os.path.exists(self.pending_file)
708
807
 
709
808
  def _commit_puts(self, item_list=None):
710
809
  """
@@ -733,15 +832,12 @@ class DatabaseBroker(object):
733
832
  for entry in fp.read().split(b':'):
734
833
  if entry:
735
834
  try:
736
- if six.PY2:
737
- data = pickle.loads(base64.b64decode(entry))
738
- else:
739
- data = pickle.loads(base64.b64decode(entry),
740
- encoding='utf8')
835
+ data = pickle.loads(base64.b64decode(entry),
836
+ encoding='utf8') # nosec: B301
741
837
  self._commit_puts_load(item_list, data)
742
838
  except Exception:
743
839
  self.logger.exception(
744
- _('Invalid pending entry %(file)s: %(entry)s'),
840
+ 'Invalid pending entry %(file)s: %(entry)s',
745
841
  {'file': self.pending_file, 'entry': entry})
746
842
  if item_list:
747
843
  self.merge_items(item_list)
@@ -818,7 +914,7 @@ class DatabaseBroker(object):
818
914
  within 512k of a boundary, it allocates to the next boundary.
819
915
  Boundaries are 2m, 5m, 10m, 25m, 50m, then every 50m after.
820
916
  """
821
- if not DB_PREALLOCATION or self._db_file == ':memory:':
917
+ if not DB_PREALLOCATION:
822
918
  return
823
919
  MB = (1024 * 1024)
824
920
 
@@ -865,7 +961,7 @@ class DatabaseBroker(object):
865
961
  metadata = self.get_raw_metadata()
866
962
  if metadata:
867
963
  metadata = json.loads(metadata)
868
- native_str_keys(metadata)
964
+ native_str_keys_and_values(metadata)
869
965
  else:
870
966
  metadata = {}
871
967
  return metadata
@@ -927,7 +1023,7 @@ class DatabaseBroker(object):
927
1023
  self.db_type)
928
1024
  md = row[0]
929
1025
  md = json.loads(md) if md else {}
930
- native_str_keys(md)
1026
+ native_str_keys_and_values(md)
931
1027
  except sqlite3.OperationalError as err:
932
1028
  if 'no such column: metadata' not in str(err):
933
1029
  raise
@@ -964,47 +1060,22 @@ class DatabaseBroker(object):
964
1060
  with lock_parent_directory(self.pending_file,
965
1061
  self.pending_timeout):
966
1062
  self._commit_puts()
967
- marker = ''
968
- finished = False
969
- while not finished:
970
- with self.get() as conn:
971
- marker = self._reclaim(conn, age_timestamp, marker)
972
- if not marker:
973
- finished = True
974
- self._reclaim_other_stuff(
975
- conn, age_timestamp, sync_timestamp)
976
- 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
977
1070
 
978
1071
  def _reclaim_other_stuff(self, conn, age_timestamp, sync_timestamp):
979
1072
  """
980
- This is only called once at the end of reclaim after _reclaim has been
981
- called for each page.
1073
+ This is only called once at the end of reclaim after tombstone reclaim
1074
+ has been completed.
982
1075
  """
983
1076
  self._reclaim_sync(conn, sync_timestamp)
984
1077
  self._reclaim_metadata(conn, age_timestamp)
985
1078
 
986
- def _reclaim(self, conn, age_timestamp, marker):
987
- clean_batch_qry = '''
988
- DELETE FROM %s WHERE deleted = 1
989
- AND name > ? AND %s < ?
990
- ''' % (self.db_contains_type, self.db_reclaim_timestamp)
991
- curs = conn.execute('''
992
- SELECT name FROM %s WHERE deleted = 1
993
- AND name > ?
994
- ORDER BY NAME LIMIT 1 OFFSET ?
995
- ''' % (self.db_contains_type,), (marker, RECLAIM_PAGE_SIZE))
996
- row = curs.fetchone()
997
- if row:
998
- # do a single book-ended DELETE and bounce out
999
- end_marker = row[0]
1000
- conn.execute(clean_batch_qry + ' AND name <= ?', (
1001
- marker, age_timestamp, end_marker))
1002
- else:
1003
- # delete off the end and reset marker to indicate we're done
1004
- end_marker = ''
1005
- conn.execute(clean_batch_qry, (marker, age_timestamp))
1006
- return end_marker
1007
-
1008
1079
  def _reclaim_sync(self, conn, sync_timestamp):
1009
1080
  try:
1010
1081
  conn.execute('''