Ticket #999: pluggable-backends-davidsarah.darcs.patch

File pluggable-backends-davidsarah.darcs.patch, 208.4 KB (added by davidsarah, at 2011-09-15T02:50:08Z)

This is just a "flat" recording of my refactoring of pluggable backends. I'll do a better recording tomorrow, and explain the refactoring.

Line 
12 patches for repository http://tahoe-lafs.org/source/tahoe/trunk:
2
3Thu Aug 25 01:16:34 BST 2011  david-sarah@jacaranda.org
4  * Enforce zope interfaces (relative to trunk). refs #1474
5
6Thu Sep 15 03:38:00 BST 2011  david-sarah@jacaranda.org
7  * Rerecording of pluggable backend patches by David-Sarah. refs #999
8
9New patches:
10
11[Enforce zope interfaces (relative to trunk). refs #1474
12david-sarah@jacaranda.org**20110825001634
13 Ignore-this: 34ab9f2c433e5b8fee2eeacf42690ca9
14] {
15hunk ./src/allmydata/check_results.py 2
16 
17-from zope.interface import implements
18+from allmydata.util.interfaceutil import implements
19 from allmydata.interfaces import ICheckResults, ICheckAndRepairResults, \
20      IDeepCheckResults, IDeepCheckAndRepairResults, IURI
21 from allmydata.util import base32
22hunk ./src/allmydata/client.py 5
23 from allmydata.interfaces import RIStorageServer
24 from allmydata import node
25 
26-from zope.interface import implements
27+from allmydata.util.interfaceutil import implements
28 from twisted.internet import reactor, defer
29 from twisted.application import service
30 from twisted.application.internet import TimerService
31hunk ./src/allmydata/codec.py 3
32 # -*- test-case-name: allmydata.test.test_encode_share -*-
33 
34-from zope.interface import implements
35+from allmydata.util.interfaceutil import implements
36 from twisted.internet import defer
37 from allmydata.util import mathutil
38 from allmydata.util.assertutil import precondition
39hunk ./src/allmydata/control.py 3
40 
41 import os, time
42-from zope.interface import implements
43+from allmydata.util.interfaceutil import implements
44 from twisted.application import service
45 from twisted.internet import defer
46 from twisted.internet.interfaces import IConsumer
47hunk ./src/allmydata/dirnode.py 4
48 
49 import time, math, unicodedata
50 
51-from zope.interface import implements
52+from allmydata.util.interfaceutil import implements
53 from twisted.internet import defer
54 from foolscap.api import fireEventually
55 import simplejson
56hunk ./src/allmydata/frontends/auth.py 2
57 import os
58-from zope.interface import implements
59+from allmydata.util.interfaceutil import implements
60 from twisted.web.client import getPage
61 from twisted.internet import defer
62 from twisted.cred import error, checkers, credentials
63hunk ./src/allmydata/frontends/ftpd.py 2
64 
65-from zope.interface import implements
66+from allmydata.util.interfaceutil import implements
67 from twisted.application import service, strports
68 from twisted.internet import defer
69 from twisted.internet.interfaces import IConsumer
70hunk ./src/allmydata/frontends/sftpd.py 7
71 from stat import S_IFREG, S_IFDIR
72 from time import time, strftime, localtime
73 
74-from zope.interface import implements
75+from allmydata.util.interfaceutil import implements
76 from twisted.python import components
77 from twisted.application import service, strports
78 from twisted.conch.ssh import factory, keys, session
79hunk ./src/allmydata/immutable/checker.py 1
80-from zope.interface import implements
81+from allmydata.util.interfaceutil import implements
82 from twisted.internet import defer
83 from foolscap.api import DeadReferenceError, RemoteException
84 from allmydata import hashtree, codec, uri
85hunk ./src/allmydata/immutable/downloader/node.py 4
86 
87 import time
88 now = time.time
89-from zope.interface import Interface
90+from allmydata.util.interfaceutil import Interface
91 from twisted.python.failure import Failure
92 from twisted.internet import defer
93 from foolscap.api import eventually
94hunk ./src/allmydata/immutable/downloader/segmentation.py 4
95 
96 import time
97 now = time.time
98-from zope.interface import implements
99+from allmydata.util.interfaceutil import implements
100 from twisted.internet import defer
101 from twisted.internet.interfaces import IPushProducer
102 from foolscap.api import eventually
103hunk ./src/allmydata/immutable/downloader/status.py 3
104 
105 import itertools
106-from zope.interface import implements
107+from allmydata.util.interfaceutil import implements
108 from allmydata.interfaces import IDownloadStatus
109 
110 class ReadEvent:
111hunk ./src/allmydata/immutable/encode.py 4
112 # -*- test-case-name: allmydata.test.test_encode -*-
113 
114 import time
115-from zope.interface import implements
116+from allmydata.util.interfaceutil import implements
117 from twisted.internet import defer
118 from foolscap.api import fireEventually
119 from allmydata import uri
120hunk ./src/allmydata/immutable/filenode.py 6
121 import copy
122 import time
123 now = time.time
124-from zope.interface import implements
125+from allmydata.util.interfaceutil import implements
126 from twisted.internet import defer
127 
128 from allmydata import uri
129hunk ./src/allmydata/immutable/layout.py 2
130 import struct
131-from zope.interface import implements
132+from allmydata.util.interfaceutil import implements
133 from twisted.internet import defer
134 from allmydata.interfaces import IStorageBucketWriter, IStorageBucketReader, \
135      FileTooLargeError, HASH_SIZE
136hunk ./src/allmydata/immutable/literal.py 2
137 from cStringIO import StringIO
138-from zope.interface import implements
139+from allmydata.util.interfaceutil import implements
140 from twisted.internet import defer
141 from twisted.internet.interfaces import IPushProducer
142 from twisted.protocols import basic
143hunk ./src/allmydata/immutable/offloaded.py 3
144 
145 import os, stat, time, weakref
146-from zope.interface import implements
147+from allmydata.util.interfaceutil import implements
148 from twisted.internet import defer
149 from foolscap.api import Referenceable, DeadReferenceError, eventually
150 import allmydata # for __full_version__
151hunk ./src/allmydata/immutable/repairer.py 1
152-from zope.interface import implements
153+from allmydata.util.interfaceutil import implements
154 from twisted.internet import defer
155 from allmydata.storage.server import si_b2a
156 from allmydata.util import log, consumer
157hunk ./src/allmydata/immutable/upload.py 2
158 import os, time, weakref, itertools
159-from zope.interface import implements
160+from allmydata.util.interfaceutil import implements
161 from twisted.python import failure
162 from twisted.internet import defer
163 from twisted.application import service
164hunk ./src/allmydata/interfaces.py 2
165 
166-from zope.interface import Interface
167+from allmydata.util.interfaceutil import Interface
168 from foolscap.api import StringConstraint, ListOf, TupleOf, SetOf, DictOf, \
169      ChoiceOf, IntegerConstraint, Any, RemoteInterface, Referenceable
170 
171hunk ./src/allmydata/introducer/client.py 3
172 
173 from base64 import b32decode
174-from zope.interface import implements
175+from allmydata.util.interfaceutil import implements
176 from twisted.application import service
177 from foolscap.api import Referenceable, SturdyRef, eventually
178 from allmydata.interfaces import InsufficientVersionError
179hunk ./src/allmydata/introducer/interfaces.py 2
180 
181-from zope.interface import Interface
182+from allmydata.util.interfaceutil import Interface
183 from foolscap.api import StringConstraint, TupleOf, SetOf, DictOf, Any, \
184     RemoteInterface
185 FURL = StringConstraint(1000)
186hunk ./src/allmydata/introducer/server.py 4
187 
188 import time, os.path
189 from base64 import b32decode
190-from zope.interface import implements
191+from allmydata.util.interfaceutil import implements
192 from twisted.application import service
193 from foolscap.api import Referenceable, SturdyRef
194 import allmydata
195hunk ./src/allmydata/key_generator.py 6
196 import time
197 
198 from foolscap.api import Referenceable, Tub
199-from zope.interface import implements
200+from allmydata.util.interfaceutil import implements
201 from twisted.internet import reactor
202 from twisted.application import service
203 from allmydata.util import log
204hunk ./src/allmydata/manhole.py 13
205 from twisted.conch.insults import insults
206 from twisted.internet import protocol
207 
208-from zope.interface import implements
209+from allmydata.util.interfaceutil import implements
210 
211 # makeTelnetProtocol and _TelnetRealm are for the TelnetManhole
212 
213hunk ./src/allmydata/monitor.py 2
214 
215-from zope.interface import Interface, implements
216+from allmydata.util.interfaceutil import Interface, implements
217 from allmydata.util import observer
218 
219 class IMonitor(Interface):
220hunk ./src/allmydata/mutable/filenode.py 4
221 
222 import random
223 
224-from zope.interface import implements
225+from allmydata.util.interfaceutil import implements
226 from twisted.internet import defer, reactor
227 from foolscap.api import eventually
228 from allmydata.interfaces import IMutableFileNode, ICheckable, ICheckResults, \
229hunk ./src/allmydata/mutable/layout.py 9
230 from allmydata.util import mathutil
231 from twisted.python import failure
232 from twisted.internet import defer
233-from zope.interface import implements
234+from allmydata.util.interfaceutil import implements
235 
236 
237 # These strings describe the format of the packed structs they help process
238hunk ./src/allmydata/mutable/publish.py 6
239 import os, time
240 from StringIO import StringIO
241 from itertools import count
242-from zope.interface import implements
243+from allmydata.util.interfaceutil import implements
244 from twisted.internet import defer
245 from twisted.python import failure
246 from allmydata.interfaces import IPublishStatus, SDMF_VERSION, MDMF_VERSION, \
247hunk ./src/allmydata/mutable/repairer.py 2
248 
249-from zope.interface import implements
250+from allmydata.util.interfaceutil import implements
251 from twisted.internet import defer
252 from allmydata.interfaces import IRepairResults, ICheckResults
253 from allmydata.mutable.publish import MutableData
254hunk ./src/allmydata/mutable/retrieve.py 4
255 
256 import time
257 from itertools import count
258-from zope.interface import implements
259+from allmydata.util.interfaceutil import implements
260 from twisted.internet import defer
261 from twisted.python import failure
262 from twisted.internet.interfaces import IPushProducer, IConsumer
263hunk ./src/allmydata/mutable/servermap.py 3
264 
265 import sys, time
266-from zope.interface import implements
267+from allmydata.util.interfaceutil import implements
268 from itertools import count
269 from twisted.internet import defer
270 from twisted.python import failure
271hunk ./src/allmydata/nodemaker.py 2
272 import weakref
273-from zope.interface import implements
274+from allmydata.util.interfaceutil import implements
275 from allmydata.util.assertutil import precondition
276 from allmydata.interfaces import INodeMaker, SDMF_VERSION
277 from allmydata.immutable.literal import LiteralFileNode
278hunk ./src/allmydata/stats.py 11
279 from twisted.internet import reactor
280 from twisted.application import service
281 from twisted.application.internet import TimerService
282-from zope.interface import implements
283+from allmydata.util.interfaceutil import implements
284 from foolscap.api import eventually, DeadReferenceError, Referenceable, Tub
285 
286 from allmydata.util import log
287hunk ./src/allmydata/storage/immutable.py 5
288 
289 from foolscap.api import Referenceable
290 
291-from zope.interface import implements
292+from allmydata.util.interfaceutil import implements
293 from allmydata.interfaces import RIBucketWriter, RIBucketReader
294 from allmydata.util import base32, fileutil, log
295 from allmydata.util.assertutil import precondition
296hunk ./src/allmydata/storage/server.py 6
297 from foolscap.api import Referenceable
298 from twisted.application import service
299 
300-from zope.interface import implements
301+from allmydata.util.interfaceutil import implements
302 from allmydata.interfaces import RIStorageServer, IStatsProducer
303 from allmydata.util import fileutil, idlib, log, time_format
304 import allmydata # for __full_version__
305hunk ./src/allmydata/storage_client.py 33
306 
307 
308 import time
309-from zope.interface import implements, Interface
310+from allmydata.util.interfaceutil import implements, Interface
311 from foolscap.api import eventually
312 from allmydata.interfaces import IStorageBroker
313 from allmydata.util import idlib, log
314hunk ./src/allmydata/test/bench_dirnode.py 5
315 
316 from pyutil import benchutil, randutil # http://tahoe-lafs.org/trac/pyutil
317 
318-from zope.interface import implements
319+from allmydata.util.interfaceutil import implements
320 from allmydata import dirnode, uri
321 from allmydata.interfaces import IFileNode
322 from allmydata.mutable.filenode import MutableFileNode
323hunk ./src/allmydata/test/common.py 2
324 import os, random, struct
325-from zope.interface import implements
326+from allmydata.util.interfaceutil import implements
327 from twisted.internet import defer
328 from twisted.internet.interfaces import IPullProducer
329 from twisted.python import failure
330hunk ./src/allmydata/test/no_network.py 17
331 # or the control.furl .
332 
333 import os.path
334-from zope.interface import implements
335+from allmydata.util.interfaceutil import implements
336 from twisted.application import service
337 from twisted.internet import defer, reactor
338 from twisted.python.failure import Failure
339hunk ./src/allmydata/test/test_dirnode.py 3
340 import time
341 import unicodedata
342-from zope.interface import implements
343+from allmydata.util.interfaceutil import implements
344 from twisted.trial import unittest
345 from twisted.internet import defer
346 from twisted.internet.interfaces import IConsumer
347hunk ./src/allmydata/test/test_encode.py 1
348-from zope.interface import implements
349+from allmydata.util.interfaceutil import implements
350 from twisted.trial import unittest
351 from twisted.internet import defer
352 from twisted.python.failure import Failure
353merger 0.0 (
354hunk ./src/allmydata/test/test_mutable.py 6
355-from twisted.internet.interfaces import IConsumer
356-from zope.interface import implements
357hunk ./src/allmydata/test/test_mutable.py 7
358-from zope.interface import implements
359+from allmydata.util.interfaceutil import implements
360)
361hunk ./src/allmydata/test/test_provisioning.py 11
362     pass # might not be importable, since it needs NumPy
363 
364 from nevow import inevow
365-from zope.interface import implements
366+from allmydata.util.interfaceutil import implements
367 
368 class MyRequest:
369     implements(inevow.IRequest)
370hunk ./src/allmydata/unknown.py 2
371 
372-from zope.interface import implements
373+from allmydata.util.interfaceutil import implements
374 from twisted.internet import defer
375 from allmydata.interfaces import IFilesystemNode, MustNotBeUnknownRWError, \
376     MustBeDeepImmutableError
377hunk ./src/allmydata/uri.py 3
378 
379 import re, urllib
380-from zope.interface import implements
381+from allmydata.util.interfaceutil import implements
382 from twisted.python.components import registerAdapter
383 from allmydata.storage.server import si_a2b, si_b2a
384 from allmydata.util import base32, hashutil
385hunk ./src/allmydata/util/consumer.py 6
386 a filenode's read() method. See download_to_data() for an example of its use.
387 """
388 
389-from zope.interface import implements
390+from allmydata.util.interfaceutil import implements
391 from twisted.internet.interfaces import IConsumer
392 
393 class MemoryConsumer:
394addfile ./src/allmydata/util/interfaceutil.py
395hunk ./src/allmydata/util/interfaceutil.py 1
396+
397+import sys
398+from zope.interface import *
399+from zope.interface.verify import verifyClass
400+from zope.interface.advice import addClassAdvisor
401+
402+
403+def implements(*interfaces):
404+    frame = sys._getframe(1)
405+    f_locals = frame.f_locals
406+
407+    # Try to make sure we were called from a class def. Assumes Python > 2.2.
408+    if f_locals is frame.f_globals or '__module__' not in f_locals:
409+        raise TypeError("implements can be used only from a class definition.")
410+
411+    if '__implements_advice_data__' in f_locals:
412+        raise TypeError("implements can be used only once in a class definition.")
413+
414+    def _implements_advice(cls):
415+        interfaces, classImplements = cls.__dict__['__implements_advice_data__']
416+        del cls.__implements_advice_data__
417+        classImplements(cls, *interfaces)
418+
419+        if not cls.__name__.startswith('_'):
420+            for interface in interfaces:
421+                try:
422+                    verifyClass(interface, cls)
423+                except Exception, e:
424+                    print >>sys.stderr, "%s does not implement %s\n%s" % (cls, interface, e)
425+        return cls
426+
427+    f_locals['__implements_advice_data__'] = interfaces, classImplements
428+    addClassAdvisor(_implements_advice, depth=2)
429hunk ./src/allmydata/web/common.py 5
430 import simplejson
431 from twisted.web import http, server
432 from twisted.python import log
433-from zope.interface import Interface
434+from allmydata.util.interfaceutil import Interface
435 from nevow import loaders, appserver
436 from nevow.inevow import IRequest
437 from nevow.util import resource_filename
438hunk ./src/allmydata/web/directory.py 5
439 import simplejson
440 import urllib
441 
442-from zope.interface import implements
443+from allmydata.util.interfaceutil import implements
444 from twisted.internet import defer
445 from twisted.internet.interfaces import IPushProducer
446 from twisted.python.failure import Failure
447hunk ./src/allmydata/web/operations.py 3
448 
449 import time
450-from zope.interface import implements
451+from allmydata.util.interfaceutil import implements
452 from nevow import rend, url, tags as T
453 from nevow.inevow import IRequest
454 from twisted.python.failure import Failure
455}
456[Rerecording of pluggable backend patches by David-Sarah. refs #999
457david-sarah@jacaranda.org**20110915023800
458 Ignore-this: 7b0121a99a0ac9fc862960138be37e0
459] {
460adddir ./src/allmydata/storage/backends
461adddir ./src/allmydata/storage/backends/disk
462move ./src/allmydata/storage/immutable.py ./src/allmydata/storage/backends/disk/immutable.py
463move ./src/allmydata/storage/mutable.py ./src/allmydata/storage/backends/disk/mutable.py
464adddir ./src/allmydata/storage/backends/null
465hunk ./docs/garbage-collection.rst 177
466     use this parameter to implement it.
467 
468     This key is only valid when age-based expiration is in use (i.e. when
469-    ``expire.mode = age`` is used). It will be rejected if cutoff-date
470+    ``expire.mode = age`` is used). It will be ignored if cutoff-date
471     expiration is in use.
472 
473 ``expire.cutoff_date = (date string, required if mode=cutoff-date)``
474hunk ./docs/garbage-collection.rst 196
475     the last renewal time and the cutoff date.
476 
477     This key is only valid when cutoff-based expiration is in use (i.e. when
478-    "expire.mode = cutoff-date"). It will be rejected if age-based expiration
479+    "expire.mode = cutoff-date"). It will be ignored if age-based expiration
480     is in use.
481 
482   expire.immutable = (boolean, optional)
483hunk ./src/allmydata/client.py 245
484             sharetypes.append("immutable")
485         if self.get_config("storage", "expire.mutable", True, boolean=True):
486             sharetypes.append("mutable")
487-        expiration_sharetypes = tuple(sharetypes)
488 
489hunk ./src/allmydata/client.py 246
490+        expiration_policy = {
491+            'enabled': expire,
492+            'mode': mode,
493+            'override_lease_duration': o_l_d,
494+            'cutoff_date': cutoff_date,
495+            'sharetypes': tuple(sharetypes),
496+        }
497         ss = StorageServer(storedir, self.nodeid,
498                            reserved_space=reserved,
499                            discard_storage=discard,
500hunk ./src/allmydata/client.py 258
501                            readonly_storage=readonly,
502                            stats_provider=self.stats_provider,
503-                           expiration_enabled=expire,
504-                           expiration_mode=mode,
505-                           expiration_override_lease_duration=o_l_d,
506-                           expiration_cutoff_date=cutoff_date,
507-                           expiration_sharetypes=expiration_sharetypes)
508+                           expiration_policy=expiration_policy)
509         self.add_service(ss)
510 
511         d = self.when_tub_ready()
512hunk ./src/allmydata/interfaces.py 29
513 Number = IntegerConstraint(8) # 2**(8*8) == 16EiB ~= 18e18 ~= 18 exabytes
514 Offset = Number
515 ReadSize = int # the 'int' constraint is 2**31 == 2Gib -- large files are processed in not-so-large increments
516-WriteEnablerSecret = Hash # used to protect mutable bucket modifications
517-LeaseRenewSecret = Hash # used to protect bucket lease renewal requests
518-LeaseCancelSecret = Hash # used to protect bucket lease cancellation requests
519+WriteEnablerSecret = Hash # used to protect mutable share modifications
520+LeaseRenewSecret = Hash # used to protect lease renewal requests
521+LeaseCancelSecret = Hash # used to protect lease cancellation requests
522 
523 class RIStubClient(RemoteInterface):
524     """Each client publishes a service announcement for a dummy object called
525hunk ./src/allmydata/interfaces.py 106
526                          sharenums=SetOf(int, maxLength=MAX_BUCKETS),
527                          allocated_size=Offset, canary=Referenceable):
528         """
529-        @param storage_index: the index of the bucket to be created or
530+        @param storage_index: the index of the shareset to be created or
531                               increfed.
532         @param sharenums: these are the share numbers (probably between 0 and
533                           99) that the sender is proposing to store on this
534hunk ./src/allmydata/interfaces.py 111
535                           server.
536-        @param renew_secret: This is the secret used to protect bucket refresh
537+        @param renew_secret: This is the secret used to protect lease renewal.
538                              This secret is generated by the client and
539                              stored for later comparison by the server. Each
540                              server is given a different secret.
541hunk ./src/allmydata/interfaces.py 115
542-        @param cancel_secret: Like renew_secret, but protects bucket decref.
543-        @param canary: If the canary is lost before close(), the bucket is
544+        @param cancel_secret: ignored
545+        @param canary: If the canary is lost before close(), the allocation is
546                        deleted.
547         @return: tuple of (alreadygot, allocated), where alreadygot is what we
548                  already have and allocated is what we hereby agree to accept.
549hunk ./src/allmydata/interfaces.py 129
550                   renew_secret=LeaseRenewSecret,
551                   cancel_secret=LeaseCancelSecret):
552         """
553-        Add a new lease on the given bucket. If the renew_secret matches an
554+        Add a new lease on the given shareset. If the renew_secret matches an
555         existing lease, that lease will be renewed instead. If there is no
556hunk ./src/allmydata/interfaces.py 131
557-        bucket for the given storage_index, return silently. (note that in
558+        shareset for the given storage_index, return silently. (Note that in
559         tahoe-1.3.0 and earlier, IndexError was raised if there was no
560hunk ./src/allmydata/interfaces.py 133
561-        bucket)
562+        shareset.)
563         """
564         return Any() # returns None now, but future versions might change
565 
566hunk ./src/allmydata/interfaces.py 139
567     def renew_lease(storage_index=StorageIndex, renew_secret=LeaseRenewSecret):
568         """
569-        Renew the lease on a given bucket, resetting the timer to 31 days.
570-        Some networks will use this, some will not. If there is no bucket for
571+        Renew the lease on a given shareset, resetting the timer to 31 days.
572+        Some networks will use this, some will not. If there is no shareset for
573         the given storage_index, IndexError will be raised.
574 
575         For mutable shares, if the given renew_secret does not match an
576hunk ./src/allmydata/interfaces.py 146
577         existing lease, IndexError will be raised with a note listing the
578         server-nodeids on the existing leases, so leases on migrated shares
579-        can be renewed or cancelled. For immutable shares, IndexError
580-        (without the note) will be raised.
581+        can be renewed. For immutable shares, IndexError (without the note)
582+        will be raised.
583         """
584         return Any()
585 
586hunk ./src/allmydata/interfaces.py 154
587     def get_buckets(storage_index=StorageIndex):
588         return DictOf(int, RIBucketReader, maxKeys=MAX_BUCKETS)
589 
590-
591-
592     def slot_readv(storage_index=StorageIndex,
593                    shares=ListOf(int), readv=ReadVector):
594         """Read a vector from the numbered shares associated with the given
595hunk ./src/allmydata/interfaces.py 163
596 
597     def slot_testv_and_readv_and_writev(storage_index=StorageIndex,
598                                         secrets=TupleOf(WriteEnablerSecret,
599-                                                        LeaseRenewSecret,
600-                                                        LeaseCancelSecret),
601+                                                        LeaseRenewSecret),
602                                         tw_vectors=TestAndWriteVectorsForShares,
603                                         r_vector=ReadVector,
604                                         ):
605hunk ./src/allmydata/interfaces.py 167
606-        """General-purpose test-and-set operation for mutable slots. Perform
607-        a bunch of comparisons against the existing shares. If they all pass,
608-        then apply a bunch of write vectors to those shares. Then use the
609-        read vectors to extract data from all the shares and return the data.
610+        """
611+        General-purpose atomic test-read-and-set operation for mutable slots.
612+        Perform a bunch of comparisons against the existing shares. If they
613+        all pass: use the read vectors to extract data from all the shares,
614+        then apply a bunch of write vectors to those shares. Return the read
615+        data, which does not include any modifications made by the writes.
616 
617         This method is, um, large. The goal is to allow clients to update all
618         the shares associated with a mutable file in a single round trip.
619hunk ./src/allmydata/interfaces.py 177
620 
621-        @param storage_index: the index of the bucket to be created or
622+        @param storage_index: the index of the shareset to be created or
623                               increfed.
624         @param write_enabler: a secret that is stored along with the slot.
625                               Writes are accepted from any caller who can
626hunk ./src/allmydata/interfaces.py 183
627                               present the matching secret. A different secret
628                               should be used for each slot*server pair.
629-        @param renew_secret: This is the secret used to protect bucket refresh
630+        @param renew_secret: This is the secret used to protect lease renewal.
631                              This secret is generated by the client and
632                              stored for later comparison by the server. Each
633                              server is given a different secret.
634hunk ./src/allmydata/interfaces.py 187
635-        @param cancel_secret: Like renew_secret, but protects bucket decref.
636+        @param cancel_secret: ignored
637 
638hunk ./src/allmydata/interfaces.py 189
639-        The 'secrets' argument is a tuple of (write_enabler, renew_secret,
640-        cancel_secret). The first is required to perform any write. The
641-        latter two are used when allocating new shares. To simply acquire a
642-        new lease on existing shares, use an empty testv and an empty writev.
643+        The 'secrets' argument is a tuple with (write_enabler, renew_secret).
644+        The write_enabler is required to perform any write. The renew_secret
645+        is used when allocating new shares.
646 
647         Each share can have a separate test vector (i.e. a list of
648         comparisons to perform). If all vectors for all shares pass, then all
649hunk ./src/allmydata/interfaces.py 280
650         store that on disk.
651         """
652 
653+
654+class IStorageBackend(Interface):
655+    """
656+    Objects of this kind live on the server side and are used by the
657+    storage server object.
658+    """
659+    def get_available_space(reserved_space):
660+        """
661+        Returns available space for share storage in bytes, or
662+        None if this information is not available or if the available
663+        space is unlimited.
664+
665+        If the backend is configured for read-only mode then this will
666+        return 0.
667+
668+        reserved_space is how many bytes to subtract from the answer, so
669+        you can pass how many bytes you would like to leave unused on this
670+        filesystem as reserved_space.
671+        """
672+
673+    def get_sharesets_for_prefix(prefix):
674+        """
675+        Generates IShareSet objects for all storage indices matching the
676+        given prefix for which this backend holds shares.
677+        """
678+
679+    def get_shareset(storageindex):
680+        """
681+        Get an IShareSet object for the given storage index.
682+        """
683+
684+    def advise_corrupt_share(storageindex, sharetype, shnum, reason):
685+        """
686+        Clients who discover hash failures in shares that they have
687+        downloaded from me will use this method to inform me about the
688+        failures. I will record their concern so that my operator can
689+        manually inspect the shares in question.
690+
691+        'sharetype' is either 'mutable' or 'immutable'. 'shnum' is the integer
692+        share number. 'reason' is a human-readable explanation of the problem,
693+        probably including some expected hash values and the computed ones
694+        that did not match. Corruption advisories for mutable shares should
695+        include a hash of the public key (the same value that appears in the
696+        mutable-file verify-cap), since the current share format does not
697+        store that on disk.
698+
699+        @param sharetype=str
700+        @param shnum=int
701+        @param reason=str
702+        """
703+
704+
705+class IShareSet(Interface):
706+    def get_storage_index():
707+        """
708+        Returns the storage index for this shareset.
709+        """
710+
711+    def get_storage_index_string():
712+        """
713+        Returns the base32-encoded storage index for this shareset.
714+        """
715+
716+    def get_overhead():
717+        """
718+        Returns the storage overhead, in bytes, of this shareset (exclusive
719+        of the space used by its shares).
720+        """
721+
722+    def get_shares():
723+        """
724+        Generates the IStoredShare objects held in this shareset.
725+        """
726+
727+    def get_incoming_shnums():
728+        """
729+        Return a frozenset of the shnums (as ints) of incoming shares.
730+        """
731+
732+    def make_bucket_writer(storageserver, shnum, max_space_per_bucket, lease_info, canary):
733+        """
734+        Create a bucket writer that can be used to write data to a given share.
735+
736+        @param storageserver=RIStorageServer
737+        @param shnum=int: A share number in this shareset
738+        @param max_space_per_bucket=int: The maximum space allocated for the
739+                 share, in bytes
740+        @param lease_info=LeaseInfo: The initial lease information
741+        @param canary=Referenceable: If the canary is lost before close(), the
742+                 bucket is deleted.
743+        @return an IStorageBucketWriter for the given share
744+        """
745+
746+    def make_bucket_reader(storageserver, share):
747+        """
748+        Create a bucket reader that can be used to read data from a given share.
749+
750+        @param storageserver=RIStorageServer
751+        @param share=IStoredShare
752+        @return an IStorageBucketReader for the given share
753+        """
754+
755+    def readv(wanted_shnums, read_vector):
756+        """
757+        Read a vector from the numbered shares in this shareset. An empty
758+        wanted_shnums list means to return data from all known shares.
759+
760+        @param wanted_shnums=ListOf(int)
761+        @param read_vector=ReadVector
762+        @return DictOf(int, ReadData): shnum -> results, with one key per share
763+        """
764+
765+    def testv_and_readv_and_writev(secrets, test_and_write_vectors, read_vector, expiration_time):
766+        """
767+        General-purpose atomic test-read-and-set operation for mutable slots.
768+        Perform a bunch of comparisons against the existing shares in this
769+        shareset. If they all pass: use the read vectors to extract data from
770+        all the shares, then apply a bunch of write vectors to those shares.
771+        Return the read data, which does not include any modifications made by
772+        the writes.
773+
774+        See the similar method in RIStorageServer for more detail.
775+
776+        @param secrets=TupleOf(WriteEnablerSecret, LeaseRenewSecret[, ...])
777+        @param test_and_write_vectors=TestAndWriteVectorsForShares
778+        @param read_vector=ReadVector
779+        @param expiration_time=int
780+        @return TupleOf(bool, DictOf(int, ReadData))
781+        """
782+
783+    def add_or_renew_lease(self, lease_info):
784+        """
785+        Add a new lease on the shares in this shareset. If the renew_secret
786+        matches an existing lease, that lease will be renewed instead. If
787+        there are no shares in this shareset, return silently. (Note that
788+        in Tahoe-LAFS v1.3.0 and earlier, IndexError was raised if there were
789+        no shares with this shareset's storage index.)
790+
791+        @param lease_info=LeaseInfo
792+        """
793+
794+    def renew_lease(renew_secret, new_expiration_time):
795+        """
796+        Renew a lease on the shares in this shareset, resetting the timer
797+        to 31 days. Some grids will use this, some will not. If there are no
798+        shares in this shareset, IndexError will be raised.
799+
800+        For mutable shares, if the given renew_secret does not match an
801+        existing lease, IndexError will be raised with a note listing the
802+        server-nodeids on the existing leases, so leases on migrated shares
803+        can be renewed. For immutable shares, IndexError (without the note)
804+        will be raised.
805+
806+        @param renew_secret=LeaseRenewSecret
807+        """
808+
809+
810+class IStoredShare(Interface):
811+    """
812+    This object contains as much as all of the share data.  It is intended
813+    for lazy evaluation, such that in many use cases substantially less than
814+    all of the share data will be accessed.
815+    """
816+    def is_complete():
817+        """
818+        Returns True if the share has been fully written and closed, False if it
819+        exists but is still open, or None if the share does not exist.
820+        """
821+
822+    def close():
823+        """
824+        Complete writing to this share.
825+        """
826+
827+    def get_size():
828+        """
829+        Returns the size of the share in bytes.
830+        """
831+
832+    def get_used_space():
833+        """
834+        Returns the amount of backend storage including overhead, in bytes, used
835+        by this share.
836+        """
837+
838+    def get_shnum():
839+        """
840+        Returns the share number.
841+        """
842+
843+    def unlink():
844+        """
845+        Signal that this share can be removed from the backend storage. This does
846+        not guarantee that the share data will be immediately inaccessible, or
847+        that it will be securely erased.
848+        """
849+
850+    def read_share_data(offset, length):
851+        """
852+        Reads beyond the end of the data are truncated. Reads that start
853+        beyond the end of the data return an empty string.
854+        @param offset=int
855+        @param length=int
856+        @return str
857+        """
858+
859+    def write_share_data(offset, data):
860+        """
861+        @param offset=int
862+        @param data=str
863+        """
864+
865+
866 class IStorageBucketWriter(Interface):
867     """
868     Objects of this kind live on the client side.
869hunk ./src/allmydata/interfaces.py 497
870     """
871-    def put_block(segmentnum=int, data=ShareData):
872-        """@param data: For most segments, this data will be 'blocksize'
873+    def put_block(segmentnum, data):
874+        """
875+        @param segmentnum=int
876+        @param data=ShareData: For most segments, this data will be 'blocksize'
877         bytes in length. The last segment might be shorter.
878         @return: a Deferred that fires (with None) when the operation completes
879         """
880hunk ./src/allmydata/interfaces.py 530
881         of plaintext, crypttext, and shares), as well as encoding parameters
882         that are necessary to recover the data. This is a serialized dict
883         mapping strings to other strings. The hash of this data is kept in
884-        the URI and verified before any of the data is used. All buckets for
885-        a given file contain identical copies of this data.
886+        the URI and verified before any of the data is used. All share
887+        containers for a given file contain identical copies of this data.
888 
889         The serialization format is specified with the following pseudocode:
890         for k in sorted(dict.keys()):
891hunk ./src/allmydata/interfaces.py 1777
892     Block Hash, and the encoding parameters, both of which must be included
893     in the URI.
894 
895-    I do not choose shareholders, that is left to the IUploader. I must be
896-    given a dict of RemoteReferences to storage buckets that are ready and
897-    willing to receive data.
898+    I do not choose shareholders, that is left to the IUploader.
899     """
900 
901     def set_size(size):
902hunk ./src/allmydata/interfaces.py 1784
903         """Specify the number of bytes that will be encoded. This must be
904         peformed before get_serialized_params() can be called.
905         """
906+
907     def set_params(params):
908         """Override the default encoding parameters. 'params' is a tuple of
909         (k,d,n), where 'k' is the number of required shares, 'd' is the
910hunk ./src/allmydata/interfaces.py 1880
911     download, validate, decode, and decrypt data from them, writing the
912     results to an output file.
913 
914-    I do not locate the shareholders, that is left to the IDownloader. I must
915-    be given a dict of RemoteReferences to storage buckets that are ready to
916-    send data.
917+    I do not locate the shareholders, that is left to the IDownloader.
918     """
919 
920     def setup(outfile):
921addfile ./src/allmydata/storage/backends/__init__.py
922addfile ./src/allmydata/storage/backends/base.py
923hunk ./src/allmydata/storage/backends/base.py 1
924+
925+from twisted.application import service
926+
927+from allmydata.util.interfaceutil import implements
928+from allmydata.interfaces import IShareSet
929+from allmydata.storage.common import si_b2a
930+from allmydata.storage.lease import LeaseInfo
931+from allmydata.storage.bucket import BucketReader
932+
933+
934+class Backend(service.MultiService):
935+    def __init__(self):
936+        service.MultiService.__init__(self)
937+
938+
939+class ShareSet(object):
940+    implements(IShareSet)
941+    """
942+    This class implements shareset logic that could work for all backends, but
943+    might be useful to override for efficiency.
944+    """
945+
946+    def __init__(self, storageindex):
947+        self.storageindex = storageindex
948+
949+    def get_storage_index(self):
950+        return self.storageindex
951+
952+    def get_storage_index_string(self):
953+        return si_b2a(self.storageindex)
954+
955+    def renew_lease(self, renew_secret, new_expiration_time):
956+        found_buckets = False
957+        for share in self.get_shares():
958+            found_buckets = True
959+            share.renew_lease(renew_secret, new_expiration_time)
960+
961+        if not found_buckets:
962+            raise IndexError("no such lease to renew")
963+
964+    def get_leases(self):
965+        # Since all shares get the same lease data, we just grab the leases
966+        # from the first share.
967+        try:
968+            sf = self.get_shares().next()
969+            return sf.get_leases()
970+        except StopIteration:
971+            return iter([])
972+
973+    def add_or_renew_lease(self, lease_info):
974+        # This implementation assumes that lease data is duplicated in
975+        # all shares of a shareset, which might not be true for all backends.
976+        for share in self.get_shares():
977+            share.add_or_renew_lease(lease_info)
978+
979+    def make_bucket_reader(self, storageserver, share):
980+        return BucketReader(storageserver, share)
981+
982+    def testv_and_readv_and_writev(self, storageserver, secrets,
983+                                   test_and_write_vectors, read_vector,
984+                                   expiration_time):
985+        # The implementation here depends on the following helper methods,
986+        # which must be provided by subclasses:
987+        #
988+        # def _clean_up_after_unlink(self):
989+        #     """clean up resources associated with the shareset after some
990+        #     shares might have been deleted"""
991+        #
992+        # def _create_mutable_share(self, storageserver, shnum, write_enabler):
993+        #     """create a mutable share with the given shnum and write_enabler"""
994+
995+        # This previously had to be a triple with cancel_secret in secrets[2],
996+        # but we now allow the cancel_secret to be omitted.
997+        write_enabler = secrets[0]
998+        renew_secret = secrets[1]
999+
1000+        si_s = self.get_storage_index_string()
1001+        shares = {}
1002+        for share in self.get_shares():
1003+            # XXX is ignoring immutable shares correct? Maybe get_shares should
1004+            # have a parameter saying what type it's expecting.
1005+            if share.sharetype == "mutable":
1006+                share.check_write_enabler(write_enabler, si_s)
1007+                shares[share.get_shnum()] = share
1008+
1009+        # write_enabler is good for all existing shares.
1010+
1011+        # Now evaluate test vectors.
1012+        testv_is_good = True
1013+        for sharenum in test_and_write_vectors:
1014+            (testv, datav, new_length) = test_and_write_vectors[sharenum]
1015+            if sharenum in shares:
1016+                if not shares[sharenum].check_testv(testv):
1017+                    self.log("testv failed: [%d]: %r" % (sharenum, testv))
1018+                    testv_is_good = False
1019+                    break
1020+            else:
1021+                # compare the vectors against an empty share, in which all
1022+                # reads return empty strings.
1023+                if not EmptyShare().check_testv(testv):
1024+                    self.log("testv failed (empty): [%d] %r" % (sharenum,
1025+                                                                testv))
1026+                    testv_is_good = False
1027+                    break
1028+
1029+        # now gather the read vectors, before we do any writes
1030+        read_data = {}
1031+        for shnum, share in shares.items():
1032+            read_data[shnum] = share.readv(read_vector)
1033+
1034+        ownerid = 1 # TODO
1035+        lease_info = LeaseInfo(ownerid, renew_secret,
1036+                               expiration_time, storageserver.get_nodeid())
1037+
1038+        if testv_is_good:
1039+            # now apply the write vectors
1040+            for shnum in test_and_write_vectors:
1041+                (testv, datav, new_length) = test_and_write_vectors[shnum]
1042+                if new_length == 0:
1043+                    if shnum in shares:
1044+                        shares[shnum].unlink()
1045+                else:
1046+                    if shnum not in shares:
1047+                        # allocate a new share
1048+                        share = self._create_mutable_share(storageserver, shnum, write_enabler)
1049+                        shares[shnum] = share
1050+                    shares[shnum].writev(datav, new_length)
1051+                    # and update the lease
1052+                    shares[shnum].add_or_renew_lease(lease_info)
1053+
1054+            if new_length == 0:
1055+                self._clean_up_after_unlink()
1056+
1057+        return (testv_is_good, read_data)
1058+
1059+    def readv(self, wanted_shnums, read_vector):
1060+        """
1061+        Read a vector from the numbered shares in this shareset. An empty
1062+        shares list means to return data from all known shares.
1063+
1064+        @param wanted_shnums=ListOf(int)
1065+        @param read_vector=ReadVector
1066+        @return DictOf(int, ReadData): shnum -> results, with one key per share
1067+        """
1068+        datavs = {}
1069+        for share in self.get_shares():
1070+            # XXX is ignoring immutable shares correct? Maybe get_shares should
1071+            # have a parameter saying what type it's expecting.
1072+            shnum = share.get_shnum()
1073+            if share.sharetype == "mutable" and (not wanted_shnums or shnum in wanted_shnums):
1074+                datavs[shnum] = share.readv(read_vector)
1075+
1076+        return datavs
1077+
1078+
1079+def testv_compare(a, op, b):
1080+    assert op in ("lt", "le", "eq", "ne", "ge", "gt")
1081+    if op == "lt":
1082+        return a < b
1083+    if op == "le":
1084+        return a <= b
1085+    if op == "eq":
1086+        return a == b
1087+    if op == "ne":
1088+        return a != b
1089+    if op == "ge":
1090+        return a >= b
1091+    if op == "gt":
1092+        return a > b
1093+    # never reached
1094+
1095+
1096+class EmptyShare:
1097+    def check_testv(self, testv):
1098+        test_good = True
1099+        for (offset, length, operator, specimen) in testv:
1100+            data = ""
1101+            if not testv_compare(data, operator, specimen):
1102+                test_good = False
1103+                break
1104+        return test_good
1105+
1106addfile ./src/allmydata/storage/backends/disk/__init__.py
1107addfile ./src/allmydata/storage/backends/disk/disk_backend.py
1108hunk ./src/allmydata/storage/backends/disk/disk_backend.py 1
1109+
1110+import re
1111+
1112+from twisted.python.filepath import FilePath, UnlistableError
1113+
1114+from allmydata.interfaceutil import implements
1115+from allmydata.interfaces import IStorageBackend, IShareSet
1116+from allmydata.util import fileutil, log, time_format
1117+from allmydata.util.assertutil import precondition
1118+from allmydata.storage.common import si_b2a, si_a2b
1119+from allmydata.storage.immutable import BucketWriter
1120+from allmydata.storage.backends.base import Backend, ShareSet
1121+from allmydata.storage.backends.disk.immutable import ImmutableDiskShare
1122+from allmydata.storage.backends.disk.mutable import MutableDiskShare, create_mutable_sharefile
1123+
1124+# storage/
1125+# storage/shares/incoming
1126+#   incoming/ holds temp dirs named $START/$STORAGEINDEX/$SHARENUM which will
1127+#   be moved to storage/shares/$START/$STORAGEINDEX/$SHARENUM upon success
1128+# storage/shares/$START/$STORAGEINDEX
1129+# storage/shares/$START/$STORAGEINDEX/$SHARENUM
1130+
1131+# Where "$START" denotes the first 10 bits worth of $STORAGEINDEX (that's 2
1132+# base-32 chars).
1133+# $SHARENUM matches this regex:
1134+NUM_RE=re.compile("^[0-9]+$")
1135+
1136+
1137+def si_si2dir(startfp, storageindex):
1138+    sia = si_b2a(storageindex)
1139+    newfp = startfp.child(sia[:2])
1140+    return newfp.child(sia)
1141+
1142+
1143+def get_share(fp):
1144+    f = fp.open('rb')
1145+    try:
1146+        prefix = f.read(32)
1147+    finally:
1148+        f.close()
1149+
1150+    if prefix == MutableDiskShare.MAGIC:
1151+        return MutableDiskShare(fp)
1152+    else:
1153+        # assume it's immutable
1154+        return ImmutableDiskShare(fp)
1155+
1156+
1157+class DiskBackend(Backend):
1158+    implements(IStorageBackend)
1159+
1160+    def __init__(self, storedir, expiration_policy, readonly=False, reserved_space=0):
1161+        Backend.__init__(self)
1162+        self._setup_storage(storedir, readonly, reserved_space)
1163+        self._setup_corruption_advisory()
1164+
1165+    def _setup_storage(self, storedir, readonly, reserved_space):
1166+        precondition(isinstance(storedir, FilePath), storedir, FilePath)
1167+        self.storedir = storedir
1168+        self.readonly = readonly
1169+        self.reserved_space = int(reserved_space)
1170+        self.sharedir = self.storedir.child("shares")
1171+        fileutil.fp_make_dirs(self.sharedir)
1172+        self.incomingdir = self.sharedir.child('incoming')
1173+        self._clean_incomplete()
1174+        if self.reserved_space and (self.get_available_space() is None):
1175+            log.msg("warning: [storage]reserved_space= is set, but this platform does not support an API to get disk statistics (statvfs(2) or GetDiskFreeSpaceEx), so this reservation cannot be honored",
1176+                    umid="0wZ27w", level=log.UNUSUAL)
1177+
1178+    def _clean_incomplete(self):
1179+        fileutil.fp_remove(self.incomingdir)
1180+        fileutil.fp_make_dirs(self.incomingdir)
1181+
1182+    def _setup_corruption_advisory(self):
1183+        # we don't actually create the corruption-advisory dir until necessary
1184+        self.corruption_advisory_dir = self.storedir.child("corruption-advisories")
1185+
1186+    def _make_shareset(self, sharehomedir):
1187+        return self.get_shareset(si_a2b(sharehomedir.basename()))
1188+
1189+    def get_sharesets_for_prefix(self, prefix):
1190+        prefixfp = self.sharedir.child(prefix)
1191+        try:
1192+            sharesets = map(self._make_shareset, prefixfp.children())
1193+            def _by_base32si(b):
1194+                return b.get_storage_index_string()
1195+            sharesets.sort(key=_by_base32si)
1196+        except EnvironmentError:
1197+            sharesets = []
1198+        return sharesets
1199+
1200+    def get_shareset(self, storageindex):
1201+        sharehomedir = si_si2dir(self.sharedir, storageindex)
1202+        incominghomedir = si_si2dir(self.incomingdir, storageindex)
1203+        return DiskShareSet(storageindex, sharehomedir, incominghomedir)
1204+
1205+    def fill_in_space_stats(self, stats):
1206+        try:
1207+            disk = fileutil.get_disk_stats(self.sharedir, self.reserved_space)
1208+            writeable = disk['avail'] > 0
1209+
1210+            # spacetime predictors should use disk_avail / (d(disk_used)/dt)
1211+            stats['storage_server.disk_total'] = disk['total']
1212+            stats['storage_server.disk_used'] = disk['used']
1213+            stats['storage_server.disk_free_for_root'] = disk['free_for_root']
1214+            stats['storage_server.disk_free_for_nonroot'] = disk['free_for_nonroot']
1215+            stats['storage_server.disk_avail'] = disk['avail']
1216+        except AttributeError:
1217+            writeable = True
1218+        except EnvironmentError:
1219+            log.msg("OS call to get disk statistics failed", level=log.UNUSUAL)
1220+            writeable = False
1221+
1222+        if self.readonly_storage:
1223+            stats['storage_server.disk_avail'] = 0
1224+            writeable = False
1225+
1226+        stats['storage_server.accepting_immutable_shares'] = int(writeable)
1227+
1228+    def get_available_space(self):
1229+        if self.readonly:
1230+            return 0
1231+        return fileutil.get_available_space(self.sharedir, self.reserved_space)
1232+
1233+    #def set_storage_server(self, ss):
1234+    #    self.ss = ss
1235+
1236+    def advise_corrupt_share(self, storageindex, sharetype, shnum, reason):
1237+        fileutil.fp_make_dirs(self.corruption_advisory_dir)
1238+        now = time_format.iso_utc(sep="T")
1239+        si_s = si_b2a(storageindex)
1240+
1241+        # Windows can't handle colons in the filename.
1242+        name = ("%s--%s-%d" % (now, si_s, shnum)).replace(":", "")
1243+        f = self.corruption_advisory_dir.child(name).open("w")
1244+        try:
1245+            f.write("report: Share Corruption\n")
1246+            f.write("type: %s\n" % sharetype)
1247+            f.write("storage_index: %s\n" % si_s)
1248+            f.write("share_number: %d\n" % shnum)
1249+            f.write("\n")
1250+            f.write(reason)
1251+            f.write("\n")
1252+        finally:
1253+            f.close()
1254+        log.msg(format=("client claims corruption in (%(share_type)s) " +
1255+                        "%(si)s-%(shnum)d: %(reason)s"),
1256+                share_type=sharetype, si=si_s, shnum=shnum, reason=reason,
1257+                level=log.SCARY, umid="SGx2fA")
1258+
1259+
1260+class DiskShareSet(ShareSet):
1261+    implements(IShareSet)
1262+
1263+    def __init__(self, storageindex, sharehomedir, incominghomedir=None):
1264+        ShareSet.__init__(storageindex)
1265+        self.sharehomedir = sharehomedir
1266+        self.incominghomedir = incominghomedir
1267+
1268+    def get_overhead(self):
1269+        return (fileutil.get_disk_usage(self.sharehomedir) +
1270+                fileutil.get_disk_usage(self.incominghomedir))
1271+
1272+    def get_shares(self):
1273+        """
1274+        Generate IStorageBackendShare objects for shares we have for this storage index.
1275+        ("Shares we have" means completed ones, excluding incoming ones.)
1276+        """
1277+        try:
1278+            for fp in self.sharehomedir.children():
1279+                shnumstr = fp.basename()
1280+                if not NUM_RE.match(shnumstr):
1281+                    continue
1282+                sharehome = self.sharehomedir.child(shnumstr)
1283+                yield self.get_share(sharehome)
1284+        except UnlistableError:
1285+            # There is no shares directory at all.
1286+            pass
1287+
1288+    def get_incoming_shnums(self):
1289+        """
1290+        Return a frozenset of the shnum (as ints) of incoming shares.
1291+        """
1292+        if self.incominghomedir is None:
1293+            return frozenset()
1294+        try:
1295+            childfps = [ fp for fp in self.incominghomedir.children() if NUM_RE.match(fp.basename()) ]
1296+            shnums = [ int(fp.basename()) for fp in childfps]
1297+            return frozenset(shnums)
1298+        except UnlistableError:
1299+            # There is no incoming directory at all.
1300+            return frozenset()
1301+
1302+    def make_bucket_writer(self, storageserver, shnum, max_space_per_bucket, lease_info, canary):
1303+        sharehome = self.sharehomedir.child(str(shnum))
1304+        incominghome = self.incominghomedir.child(str(shnum))
1305+        immsh = ImmutableDiskShare(self.storageindex, shnum, sharehome, incominghome,
1306+                                   max_size=max_space_per_bucket, create=True)
1307+        bw = BucketWriter(storageserver, immsh, max_space_per_bucket, lease_info, canary)
1308+        return bw
1309+
1310+    def _create_mutable_share(self, storageserver, shnum, write_enabler):
1311+        fileutil.fp_make_dirs(self.sharehomedir)
1312+        sharehome = self.sharehomedir.child(str(shnum))
1313+        nodeid = storageserver.get_nodeid()
1314+        return create_mutable_sharefile(sharehome, nodeid, write_enabler, storageserver)
1315+
1316+    def _clean_up_after_unlink(self):
1317+        fileutil.fp_rmdir_if_empty(self.sharehomedir)
1318+
1319hunk ./src/allmydata/storage/backends/disk/immutable.py 1
1320-import os, stat, struct, time
1321 
1322hunk ./src/allmydata/storage/backends/disk/immutable.py 2
1323-from foolscap.api import Referenceable
1324+import struct
1325 
1326 from allmydata.util.interfaceutil import implements
1327hunk ./src/allmydata/storage/backends/disk/immutable.py 5
1328-from allmydata.interfaces import RIBucketWriter, RIBucketReader
1329-from allmydata.util import base32, fileutil, log
1330+
1331+from allmydata.interfaces import IStoredShare
1332+from allmydata.util import fileutil
1333 from allmydata.util.assertutil import precondition
1334hunk ./src/allmydata/storage/backends/disk/immutable.py 9
1335+from allmydata.util.fileutil import fp_make_dirs
1336 from allmydata.util.hashutil import constant_time_compare
1337hunk ./src/allmydata/storage/backends/disk/immutable.py 11
1338+from allmydata.util.encodingutil import quote_filepath
1339+from allmydata.storage.common import si_b2a, UnknownImmutableContainerVersionError, DataTooLargeError
1340 from allmydata.storage.lease import LeaseInfo
1341hunk ./src/allmydata/storage/backends/disk/immutable.py 14
1342-from allmydata.storage.common import UnknownImmutableContainerVersionError, \
1343-     DataTooLargeError
1344+
1345 
1346 # each share file (in storage/shares/$SI/$SHNUM) contains lease information
1347 # and share data. The share data is accessed by RIBucketWriter.write and
1348hunk ./src/allmydata/storage/backends/disk/immutable.py 41
1349 # then the value stored in this field will be the actual share data length
1350 # modulo 2**32.
1351 
1352-class ShareFile:
1353-    LEASE_SIZE = struct.calcsize(">L32s32sL")
1354+class ImmutableDiskShare(object):
1355+    implements(IStoredShare)
1356+
1357     sharetype = "immutable"
1358hunk ./src/allmydata/storage/backends/disk/immutable.py 45
1359+    LEASE_SIZE = struct.calcsize(">L32s32sL")
1360+
1361 
1362hunk ./src/allmydata/storage/backends/disk/immutable.py 48
1363-    def __init__(self, filename, max_size=None, create=False):
1364-        """ If max_size is not None then I won't allow more than max_size to be written to me. If create=True and max_size must not be None. """
1365+    def __init__(self, storageindex, shnum, finalhome=None, incominghome=None, max_size=None, create=False):
1366+        """ If max_size is not None then I won't allow more than
1367+        max_size to be written to me. If create=True then max_size
1368+        must not be None. """
1369         precondition((max_size is not None) or (not create), max_size, create)
1370hunk ./src/allmydata/storage/backends/disk/immutable.py 53
1371-        self.home = filename
1372+        self.storageindex = storageindex
1373         self._max_size = max_size
1374hunk ./src/allmydata/storage/backends/disk/immutable.py 55
1375+        self.incominghome = incominghome
1376+        self.finalhome = finalhome
1377+        self.shnum = shnum
1378         if create:
1379             # touch the file, so later callers will see that we're working on
1380             # it. Also construct the metadata.
1381hunk ./src/allmydata/storage/backends/disk/immutable.py 61
1382-            assert not os.path.exists(self.home)
1383-            fileutil.make_dirs(os.path.dirname(self.home))
1384-            f = open(self.home, 'wb')
1385+            assert not finalhome.exists()
1386+            fp_make_dirs(self.incominghome.parent())
1387             # The second field -- the four-byte share data length -- is no
1388             # longer used as of Tahoe v1.3.0, but we continue to write it in
1389             # there in case someone downgrades a storage server from >=
1390hunk ./src/allmydata/storage/backends/disk/immutable.py 72
1391             # the largest length that can fit into the field. That way, even
1392             # if this does happen, the old < v1.3.0 server will still allow
1393             # clients to read the first part of the share.
1394-            f.write(struct.pack(">LLL", 1, min(2**32-1, max_size), 0))
1395-            f.close()
1396+            self.incominghome.setContent(struct.pack(">LLL", 1, min(2**32-1, max_size), 0) )
1397             self._lease_offset = max_size + 0x0c
1398             self._num_leases = 0
1399         else:
1400hunk ./src/allmydata/storage/backends/disk/immutable.py 76
1401-            f = open(self.home, 'rb')
1402-            filesize = os.path.getsize(self.home)
1403-            (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1404-            f.close()
1405+            f = self.finalhome.open(mode='rb')
1406+            try:
1407+                (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1408+            finally:
1409+                f.close()
1410+            filesize = self.finalhome.getsize()
1411             if version != 1:
1412                 msg = "sharefile %s had version %d but we wanted 1" % \
1413hunk ./src/allmydata/storage/backends/disk/immutable.py 84
1414-                      (filename, version)
1415+                      (self.finalhome, version)
1416                 raise UnknownImmutableContainerVersionError(msg)
1417             self._num_leases = num_leases
1418             self._lease_offset = filesize - (num_leases * self.LEASE_SIZE)
1419hunk ./src/allmydata/storage/backends/disk/immutable.py 90
1420         self._data_offset = 0xc
1421 
1422+    def __repr__(self):
1423+        return ("<ImmutableDiskShare %s:%r at %s>"
1424+                % (si_b2a(self.storageindex), self.shnum, quote_filepath(self.finalhome)))
1425+
1426+    def close(self):
1427+        fileutil.fp_make_dirs(self.finalhome.parent())
1428+        self.incominghome.moveTo(self.finalhome)
1429+        try:
1430+            # self.incominghome is like storage/shares/incoming/ab/abcde/4 .
1431+            # We try to delete the parent (.../ab/abcde) to avoid leaving
1432+            # these directories lying around forever, but the delete might
1433+            # fail if we're working on another share for the same storage
1434+            # index (like ab/abcde/5). The alternative approach would be to
1435+            # use a hierarchy of objects (PrefixHolder, BucketHolder,
1436+            # ShareWriter), each of which is responsible for a single
1437+            # directory on disk, and have them use reference counting of
1438+            # their children to know when they should do the rmdir. This
1439+            # approach is simpler, but relies on os.rmdir refusing to delete
1440+            # a non-empty directory. Do *not* use fileutil.rm_dir() here!
1441+            fileutil.fp_rmdir_if_empty(self.incominghome.parent())
1442+            # we also delete the grandparent (prefix) directory, .../ab ,
1443+            # again to avoid leaving directories lying around. This might
1444+            # fail if there is another bucket open that shares a prefix (like
1445+            # ab/abfff).
1446+            fileutil.fp_rmdir_if_empty(self.incominghome.parent().parent())
1447+            # we leave the great-grandparent (incoming/) directory in place.
1448+        except EnvironmentError:
1449+            # ignore the "can't rmdir because the directory is not empty"
1450+            # exceptions, those are normal consequences of the
1451+            # above-mentioned conditions.
1452+            pass
1453+        pass
1454+
1455+    def get_used_space(self):
1456+        return (fileutil.get_used_space(self.finalhome) +
1457+                fileutil.get_used_space(self.incominghome))
1458+
1459+    def get_shnum(self):
1460+        return self.shnum
1461+
1462     def unlink(self):
1463hunk ./src/allmydata/storage/backends/disk/immutable.py 131
1464-        os.unlink(self.home)
1465+        self.finalhome.remove()
1466 
1467     def read_share_data(self, offset, length):
1468         precondition(offset >= 0)
1469hunk ./src/allmydata/storage/backends/disk/immutable.py 135
1470-        # reads beyond the end of the data are truncated. Reads that start
1471+
1472+        # Reads beyond the end of the data are truncated. Reads that start
1473         # beyond the end of the data return an empty string.
1474         seekpos = self._data_offset+offset
1475         actuallength = max(0, min(length, self._lease_offset-seekpos))
1476hunk ./src/allmydata/storage/backends/disk/immutable.py 142
1477         if actuallength == 0:
1478             return ""
1479-        f = open(self.home, 'rb')
1480-        f.seek(seekpos)
1481-        return f.read(actuallength)
1482+        f = self.finalhome.open(mode='rb')
1483+        try:
1484+            f.seek(seekpos)
1485+            sharedata = f.read(actuallength)
1486+        finally:
1487+            f.close()
1488+        return sharedata
1489 
1490     def write_share_data(self, offset, data):
1491         length = len(data)
1492hunk ./src/allmydata/storage/backends/disk/immutable.py 155
1493         precondition(offset >= 0, offset)
1494         if self._max_size is not None and offset+length > self._max_size:
1495             raise DataTooLargeError(self._max_size, offset, length)
1496-        f = open(self.home, 'rb+')
1497-        real_offset = self._data_offset+offset
1498-        f.seek(real_offset)
1499-        assert f.tell() == real_offset
1500-        f.write(data)
1501-        f.close()
1502+        f = self.incominghome.open(mode='rb+')
1503+        try:
1504+            real_offset = self._data_offset+offset
1505+            f.seek(real_offset)
1506+            assert f.tell() == real_offset
1507+            f.write(data)
1508+        finally:
1509+            f.close()
1510 
1511     def _write_lease_record(self, f, lease_number, lease_info):
1512         offset = self._lease_offset + lease_number * self.LEASE_SIZE
1513hunk ./src/allmydata/storage/backends/disk/immutable.py 172
1514 
1515     def _read_num_leases(self, f):
1516         f.seek(0x08)
1517-        (num_leases,) = struct.unpack(">L", f.read(4))
1518+        ro = f.read(4)
1519+        (num_leases,) = struct.unpack(">L", ro)
1520         return num_leases
1521 
1522     def _write_num_leases(self, f, num_leases):
1523hunk ./src/allmydata/storage/backends/disk/immutable.py 183
1524     def _truncate_leases(self, f, num_leases):
1525         f.truncate(self._lease_offset + num_leases * self.LEASE_SIZE)
1526 
1527+    # These lease operations are intended for use by disk_backend.py.
1528+    # Other clients should not depend on the fact that the disk backend
1529+    # stores leases in share files.
1530+
1531     def get_leases(self):
1532         """Yields a LeaseInfo instance for all leases."""
1533hunk ./src/allmydata/storage/backends/disk/immutable.py 189
1534-        f = open(self.home, 'rb')
1535-        (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1536-        f.seek(self._lease_offset)
1537-        for i in range(num_leases):
1538-            data = f.read(self.LEASE_SIZE)
1539-            if data:
1540-                yield LeaseInfo().from_immutable_data(data)
1541+        f = self.finalhome.open(mode='rb')
1542+        try:
1543+            (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1544+            f.seek(self._lease_offset)
1545+            for i in range(num_leases):
1546+                data = f.read(self.LEASE_SIZE)
1547+                if data:
1548+                    yield LeaseInfo().from_immutable_data(data)
1549+        finally:
1550+            f.close()
1551 
1552     def add_lease(self, lease_info):
1553hunk ./src/allmydata/storage/backends/disk/immutable.py 201
1554-        f = open(self.home, 'rb+')
1555-        num_leases = self._read_num_leases(f)
1556-        self._write_lease_record(f, num_leases, lease_info)
1557-        self._write_num_leases(f, num_leases+1)
1558-        f.close()
1559+        num_leases = self._read_num_leases(self.incominghome)
1560+        f = self.finalhome.open(mode='wb+')
1561+        try:
1562+            self._write_lease_record(f, num_leases, lease_info)
1563+            self._write_num_leases(f, num_leases+1)
1564+        finally:
1565+            f.close()
1566 
1567     def renew_lease(self, renew_secret, new_expire_time):
1568hunk ./src/allmydata/storage/backends/disk/immutable.py 210
1569-        for i,lease in enumerate(self.get_leases()):
1570+        for i, lease in enumerate(self.get_leases()):
1571             if constant_time_compare(lease.renew_secret, renew_secret):
1572                 # yup. See if we need to update the owner time.
1573                 if new_expire_time > lease.expiration_time:
1574hunk ./src/allmydata/storage/backends/disk/immutable.py 216
1575                     # yes
1576                     lease.expiration_time = new_expire_time
1577-                    f = open(self.home, 'rb+')
1578-                    self._write_lease_record(f, i, lease)
1579-                    f.close()
1580+                    f = self.finalhome.open('rb+')
1581+                    try:
1582+                        self._write_lease_record(f, i, lease)
1583+                    finally:
1584+                        f.close()
1585                 return
1586         raise IndexError("unable to renew non-existent lease")
1587 
1588hunk ./src/allmydata/storage/backends/disk/immutable.py 230
1589                              lease_info.expiration_time)
1590         except IndexError:
1591             self.add_lease(lease_info)
1592-
1593-
1594-    def cancel_lease(self, cancel_secret):
1595-        """Remove a lease with the given cancel_secret. If the last lease is
1596-        cancelled, the file will be removed. Return the number of bytes that
1597-        were freed (by truncating the list of leases, and possibly by
1598-        deleting the file. Raise IndexError if there was no lease with the
1599-        given cancel_secret.
1600-        """
1601-
1602-        leases = list(self.get_leases())
1603-        num_leases_removed = 0
1604-        for i,lease in enumerate(leases):
1605-            if constant_time_compare(lease.cancel_secret, cancel_secret):
1606-                leases[i] = None
1607-                num_leases_removed += 1
1608-        if not num_leases_removed:
1609-            raise IndexError("unable to find matching lease to cancel")
1610-        if num_leases_removed:
1611-            # pack and write out the remaining leases. We write these out in
1612-            # the same order as they were added, so that if we crash while
1613-            # doing this, we won't lose any non-cancelled leases.
1614-            leases = [l for l in leases if l] # remove the cancelled leases
1615-            f = open(self.home, 'rb+')
1616-            for i,lease in enumerate(leases):
1617-                self._write_lease_record(f, i, lease)
1618-            self._write_num_leases(f, len(leases))
1619-            self._truncate_leases(f, len(leases))
1620-            f.close()
1621-        space_freed = self.LEASE_SIZE * num_leases_removed
1622-        if not len(leases):
1623-            space_freed += os.stat(self.home)[stat.ST_SIZE]
1624-            self.unlink()
1625-        return space_freed
1626-
1627-
1628-class BucketWriter(Referenceable):
1629-    implements(RIBucketWriter)
1630-
1631-    def __init__(self, ss, incominghome, finalhome, max_size, lease_info, canary):
1632-        self.ss = ss
1633-        self.incominghome = incominghome
1634-        self.finalhome = finalhome
1635-        self._max_size = max_size # don't allow the client to write more than this
1636-        self._canary = canary
1637-        self._disconnect_marker = canary.notifyOnDisconnect(self._disconnected)
1638-        self.closed = False
1639-        self.throw_out_all_data = False
1640-        self._sharefile = ShareFile(incominghome, create=True, max_size=max_size)
1641-        # also, add our lease to the file now, so that other ones can be
1642-        # added by simultaneous uploaders
1643-        self._sharefile.add_lease(lease_info)
1644-
1645-    def allocated_size(self):
1646-        return self._max_size
1647-
1648-    def remote_write(self, offset, data):
1649-        start = time.time()
1650-        precondition(not self.closed)
1651-        if self.throw_out_all_data:
1652-            return
1653-        self._sharefile.write_share_data(offset, data)
1654-        self.ss.add_latency("write", time.time() - start)
1655-        self.ss.count("write")
1656-
1657-    def remote_close(self):
1658-        precondition(not self.closed)
1659-        start = time.time()
1660-
1661-        fileutil.make_dirs(os.path.dirname(self.finalhome))
1662-        fileutil.rename(self.incominghome, self.finalhome)
1663-        try:
1664-            # self.incominghome is like storage/shares/incoming/ab/abcde/4 .
1665-            # We try to delete the parent (.../ab/abcde) to avoid leaving
1666-            # these directories lying around forever, but the delete might
1667-            # fail if we're working on another share for the same storage
1668-            # index (like ab/abcde/5). The alternative approach would be to
1669-            # use a hierarchy of objects (PrefixHolder, BucketHolder,
1670-            # ShareWriter), each of which is responsible for a single
1671-            # directory on disk, and have them use reference counting of
1672-            # their children to know when they should do the rmdir. This
1673-            # approach is simpler, but relies on os.rmdir refusing to delete
1674-            # a non-empty directory. Do *not* use fileutil.rm_dir() here!
1675-            os.rmdir(os.path.dirname(self.incominghome))
1676-            # we also delete the grandparent (prefix) directory, .../ab ,
1677-            # again to avoid leaving directories lying around. This might
1678-            # fail if there is another bucket open that shares a prefix (like
1679-            # ab/abfff).
1680-            os.rmdir(os.path.dirname(os.path.dirname(self.incominghome)))
1681-            # we leave the great-grandparent (incoming/) directory in place.
1682-        except EnvironmentError:
1683-            # ignore the "can't rmdir because the directory is not empty"
1684-            # exceptions, those are normal consequences of the
1685-            # above-mentioned conditions.
1686-            pass
1687-        self._sharefile = None
1688-        self.closed = True
1689-        self._canary.dontNotifyOnDisconnect(self._disconnect_marker)
1690-
1691-        filelen = os.stat(self.finalhome)[stat.ST_SIZE]
1692-        self.ss.bucket_writer_closed(self, filelen)
1693-        self.ss.add_latency("close", time.time() - start)
1694-        self.ss.count("close")
1695-
1696-    def _disconnected(self):
1697-        if not self.closed:
1698-            self._abort()
1699-
1700-    def remote_abort(self):
1701-        log.msg("storage: aborting sharefile %s" % self.incominghome,
1702-                facility="tahoe.storage", level=log.UNUSUAL)
1703-        if not self.closed:
1704-            self._canary.dontNotifyOnDisconnect(self._disconnect_marker)
1705-        self._abort()
1706-        self.ss.count("abort")
1707-
1708-    def _abort(self):
1709-        if self.closed:
1710-            return
1711-
1712-        os.remove(self.incominghome)
1713-        # if we were the last share to be moved, remove the incoming/
1714-        # directory that was our parent
1715-        parentdir = os.path.split(self.incominghome)[0]
1716-        if not os.listdir(parentdir):
1717-            os.rmdir(parentdir)
1718-        self._sharefile = None
1719-
1720-        # We are now considered closed for further writing. We must tell
1721-        # the storage server about this so that it stops expecting us to
1722-        # use the space it allocated for us earlier.
1723-        self.closed = True
1724-        self.ss.bucket_writer_closed(self, 0)
1725-
1726-
1727-class BucketReader(Referenceable):
1728-    implements(RIBucketReader)
1729-
1730-    def __init__(self, ss, sharefname, storage_index=None, shnum=None):
1731-        self.ss = ss
1732-        self._share_file = ShareFile(sharefname)
1733-        self.storage_index = storage_index
1734-        self.shnum = shnum
1735-
1736-    def __repr__(self):
1737-        return "<%s %s %s>" % (self.__class__.__name__,
1738-                               base32.b2a_l(self.storage_index[:8], 60),
1739-                               self.shnum)
1740-
1741-    def remote_read(self, offset, length):
1742-        start = time.time()
1743-        data = self._share_file.read_share_data(offset, length)
1744-        self.ss.add_latency("read", time.time() - start)
1745-        self.ss.count("read")
1746-        return data
1747-
1748-    def remote_advise_corrupt_share(self, reason):
1749-        return self.ss.remote_advise_corrupt_share("immutable",
1750-                                                   self.storage_index,
1751-                                                   self.shnum,
1752-                                                   reason)
1753hunk ./src/allmydata/storage/backends/disk/mutable.py 1
1754-import os, stat, struct
1755 
1756hunk ./src/allmydata/storage/backends/disk/mutable.py 2
1757-from allmydata.interfaces import BadWriteEnablerError
1758-from allmydata.util import idlib, log
1759+import struct
1760+
1761+from allmydata.interfaceutil import implements
1762+from allmydata.interfaces import IStoredShare, BadWriteEnablerError
1763+from allmydata.util import fileutil, idlib, log
1764 from allmydata.util.assertutil import precondition
1765 from allmydata.util.hashutil import constant_time_compare
1766hunk ./src/allmydata/storage/backends/disk/mutable.py 9
1767-from allmydata.storage.lease import LeaseInfo
1768-from allmydata.storage.common import UnknownMutableContainerVersionError, \
1769+from allmydata.util.encodingutil import quote_filepath
1770+from allmydata.storage.common import si_b2a, UnknownMutableContainerVersionError, \
1771      DataTooLargeError
1772hunk ./src/allmydata/storage/backends/disk/mutable.py 12
1773+from allmydata.storage.lease import LeaseInfo
1774+from allmydata.storage.backends.base import testv_compare
1775+
1776 
1777hunk ./src/allmydata/storage/backends/disk/mutable.py 16
1778-# the MutableShareFile is like the ShareFile, but used for mutable data. It
1779-# has a different layout. See docs/mutable.txt for more details.
1780+# The MutableDiskShare is like the ImmutableDiskShare, but used for mutable data.
1781+# It has a different layout. See docs/mutable.rst for more details.
1782 
1783 # #   offset    size    name
1784 # 1   0         32      magic verstr "tahoe mutable container v1" plus binary
1785hunk ./src/allmydata/storage/backends/disk/mutable.py 30
1786 #                        4    4   expiration timestamp
1787 #                        8   32   renewal token
1788 #                        40  32   cancel token
1789-#                        72  20   nodeid which accepted the tokens
1790+#                        72  20   nodeid that accepted the tokens
1791 # 7   468       (a)     data
1792 # 8   ??        4       count of extra leases
1793 # 9   ??        n*92    extra leases
1794hunk ./src/allmydata/storage/backends/disk/mutable.py 36
1795 
1796 
1797-# The struct module doc says that L's are 4 bytes in size., and that Q's are
1798+# The struct module doc says that L's are 4 bytes in size, and that Q's are
1799 # 8 bytes in size. Since compatibility depends upon this, double-check it.
1800 assert struct.calcsize(">L") == 4, struct.calcsize(">L")
1801 assert struct.calcsize(">Q") == 8, struct.calcsize(">Q")
1802hunk ./src/allmydata/storage/backends/disk/mutable.py 41
1803 
1804-class MutableShareFile:
1805+
1806+class MutableDiskShare(object):
1807+    implements(IStoredShare)
1808 
1809     sharetype = "mutable"
1810     DATA_LENGTH_OFFSET = struct.calcsize(">32s20s32s")
1811hunk ./src/allmydata/storage/backends/disk/mutable.py 53
1812     assert LEASE_SIZE == 92
1813     DATA_OFFSET = HEADER_SIZE + 4*LEASE_SIZE
1814     assert DATA_OFFSET == 468, DATA_OFFSET
1815+
1816     # our sharefiles share with a recognizable string, plus some random
1817     # binary data to reduce the chance that a regular text file will look
1818     # like a sharefile.
1819hunk ./src/allmydata/storage/backends/disk/mutable.py 62
1820     MAX_SIZE = 2*1000*1000*1000 # 2GB, kind of arbitrary
1821     # TODO: decide upon a policy for max share size
1822 
1823-    def __init__(self, filename, parent=None):
1824-        self.home = filename
1825-        if os.path.exists(self.home):
1826+    def __init__(self, storageindex, shnum, home, parent=None):
1827+        self.storageindex = storageindex
1828+        self.shnum = shnum
1829+        self.home = home
1830+        if self.home.exists():
1831             # we don't cache anything, just check the magic
1832hunk ./src/allmydata/storage/backends/disk/mutable.py 68
1833-            f = open(self.home, 'rb')
1834-            data = f.read(self.HEADER_SIZE)
1835-            (magic,
1836-             write_enabler_nodeid, write_enabler,
1837-             data_length, extra_least_offset) = \
1838-             struct.unpack(">32s20s32sQQ", data)
1839-            if magic != self.MAGIC:
1840-                msg = "sharefile %s had magic '%r' but we wanted '%r'" % \
1841-                      (filename, magic, self.MAGIC)
1842-                raise UnknownMutableContainerVersionError(msg)
1843+            f = self.home.open('rb')
1844+            try:
1845+                data = f.read(self.HEADER_SIZE)
1846+                (magic,
1847+                 write_enabler_nodeid, write_enabler,
1848+                 data_length, extra_least_offset) = \
1849+                 struct.unpack(">32s20s32sQQ", data)
1850+                if magic != self.MAGIC:
1851+                    msg = "sharefile %s had magic '%r' but we wanted '%r'" % \
1852+                          (quote_filepath(self.home), magic, self.MAGIC)
1853+                    raise UnknownMutableContainerVersionError(msg)
1854+            finally:
1855+                f.close()
1856         self.parent = parent # for logging
1857 
1858     def log(self, *args, **kwargs):
1859hunk ./src/allmydata/storage/backends/disk/mutable.py 87
1860         return self.parent.log(*args, **kwargs)
1861 
1862     def create(self, my_nodeid, write_enabler):
1863-        assert not os.path.exists(self.home)
1864+        assert not self.home.exists()
1865         data_length = 0
1866         extra_lease_offset = (self.HEADER_SIZE
1867                               + 4 * self.LEASE_SIZE
1868hunk ./src/allmydata/storage/backends/disk/mutable.py 94
1869                               + data_length)
1870         assert extra_lease_offset == self.DATA_OFFSET # true at creation
1871         num_extra_leases = 0
1872-        f = open(self.home, 'wb')
1873-        header = struct.pack(">32s20s32sQQ",
1874-                             self.MAGIC, my_nodeid, write_enabler,
1875-                             data_length, extra_lease_offset,
1876-                             )
1877-        leases = ("\x00"*self.LEASE_SIZE) * 4
1878-        f.write(header + leases)
1879-        # data goes here, empty after creation
1880-        f.write(struct.pack(">L", num_extra_leases))
1881-        # extra leases go here, none at creation
1882-        f.close()
1883+        f = self.home.open('wb')
1884+        try:
1885+            header = struct.pack(">32s20s32sQQ",
1886+                                 self.MAGIC, my_nodeid, write_enabler,
1887+                                 data_length, extra_lease_offset,
1888+                                 )
1889+            leases = ("\x00"*self.LEASE_SIZE) * 4
1890+            f.write(header + leases)
1891+            # data goes here, empty after creation
1892+            f.write(struct.pack(">L", num_extra_leases))
1893+            # extra leases go here, none at creation
1894+        finally:
1895+            f.close()
1896+
1897+    def __repr__(self):
1898+        return ("<MutableDiskShare %s:%r at %s>"
1899+                % (si_b2a(self.storageindex), self.shnum, quote_filepath(self.home)))
1900+
1901+    def get_used_space(self):
1902+        return fileutil.get_used_space(self.home)
1903+
1904+    def get_shnum(self):
1905+        return self.shnum
1906 
1907     def unlink(self):
1908hunk ./src/allmydata/storage/backends/disk/mutable.py 119
1909-        os.unlink(self.home)
1910+        self.home.remove()
1911 
1912     def _read_data_length(self, f):
1913         f.seek(self.DATA_LENGTH_OFFSET)
1914hunk ./src/allmydata/storage/backends/disk/mutable.py 287
1915 
1916     def get_leases(self):
1917         """Yields a LeaseInfo instance for all leases."""
1918-        f = open(self.home, 'rb')
1919-        for i, lease in self._enumerate_leases(f):
1920-            yield lease
1921-        f.close()
1922+        f = self.home.open('rb')
1923+        try:
1924+            for i, lease in self._enumerate_leases(f):
1925+                yield lease
1926+        finally:
1927+            f.close()
1928 
1929     def _enumerate_leases(self, f):
1930         for i in range(self._get_num_lease_slots(f)):
1931hunk ./src/allmydata/storage/backends/disk/mutable.py 299
1932             try:
1933                 data = self._read_lease_record(f, i)
1934                 if data is not None:
1935-                    yield i,data
1936+                    yield i, data
1937             except IndexError:
1938                 return
1939 
1940hunk ./src/allmydata/storage/backends/disk/mutable.py 303
1941+    # These lease operations are intended for use by disk_backend.py.
1942+    # Other clients should not depend on the fact that the disk backend
1943+    # stores leases in share files.
1944+
1945     def add_lease(self, lease_info):
1946         precondition(lease_info.owner_num != 0) # 0 means "no lease here"
1947hunk ./src/allmydata/storage/backends/disk/mutable.py 309
1948-        f = open(self.home, 'rb+')
1949-        num_lease_slots = self._get_num_lease_slots(f)
1950-        empty_slot = self._get_first_empty_lease_slot(f)
1951-        if empty_slot is not None:
1952-            self._write_lease_record(f, empty_slot, lease_info)
1953-        else:
1954-            self._write_lease_record(f, num_lease_slots, lease_info)
1955-        f.close()
1956+        f = self.home.open('rb+')
1957+        try:
1958+            num_lease_slots = self._get_num_lease_slots(f)
1959+            empty_slot = self._get_first_empty_lease_slot(f)
1960+            if empty_slot is not None:
1961+                self._write_lease_record(f, empty_slot, lease_info)
1962+            else:
1963+                self._write_lease_record(f, num_lease_slots, lease_info)
1964+        finally:
1965+            f.close()
1966 
1967     def renew_lease(self, renew_secret, new_expire_time):
1968         accepting_nodeids = set()
1969hunk ./src/allmydata/storage/backends/disk/mutable.py 322
1970-        f = open(self.home, 'rb+')
1971-        for (leasenum,lease) in self._enumerate_leases(f):
1972-            if constant_time_compare(lease.renew_secret, renew_secret):
1973-                # yup. See if we need to update the owner time.
1974-                if new_expire_time > lease.expiration_time:
1975-                    # yes
1976-                    lease.expiration_time = new_expire_time
1977-                    self._write_lease_record(f, leasenum, lease)
1978-                f.close()
1979-                return
1980-            accepting_nodeids.add(lease.nodeid)
1981-        f.close()
1982+        f = self.home.open('rb+')
1983+        try:
1984+            for (leasenum, lease) in self._enumerate_leases(f):
1985+                if constant_time_compare(lease.renew_secret, renew_secret):
1986+                    # yup. See if we need to update the owner time.
1987+                    if new_expire_time > lease.expiration_time:
1988+                        # yes
1989+                        lease.expiration_time = new_expire_time
1990+                        self._write_lease_record(f, leasenum, lease)
1991+                    return
1992+                accepting_nodeids.add(lease.nodeid)
1993+        finally:
1994+            f.close()
1995         # Return the accepting_nodeids set, to give the client a chance to
1996hunk ./src/allmydata/storage/backends/disk/mutable.py 336
1997-        # update the leases on a share which has been migrated from its
1998+        # update the leases on a share that has been migrated from its
1999         # original server to a new one.
2000         msg = ("Unable to renew non-existent lease. I have leases accepted by"
2001                " nodeids: ")
2002hunk ./src/allmydata/storage/backends/disk/mutable.py 353
2003         except IndexError:
2004             self.add_lease(lease_info)
2005 
2006-    def cancel_lease(self, cancel_secret):
2007-        """Remove any leases with the given cancel_secret. If the last lease
2008-        is cancelled, the file will be removed. Return the number of bytes
2009-        that were freed (by truncating the list of leases, and possibly by
2010-        deleting the file. Raise IndexError if there was no lease with the
2011-        given cancel_secret."""
2012-
2013-        accepting_nodeids = set()
2014-        modified = 0
2015-        remaining = 0
2016-        blank_lease = LeaseInfo(owner_num=0,
2017-                                renew_secret="\x00"*32,
2018-                                cancel_secret="\x00"*32,
2019-                                expiration_time=0,
2020-                                nodeid="\x00"*20)
2021-        f = open(self.home, 'rb+')
2022-        for (leasenum,lease) in self._enumerate_leases(f):
2023-            accepting_nodeids.add(lease.nodeid)
2024-            if constant_time_compare(lease.cancel_secret, cancel_secret):
2025-                self._write_lease_record(f, leasenum, blank_lease)
2026-                modified += 1
2027-            else:
2028-                remaining += 1
2029-        if modified:
2030-            freed_space = self._pack_leases(f)
2031-            f.close()
2032-            if not remaining:
2033-                freed_space += os.stat(self.home)[stat.ST_SIZE]
2034-                self.unlink()
2035-            return freed_space
2036-
2037-        msg = ("Unable to cancel non-existent lease. I have leases "
2038-               "accepted by nodeids: ")
2039-        msg += ",".join([("'%s'" % idlib.nodeid_b2a(anid))
2040-                         for anid in accepting_nodeids])
2041-        msg += " ."
2042-        raise IndexError(msg)
2043-
2044-    def _pack_leases(self, f):
2045-        # TODO: reclaim space from cancelled leases
2046-        return 0
2047-
2048     def _read_write_enabler_and_nodeid(self, f):
2049         f.seek(0)
2050         data = f.read(self.HEADER_SIZE)
2051hunk ./src/allmydata/storage/backends/disk/mutable.py 365
2052 
2053     def readv(self, readv):
2054         datav = []
2055-        f = open(self.home, 'rb')
2056-        for (offset, length) in readv:
2057-            datav.append(self._read_share_data(f, offset, length))
2058-        f.close()
2059+        f = self.home.open('rb')
2060+        try:
2061+            for (offset, length) in readv:
2062+                datav.append(self._read_share_data(f, offset, length))
2063+        finally:
2064+            f.close()
2065         return datav
2066 
2067hunk ./src/allmydata/storage/backends/disk/mutable.py 373
2068-#    def remote_get_length(self):
2069-#        f = open(self.home, 'rb')
2070-#        data_length = self._read_data_length(f)
2071-#        f.close()
2072-#        return data_length
2073+    def get_data_length(self):
2074+        f = self.home.open('rb')
2075+        try:
2076+            data_length = self._read_data_length(f)
2077+        finally:
2078+            f.close()
2079+        return data_length
2080 
2081     def check_write_enabler(self, write_enabler, si_s):
2082hunk ./src/allmydata/storage/backends/disk/mutable.py 382
2083-        f = open(self.home, 'rb+')
2084-        (real_write_enabler, write_enabler_nodeid) = \
2085-                             self._read_write_enabler_and_nodeid(f)
2086-        f.close()
2087+        f = self.home.open('rb+')
2088+        try:
2089+            (real_write_enabler, write_enabler_nodeid) = self._read_write_enabler_and_nodeid(f)
2090+        finally:
2091+            f.close()
2092         # avoid a timing attack
2093         #if write_enabler != real_write_enabler:
2094         if not constant_time_compare(write_enabler, real_write_enabler):
2095hunk ./src/allmydata/storage/backends/disk/mutable.py 403
2096 
2097     def check_testv(self, testv):
2098         test_good = True
2099-        f = open(self.home, 'rb+')
2100-        for (offset, length, operator, specimen) in testv:
2101-            data = self._read_share_data(f, offset, length)
2102-            if not testv_compare(data, operator, specimen):
2103-                test_good = False
2104-                break
2105-        f.close()
2106+        f = self.home.open('rb+')
2107+        try:
2108+            for (offset, length, operator, specimen) in testv:
2109+                data = self._read_share_data(f, offset, length)
2110+                if not testv_compare(data, operator, specimen):
2111+                    test_good = False
2112+                    break
2113+        finally:
2114+            f.close()
2115         return test_good
2116 
2117     def writev(self, datav, new_length):
2118hunk ./src/allmydata/storage/backends/disk/mutable.py 415
2119-        f = open(self.home, 'rb+')
2120-        for (offset, data) in datav:
2121-            self._write_share_data(f, offset, data)
2122-        if new_length is not None:
2123-            cur_length = self._read_data_length(f)
2124-            if new_length < cur_length:
2125-                self._write_data_length(f, new_length)
2126-                # TODO: if we're going to shrink the share file when the
2127-                # share data has shrunk, then call
2128-                # self._change_container_size() here.
2129-        f.close()
2130-
2131-def testv_compare(a, op, b):
2132-    assert op in ("lt", "le", "eq", "ne", "ge", "gt")
2133-    if op == "lt":
2134-        return a < b
2135-    if op == "le":
2136-        return a <= b
2137-    if op == "eq":
2138-        return a == b
2139-    if op == "ne":
2140-        return a != b
2141-    if op == "ge":
2142-        return a >= b
2143-    if op == "gt":
2144-        return a > b
2145-    # never reached
2146-
2147-class EmptyShare:
2148+        f = self.home.open('rb+')
2149+        try:
2150+            for (offset, data) in datav:
2151+                self._write_share_data(f, offset, data)
2152+            if new_length is not None:
2153+                cur_length = self._read_data_length(f)
2154+                if new_length < cur_length:
2155+                    self._write_data_length(f, new_length)
2156+                    # TODO: if we're going to shrink the share file when the
2157+                    # share data has shrunk, then call
2158+                    # self._change_container_size() here.
2159+        finally:
2160+            f.close()
2161 
2162hunk ./src/allmydata/storage/backends/disk/mutable.py 429
2163-    def check_testv(self, testv):
2164-        test_good = True
2165-        for (offset, length, operator, specimen) in testv:
2166-            data = ""
2167-            if not testv_compare(data, operator, specimen):
2168-                test_good = False
2169-                break
2170-        return test_good
2171 
2172hunk ./src/allmydata/storage/backends/disk/mutable.py 430
2173-def create_mutable_sharefile(filename, my_nodeid, write_enabler, parent):
2174-    ms = MutableShareFile(filename, parent)
2175-    ms.create(my_nodeid, write_enabler)
2176+def create_mutable_disk_share(fp, nodeid, write_enabler, parent):
2177+    ms = MutableDiskShare(fp, parent)
2178+    ms.create(nodeid, write_enabler)
2179     del ms
2180hunk ./src/allmydata/storage/backends/disk/mutable.py 434
2181-    return MutableShareFile(filename, parent)
2182-
2183+    return MutableDiskShare(fp, parent)
2184addfile ./src/allmydata/storage/backends/null/__init__.py
2185addfile ./src/allmydata/storage/backends/null/core.py
2186hunk ./src/allmydata/storage/backends/null/core.py 2
2187 
2188+import os, struct
2189+
2190+from allmydata.interfaceutil import implements
2191+from allmydata.interfaces import IStorageBackend, IShareSet, IStoredShare
2192+from allmydata.util.assertutil import precondition
2193+from allmydata.util.hashutil import constant_time_compare
2194+from allmydata.storage.backends.base import Backend
2195+from allmydata.storage.bucket import BucketWriter
2196+from allmydata.storage.common import si_b2a
2197+from allmydata.storage.lease import LeaseInfo
2198+
2199+
2200+class NullCore(Backend):
2201+    implements(IStorageBackend)
2202+
2203+    def __init__(self):
2204+        Backend.__init__(self)
2205+
2206+    def get_available_space(self):
2207+        return None
2208+
2209+    def get_shareset(self, storageindex):
2210+        return NullShareSet(storageindex)
2211+
2212+    def set_storage_server(self, ss):
2213+        self.ss = ss
2214+
2215+    def advise_corrupt_share(self, storageindex, sharetype, shnum, reason):
2216+        pass
2217+
2218+
2219+class NullShareSet(object):
2220+    implements(IShareSet)
2221+
2222+    def __init__(self, storageindex):
2223+        self.storageindex = storageindex
2224+
2225+    def get_overhead(self):
2226+        return 0
2227+
2228+    def get_incoming_shnums(self):
2229+        return frozenset()
2230+
2231+    def get_shares(self):
2232+        pass
2233+
2234+    def get_share(self, shnum):
2235+        return None
2236+
2237+    def get_storage_index(self):
2238+        return self.storageindex
2239+
2240+    def get_storage_index_string(self):
2241+        return si_b2a(self.storageindex)
2242+
2243+    def make_bucket_writer(self, shnum, max_space_per_bucket, lease_info, canary):
2244+        immutableshare = ImmutableNullShare()
2245+        return BucketWriter(self.ss, immutableshare, max_space_per_bucket, lease_info, canary)
2246+
2247+
2248+class ImmutableNullShare:
2249+    implements(IStoredShare)
2250+    sharetype = "immutable"
2251+
2252+    def __init__(self):
2253+        """ If max_size is not None then I won't allow more than
2254+        max_size to be written to me. If create=True then max_size
2255+        must not be None. """
2256+        pass
2257+
2258+    def get_shnum(self):
2259+        return self.shnum
2260+
2261+    def unlink(self):
2262+        os.unlink(self.fname)
2263+
2264+    def read_share_data(self, offset, length):
2265+        precondition(offset >= 0)
2266+        # Reads beyond the end of the data are truncated. Reads that start
2267+        # beyond the end of the data return an empty string.
2268+        seekpos = self._data_offset+offset
2269+        fsize = os.path.getsize(self.fname)
2270+        actuallength = max(0, min(length, fsize-seekpos)) # XXX #1528
2271+        if actuallength == 0:
2272+            return ""
2273+        f = open(self.fname, 'rb')
2274+        f.seek(seekpos)
2275+        return f.read(actuallength)
2276+
2277+    def write_share_data(self, offset, data):
2278+        pass
2279+
2280+    def _write_lease_record(self, f, lease_number, lease_info):
2281+        offset = self._lease_offset + lease_number * self.LEASE_SIZE
2282+        f.seek(offset)
2283+        assert f.tell() == offset
2284+        f.write(lease_info.to_immutable_data())
2285+
2286+    def _read_num_leases(self, f):
2287+        f.seek(0x08)
2288+        (num_leases,) = struct.unpack(">L", f.read(4))
2289+        return num_leases
2290+
2291+    def _write_num_leases(self, f, num_leases):
2292+        f.seek(0x08)
2293+        f.write(struct.pack(">L", num_leases))
2294+
2295+    def _truncate_leases(self, f, num_leases):
2296+        f.truncate(self._lease_offset + num_leases * self.LEASE_SIZE)
2297+
2298+    def get_leases(self):
2299+        """Yields a LeaseInfo instance for all leases."""
2300+        f = open(self.fname, 'rb')
2301+        (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
2302+        f.seek(self._lease_offset)
2303+        for i in range(num_leases):
2304+            data = f.read(self.LEASE_SIZE)
2305+            if data:
2306+                yield LeaseInfo().from_immutable_data(data)
2307+
2308+    def add_lease(self, lease):
2309+        pass
2310+
2311+    def renew_lease(self, renew_secret, new_expire_time):
2312+        for i,lease in enumerate(self.get_leases()):
2313+            if constant_time_compare(lease.renew_secret, renew_secret):
2314+                # yup. See if we need to update the owner time.
2315+                if new_expire_time > lease.expiration_time:
2316+                    # yes
2317+                    lease.expiration_time = new_expire_time
2318+                    f = open(self.fname, 'rb+')
2319+                    self._write_lease_record(f, i, lease)
2320+                    f.close()
2321+                return
2322+        raise IndexError("unable to renew non-existent lease")
2323+
2324+    def add_or_renew_lease(self, lease_info):
2325+        try:
2326+            self.renew_lease(lease_info.renew_secret,
2327+                             lease_info.expiration_time)
2328+        except IndexError:
2329+            self.add_lease(lease_info)
2330+
2331+
2332+class MutableNullShare:
2333+    implements(IStoredShare)
2334+    sharetype = "mutable"
2335+
2336+    """ XXX: TODO """
2337addfile ./src/allmydata/storage/bucket.py
2338hunk ./src/allmydata/storage/bucket.py 1
2339+
2340+import time
2341+
2342+from foolscap.api import Referenceable
2343+
2344+from zope.interface import implements
2345+from allmydata.interfaces import RIBucketWriter, RIBucketReader
2346+from allmydata.util import base32, log
2347+from allmydata.util.assertutil import precondition
2348+
2349+
2350+class BucketWriter(Referenceable):
2351+    implements(RIBucketWriter)
2352+
2353+    def __init__(self, ss, immutableshare, max_size, lease_info, canary):
2354+        self.ss = ss
2355+        self._max_size = max_size # don't allow the client to write more than this
2356+        self._canary = canary
2357+        self._disconnect_marker = canary.notifyOnDisconnect(self._disconnected)
2358+        self.closed = False
2359+        self.throw_out_all_data = False
2360+        self._share = immutableshare
2361+        # also, add our lease to the file now, so that other ones can be
2362+        # added by simultaneous uploaders
2363+        self._share.add_lease(lease_info)
2364+
2365+    def allocated_size(self):
2366+        return self._max_size
2367+
2368+    def remote_write(self, offset, data):
2369+        start = time.time()
2370+        precondition(not self.closed)
2371+        if self.throw_out_all_data:
2372+            return
2373+        self._share.write_share_data(offset, data)
2374+        self.ss.add_latency("write", time.time() - start)
2375+        self.ss.count("write")
2376+
2377+    def remote_close(self):
2378+        precondition(not self.closed)
2379+        start = time.time()
2380+
2381+        self._share.close()
2382+        filelen = self._share.stat()
2383+        self._share = None
2384+
2385+        self.closed = True
2386+        self._canary.dontNotifyOnDisconnect(self._disconnect_marker)
2387+
2388+        self.ss.bucket_writer_closed(self, filelen)
2389+        self.ss.add_latency("close", time.time() - start)
2390+        self.ss.count("close")
2391+
2392+    def _disconnected(self):
2393+        if not self.closed:
2394+            self._abort()
2395+
2396+    def remote_abort(self):
2397+        log.msg("storage: aborting write to share %r" % self._share,
2398+                facility="tahoe.storage", level=log.UNUSUAL)
2399+        if not self.closed:
2400+            self._canary.dontNotifyOnDisconnect(self._disconnect_marker)
2401+        self._abort()
2402+        self.ss.count("abort")
2403+
2404+    def _abort(self):
2405+        if self.closed:
2406+            return
2407+        self._share.unlink()
2408+        self._share = None
2409+
2410+        # We are now considered closed for further writing. We must tell
2411+        # the storage server about this so that it stops expecting us to
2412+        # use the space it allocated for us earlier.
2413+        self.closed = True
2414+        self.ss.bucket_writer_closed(self, 0)
2415+
2416+
2417+class BucketReader(Referenceable):
2418+    implements(RIBucketReader)
2419+
2420+    def __init__(self, ss, share):
2421+        self.ss = ss
2422+        self._share = share
2423+        self.storageindex = share.storageindex
2424+        self.shnum = share.shnum
2425+
2426+    def __repr__(self):
2427+        return "<%s %s %s>" % (self.__class__.__name__,
2428+                               base32.b2a_l(self.storageindex[:8], 60),
2429+                               self.shnum)
2430+
2431+    def remote_read(self, offset, length):
2432+        start = time.time()
2433+        data = self._share.read_share_data(offset, length)
2434+        self.ss.add_latency("read", time.time() - start)
2435+        self.ss.count("read")
2436+        return data
2437+
2438+    def remote_advise_corrupt_share(self, reason):
2439+        return self.ss.remote_advise_corrupt_share("immutable",
2440+                                                   self.storageindex,
2441+                                                   self.shnum,
2442+                                                   reason)
2443hunk ./src/allmydata/storage/common.py 1
2444-
2445-import os.path
2446 from allmydata.util import base32
2447 
2448 class DataTooLargeError(Exception):
2449hunk ./src/allmydata/storage/common.py 5
2450     pass
2451+
2452 class UnknownMutableContainerVersionError(Exception):
2453     pass
2454hunk ./src/allmydata/storage/common.py 8
2455+
2456 class UnknownImmutableContainerVersionError(Exception):
2457     pass
2458 
2459hunk ./src/allmydata/storage/common.py 18
2460 
2461 def si_a2b(ascii_storageindex):
2462     return base32.a2b(ascii_storageindex)
2463-
2464-def storage_index_to_dir(storageindex):
2465-    sia = si_b2a(storageindex)
2466-    return os.path.join(sia[:2], sia)
2467hunk ./src/allmydata/storage/crawler.py 2
2468 
2469-import os, time, struct
2470+import time, struct
2471 import cPickle as pickle
2472 from twisted.internet import reactor
2473 from twisted.application import service
2474hunk ./src/allmydata/storage/crawler.py 7
2475 from allmydata.storage.common import si_b2a
2476-from allmydata.util import fileutil
2477+
2478 
2479 class TimeSliceExceeded(Exception):
2480     pass
2481hunk ./src/allmydata/storage/crawler.py 12
2482 
2483+
2484 class ShareCrawler(service.MultiService):
2485hunk ./src/allmydata/storage/crawler.py 14
2486-    """A ShareCrawler subclass is attached to a StorageServer, and
2487-    periodically walks all of its shares, processing each one in some
2488-    fashion. This crawl is rate-limited, to reduce the IO burden on the host,
2489-    since large servers can easily have a terabyte of shares, in several
2490-    million files, which can take hours or days to read.
2491+    """
2492+    An instance of a subclass of ShareCrawler is attached to a storage
2493+    backend, and periodically walks the backend's shares, processing them
2494+    in some fashion. This crawl is rate-limited to reduce the I/O burden on
2495+    the host, since large servers can easily have a terabyte of shares in
2496+    several million files, which can take hours or days to read.
2497 
2498     Once the crawler starts a cycle, it will proceed at a rate limited by the
2499     allowed_cpu_percentage= and cpu_slice= parameters: yielding the reactor
2500hunk ./src/allmydata/storage/crawler.py 30
2501     long enough to ensure that 'minimum_cycle_time' elapses between the start
2502     of two consecutive cycles.
2503 
2504-    We assume that the normal upload/download/get_buckets traffic of a tahoe
2505+    We assume that the normal upload/download/DYHB traffic of a Tahoe-LAFS
2506     grid will cause the prefixdir contents to be mostly cached in the kernel,
2507hunk ./src/allmydata/storage/crawler.py 32
2508-    or that the number of buckets in each prefixdir will be small enough to
2509-    load quickly. A 1TB allmydata.com server was measured to have 2.56M
2510-    buckets, spread into the 1024 prefixdirs, with about 2500 buckets per
2511+    or that the number of sharesets in each prefixdir will be small enough to
2512+    load quickly. A 1TB allmydata.com server was measured to have 2.56 million
2513+    sharesets, spread into the 1024 prefixdirs, with about 2500 sharesets per
2514     prefix. On this server, each prefixdir took 130ms-200ms to list the first
2515     time, and 17ms to list the second time.
2516 
2517hunk ./src/allmydata/storage/crawler.py 38
2518-    To use a crawler, create a subclass which implements the process_bucket()
2519-    method. It will be called with a prefixdir and a base32 storage index
2520-    string. process_bucket() must run synchronously. Any keys added to
2521-    self.state will be preserved. Override add_initial_state() to set up
2522-    initial state keys. Override finished_cycle() to perform additional
2523-    processing when the cycle is complete. Any status that the crawler
2524-    produces should be put in the self.state dictionary. Status renderers
2525-    (like a web page which describes the accomplishments of your crawler)
2526-    will use crawler.get_state() to retrieve this dictionary; they can
2527-    present the contents as they see fit.
2528+    To implement a crawler, create a subclass that implements the
2529+    process_shareset() method. It will be called with a prefixdir and an
2530+    object providing the IShareSet interface. process_shareset() must run
2531+    synchronously. Any keys added to self.state will be preserved. Override
2532+    add_initial_state() to set up initial state keys. Override
2533+    finished_cycle() to perform additional processing when the cycle is
2534+    complete. Any status that the crawler produces should be put in the
2535+    self.state dictionary. Status renderers (like a web page describing the
2536+    accomplishments of your crawler) will use crawler.get_state() to retrieve
2537+    this dictionary; they can present the contents as they see fit.
2538 
2539hunk ./src/allmydata/storage/crawler.py 49
2540-    Then create an instance, with a reference to a StorageServer and a
2541-    filename where it can store persistent state. The statefile is used to
2542-    keep track of how far around the ring the process has travelled, as well
2543-    as timing history to allow the pace to be predicted and controlled. The
2544-    statefile will be updated and written to disk after each time slice (just
2545-    before the crawler yields to the reactor), and also after each cycle is
2546-    finished, and also when stopService() is called. Note that this means
2547-    that a crawler which is interrupted with SIGKILL while it is in the
2548-    middle of a time slice will lose progress: the next time the node is
2549-    started, the crawler will repeat some unknown amount of work.
2550+    Then create an instance, with a reference to a backend object providing
2551+    the IStorageBackend interface, and a filename where it can store
2552+    persistent state. The statefile is used to keep track of how far around
2553+    the ring the process has travelled, as well as timing history to allow
2554+    the pace to be predicted and controlled. The statefile will be updated
2555+    and written to disk after each time slice (just before the crawler yields
2556+    to the reactor), and also after each cycle is finished, and also when
2557+    stopService() is called. Note that this means that a crawler that is
2558+    interrupted with SIGKILL while it is in the middle of a time slice will
2559+    lose progress: the next time the node is started, the crawler will repeat
2560+    some unknown amount of work.
2561 
2562     The crawler instance must be started with startService() before it will
2563hunk ./src/allmydata/storage/crawler.py 62
2564-    do any work. To make it stop doing work, call stopService().
2565+    do any work. To make it stop doing work, call stopService(). A crawler
2566+    is usually a child service of a StorageServer, although it should not
2567+    depend on that.
2568+
2569+    For historical reasons, some dictionary key names use the term "bucket"
2570+    for what is now preferably called a "shareset" (the set of shares that a
2571+    server holds under a given storage index).
2572     """
2573 
2574     slow_start = 300 # don't start crawling for 5 minutes after startup
2575hunk ./src/allmydata/storage/crawler.py 77
2576     cpu_slice = 1.0 # use up to 1.0 seconds before yielding
2577     minimum_cycle_time = 300 # don't run a cycle faster than this
2578 
2579-    def __init__(self, server, statefile, allowed_cpu_percentage=None):
2580+    def __init__(self, backend, statefp, allowed_cpu_percentage=None):
2581         service.MultiService.__init__(self)
2582hunk ./src/allmydata/storage/crawler.py 79
2583+        self.backend = backend
2584+        self.statefp = statefp
2585         if allowed_cpu_percentage is not None:
2586             self.allowed_cpu_percentage = allowed_cpu_percentage
2587hunk ./src/allmydata/storage/crawler.py 83
2588-        self.server = server
2589-        self.sharedir = server.sharedir
2590-        self.statefile = statefile
2591         self.prefixes = [si_b2a(struct.pack(">H", i << (16-10)))[:2]
2592                          for i in range(2**10)]
2593         self.prefixes.sort()
2594hunk ./src/allmydata/storage/crawler.py 87
2595         self.timer = None
2596-        self.bucket_cache = (None, [])
2597+        self.shareset_cache = (None, [])
2598         self.current_sleep_time = None
2599         self.next_wake_time = None
2600         self.last_prefix_finished_time = None
2601hunk ./src/allmydata/storage/crawler.py 150
2602                 left = len(self.prefixes) - self.last_complete_prefix_index
2603                 remaining = left * self.last_prefix_elapsed_time
2604                 # TODO: remainder of this prefix: we need to estimate the
2605-                # per-bucket time, probably by measuring the time spent on
2606-                # this prefix so far, divided by the number of buckets we've
2607+                # per-shareset time, probably by measuring the time spent on
2608+                # this prefix so far, divided by the number of sharesets we've
2609                 # processed.
2610             d["estimated-cycle-complete-time-left"] = remaining
2611             # it's possible to call get_progress() from inside a crawler's
2612hunk ./src/allmydata/storage/crawler.py 171
2613         state dictionary.
2614 
2615         If we are not currently sleeping (i.e. get_state() was called from
2616-        inside the process_prefixdir, process_bucket, or finished_cycle()
2617+        inside the process_prefixdir, process_shareset, or finished_cycle()
2618         methods, or if startService has not yet been called on this crawler),
2619         these two keys will be None.
2620 
2621hunk ./src/allmydata/storage/crawler.py 184
2622     def load_state(self):
2623         # we use this to store state for both the crawler's internals and
2624         # anything the subclass-specific code needs. The state is stored
2625-        # after each bucket is processed, after each prefixdir is processed,
2626+        # after each shareset is processed, after each prefixdir is processed,
2627         # and after a cycle is complete. The internal keys we use are:
2628         #  ["version"]: int, always 1
2629         #  ["last-cycle-finished"]: int, or None if we have not yet finished
2630hunk ./src/allmydata/storage/crawler.py 198
2631         #                            are sleeping between cycles, or if we
2632         #                            have not yet finished any prefixdir since
2633         #                            a cycle was started
2634-        #  ["last-complete-bucket"]: str, base32 storage index bucket name
2635-        #                            of the last bucket to be processed, or
2636-        #                            None if we are sleeping between cycles
2637+        #  ["last-complete-bucket"]: str, base32 storage index of the last
2638+        #                            shareset to be processed, or None if we
2639+        #                            are sleeping between cycles
2640         try:
2641hunk ./src/allmydata/storage/crawler.py 202
2642-            f = open(self.statefile, "rb")
2643-            state = pickle.load(f)
2644-            f.close()
2645+            state = pickle.loads(self.statefp.getContent())
2646         except EnvironmentError:
2647             state = {"version": 1,
2648                      "last-cycle-finished": None,
2649hunk ./src/allmydata/storage/crawler.py 238
2650         else:
2651             last_complete_prefix = self.prefixes[lcpi]
2652         self.state["last-complete-prefix"] = last_complete_prefix
2653-        tmpfile = self.statefile + ".tmp"
2654-        f = open(tmpfile, "wb")
2655-        pickle.dump(self.state, f)
2656-        f.close()
2657-        fileutil.move_into_place(tmpfile, self.statefile)
2658+        self.statefp.setContent(pickle.dumps(self.state))
2659 
2660     def startService(self):
2661         # arrange things to look like we were just sleeping, so
2662hunk ./src/allmydata/storage/crawler.py 280
2663         sleep_time = (this_slice / self.allowed_cpu_percentage) - this_slice
2664         # if the math gets weird, or a timequake happens, don't sleep
2665         # forever. Note that this means that, while a cycle is running, we
2666-        # will process at least one bucket every 5 minutes, no matter how
2667-        # long that bucket takes.
2668+        # will process at least one shareset every 5 minutes, no matter how
2669+        # long that shareset takes.
2670         sleep_time = max(0.0, min(sleep_time, 299))
2671         if finished_cycle:
2672             # how long should we sleep between cycles? Don't run faster than
2673hunk ./src/allmydata/storage/crawler.py 311
2674         for i in range(self.last_complete_prefix_index+1, len(self.prefixes)):
2675             # if we want to yield earlier, just raise TimeSliceExceeded()
2676             prefix = self.prefixes[i]
2677-            prefixdir = os.path.join(self.sharedir, prefix)
2678-            if i == self.bucket_cache[0]:
2679-                buckets = self.bucket_cache[1]
2680+            if i == self.shareset_cache[0]:
2681+                sharesets = self.shareset_cache[1]
2682             else:
2683hunk ./src/allmydata/storage/crawler.py 314
2684-                try:
2685-                    buckets = os.listdir(prefixdir)
2686-                    buckets.sort()
2687-                except EnvironmentError:
2688-                    buckets = []
2689-                self.bucket_cache = (i, buckets)
2690-            self.process_prefixdir(cycle, prefix, prefixdir,
2691-                                   buckets, start_slice)
2692+                sharesets = self.backend.get_sharesets_for_prefix(prefix)
2693+                self.shareset_cache = (i, sharesets)
2694+            self.process_prefixdir(cycle, prefix, sharesets, start_slice)
2695             self.last_complete_prefix_index = i
2696 
2697             now = time.time()
2698hunk ./src/allmydata/storage/crawler.py 341
2699         self.finished_cycle(cycle)
2700         self.save_state()
2701 
2702-    def process_prefixdir(self, cycle, prefix, prefixdir, buckets, start_slice):
2703-        """This gets a list of bucket names (i.e. storage index strings,
2704+    def process_prefixdir(self, cycle, prefix, sharesets, start_slice):
2705+        """
2706+        This gets a list of shareset names (i.e. storage index strings,
2707         base32-encoded) in sorted order.
2708 
2709         You can override this if your crawler doesn't care about the actual
2710hunk ./src/allmydata/storage/crawler.py 348
2711         shares, for example a crawler which merely keeps track of how many
2712-        buckets are being managed by this server.
2713+        sharesets are being managed by this server.
2714 
2715hunk ./src/allmydata/storage/crawler.py 350
2716-        Subclasses which *do* care about actual bucket should leave this
2717-        method along, and implement process_bucket() instead.
2718+        Subclasses which *do* care about actual shareset should leave this
2719+        method alone, and implement process_shareset() instead.
2720         """
2721 
2722hunk ./src/allmydata/storage/crawler.py 354
2723-        for bucket in buckets:
2724-            if bucket <= self.state["last-complete-bucket"]:
2725+        for shareset in sharesets:
2726+            base32si = shareset.get_storage_index_string()
2727+            if base32si <= self.state["last-complete-bucket"]:
2728                 continue
2729hunk ./src/allmydata/storage/crawler.py 358
2730-            self.process_bucket(cycle, prefix, prefixdir, bucket)
2731-            self.state["last-complete-bucket"] = bucket
2732+            self.process_shareset(cycle, prefix, shareset)
2733+            self.state["last-complete-bucket"] = base32si
2734             if time.time() >= start_slice + self.cpu_slice:
2735                 raise TimeSliceExceeded()
2736 
2737hunk ./src/allmydata/storage/crawler.py 366
2738     # the remaining methods are explictly for subclasses to implement.
2739 
2740     def started_cycle(self, cycle):
2741-        """Notify a subclass that the crawler is about to start a cycle.
2742+        """
2743+        Notify a subclass that the crawler is about to start a cycle.
2744 
2745         This method is for subclasses to override. No upcall is necessary.
2746         """
2747hunk ./src/allmydata/storage/crawler.py 373
2748         pass
2749 
2750-    def process_bucket(self, cycle, prefix, prefixdir, storage_index_b32):
2751-        """Examine a single bucket. Subclasses should do whatever they want
2752+    def process_shareset(self, cycle, prefix, shareset):
2753+        """
2754+        Examine a single shareset. Subclasses should do whatever they want
2755         to do to the shares therein, then update self.state as necessary.
2756 
2757         If the crawler is never interrupted by SIGKILL, this method will be
2758hunk ./src/allmydata/storage/crawler.py 379
2759-        called exactly once per share (per cycle). If it *is* interrupted,
2760+        called exactly once per shareset (per cycle). If it *is* interrupted,
2761         then the next time the node is started, some amount of work will be
2762         duplicated, according to when self.save_state() was last called. By
2763         default, save_state() is called at the end of each timeslice, and
2764hunk ./src/allmydata/storage/crawler.py 387
2765 
2766         To reduce the chance of duplicate work (i.e. to avoid adding multiple
2767         records to a database), you can call save_state() at the end of your
2768-        process_bucket() method. This will reduce the maximum duplicated work
2769-        to one bucket per SIGKILL. It will also add overhead, probably 1-20ms
2770-        per bucket (and some disk writes), which will count against your
2771-        allowed_cpu_percentage, and which may be considerable if
2772-        process_bucket() runs quickly.
2773+        process_shareset() method. This will reduce the maximum duplicated
2774+        work to one shareset per SIGKILL. It will also add overhead, probably
2775+        1-20ms per shareset (and some disk writes), which will count against
2776+        your allowed_cpu_percentage, and which may be considerable if
2777+        process_shareset() runs quickly.
2778 
2779         This method is for subclasses to override. No upcall is necessary.
2780         """
2781hunk ./src/allmydata/storage/crawler.py 398
2782         pass
2783 
2784     def finished_prefix(self, cycle, prefix):
2785-        """Notify a subclass that the crawler has just finished processing a
2786-        prefix directory (all buckets with the same two-character/10bit
2787+        """
2788+        Notify a subclass that the crawler has just finished processing a
2789+        prefix directory (all sharesets with the same two-character/10-bit
2790         prefix). To impose a limit on how much work might be duplicated by a
2791         SIGKILL that occurs during a timeslice, you can call
2792         self.save_state() here, but be aware that it may represent a
2793hunk ./src/allmydata/storage/crawler.py 411
2794         pass
2795 
2796     def finished_cycle(self, cycle):
2797-        """Notify subclass that a cycle (one complete traversal of all
2798+        """
2799+        Notify subclass that a cycle (one complete traversal of all
2800         prefixdirs) has just finished. 'cycle' is the number of the cycle
2801         that just finished. This method should perform summary work and
2802         update self.state to publish information to status displays.
2803hunk ./src/allmydata/storage/crawler.py 429
2804         pass
2805 
2806     def yielding(self, sleep_time):
2807-        """The crawler is about to sleep for 'sleep_time' seconds. This
2808+        """
2809+        The crawler is about to sleep for 'sleep_time' seconds. This
2810         method is mostly for the convenience of unit tests.
2811 
2812         This method is for subclasses to override. No upcall is necessary.
2813hunk ./src/allmydata/storage/crawler.py 439
2814 
2815 
2816 class BucketCountingCrawler(ShareCrawler):
2817-    """I keep track of how many buckets are being managed by this server.
2818-    This is equivalent to the number of distributed files and directories for
2819-    which I am providing storage. The actual number of files+directories in
2820-    the full grid is probably higher (especially when there are more servers
2821-    than 'N', the number of generated shares), because some files+directories
2822-    will have shares on other servers instead of me. Also note that the
2823-    number of buckets will differ from the number of shares in small grids,
2824-    when more than one share is placed on a single server.
2825+    """
2826+    I keep track of how many sharesets, each corresponding to a storage index,
2827+    are being managed by this server. This is equivalent to the number of
2828+    distributed files and directories for which I am providing storage. The
2829+    actual number of files and directories in the full grid is probably higher
2830+    (especially when there are more servers than 'N', the number of generated
2831+    shares), because some files and directories will have shares on other
2832+    servers instead of me. Also note that the number of sharesets will differ
2833+    from the number of shares in small grids, when more than one share is
2834+    placed on a single server.
2835     """
2836 
2837     minimum_cycle_time = 60*60 # we don't need this more than once an hour
2838hunk ./src/allmydata/storage/crawler.py 453
2839 
2840-    def __init__(self, server, statefile, num_sample_prefixes=1):
2841-        ShareCrawler.__init__(self, server, statefile)
2842+    def __init__(self, backend, statefp, num_sample_prefixes=1):
2843+        ShareCrawler.__init__(self, backend, statefp)
2844         self.num_sample_prefixes = num_sample_prefixes
2845 
2846     def add_initial_state(self):
2847hunk ./src/allmydata/storage/crawler.py 467
2848         self.state.setdefault("last-complete-bucket-count", None)
2849         self.state.setdefault("storage-index-samples", {})
2850 
2851-    def process_prefixdir(self, cycle, prefix, prefixdir, buckets, start_slice):
2852+    def process_prefixdir(self, cycle, prefix, sharesets, start_slice):
2853         # we override process_prefixdir() because we don't want to look at
2854hunk ./src/allmydata/storage/crawler.py 469
2855-        # the individual buckets. We'll save state after each one. On my
2856+        # the individual sharesets. We'll save state after each one. On my
2857         # laptop, a mostly-empty storage server can process about 70
2858         # prefixdirs in a 1.0s slice.
2859         if cycle not in self.state["bucket-counts"]:
2860hunk ./src/allmydata/storage/crawler.py 474
2861             self.state["bucket-counts"][cycle] = {}
2862-        self.state["bucket-counts"][cycle][prefix] = len(buckets)
2863+        self.state["bucket-counts"][cycle][prefix] = len(sharesets)
2864         if prefix in self.prefixes[:self.num_sample_prefixes]:
2865hunk ./src/allmydata/storage/crawler.py 476
2866-            self.state["storage-index-samples"][prefix] = (cycle, buckets)
2867+            self.state["storage-index-samples"][prefix] = (cycle, sharesets)
2868 
2869     def finished_cycle(self, cycle):
2870         last_counts = self.state["bucket-counts"].get(cycle, [])
2871hunk ./src/allmydata/storage/crawler.py 482
2872         if len(last_counts) == len(self.prefixes):
2873             # great, we have a whole cycle.
2874-            num_buckets = sum(last_counts.values())
2875-            self.state["last-complete-bucket-count"] = num_buckets
2876+            num_sharesets = sum(last_counts.values())
2877+            self.state["last-complete-bucket-count"] = num_sharesets
2878             # get rid of old counts
2879             for old_cycle in list(self.state["bucket-counts"].keys()):
2880                 if old_cycle != cycle:
2881hunk ./src/allmydata/storage/crawler.py 490
2882                     del self.state["bucket-counts"][old_cycle]
2883         # get rid of old samples too
2884         for prefix in list(self.state["storage-index-samples"].keys()):
2885-            old_cycle,buckets = self.state["storage-index-samples"][prefix]
2886+            old_cycle, storage_indices = self.state["storage-index-samples"][prefix]
2887             if old_cycle != cycle:
2888                 del self.state["storage-index-samples"][prefix]
2889hunk ./src/allmydata/storage/crawler.py 493
2890-
2891hunk ./src/allmydata/storage/expirer.py 1
2892-import time, os, pickle, struct
2893+
2894+import time, pickle, struct
2895+from twisted.python import log as twlog
2896+
2897 from allmydata.storage.crawler import ShareCrawler
2898hunk ./src/allmydata/storage/expirer.py 6
2899-from allmydata.storage.shares import get_share_file
2900-from allmydata.storage.common import UnknownMutableContainerVersionError, \
2901+from allmydata.storage.common import si_b2a, UnknownMutableContainerVersionError, \
2902      UnknownImmutableContainerVersionError
2903hunk ./src/allmydata/storage/expirer.py 8
2904-from twisted.python import log as twlog
2905+
2906 
2907 class LeaseCheckingCrawler(ShareCrawler):
2908     """I examine the leases on all shares, determining which are still valid
2909hunk ./src/allmydata/storage/expirer.py 17
2910     removed.
2911 
2912     I collect statistics on the leases and make these available to a web
2913-    status page, including::
2914+    status page, including:
2915 
2916     Space recovered during this cycle-so-far:
2917      actual (only if expiration_enabled=True):
2918hunk ./src/allmydata/storage/expirer.py 21
2919-      num-buckets, num-shares, sum of share sizes, real disk usage
2920+      num-storage-indices, num-shares, sum of share sizes, real disk usage
2921       ('real disk usage' means we use stat(fn).st_blocks*512 and include any
2922        space used by the directory)
2923      what it would have been with the original lease expiration time
2924hunk ./src/allmydata/storage/expirer.py 32
2925 
2926     Space recovered during the last 10 cycles  <-- saved in separate pickle
2927 
2928-    Shares/buckets examined:
2929+    Shares/storage-indices examined:
2930      this cycle-so-far
2931      prediction of rest of cycle
2932      during last 10 cycles <-- separate pickle
2933hunk ./src/allmydata/storage/expirer.py 42
2934     Histogram of leases-per-share:
2935      this-cycle-to-date
2936      last 10 cycles <-- separate pickle
2937-    Histogram of lease ages, buckets = 1day
2938+    Histogram of lease ages, storage-indices over 1 day
2939      cycle-to-date
2940      last 10 cycles <-- separate pickle
2941 
2942hunk ./src/allmydata/storage/expirer.py 53
2943     slow_start = 360 # wait 6 minutes after startup
2944     minimum_cycle_time = 12*60*60 # not more than twice per day
2945 
2946-    def __init__(self, server, statefile, historyfile,
2947-                 expiration_enabled, mode,
2948-                 override_lease_duration, # used if expiration_mode=="age"
2949-                 cutoff_date, # used if expiration_mode=="cutoff-date"
2950-                 sharetypes):
2951-        self.historyfile = historyfile
2952-        self.expiration_enabled = expiration_enabled
2953-        self.mode = mode
2954+    def __init__(self, backend, statefp, historyfp, expiration_policy):
2955+        ShareCrawler.__init__(self, backend, statefp)
2956+        self.historyfp = historyfp
2957+        self.expiration_enabled = expiration_policy['enabled']
2958+        self.mode = expiration_policy['mode']
2959         self.override_lease_duration = None
2960         self.cutoff_date = None
2961         if self.mode == "age":
2962hunk ./src/allmydata/storage/expirer.py 61
2963-            assert isinstance(override_lease_duration, (int, type(None)))
2964-            self.override_lease_duration = override_lease_duration # seconds
2965+            assert isinstance(expiration_policy['override_lease_duration'], (int, type(None)))
2966+            self.override_lease_duration = expiration_policy['override_lease_duration'] # seconds
2967         elif self.mode == "cutoff-date":
2968hunk ./src/allmydata/storage/expirer.py 64
2969-            assert isinstance(cutoff_date, int) # seconds-since-epoch
2970-            assert cutoff_date is not None
2971-            self.cutoff_date = cutoff_date
2972+            assert isinstance(expiration_policy['cutoff_date'], int) # seconds-since-epoch
2973+            self.cutoff_date = expiration_policy['cutoff_date']
2974         else:
2975hunk ./src/allmydata/storage/expirer.py 67
2976-            raise ValueError("GC mode '%s' must be 'age' or 'cutoff-date'" % mode)
2977-        self.sharetypes_to_expire = sharetypes
2978-        ShareCrawler.__init__(self, server, statefile)
2979+            raise ValueError("GC mode '%s' must be 'age' or 'cutoff-date'" % expiration_policy['mode'])
2980+        self.sharetypes_to_expire = expiration_policy['sharetypes']
2981 
2982     def add_initial_state(self):
2983         # we fill ["cycle-to-date"] here (even though they will be reset in
2984hunk ./src/allmydata/storage/expirer.py 82
2985             self.state["cycle-to-date"].setdefault(k, so_far[k])
2986 
2987         # initialize history
2988-        if not os.path.exists(self.historyfile):
2989+        if not self.historyfp.exists():
2990             history = {} # cyclenum -> dict
2991hunk ./src/allmydata/storage/expirer.py 84
2992-            f = open(self.historyfile, "wb")
2993-            pickle.dump(history, f)
2994-            f.close()
2995+            self.historyfp.setContent(pickle.dumps(history))
2996 
2997     def create_empty_cycle_dict(self):
2998         recovered = self.create_empty_recovered_dict()
2999hunk ./src/allmydata/storage/expirer.py 97
3000 
3001     def create_empty_recovered_dict(self):
3002         recovered = {}
3003+        # "buckets" is ambiguous; here it means the number of sharesets (one per storage index per server)
3004         for a in ("actual", "original", "configured", "examined"):
3005             for b in ("buckets", "shares", "sharebytes", "diskbytes"):
3006                 recovered[a+"-"+b] = 0
3007hunk ./src/allmydata/storage/expirer.py 108
3008     def started_cycle(self, cycle):
3009         self.state["cycle-to-date"] = self.create_empty_cycle_dict()
3010 
3011-    def stat(self, fn):
3012-        return os.stat(fn)
3013-
3014-    def process_bucket(self, cycle, prefix, prefixdir, storage_index_b32):
3015-        bucketdir = os.path.join(prefixdir, storage_index_b32)
3016-        s = self.stat(bucketdir)
3017+    def process_storage_index(self, cycle, prefix, container):
3018         would_keep_shares = []
3019         wks = None
3020hunk ./src/allmydata/storage/expirer.py 111
3021+        sharetype = None
3022 
3023hunk ./src/allmydata/storage/expirer.py 113
3024-        for fn in os.listdir(bucketdir):
3025-            try:
3026-                shnum = int(fn)
3027-            except ValueError:
3028-                continue # non-numeric means not a sharefile
3029-            sharefile = os.path.join(bucketdir, fn)
3030+        for share in container.get_shares():
3031+            sharetype = share.sharetype
3032             try:
3033hunk ./src/allmydata/storage/expirer.py 116
3034-                wks = self.process_share(sharefile)
3035+                wks = self.process_share(share)
3036             except (UnknownMutableContainerVersionError,
3037                     UnknownImmutableContainerVersionError,
3038                     struct.error):
3039hunk ./src/allmydata/storage/expirer.py 120
3040-                twlog.msg("lease-checker error processing %s" % sharefile)
3041+                twlog.msg("lease-checker error processing %r" % (share,))
3042                 twlog.err()
3043hunk ./src/allmydata/storage/expirer.py 122
3044-                which = (storage_index_b32, shnum)
3045+                which = (si_b2a(share.storageindex), share.get_shnum())
3046                 self.state["cycle-to-date"]["corrupt-shares"].append(which)
3047                 wks = (1, 1, 1, "unknown")
3048             would_keep_shares.append(wks)
3049hunk ./src/allmydata/storage/expirer.py 127
3050 
3051-        sharetype = None
3052+        container_type = None
3053         if wks:
3054hunk ./src/allmydata/storage/expirer.py 129
3055-            # use the last share's sharetype as the buckettype
3056-            sharetype = wks[3]
3057+            # use the last share's sharetype as the container type
3058+            container_type = wks[3]
3059         rec = self.state["cycle-to-date"]["space-recovered"]
3060         self.increment(rec, "examined-buckets", 1)
3061         if sharetype:
3062hunk ./src/allmydata/storage/expirer.py 134
3063-            self.increment(rec, "examined-buckets-"+sharetype, 1)
3064+            self.increment(rec, "examined-buckets-"+container_type, 1)
3065+
3066+        container_diskbytes = container.get_overhead()
3067 
3068hunk ./src/allmydata/storage/expirer.py 138
3069-        try:
3070-            bucket_diskbytes = s.st_blocks * 512
3071-        except AttributeError:
3072-            bucket_diskbytes = 0 # no stat().st_blocks on windows
3073         if sum([wks[0] for wks in would_keep_shares]) == 0:
3074hunk ./src/allmydata/storage/expirer.py 139
3075-            self.increment_bucketspace("original", bucket_diskbytes, sharetype)
3076+            self.increment_container_space("original", container_diskbytes, sharetype)
3077         if sum([wks[1] for wks in would_keep_shares]) == 0:
3078hunk ./src/allmydata/storage/expirer.py 141
3079-            self.increment_bucketspace("configured", bucket_diskbytes, sharetype)
3080+            self.increment_container_space("configured", container_diskbytes, sharetype)
3081         if sum([wks[2] for wks in would_keep_shares]) == 0:
3082hunk ./src/allmydata/storage/expirer.py 143
3083-            self.increment_bucketspace("actual", bucket_diskbytes, sharetype)
3084+            self.increment_container_space("actual", container_diskbytes, sharetype)
3085 
3086hunk ./src/allmydata/storage/expirer.py 145
3087-    def process_share(self, sharefilename):
3088-        # first, find out what kind of a share it is
3089-        sf = get_share_file(sharefilename)
3090-        sharetype = sf.sharetype
3091+    def process_share(self, share):
3092+        sharetype = share.sharetype
3093         now = time.time()
3094hunk ./src/allmydata/storage/expirer.py 148
3095-        s = self.stat(sharefilename)
3096+        sharebytes = share.get_size()
3097+        diskbytes = share.get_used_space()
3098 
3099         num_leases = 0
3100         num_valid_leases_original = 0
3101hunk ./src/allmydata/storage/expirer.py 156
3102         num_valid_leases_configured = 0
3103         expired_leases_configured = []
3104 
3105-        for li in sf.get_leases():
3106+        for li in share.get_leases():
3107             num_leases += 1
3108             original_expiration_time = li.get_expiration_time()
3109             grant_renew_time = li.get_grant_renew_time_time()
3110hunk ./src/allmydata/storage/expirer.py 169
3111 
3112             #  expired-or-not according to our configured age limit
3113             expired = False
3114-            if self.mode == "age":
3115-                age_limit = original_expiration_time
3116-                if self.override_lease_duration is not None:
3117-                    age_limit = self.override_lease_duration
3118-                if age > age_limit:
3119-                    expired = True
3120-            else:
3121-                assert self.mode == "cutoff-date"
3122-                if grant_renew_time < self.cutoff_date:
3123-                    expired = True
3124-            if sharetype not in self.sharetypes_to_expire:
3125-                expired = False
3126+            if sharetype in self.sharetypes_to_expire:
3127+                if self.mode == "age":
3128+                    age_limit = original_expiration_time
3129+                    if self.override_lease_duration is not None:
3130+                        age_limit = self.override_lease_duration
3131+                    if age > age_limit:
3132+                        expired = True
3133+                else:
3134+                    assert self.mode == "cutoff-date"
3135+                    if grant_renew_time < self.cutoff_date:
3136+                        expired = True
3137 
3138             if expired:
3139                 expired_leases_configured.append(li)
3140hunk ./src/allmydata/storage/expirer.py 188
3141 
3142         so_far = self.state["cycle-to-date"]
3143         self.increment(so_far["leases-per-share-histogram"], num_leases, 1)
3144-        self.increment_space("examined", s, sharetype)
3145+        self.increment_space("examined", diskbytes, sharetype)
3146 
3147         would_keep_share = [1, 1, 1, sharetype]
3148 
3149hunk ./src/allmydata/storage/expirer.py 194
3150         if self.expiration_enabled:
3151             for li in expired_leases_configured:
3152-                sf.cancel_lease(li.cancel_secret)
3153+                share.cancel_lease(li.cancel_secret)
3154 
3155         if num_valid_leases_original == 0:
3156             would_keep_share[0] = 0
3157hunk ./src/allmydata/storage/expirer.py 198
3158-            self.increment_space("original", s, sharetype)
3159+            self.increment_space("original", sharebytes, diskbytes, sharetype)
3160 
3161         if num_valid_leases_configured == 0:
3162             would_keep_share[1] = 0
3163hunk ./src/allmydata/storage/expirer.py 202
3164-            self.increment_space("configured", s, sharetype)
3165+            self.increment_space("configured", sharebytes, diskbytes, sharetype)
3166             if self.expiration_enabled:
3167                 would_keep_share[2] = 0
3168hunk ./src/allmydata/storage/expirer.py 205
3169-                self.increment_space("actual", s, sharetype)
3170+                self.increment_space("actual", sharebytes, diskbytes, sharetype)
3171 
3172         return would_keep_share
3173 
3174hunk ./src/allmydata/storage/expirer.py 209
3175-    def increment_space(self, a, s, sharetype):
3176-        sharebytes = s.st_size
3177-        try:
3178-            # note that stat(2) says that st_blocks is 512 bytes, and that
3179-            # st_blksize is "optimal file sys I/O ops blocksize", which is
3180-            # independent of the block-size that st_blocks uses.
3181-            diskbytes = s.st_blocks * 512
3182-        except AttributeError:
3183-            # the docs say that st_blocks is only on linux. I also see it on
3184-            # MacOS. But it isn't available on windows.
3185-            diskbytes = sharebytes
3186+    def increment_space(self, a, sharebytes, diskbytes, sharetype):
3187         so_far_sr = self.state["cycle-to-date"]["space-recovered"]
3188         self.increment(so_far_sr, a+"-shares", 1)
3189         self.increment(so_far_sr, a+"-sharebytes", sharebytes)
3190hunk ./src/allmydata/storage/expirer.py 219
3191             self.increment(so_far_sr, a+"-sharebytes-"+sharetype, sharebytes)
3192             self.increment(so_far_sr, a+"-diskbytes-"+sharetype, diskbytes)
3193 
3194-    def increment_bucketspace(self, a, bucket_diskbytes, sharetype):
3195+    def increment_container_space(self, a, container_diskbytes, container_type):
3196         rec = self.state["cycle-to-date"]["space-recovered"]
3197hunk ./src/allmydata/storage/expirer.py 221
3198-        self.increment(rec, a+"-diskbytes", bucket_diskbytes)
3199+        self.increment(rec, a+"-diskbytes", container_diskbytes)
3200         self.increment(rec, a+"-buckets", 1)
3201hunk ./src/allmydata/storage/expirer.py 223
3202-        if sharetype:
3203-            self.increment(rec, a+"-diskbytes-"+sharetype, bucket_diskbytes)
3204-            self.increment(rec, a+"-buckets-"+sharetype, 1)
3205+        if container_type:
3206+            self.increment(rec, a+"-diskbytes-"+container_type, container_diskbytes)
3207+            self.increment(rec, a+"-buckets-"+container_type, 1)
3208 
3209     def increment(self, d, k, delta=1):
3210         if k not in d:
3211hunk ./src/allmydata/storage/expirer.py 279
3212         # copy() needs to become a deepcopy
3213         h["space-recovered"] = s["space-recovered"].copy()
3214 
3215-        history = pickle.load(open(self.historyfile, "rb"))
3216+        history = pickle.load(self.historyfp.getContent())
3217         history[cycle] = h
3218         while len(history) > 10:
3219             oldcycles = sorted(history.keys())
3220hunk ./src/allmydata/storage/expirer.py 284
3221             del history[oldcycles[0]]
3222-        f = open(self.historyfile, "wb")
3223-        pickle.dump(history, f)
3224-        f.close()
3225+        self.historyfp.setContent(pickle.dumps(history))
3226 
3227     def get_state(self):
3228         """In addition to the crawler state described in
3229hunk ./src/allmydata/storage/expirer.py 353
3230         progress = self.get_progress()
3231 
3232         state = ShareCrawler.get_state(self) # does a shallow copy
3233-        history = pickle.load(open(self.historyfile, "rb"))
3234+        history = pickle.load(self.historyfp.getContent())
3235         state["history"] = history
3236 
3237         if not progress["cycle-in-progress"]:
3238hunk ./src/allmydata/storage/lease.py 17
3239 
3240     def get_expiration_time(self):
3241         return self.expiration_time
3242+
3243     def get_grant_renew_time_time(self):
3244         # hack, based upon fixed 31day expiration period
3245         return self.expiration_time - 31*24*60*60
3246hunk ./src/allmydata/storage/lease.py 21
3247+
3248     def get_age(self):
3249         return time.time() - self.get_grant_renew_time_time()
3250 
3251hunk ./src/allmydata/storage/lease.py 32
3252          self.expiration_time) = struct.unpack(">L32s32sL", data)
3253         self.nodeid = None
3254         return self
3255+
3256     def to_immutable_data(self):
3257         return struct.pack(">L32s32sL",
3258                            self.owner_num,
3259hunk ./src/allmydata/storage/lease.py 45
3260                            int(self.expiration_time),
3261                            self.renew_secret, self.cancel_secret,
3262                            self.nodeid)
3263+
3264     def from_mutable_data(self, data):
3265         (self.owner_num,
3266          self.expiration_time,
3267hunk ./src/allmydata/storage/server.py 1
3268-import os, re, weakref, struct, time
3269+import os, weakref, time
3270 
3271 from foolscap.api import Referenceable
3272 from twisted.application import service
3273hunk ./src/allmydata/storage/server.py 11
3274 from allmydata.util import fileutil, idlib, log, time_format
3275 import allmydata # for __full_version__
3276 
3277-from allmydata.storage.common import si_b2a, si_a2b, storage_index_to_dir
3278-_pyflakes_hush = [si_b2a, si_a2b, storage_index_to_dir] # re-exported
3279+from allmydata.storage.common import si_b2a
3280 from allmydata.storage.lease import LeaseInfo
3281hunk ./src/allmydata/storage/server.py 13
3282-from allmydata.storage.mutable import MutableShareFile, EmptyShare, \
3283-     create_mutable_sharefile
3284-from allmydata.storage.immutable import ShareFile, BucketWriter, BucketReader
3285-from allmydata.storage.crawler import BucketCountingCrawler
3286 from allmydata.storage.expirer import LeaseCheckingCrawler
3287hunk ./src/allmydata/storage/server.py 14
3288-
3289-# storage/
3290-# storage/shares/incoming
3291-#   incoming/ holds temp dirs named $START/$STORAGEINDEX/$SHARENUM which will
3292-#   be moved to storage/shares/$START/$STORAGEINDEX/$SHARENUM upon success
3293-# storage/shares/$START/$STORAGEINDEX
3294-# storage/shares/$START/$STORAGEINDEX/$SHARENUM
3295-
3296-# Where "$START" denotes the first 10 bits worth of $STORAGEINDEX (that's 2
3297-# base-32 chars).
3298-
3299-# $SHARENUM matches this regex:
3300-NUM_RE=re.compile("^[0-9]+$")
3301-
3302+from allmydata.storage.crawler import BucketCountingCrawler
3303 
3304 
3305 class StorageServer(service.MultiService, Referenceable):
3306hunk ./src/allmydata/storage/server.py 19
3307     implements(RIStorageServer, IStatsProducer)
3308+
3309     name = 'storage'
3310     LeaseCheckerClass = LeaseCheckingCrawler
3311hunk ./src/allmydata/storage/server.py 22
3312+    DEFAULT_EXPIRATION_POLICY = {
3313+        'enabled': False,
3314+        'mode': 'age',
3315+        'override_lease_duration': None,
3316+        'cutoff_date': None,
3317+        'sharetypes': ('mutable', 'immutable'),
3318+    }
3319 
3320hunk ./src/allmydata/storage/server.py 30
3321-    def __init__(self, storedir, nodeid, reserved_space=0,
3322-                 discard_storage=False, readonly_storage=False,
3323+    def __init__(self, nodeid, backend, reserved_space=0,
3324+                 readonly_storage=False,
3325                  stats_provider=None,
3326hunk ./src/allmydata/storage/server.py 33
3327-                 expiration_enabled=False,
3328-                 expiration_mode="age",
3329-                 expiration_override_lease_duration=None,
3330-                 expiration_cutoff_date=None,
3331-                 expiration_sharetypes=("mutable", "immutable")):
3332+                 expiration_policy=None):
3333         service.MultiService.__init__(self)
3334         assert isinstance(nodeid, str)
3335         assert len(nodeid) == 20
3336hunk ./src/allmydata/storage/server.py 38
3337         self.my_nodeid = nodeid
3338-        self.storedir = storedir
3339-        sharedir = os.path.join(storedir, "shares")
3340-        fileutil.make_dirs(sharedir)
3341-        self.sharedir = sharedir
3342-        # we don't actually create the corruption-advisory dir until necessary
3343-        self.corruption_advisory_dir = os.path.join(storedir,
3344-                                                    "corruption-advisories")
3345-        self.reserved_space = int(reserved_space)
3346-        self.no_storage = discard_storage
3347-        self.readonly_storage = readonly_storage
3348         self.stats_provider = stats_provider
3349         if self.stats_provider:
3350             self.stats_provider.register_producer(self)
3351hunk ./src/allmydata/storage/server.py 41
3352-        self.incomingdir = os.path.join(sharedir, 'incoming')
3353-        self._clean_incomplete()
3354-        fileutil.make_dirs(self.incomingdir)
3355         self._active_writers = weakref.WeakKeyDictionary()
3356hunk ./src/allmydata/storage/server.py 42
3357+        self.backend = backend
3358+        self.backend.setServiceParent(self)
3359+        self.backend.set_storage_server(self)
3360         log.msg("StorageServer created", facility="tahoe.storage")
3361 
3362hunk ./src/allmydata/storage/server.py 47
3363-        if reserved_space:
3364-            if self.get_available_space() is None:
3365-                log.msg("warning: [storage]reserved_space= is set, but this platform does not support an API to get disk statistics (statvfs(2) or GetDiskFreeSpaceEx), so this reservation cannot be honored",
3366-                        umin="0wZ27w", level=log.UNUSUAL)
3367-
3368         self.latencies = {"allocate": [], # immutable
3369                           "write": [],
3370                           "close": [],
3371hunk ./src/allmydata/storage/server.py 58
3372                           "renew": [],
3373                           "cancel": [],
3374                           }
3375-        self.add_bucket_counter()
3376-
3377-        statefile = os.path.join(self.storedir, "lease_checker.state")
3378-        historyfile = os.path.join(self.storedir, "lease_checker.history")
3379-        klass = self.LeaseCheckerClass
3380-        self.lease_checker = klass(self, statefile, historyfile,
3381-                                   expiration_enabled, expiration_mode,
3382-                                   expiration_override_lease_duration,
3383-                                   expiration_cutoff_date,
3384-                                   expiration_sharetypes)
3385-        self.lease_checker.setServiceParent(self)
3386+        self._setup_bucket_counter()
3387+        self._setup_lease_checker(expiration_policy or self.DEFAULT_EXPIRATION_POLICY)
3388 
3389     def __repr__(self):
3390         return "<StorageServer %s>" % (idlib.shortnodeid_b2a(self.my_nodeid),)
3391hunk ./src/allmydata/storage/server.py 64
3392 
3393-    def add_bucket_counter(self):
3394-        statefile = os.path.join(self.storedir, "bucket_counter.state")
3395-        self.bucket_counter = BucketCountingCrawler(self, statefile)
3396+    def _setup_bucket_counter(self):
3397+        statefp = self.storedir.child("bucket_counter.state")
3398+        self.bucket_counter = BucketCountingCrawler(statefp)
3399         self.bucket_counter.setServiceParent(self)
3400 
3401hunk ./src/allmydata/storage/server.py 69
3402+    def _setup_lease_checker(self, expiration_policy):
3403+        statefp = self.storedir.child("lease_checker.state")
3404+        historyfp = self.storedir.child("lease_checker.history")
3405+        self.lease_checker = self.LeaseCheckerClass(statefp, historyfp, expiration_policy)
3406+        self.lease_checker.setServiceParent(self)
3407+
3408     def count(self, name, delta=1):
3409         if self.stats_provider:
3410             self.stats_provider.count("storage_server." + name, delta)
3411hunk ./src/allmydata/storage/server.py 89
3412         """Return a dict, indexed by category, that contains a dict of
3413         latency numbers for each category. If there are sufficient samples
3414         for unambiguous interpretation, each dict will contain the
3415-        following keys: mean, 01_0_percentile, 10_0_percentile,
3416+        following keys: samplesize, mean, 01_0_percentile, 10_0_percentile,
3417         50_0_percentile (median), 90_0_percentile, 95_0_percentile,
3418         99_0_percentile, 99_9_percentile.  If there are insufficient
3419         samples for a given percentile to be interpreted unambiguously
3420hunk ./src/allmydata/storage/server.py 111
3421             else:
3422                 stats["mean"] = None
3423 
3424-            orderstatlist = [(0.01, "01_0_percentile", 100), (0.1, "10_0_percentile", 10),\
3425-                             (0.50, "50_0_percentile", 10), (0.90, "90_0_percentile", 10),\
3426-                             (0.95, "95_0_percentile", 20), (0.99, "99_0_percentile", 100),\
3427+            orderstatlist = [(0.1, "10_0_percentile", 10), (0.5, "50_0_percentile", 10), \
3428+                             (0.9, "90_0_percentile", 10), (0.95, "95_0_percentile", 20), \
3429+                             (0.01, "01_0_percentile", 100),  (0.99, "99_0_percentile", 100),\
3430                              (0.999, "99_9_percentile", 1000)]
3431 
3432             for percentile, percentilestring, minnumtoobserve in orderstatlist:
3433hunk ./src/allmydata/storage/server.py 130
3434             kwargs["facility"] = "tahoe.storage"
3435         return log.msg(*args, **kwargs)
3436 
3437-    def _clean_incomplete(self):
3438-        fileutil.rm_dir(self.incomingdir)
3439+    def get_nodeid(self):
3440+        return self.my_nodeid
3441 
3442     def get_stats(self):
3443         # remember: RIStatsProvider requires that our return dict
3444hunk ./src/allmydata/storage/server.py 135
3445-        # contains numeric values.
3446+        # contains numeric, or None values.
3447         stats = { 'storage_server.allocated': self.allocated_size(), }
3448         stats['storage_server.reserved_space'] = self.reserved_space
3449         for category,ld in self.get_latencies().items():
3450hunk ./src/allmydata/storage/server.py 142
3451             for name,v in ld.items():
3452                 stats['storage_server.latencies.%s.%s' % (category, name)] = v
3453 
3454-        try:
3455-            disk = fileutil.get_disk_stats(self.sharedir, self.reserved_space)
3456-            writeable = disk['avail'] > 0
3457+        self.backend.fill_in_space_stats(stats)
3458 
3459hunk ./src/allmydata/storage/server.py 144
3460-            # spacetime predictors should use disk_avail / (d(disk_used)/dt)
3461-            stats['storage_server.disk_total'] = disk['total']
3462-            stats['storage_server.disk_used'] = disk['used']
3463-            stats['storage_server.disk_free_for_root'] = disk['free_for_root']
3464-            stats['storage_server.disk_free_for_nonroot'] = disk['free_for_nonroot']
3465-            stats['storage_server.disk_avail'] = disk['avail']
3466-        except AttributeError:
3467-            writeable = True
3468-        except EnvironmentError:
3469-            log.msg("OS call to get disk statistics failed", level=log.UNUSUAL)
3470-            writeable = False
3471-
3472-        if self.readonly_storage:
3473-            stats['storage_server.disk_avail'] = 0
3474-            writeable = False
3475-
3476-        stats['storage_server.accepting_immutable_shares'] = int(writeable)
3477         s = self.bucket_counter.get_state()
3478         bucket_count = s.get("last-complete-bucket-count")
3479         if bucket_count:
3480hunk ./src/allmydata/storage/server.py 151
3481         return stats
3482 
3483     def get_available_space(self):
3484-        """Returns available space for share storage in bytes, or None if no
3485-        API to get this information is available."""
3486-
3487-        if self.readonly_storage:
3488-            return 0
3489-        return fileutil.get_available_space(self.sharedir, self.reserved_space)
3490+        return self.backend.get_available_space()
3491 
3492     def allocated_size(self):
3493         space = 0
3494hunk ./src/allmydata/storage/server.py 160
3495         return space
3496 
3497     def remote_get_version(self):
3498-        remaining_space = self.get_available_space()
3499+        remaining_space = self.backend.get_available_space()
3500         if remaining_space is None:
3501             # We're on a platform that has no API to get disk stats.
3502             remaining_space = 2**64
3503hunk ./src/allmydata/storage/server.py 176
3504                     }
3505         return version
3506 
3507-    def remote_allocate_buckets(self, storage_index,
3508+    def remote_allocate_buckets(self, storageindex,
3509                                 renew_secret, cancel_secret,
3510                                 sharenums, allocated_size,
3511                                 canary, owner_num=0):
3512hunk ./src/allmydata/storage/server.py 180
3513+        # cancel_secret is no longer used.
3514         # owner_num is not for clients to set, but rather it should be
3515hunk ./src/allmydata/storage/server.py 182
3516-        # curried into the PersonalStorageServer instance that is dedicated
3517-        # to a particular owner.
3518+        # curried into a StorageServer instance dedicated to a particular
3519+        # owner.
3520         start = time.time()
3521         self.count("allocate")
3522hunk ./src/allmydata/storage/server.py 186
3523-        alreadygot = set()
3524+        incoming = set()
3525         bucketwriters = {} # k: shnum, v: BucketWriter
3526hunk ./src/allmydata/storage/server.py 188
3527-        si_dir = storage_index_to_dir(storage_index)
3528-        si_s = si_b2a(storage_index)
3529 
3530hunk ./src/allmydata/storage/server.py 189
3531+        si_s = si_b2a(storageindex)
3532         log.msg("storage: allocate_buckets %s" % si_s)
3533 
3534hunk ./src/allmydata/storage/server.py 192
3535-        # in this implementation, the lease information (including secrets)
3536-        # goes into the share files themselves. It could also be put into a
3537-        # separate database. Note that the lease should not be added until
3538-        # the BucketWriter has been closed.
3539+        # Note that the lease should not be added until the BucketWriter
3540+        # has been closed.
3541         expire_time = time.time() + 31*24*60*60
3542hunk ./src/allmydata/storage/server.py 195
3543-        lease_info = LeaseInfo(owner_num,
3544-                               renew_secret, cancel_secret,
3545+        lease_info = LeaseInfo(owner_num, renew_secret,
3546                                expire_time, self.my_nodeid)
3547 
3548         max_space_per_bucket = allocated_size
3549hunk ./src/allmydata/storage/server.py 200
3550 
3551-        remaining_space = self.get_available_space()
3552+        remaining_space = self.backend.get_available_space()
3553         limited = remaining_space is not None
3554         if limited:
3555hunk ./src/allmydata/storage/server.py 203
3556-            # this is a bit conservative, since some of this allocated_size()
3557-            # has already been written to disk, where it will show up in
3558+            # This is a bit conservative, since some of this allocated_size()
3559+            # has already been written to the backend, where it will show up in
3560             # get_available_space.
3561             remaining_space -= self.allocated_size()
3562         # self.readonly_storage causes remaining_space <= 0
3563hunk ./src/allmydata/storage/server.py 209
3564 
3565-        # fill alreadygot with all shares that we have, not just the ones
3566+        # Fill alreadygot with all shares that we have, not just the ones
3567         # they asked about: this will save them a lot of work. Add or update
3568         # leases for all of them: if they want us to hold shares for this
3569hunk ./src/allmydata/storage/server.py 212
3570-        # file, they'll want us to hold leases for this file.
3571-        for (shnum, fn) in self._get_bucket_shares(storage_index):
3572-            alreadygot.add(shnum)
3573-            sf = ShareFile(fn)
3574-            sf.add_or_renew_lease(lease_info)
3575+        # file, they'll want us to hold leases for all the shares of it.
3576+        #
3577+        # XXX should we be making the assumption here that lease info is
3578+        # duplicated in all shares?
3579+        alreadygot = set()
3580+        for share in self.backend.get_shares(storageindex):
3581+            share.add_or_renew_lease(lease_info)
3582+            alreadygot.add(share.shnum)
3583 
3584hunk ./src/allmydata/storage/server.py 221
3585-        for shnum in sharenums:
3586-            incominghome = os.path.join(self.incomingdir, si_dir, "%d" % shnum)
3587-            finalhome = os.path.join(self.sharedir, si_dir, "%d" % shnum)
3588-            if os.path.exists(finalhome):
3589-                # great! we already have it. easy.
3590-                pass
3591-            elif os.path.exists(incominghome):
3592-                # Note that we don't create BucketWriters for shnums that
3593-                # have a partial share (in incoming/), so if a second upload
3594-                # occurs while the first is still in progress, the second
3595-                # uploader will use different storage servers.
3596-                pass
3597-            elif (not limited) or (remaining_space >= max_space_per_bucket):
3598-                # ok! we need to create the new share file.
3599-                bw = BucketWriter(self, incominghome, finalhome,
3600-                                  max_space_per_bucket, lease_info, canary)
3601-                if self.no_storage:
3602-                    bw.throw_out_all_data = True
3603+        # all share numbers that are incoming
3604+        incoming = self.backend.get_incoming_shnums(storageindex)
3605+
3606+        for shnum in ((sharenums - alreadygot) - incoming):
3607+            if (not limited) or (remaining_space >= max_space_per_bucket):
3608+                bw = self.backend.make_bucket_writer(storageindex, shnum, max_space_per_bucket,
3609+                                                     lease_info, canary)
3610                 bucketwriters[shnum] = bw
3611                 self._active_writers[bw] = 1
3612                 if limited:
3613hunk ./src/allmydata/storage/server.py 233
3614                     remaining_space -= max_space_per_bucket
3615             else:
3616-                # bummer! not enough space to accept this bucket
3617+                # Bummer not enough space to accept this share.
3618                 pass
3619 
3620hunk ./src/allmydata/storage/server.py 236
3621-        if bucketwriters:
3622-            fileutil.make_dirs(os.path.join(self.sharedir, si_dir))
3623-
3624         self.add_latency("allocate", time.time() - start)
3625         return alreadygot, bucketwriters
3626 
3627hunk ./src/allmydata/storage/server.py 239
3628-    def _iter_share_files(self, storage_index):
3629-        for shnum, filename in self._get_bucket_shares(storage_index):
3630-            f = open(filename, 'rb')
3631-            header = f.read(32)
3632-            f.close()
3633-            if header[:32] == MutableShareFile.MAGIC:
3634-                sf = MutableShareFile(filename, self)
3635-                # note: if the share has been migrated, the renew_lease()
3636-                # call will throw an exception, with information to help the
3637-                # client update the lease.
3638-            elif header[:4] == struct.pack(">L", 1):
3639-                sf = ShareFile(filename)
3640-            else:
3641-                continue # non-sharefile
3642-            yield sf
3643-
3644-    def remote_add_lease(self, storage_index, renew_secret, cancel_secret,
3645+    def remote_add_lease(self, storageindex, renew_secret, cancel_secret,
3646                          owner_num=1):
3647hunk ./src/allmydata/storage/server.py 241
3648+        # cancel_secret is no longer used.
3649         start = time.time()
3650         self.count("add-lease")
3651         new_expire_time = time.time() + 31*24*60*60
3652hunk ./src/allmydata/storage/server.py 245
3653-        lease_info = LeaseInfo(owner_num,
3654-                               renew_secret, cancel_secret,
3655+        lease_info = LeaseInfo(owner_num, renew_secret,
3656                                new_expire_time, self.my_nodeid)
3657hunk ./src/allmydata/storage/server.py 247
3658-        for sf in self._iter_share_files(storage_index):
3659-            sf.add_or_renew_lease(lease_info)
3660-        self.add_latency("add-lease", time.time() - start)
3661-        return None
3662 
3663hunk ./src/allmydata/storage/server.py 248
3664-    def remote_renew_lease(self, storage_index, renew_secret):
3665+        try:
3666+            self.backend.add_or_renew_lease(lease_info)
3667+        finally:
3668+            self.add_latency("add-lease", time.time() - start)
3669+
3670+    def remote_renew_lease(self, storageindex, renew_secret):
3671         start = time.time()
3672         self.count("renew")
3673hunk ./src/allmydata/storage/server.py 256
3674-        new_expire_time = time.time() + 31*24*60*60
3675-        found_buckets = False
3676-        for sf in self._iter_share_files(storage_index):
3677-            found_buckets = True
3678-            sf.renew_lease(renew_secret, new_expire_time)
3679-        self.add_latency("renew", time.time() - start)
3680-        if not found_buckets:
3681-            raise IndexError("no such lease to renew")
3682+
3683+        try:
3684+            shareset = self.backend.get_shareset(storageindex)
3685+            new_expiration_time = start + 31*24*60*60   # one month from now
3686+            shareset.renew_lease(renew_secret, new_expiration_time)
3687+        finally:
3688+            self.add_latency("renew", time.time() - start)
3689 
3690     def bucket_writer_closed(self, bw, consumed_size):
3691         if self.stats_provider:
3692hunk ./src/allmydata/storage/server.py 269
3693             self.stats_provider.count('storage_server.bytes_added', consumed_size)
3694         del self._active_writers[bw]
3695 
3696-    def _get_bucket_shares(self, storage_index):
3697-        """Return a list of (shnum, pathname) tuples for files that hold
3698-        shares for this storage_index. In each tuple, 'shnum' will always be
3699-        the integer form of the last component of 'pathname'."""
3700-        storagedir = os.path.join(self.sharedir, storage_index_to_dir(storage_index))
3701-        try:
3702-            for f in os.listdir(storagedir):
3703-                if NUM_RE.match(f):
3704-                    filename = os.path.join(storagedir, f)
3705-                    yield (int(f), filename)
3706-        except OSError:
3707-            # Commonly caused by there being no buckets at all.
3708-            pass
3709-
3710-    def remote_get_buckets(self, storage_index):
3711+    def remote_get_buckets(self, storageindex):
3712         start = time.time()
3713         self.count("get")
3714hunk ./src/allmydata/storage/server.py 272
3715-        si_s = si_b2a(storage_index)
3716+        si_s = si_b2a(storageindex)
3717         log.msg("storage: get_buckets %s" % si_s)
3718         bucketreaders = {} # k: sharenum, v: BucketReader
3719hunk ./src/allmydata/storage/server.py 275
3720-        for shnum, filename in self._get_bucket_shares(storage_index):
3721-            bucketreaders[shnum] = BucketReader(self, filename,
3722-                                                storage_index, shnum)
3723-        self.add_latency("get", time.time() - start)
3724-        return bucketreaders
3725 
3726hunk ./src/allmydata/storage/server.py 276
3727-    def get_leases(self, storage_index):
3728-        """Provide an iterator that yields all of the leases attached to this
3729-        bucket. Each lease is returned as a LeaseInfo instance.
3730+        try:
3731+            shareset = self.backend.get_shareset(storageindex)
3732+            for share in shareset.get_shares(storageindex):
3733+                bucketreaders[share.get_shnum()] = self.backend.make_bucket_reader(self, share)
3734+            return bucketreaders
3735+        finally:
3736+            self.add_latency("get", time.time() - start)
3737 
3738hunk ./src/allmydata/storage/server.py 284
3739-        This method is not for client use.
3740+    def get_leases(self, storageindex):
3741         """
3742hunk ./src/allmydata/storage/server.py 286
3743+        Provide an iterator that yields all of the leases attached to this
3744+        bucket. Each lease is returned as a LeaseInfo instance.
3745 
3746hunk ./src/allmydata/storage/server.py 289
3747-        # since all shares get the same lease data, we just grab the leases
3748-        # from the first share
3749-        try:
3750-            shnum, filename = self._get_bucket_shares(storage_index).next()
3751-            sf = ShareFile(filename)
3752-            return sf.get_leases()
3753-        except StopIteration:
3754-            return iter([])
3755+        This method is not for client use. XXX do we need it at all?
3756+        """
3757+        return self.backend.get_shareset(storageindex).get_leases()
3758 
3759hunk ./src/allmydata/storage/server.py 293
3760-    def remote_slot_testv_and_readv_and_writev(self, storage_index,
3761+    def remote_slot_testv_and_readv_and_writev(self, storageindex,
3762                                                secrets,
3763                                                test_and_write_vectors,
3764                                                read_vector):
3765hunk ./src/allmydata/storage/server.py 299
3766         start = time.time()
3767         self.count("writev")
3768-        si_s = si_b2a(storage_index)
3769+        si_s = si_b2a(storageindex)
3770         log.msg("storage: slot_writev %s" % si_s)
3771hunk ./src/allmydata/storage/server.py 301
3772-        si_dir = storage_index_to_dir(storage_index)
3773-        (write_enabler, renew_secret, cancel_secret) = secrets
3774-        # shares exist if there is a file for them
3775-        bucketdir = os.path.join(self.sharedir, si_dir)
3776-        shares = {}
3777-        if os.path.isdir(bucketdir):
3778-            for sharenum_s in os.listdir(bucketdir):
3779-                try:
3780-                    sharenum = int(sharenum_s)
3781-                except ValueError:
3782-                    continue
3783-                filename = os.path.join(bucketdir, sharenum_s)
3784-                msf = MutableShareFile(filename, self)
3785-                msf.check_write_enabler(write_enabler, si_s)
3786-                shares[sharenum] = msf
3787-        # write_enabler is good for all existing shares.
3788-
3789-        # Now evaluate test vectors.
3790-        testv_is_good = True
3791-        for sharenum in test_and_write_vectors:
3792-            (testv, datav, new_length) = test_and_write_vectors[sharenum]
3793-            if sharenum in shares:
3794-                if not shares[sharenum].check_testv(testv):
3795-                    self.log("testv failed: [%d]: %r" % (sharenum, testv))
3796-                    testv_is_good = False
3797-                    break
3798-            else:
3799-                # compare the vectors against an empty share, in which all
3800-                # reads return empty strings.
3801-                if not EmptyShare().check_testv(testv):
3802-                    self.log("testv failed (empty): [%d] %r" % (sharenum,
3803-                                                                testv))
3804-                    testv_is_good = False
3805-                    break
3806 
3807hunk ./src/allmydata/storage/server.py 302
3808-        # now gather the read vectors, before we do any writes
3809-        read_data = {}
3810-        for sharenum, share in shares.items():
3811-            read_data[sharenum] = share.readv(read_vector)
3812-
3813-        ownerid = 1 # TODO
3814-        expire_time = time.time() + 31*24*60*60   # one month
3815-        lease_info = LeaseInfo(ownerid,
3816-                               renew_secret, cancel_secret,
3817-                               expire_time, self.my_nodeid)
3818-
3819-        if testv_is_good:
3820-            # now apply the write vectors
3821-            for sharenum in test_and_write_vectors:
3822-                (testv, datav, new_length) = test_and_write_vectors[sharenum]
3823-                if new_length == 0:
3824-                    if sharenum in shares:
3825-                        shares[sharenum].unlink()
3826-                else:
3827-                    if sharenum not in shares:
3828-                        # allocate a new share
3829-                        allocated_size = 2000 # arbitrary, really
3830-                        share = self._allocate_slot_share(bucketdir, secrets,
3831-                                                          sharenum,
3832-                                                          allocated_size,
3833-                                                          owner_num=0)
3834-                        shares[sharenum] = share
3835-                    shares[sharenum].writev(datav, new_length)
3836-                    # and update the lease
3837-                    shares[sharenum].add_or_renew_lease(lease_info)
3838-
3839-            if new_length == 0:
3840-                # delete empty bucket directories
3841-                if not os.listdir(bucketdir):
3842-                    os.rmdir(bucketdir)
3843-
3844-
3845-        # all done
3846-        self.add_latency("writev", time.time() - start)
3847-        return (testv_is_good, read_data)
3848-
3849-    def _allocate_slot_share(self, bucketdir, secrets, sharenum,
3850-                             allocated_size, owner_num=0):
3851-        (write_enabler, renew_secret, cancel_secret) = secrets
3852-        my_nodeid = self.my_nodeid
3853-        fileutil.make_dirs(bucketdir)
3854-        filename = os.path.join(bucketdir, "%d" % sharenum)
3855-        share = create_mutable_sharefile(filename, my_nodeid, write_enabler,
3856-                                         self)
3857-        return share
3858+        try:
3859+            shareset = self.backend.get_shareset(storageindex)
3860+            expiration_time = start + 31*24*60*60   # one month from now
3861+            return shareset.testv_and_readv_and_writev(self, secrets, test_and_write_vectors,
3862+                                                       read_vector, expiration_time)
3863+        finally:
3864+            self.add_latency("writev", time.time() - start)
3865 
3866hunk ./src/allmydata/storage/server.py 310
3867-    def remote_slot_readv(self, storage_index, shares, readv):
3868+    def remote_slot_readv(self, storageindex, shares, readv):
3869         start = time.time()
3870         self.count("readv")
3871hunk ./src/allmydata/storage/server.py 313
3872-        si_s = si_b2a(storage_index)
3873-        lp = log.msg("storage: slot_readv %s %s" % (si_s, shares),
3874-                     facility="tahoe.storage", level=log.OPERATIONAL)
3875-        si_dir = storage_index_to_dir(storage_index)
3876-        # shares exist if there is a file for them
3877-        bucketdir = os.path.join(self.sharedir, si_dir)
3878-        if not os.path.isdir(bucketdir):
3879+        si_s = si_b2a(storageindex)
3880+        log.msg("storage: slot_readv %s %s" % (si_s, shares),
3881+                facility="tahoe.storage", level=log.OPERATIONAL)
3882+
3883+        try:
3884+            shareset = self.backend.get_shareset(storageindex)
3885+            return shareset.readv(self, shares, readv)
3886+        finally:
3887             self.add_latency("readv", time.time() - start)
3888hunk ./src/allmydata/storage/server.py 322
3889-            return {}
3890-        datavs = {}
3891-        for sharenum_s in os.listdir(bucketdir):
3892-            try:
3893-                sharenum = int(sharenum_s)
3894-            except ValueError:
3895-                continue
3896-            if sharenum in shares or not shares:
3897-                filename = os.path.join(bucketdir, sharenum_s)
3898-                msf = MutableShareFile(filename, self)
3899-                datavs[sharenum] = msf.readv(readv)
3900-        log.msg("returning shares %s" % (datavs.keys(),),
3901-                facility="tahoe.storage", level=log.NOISY, parent=lp)
3902-        self.add_latency("readv", time.time() - start)
3903-        return datavs
3904 
3905hunk ./src/allmydata/storage/server.py 323
3906-    def remote_advise_corrupt_share(self, share_type, storage_index, shnum,
3907+    def remote_advise_corrupt_share(self, share_type, storageindex, shnum,
3908                                     reason):
3909         fileutil.make_dirs(self.corruption_advisory_dir)
3910         now = time_format.iso_utc(sep="T")
3911hunk ./src/allmydata/storage/server.py 327
3912-        si_s = si_b2a(storage_index)
3913+        si_s = si_b2a(storageindex)
3914         # windows can't handle colons in the filename
3915         fn = os.path.join(self.corruption_advisory_dir,
3916                           "%s--%s-%d" % (now, si_s, shnum)).replace(":","")
3917hunk ./src/allmydata/storage/server.py 334
3918         f = open(fn, "w")
3919         f.write("report: Share Corruption\n")
3920         f.write("type: %s\n" % share_type)
3921-        f.write("storage_index: %s\n" % si_s)
3922+        f.write("storageindex: %s\n" % si_s)
3923         f.write("share_number: %d\n" % shnum)
3924         f.write("\n")
3925         f.write(reason)
3926addfile ./src/allmydata/test/test_backends.py
3927hunk ./src/allmydata/test/test_backends.py 1
3928+import os, stat
3929+from twisted.trial import unittest
3930+from allmydata.util.log import msg
3931+from allmydata.test.common_util import ReallyEqualMixin
3932+import mock
3933+# This is the code that we're going to be testing.
3934+from allmydata.storage.server import StorageServer
3935+from allmydata.storage.backends.disk.core import DiskBackend
3936+from allmydata.storage.backends.null.core import NullBackend
3937+from allmydata.storage.common import si_si2dir
3938+# The following share file content was generated with
3939+# storage.immutable.ShareFile from Tahoe-LAFS v1.8.2
3940+# with share data == 'a'. The total size of this input
3941+# is 85 bytes.
3942+shareversionnumber = '\x00\x00\x00\x01'
3943+sharedatalength = '\x00\x00\x00\x01'
3944+numberofleases = '\x00\x00\x00\x01'
3945+shareinputdata = 'a'
3946+ownernumber = '\x00\x00\x00\x00'
3947+renewsecret  = 'x'*32
3948+cancelsecret = 'y'*32
3949+expirationtime = '\x00(\xde\x80'
3950+nextlease = ''
3951+containerdata = shareversionnumber + sharedatalength + numberofleases
3952+client_data = shareinputdata + ownernumber + renewsecret + \
3953+    cancelsecret + expirationtime + nextlease
3954+share_data = containerdata + client_data
3955+testnodeid = 'testnodeidxxxxxxxxxx'
3956+
3957+
3958+class MockFileSystem(unittest.TestCase):
3959+    """ I simulate a filesystem that the code under test can use. I simulate
3960+    just the parts of the filesystem that the current implementation of Disk
3961+    backend needs. """
3962+    def setUp(self):
3963+        # Make patcher, patch, and effects for disk-using functions.
3964+        msg( "%s.setUp()" % (self,))
3965+        self.mockedfilepaths = {}
3966+        # keys are pathnames, values are MockFilePath objects. This is necessary because
3967+        # MockFilePath behavior sometimes depends on the filesystem. Where it does,
3968+        # self.mockedfilepaths has the relevant information.
3969+        self.storedir = MockFilePath('teststoredir', self.mockedfilepaths)
3970+        self.basedir = self.storedir.child('shares')
3971+        self.baseincdir = self.basedir.child('incoming')
3972+        self.sharedirfinalname = self.basedir.child('or').child('orsxg5dtorxxeylhmvpws3temv4a')
3973+        self.sharedirincomingname = self.baseincdir.child('or').child('orsxg5dtorxxeylhmvpws3temv4a')
3974+        self.shareincomingname = self.sharedirincomingname.child('0')
3975+        self.sharefinalname = self.sharedirfinalname.child('0')
3976+
3977+        self.FilePathFake = mock.patch('allmydata.storage.backends.disk.core.FilePath', new = MockFilePath)
3978+        self.FilePathFake.__enter__()
3979+
3980+        self.BCountingCrawler = mock.patch('allmydata.storage.backends.disk.core.BucketCountingCrawler')
3981+        FakeBCC = self.BCountingCrawler.__enter__()
3982+        FakeBCC.side_effect = self.call_FakeBCC
3983+
3984+        self.LeaseCheckingCrawler = mock.patch('allmydata.storage.backends.disk.core.LeaseCheckingCrawler')
3985+        FakeLCC = self.LeaseCheckingCrawler.__enter__()
3986+        FakeLCC.side_effect = self.call_FakeLCC
3987+
3988+        self.get_available_space = mock.patch('allmydata.util.fileutil.get_available_space')
3989+        GetSpace = self.get_available_space.__enter__()
3990+        GetSpace.side_effect = self.call_get_available_space
3991+
3992+        self.statforsize = mock.patch('allmydata.storage.backends.disk.core.filepath.stat')
3993+        getsize = self.statforsize.__enter__()
3994+        getsize.side_effect = self.call_statforsize
3995+
3996+    def call_FakeBCC(self, StateFile):
3997+        return MockBCC()
3998+
3999+    def call_FakeLCC(self, StateFile, HistoryFile, ExpirationPolicy):
4000+        return MockLCC()
4001+
4002+    def call_get_available_space(self, storedir, reservedspace):
4003+        # The input vector has an input size of 85.
4004+        return 85 - reservedspace
4005+
4006+    def call_statforsize(self, fakefpname):
4007+        return self.mockedfilepaths[fakefpname].fileobject.size()
4008+
4009+    def tearDown(self):
4010+        msg( "%s.tearDown()" % (self,))
4011+        self.FilePathFake.__exit__()
4012+        self.mockedfilepaths = {}
4013+
4014+
4015+class MockFilePath:
4016+    def __init__(self, pathstring, ffpathsenvironment, existence=False):
4017+        #  I can't just make the values MockFileObjects because they may be directories.
4018+        self.mockedfilepaths = ffpathsenvironment
4019+        self.path = pathstring
4020+        self.existence = existence
4021+        if not self.mockedfilepaths.has_key(self.path):
4022+            #  The first MockFilePath object is special
4023+            self.mockedfilepaths[self.path] = self
4024+            self.fileobject = None
4025+        else:
4026+            self.fileobject = self.mockedfilepaths[self.path].fileobject
4027+        self.spawn = {}
4028+        self.antecedent = os.path.dirname(self.path)
4029+
4030+    def setContent(self, contentstring):
4031+        # This method rewrites the data in the file that corresponds to its path
4032+        # name whether it preexisted or not.
4033+        self.fileobject = MockFileObject(contentstring)
4034+        self.existence = True
4035+        self.mockedfilepaths[self.path].fileobject = self.fileobject
4036+        self.mockedfilepaths[self.path].existence = self.existence
4037+        self.setparents()
4038+
4039+    def create(self):
4040+        # This method chokes if there's a pre-existing file!
4041+        if self.mockedfilepaths[self.path].fileobject:
4042+            raise OSError
4043+        else:
4044+            self.existence = True
4045+            self.mockedfilepaths[self.path].fileobject = self.fileobject
4046+            self.mockedfilepaths[self.path].existence = self.existence
4047+            self.setparents()
4048+
4049+    def open(self, mode='r'):
4050+        # XXX Makes no use of mode.
4051+        if not self.mockedfilepaths[self.path].fileobject:
4052+            # If there's no fileobject there already then make one and put it there.
4053+            self.fileobject = MockFileObject()
4054+            self.existence = True
4055+            self.mockedfilepaths[self.path].fileobject = self.fileobject
4056+            self.mockedfilepaths[self.path].existence = self.existence
4057+        else:
4058+            # Otherwise get a ref to it.
4059+            self.fileobject = self.mockedfilepaths[self.path].fileobject
4060+            self.existence = self.mockedfilepaths[self.path].existence
4061+        return self.fileobject.open(mode)
4062+
4063+    def child(self, childstring):
4064+        arg2child = os.path.join(self.path, childstring)
4065+        child = MockFilePath(arg2child, self.mockedfilepaths)
4066+        return child
4067+
4068+    def children(self):
4069+        childrenfromffs = [ffp for ffp in self.mockedfilepaths.values() if ffp.path.startswith(self.path)]
4070+        childrenfromffs = [ffp for ffp in childrenfromffs if not ffp.path.endswith(self.path)]
4071+        childrenfromffs = [ffp for ffp in childrenfromffs if ffp.exists()]
4072+        self.spawn = frozenset(childrenfromffs)
4073+        return self.spawn
4074+
4075+    def parent(self):
4076+        if self.mockedfilepaths.has_key(self.antecedent):
4077+            parent = self.mockedfilepaths[self.antecedent]
4078+        else:
4079+            parent = MockFilePath(self.antecedent, self.mockedfilepaths)
4080+        return parent
4081+
4082+    def parents(self):
4083+        antecedents = []
4084+        def f(fps, antecedents):
4085+            newfps = os.path.split(fps)[0]
4086+            if newfps:
4087+                antecedents.append(newfps)
4088+                f(newfps, antecedents)
4089+        f(self.path, antecedents)
4090+        return antecedents
4091+
4092+    def setparents(self):
4093+        for fps in self.parents():
4094+            if not self.mockedfilepaths.has_key(fps):
4095+                self.mockedfilepaths[fps] = MockFilePath(fps, self.mockedfilepaths, exists=True)
4096+
4097+    def basename(self):
4098+        return os.path.split(self.path)[1]
4099+
4100+    def moveTo(self, newffp):
4101+        #  XXX Makes no distinction between file and directory arguments, this is deviation from filepath.moveTo
4102+        if self.mockedfilepaths[newffp.path].exists():
4103+            raise OSError
4104+        else:
4105+            self.mockedfilepaths[newffp.path] = self
4106+            self.path = newffp.path
4107+
4108+    def getsize(self):
4109+        return self.fileobject.getsize()
4110+
4111+    def exists(self):
4112+        return self.existence
4113+
4114+    def isdir(self):
4115+        return True
4116+
4117+    def makedirs(self):
4118+        # XXX These methods assume that fp_<FOO> functions in fileutil will be tested elsewhere!
4119+        pass
4120+
4121+    def remove(self):
4122+        pass
4123+
4124+
4125+class MockFileObject:
4126+    def __init__(self, contentstring=''):
4127+        self.buffer = contentstring
4128+        self.pos = 0
4129+    def open(self, mode='r'):
4130+        return self
4131+    def write(self, instring):
4132+        begin = self.pos
4133+        padlen = begin - len(self.buffer)
4134+        if padlen > 0:
4135+            self.buffer += '\x00' * padlen
4136+        end = self.pos + len(instring)
4137+        self.buffer = self.buffer[:begin]+instring+self.buffer[end:]
4138+        self.pos = end
4139+    def close(self):
4140+        self.pos = 0
4141+    def seek(self, pos):
4142+        self.pos = pos
4143+    def read(self, numberbytes):
4144+        return self.buffer[self.pos:self.pos+numberbytes]
4145+    def tell(self):
4146+        return self.pos
4147+    def size(self):
4148+        # XXX This method A: Is not to be found in a real file B: Is part of a wild-mung-up of filepath.stat!
4149+        # XXX Finally we shall hopefully use a getsize method soon, must consult first though.
4150+        # Hmmm...  perhaps we need to sometimes stat the address when there's not a mockfileobject present?
4151+        return {stat.ST_SIZE:len(self.buffer)}
4152+    def getsize(self):
4153+        return len(self.buffer)
4154+
4155+class MockBCC:
4156+    def setServiceParent(self, Parent):
4157+        pass
4158+
4159+
4160+class MockLCC:
4161+    def setServiceParent(self, Parent):
4162+        pass
4163+
4164+
4165+class TestServerWithNullBackend(unittest.TestCase, ReallyEqualMixin):
4166+    """ NullBackend is just for testing and executable documentation, so
4167+    this test is actually a test of StorageServer in which we're using
4168+    NullBackend as helper code for the test, rather than a test of
4169+    NullBackend. """
4170+    def setUp(self):
4171+        self.ss = StorageServer(testnodeid, NullBackend())
4172+
4173+    @mock.patch('os.mkdir')
4174+    @mock.patch('__builtin__.open')
4175+    @mock.patch('os.listdir')
4176+    @mock.patch('os.path.isdir')
4177+    def test_write_share(self, mockisdir, mocklistdir, mockopen, mockmkdir):
4178+        """
4179+        Write a new share. This tests that StorageServer's remote_allocate_buckets
4180+        generates the correct return types when given test-vector arguments. That
4181+        bs is of the correct type is verified by attempting to invoke remote_write
4182+        on bs[0].
4183+        """
4184+        alreadygot, bs = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
4185+        bs[0].remote_write(0, 'a')
4186+        self.failIf(mockisdir.called)
4187+        self.failIf(mocklistdir.called)
4188+        self.failIf(mockopen.called)
4189+        self.failIf(mockmkdir.called)
4190+
4191+
4192+class TestServerConstruction(MockFileSystem, ReallyEqualMixin):
4193+    def test_create_server_disk_backend(self):
4194+        """ This tests whether a server instance can be constructed with a
4195+        filesystem backend. To pass the test, it mustn't use the filesystem
4196+        outside of its configured storedir. """
4197+        StorageServer(testnodeid, DiskBackend(self.storedir))
4198+
4199+
4200+class TestServerAndDiskBackend(MockFileSystem, ReallyEqualMixin):
4201+    """ This tests both the StorageServer and the Disk backend together. """
4202+    def setUp(self):
4203+        MockFileSystem.setUp(self)
4204+        try:
4205+            self.backend = DiskBackend(self.storedir)
4206+            self.ss = StorageServer(testnodeid, self.backend)
4207+
4208+            self.backendwithreserve = DiskBackend(self.storedir, reserved_space = 1)
4209+            self.sswithreserve = StorageServer(testnodeid, self.backendwithreserve)
4210+        except:
4211+            MockFileSystem.tearDown(self)
4212+            raise
4213+
4214+    @mock.patch('time.time')
4215+    @mock.patch('allmydata.util.fileutil.get_available_space')
4216+    def test_out_of_space(self, mockget_available_space, mocktime):
4217+        mocktime.return_value = 0
4218+
4219+        def call_get_available_space(dir, reserve):
4220+            return 0
4221+
4222+        mockget_available_space.side_effect = call_get_available_space
4223+        alreadygotc, bsc = self.sswithreserve.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
4224+        self.failUnlessReallyEqual(bsc, {})
4225+
4226+    @mock.patch('time.time')
4227+    def test_write_and_read_share(self, mocktime):
4228+        """
4229+        Write a new share, read it, and test the server's (and disk backend's)
4230+        handling of simultaneous and successive attempts to write the same
4231+        share.
4232+        """
4233+        mocktime.return_value = 0
4234+        # Inspect incoming and fail unless it's empty.
4235+        incomingset = self.ss.backend.get_incoming_shnums('teststorage_index')
4236+
4237+        self.failUnlessReallyEqual(incomingset, frozenset())
4238+
4239+        # Populate incoming with the sharenum: 0.
4240+        alreadygot, bs = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, frozenset((0,)), 1, mock.Mock())
4241+
4242+        # This is a transparent-box test: Inspect incoming and fail unless the sharenum: 0 is listed there.
4243+        self.failUnlessReallyEqual(self.ss.backend.get_incoming_shnums('teststorage_index'), frozenset((0,)))
4244+
4245+
4246+
4247+        # Attempt to create a second share writer with the same sharenum.
4248+        alreadygota, bsa = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, frozenset((0,)), 1, mock.Mock())
4249+
4250+        # Show that no sharewriter results from a remote_allocate_buckets
4251+        # with the same si and sharenum, until BucketWriter.remote_close()
4252+        # has been called.
4253+        self.failIf(bsa)
4254+
4255+        # Test allocated size.
4256+        spaceint = self.ss.allocated_size()
4257+        self.failUnlessReallyEqual(spaceint, 1)
4258+
4259+        # Write 'a' to shnum 0. Only tested together with close and read.
4260+        bs[0].remote_write(0, 'a')
4261+
4262+        # Preclose: Inspect final, failUnless nothing there.
4263+        self.failUnlessReallyEqual(len(list(self.backend.get_shares('teststorage_index'))), 0)
4264+        bs[0].remote_close()
4265+
4266+        # Postclose: (Omnibus) failUnless written data is in final.
4267+        sharesinfinal = list(self.backend.get_shares('teststorage_index'))
4268+        self.failUnlessReallyEqual(len(sharesinfinal), 1)
4269+        contents = sharesinfinal[0].read_share_data(0, 73)
4270+        self.failUnlessReallyEqual(contents, client_data)
4271+
4272+        # Exercise the case that the share we're asking to allocate is
4273+        # already (completely) uploaded.
4274+        self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
4275+
4276+
4277+    def test_read_old_share(self):
4278+        """ This tests whether the code correctly finds and reads
4279+        shares written out by old (Tahoe-LAFS <= v1.8.2)
4280+        servers. There is a similar test in test_download, but that one
4281+        is from the perspective of the client and exercises a deeper
4282+        stack of code. This one is for exercising just the
4283+        StorageServer object. """
4284+        # Contruct a file with the appropriate contents in the mockfilesystem.
4285+        datalen = len(share_data)
4286+        finalhome = si_si2dir(self.basedir, 'teststorage_index').child(str(0))
4287+        finalhome.setContent(share_data)
4288+
4289+        # Now begin the test.
4290+        bs = self.ss.remote_get_buckets('teststorage_index')
4291+
4292+        self.failUnlessEqual(len(bs), 1)
4293+        b = bs['0']
4294+        # These should match by definition, the next two cases cover cases without (completely) unambiguous behaviors.
4295+        self.failUnlessReallyEqual(b.remote_read(0, datalen), client_data)
4296+        # If you try to read past the end you get the as much data as is there.
4297+        self.failUnlessReallyEqual(b.remote_read(0, datalen+20), client_data)
4298+        # If you start reading past the end of the file you get the empty string.
4299+        self.failUnlessReallyEqual(b.remote_read(datalen+1, 3), '')
4300hunk ./src/allmydata/test/test_mutable.py 6
4301 from cStringIO import StringIO
4302 from twisted.trial import unittest
4303 from twisted.internet import defer, reactor
4304-from twisted.internet.interfaces import IConsumer
4305-from zope.interface import implements
4306 from allmydata import uri, client
4307 from allmydata.nodemaker import NodeMaker
4308 from allmydata.util import base32, consumer, fileutil, mathutil
4309hunk ./src/allmydata/test/test_storage.py 3289
4310     def test_expire_age(self):
4311         basedir = "storage/LeaseCrawler/expire_age"
4312         fileutil.make_dirs(basedir)
4313-        # setting expiration_time to 2000 means that any lease which is more
4314-        # than 2000s old will be expired.
4315-        ss = InstrumentedStorageServer(basedir, "\x00" * 20,
4316-                                       expiration_enabled=True,
4317-                                       expiration_mode="age",
4318-                                       expiration_override_lease_duration=2000)
4319+        # setting 'override_lease_duration' to 2000 means that any lease that
4320+        # is more than 2000 seconds old will be expired.
4321+        expiration_policy = {
4322+            'enabled': True,
4323+            'mode': 'age',
4324+            'override_lease_duration': 2000,
4325+            'sharetypes': ('mutable', 'immutable'),
4326+        }
4327+        ss = InstrumentedStorageServer(basedir, "\x00" * 20, expiration_policy)
4328         # make it start sooner than usual.
4329         lc = ss.lease_checker
4330         lc.slow_start = 0
4331hunk ./src/allmydata/test/test_storage.py 3430
4332     def test_expire_cutoff_date(self):
4333         basedir = "storage/LeaseCrawler/expire_cutoff_date"
4334         fileutil.make_dirs(basedir)
4335-        # setting cutoff-date to 2000 seconds ago means that any lease which
4336-        # is more than 2000s old will be expired.
4337+        # setting 'cutoff_date' to 2000 seconds ago means that any lease that
4338+        # is more than 2000 seconds old will be expired.
4339         now = time.time()
4340         then = int(now - 2000)
4341hunk ./src/allmydata/test/test_storage.py 3434
4342-        ss = InstrumentedStorageServer(basedir, "\x00" * 20,
4343-                                       expiration_enabled=True,
4344-                                       expiration_mode="cutoff-date",
4345-                                       expiration_cutoff_date=then)
4346+        expiration_policy = {
4347+            'enabled': True,
4348+            'mode': 'cutoff-date',
4349+            'cutoff_date': then,
4350+            'sharetypes': ('mutable', 'immutable'),
4351+        }
4352+        ss = InstrumentedStorageServer(basedir, "\x00" * 20, expiration_policy)
4353         # make it start sooner than usual.
4354         lc = ss.lease_checker
4355         lc.slow_start = 0
4356hunk ./src/allmydata/test/test_storage.py 3582
4357     def test_only_immutable(self):
4358         basedir = "storage/LeaseCrawler/only_immutable"
4359         fileutil.make_dirs(basedir)
4360+        # setting 'cutoff_date' to 2000 seconds ago means that any lease that
4361+        # is more than 2000 seconds old will be expired.
4362         now = time.time()
4363         then = int(now - 2000)
4364hunk ./src/allmydata/test/test_storage.py 3586
4365-        ss = StorageServer(basedir, "\x00" * 20,
4366-                           expiration_enabled=True,
4367-                           expiration_mode="cutoff-date",
4368-                           expiration_cutoff_date=then,
4369-                           expiration_sharetypes=("immutable",))
4370+        expiration_policy = {
4371+            'enabled': True,
4372+            'mode': 'cutoff-date',
4373+            'cutoff_date': then,
4374+            'sharetypes': ('immutable',),
4375+        }
4376+        ss = StorageServer(basedir, "\x00" * 20, expiration_policy)
4377         lc = ss.lease_checker
4378         lc.slow_start = 0
4379         webstatus = StorageStatus(ss)
4380hunk ./src/allmydata/test/test_storage.py 3643
4381     def test_only_mutable(self):
4382         basedir = "storage/LeaseCrawler/only_mutable"
4383         fileutil.make_dirs(basedir)
4384+        # setting 'cutoff_date' to 2000 seconds ago means that any lease that
4385+        # is more than 2000 seconds old will be expired.
4386         now = time.time()
4387         then = int(now - 2000)
4388hunk ./src/allmydata/test/test_storage.py 3647
4389-        ss = StorageServer(basedir, "\x00" * 20,
4390-                           expiration_enabled=True,
4391-                           expiration_mode="cutoff-date",
4392-                           expiration_cutoff_date=then,
4393-                           expiration_sharetypes=("mutable",))
4394+        expiration_policy = {
4395+            'enabled': True,
4396+            'mode': 'cutoff-date',
4397+            'cutoff_date': then,
4398+            'sharetypes': ('mutable',),
4399+        }
4400+        ss = StorageServer(basedir, "\x00" * 20, expiration_policy)
4401         lc = ss.lease_checker
4402         lc.slow_start = 0
4403         webstatus = StorageStatus(ss)
4404hunk ./src/allmydata/test/test_storage.py 3826
4405     def test_no_st_blocks(self):
4406         basedir = "storage/LeaseCrawler/no_st_blocks"
4407         fileutil.make_dirs(basedir)
4408-        ss = No_ST_BLOCKS_StorageServer(basedir, "\x00" * 20,
4409-                                        expiration_mode="age",
4410-                                        expiration_override_lease_duration=-1000)
4411-        # a negative expiration_time= means the "configured-"
4412+        # A negative 'override_lease_duration' means that the "configured-"
4413         # space-recovered counts will be non-zero, since all shares will have
4414hunk ./src/allmydata/test/test_storage.py 3828
4415-        # expired by then
4416+        # expired by then.
4417+        expiration_policy = {
4418+            'enabled': True,
4419+            'mode': 'age',
4420+            'override_lease_duration': -1000,
4421+            'sharetypes': ('mutable', 'immutable'),
4422+        }
4423+        ss = No_ST_BLOCKS_StorageServer(basedir, "\x00" * 20, expiration_policy)
4424 
4425         # make it start sooner than usual.
4426         lc = ss.lease_checker
4427hunk ./src/allmydata/util/encodingutil.py 221
4428 def quote_path(path, quotemarks=True):
4429     return quote_output("/".join(map(to_str, path)), quotemarks=quotemarks)
4430 
4431+def quote_filepath(fp, quotemarks=True, encoding=None):
4432+    path = fp.path
4433+    if isinstance(path, str):
4434+        try:
4435+            path = path.decode(filesystem_encoding)
4436+        except UnicodeDecodeError:
4437+            return 'b"%s"' % (ESCAPABLE_8BIT.sub(_str_escape, path),)
4438+
4439+    return quote_output(path, quotemarks=quotemarks, encoding=encoding)
4440+
4441 
4442 def unicode_platform():
4443     """
4444hunk ./src/allmydata/util/fileutil.py 5
4445 Futz with files like a pro.
4446 """
4447 
4448-import sys, exceptions, os, stat, tempfile, time, binascii
4449+import errno, sys, exceptions, os, stat, tempfile, time, binascii
4450+
4451+from allmydata.util.assertutil import precondition
4452 
4453 from twisted.python import log
4454hunk ./src/allmydata/util/fileutil.py 10
4455+from twisted.python.filepath import FilePath, UnlistableError
4456 
4457 from pycryptopp.cipher.aes import AES
4458 
4459hunk ./src/allmydata/util/fileutil.py 189
4460             raise tx
4461         raise exceptions.IOError, "unknown error prevented creation of directory, or deleted the directory immediately after creation: %s" % dirname # careful not to construct an IOError with a 2-tuple, as that has a special meaning...
4462 
4463-def rm_dir(dirname):
4464+def fp_make_dirs(dirfp):
4465+    """
4466+    An idempotent version of FilePath.makedirs().  If the dir already
4467+    exists, do nothing and return without raising an exception.  If this
4468+    call creates the dir, return without raising an exception.  If there is
4469+    an error that prevents creation or if the directory gets deleted after
4470+    fp_make_dirs() creates it and before fp_make_dirs() checks that it
4471+    exists, raise an exception.
4472+    """
4473+    log.msg( "xxx 0 %s" % (dirfp,))
4474+    tx = None
4475+    try:
4476+        dirfp.makedirs()
4477+    except OSError, x:
4478+        tx = x
4479+
4480+    if not dirfp.isdir():
4481+        if tx:
4482+            raise tx
4483+        raise exceptions.IOError, "unknown error prevented creation of directory, or deleted the directory immediately after creation: %s" % dirfp # careful not to construct an IOError with a 2-tuple, as that has a special meaning...
4484+
4485+def fp_rmdir_if_empty(dirfp):
4486+    """ Remove the directory if it is empty. """
4487+    try:
4488+        os.rmdir(dirfp.path)
4489+    except OSError, e:
4490+        if e.errno != errno.ENOTEMPTY:
4491+            raise
4492+    else:
4493+        dirfp.changed()
4494+
4495+def rmtree(dirname):
4496     """
4497     A threadsafe and idempotent version of shutil.rmtree().  If the dir is
4498     already gone, do nothing and return without raising an exception.  If this
4499hunk ./src/allmydata/util/fileutil.py 239
4500             else:
4501                 remove(fullname)
4502         os.rmdir(dirname)
4503-    except Exception, le:
4504-        # Ignore "No such file or directory"
4505-        if (not isinstance(le, OSError)) or le.args[0] != 2:
4506+    except EnvironmentError, le:
4507+        # Ignore "No such file or directory", collect any other exception.
4508+        if (le.args[0] != 2 and le.args[0] != 3) or (le.args[0] != errno.ENOENT):
4509             excs.append(le)
4510hunk ./src/allmydata/util/fileutil.py 243
4511+    except Exception, le:
4512+        excs.append(le)
4513 
4514     # Okay, now we've recursively removed everything, ignoring any "No
4515     # such file or directory" errors, and collecting any other errors.
4516hunk ./src/allmydata/util/fileutil.py 256
4517             raise OSError, "Failed to remove dir for unknown reason."
4518         raise OSError, excs
4519 
4520+def fp_remove(dirfp):
4521+    """
4522+    An idempotent version of shutil.rmtree().  If the dir is already gone,
4523+    do nothing and return without raising an exception.  If this call
4524+    removes the dir, return without raising an exception.  If there is an
4525+    error that prevents removal or if the directory gets created again by
4526+    someone else after this deletes it and before this checks that it is
4527+    gone, raise an exception.
4528+    """
4529+    try:
4530+        dirfp.remove()
4531+    except UnlistableError, e:
4532+        if e.originalException.errno != errno.ENOENT:
4533+            raise
4534+    except OSError, e:
4535+        if e.errno != errno.ENOENT:
4536+            raise
4537+
4538+def rm_dir(dirname):
4539+    # Renamed to be like shutil.rmtree and unlike rmdir.
4540+    return rmtree(dirname)
4541 
4542 def remove_if_possible(f):
4543     try:
4544hunk ./src/allmydata/util/fileutil.py 387
4545         import traceback
4546         traceback.print_exc()
4547 
4548-def get_disk_stats(whichdir, reserved_space=0):
4549+def get_disk_stats(whichdirfp, reserved_space=0):
4550     """Return disk statistics for the storage disk, in the form of a dict
4551     with the following fields.
4552       total:            total bytes on disk
4553hunk ./src/allmydata/util/fileutil.py 408
4554     you can pass how many bytes you would like to leave unused on this
4555     filesystem as reserved_space.
4556     """
4557+    precondition(isinstance(whichdirfp, FilePath), whichdirfp)
4558 
4559     if have_GetDiskFreeSpaceExW:
4560         # If this is a Windows system and GetDiskFreeSpaceExW is available, use it.
4561hunk ./src/allmydata/util/fileutil.py 419
4562         n_free_for_nonroot = c_ulonglong(0)
4563         n_total            = c_ulonglong(0)
4564         n_free_for_root    = c_ulonglong(0)
4565-        retval = GetDiskFreeSpaceExW(whichdir, byref(n_free_for_nonroot),
4566+        retval = GetDiskFreeSpaceExW(whichdirfp.path, byref(n_free_for_nonroot),
4567                                                byref(n_total),
4568                                                byref(n_free_for_root))
4569         if retval == 0:
4570hunk ./src/allmydata/util/fileutil.py 424
4571             raise OSError("Windows error %d attempting to get disk statistics for %r"
4572-                          % (GetLastError(), whichdir))
4573+                          % (GetLastError(), whichdirfp.path))
4574         free_for_nonroot = n_free_for_nonroot.value
4575         total            = n_total.value
4576         free_for_root    = n_free_for_root.value
4577hunk ./src/allmydata/util/fileutil.py 433
4578         # <http://docs.python.org/library/os.html#os.statvfs>
4579         # <http://opengroup.org/onlinepubs/7990989799/xsh/fstatvfs.html>
4580         # <http://opengroup.org/onlinepubs/7990989799/xsh/sysstatvfs.h.html>
4581-        s = os.statvfs(whichdir)
4582+        s = os.statvfs(whichdirfp.path)
4583 
4584         # on my mac laptop:
4585         #  statvfs(2) is a wrapper around statfs(2).
4586hunk ./src/allmydata/util/fileutil.py 460
4587              'avail': avail,
4588            }
4589 
4590-def get_available_space(whichdir, reserved_space):
4591+def get_available_space(whichdirfp, reserved_space):
4592     """Returns available space for share storage in bytes, or None if no
4593     API to get this information is available.
4594 
4595hunk ./src/allmydata/util/fileutil.py 472
4596     you can pass how many bytes you would like to leave unused on this
4597     filesystem as reserved_space.
4598     """
4599+    precondition(isinstance(whichdirfp, FilePath), whichdirfp)
4600     try:
4601hunk ./src/allmydata/util/fileutil.py 474
4602-        return get_disk_stats(whichdir, reserved_space)['avail']
4603+        return get_disk_stats(whichdirfp, reserved_space)['avail']
4604     except AttributeError:
4605         return None
4606hunk ./src/allmydata/util/fileutil.py 477
4607-    except EnvironmentError:
4608-        log.msg("OS call to get disk statistics failed")
4609+
4610+
4611+def get_used_space(fp):
4612+    if fp is None:
4613         return 0
4614hunk ./src/allmydata/util/fileutil.py 482
4615+    try:
4616+        s = os.stat(fp.path)
4617+    except EnvironmentError:
4618+        if not fp.exists():
4619+            return 0
4620+        raise
4621+    else:
4622+        # POSIX defines st_blocks (originally a BSDism):
4623+        #   <http://pubs.opengroup.org/onlinepubs/009695399/basedefs/sys/stat.h.html>
4624+        # but does not require stat() to give it a "meaningful value"
4625+        #   <http://pubs.opengroup.org/onlinepubs/009695399/functions/stat.html>
4626+        # and says:
4627+        #   "The unit for the st_blocks member of the stat structure is not defined
4628+        #    within IEEE Std 1003.1-2001. In some implementations it is 512 bytes.
4629+        #    It may differ on a file system basis. There is no correlation between
4630+        #    values of the st_blocks and st_blksize, and the f_bsize (from <sys/statvfs.h>)
4631+        #    structure members."
4632+        #
4633+        # The Linux docs define it as "the number of blocks allocated to the file,
4634+        # [in] 512-byte units." It is also defined that way on MacOS X. Python does
4635+        # not set the attribute on Windows.
4636+        #
4637+        # We consider platforms that define st_blocks but give it a wrong value, or
4638+        # measure it in a unit other than 512 bytes, to be broken. See also
4639+        # <http://bugs.python.org/issue12350>.
4640+
4641+        if hasattr(s, 'st_blocks'):
4642+            return s.st_blocks * 512
4643+        else:
4644+            return s.st_size
4645}
4646
4647Context:
4648
4649[tests: bump up the timeout in this test that fails on FreeStorm's CentOS in order to see if it is just very slow
4650zooko@zooko.com**20110913024255
4651 Ignore-this: 6a86d691e878cec583722faad06fb8e4
4652]
4653[interfaces: document that the 'fills-holes-with-zero-bytes' key should be used to detect whether a storage server has that behavior. refs #1528
4654david-sarah@jacaranda.org**20110913002843
4655 Ignore-this: 1a00a6029d40f6792af48c5578c1fd69
4656]
4657[CREDITS: more CREDITS for Kevan and David-Sarah
4658zooko@zooko.com**20110912223357
4659 Ignore-this: 4ea8f0d6f2918171d2f5359c25ad1ada
4660]
4661[merge NEWS about the mutable file bounds fixes with NEWS about work-in-progress
4662zooko@zooko.com**20110913205521
4663 Ignore-this: 4289a4225f848d6ae6860dd39bc92fa8
4664]
4665[doc: add NEWS item about fixes to potential palimpsest issues in mutable files
4666zooko@zooko.com**20110912223329
4667 Ignore-this: 9d63c95ddf95c7d5453c94a1ba4d406a
4668 ref. #1528
4669]
4670[merge the NEWS about the security fix (#1528) with the work-in-progress NEWS
4671zooko@zooko.com**20110913205153
4672 Ignore-this: 88e88a2ad140238c62010cf7c66953fc
4673]
4674[doc: add NEWS entry about the issue which allows unauthorized deletion of shares
4675zooko@zooko.com**20110912223246
4676 Ignore-this: 77e06d09103d2ef6bb51ea3e5d6e80b0
4677 ref. #1528
4678]
4679[doc: add entry in known_issues.rst about the issue which allows unauthorized deletion of shares
4680zooko@zooko.com**20110912223135
4681 Ignore-this: b26c6ea96b6c8740b93da1f602b5a4cd
4682 ref. #1528
4683]
4684[storage: more paranoid handling of bounds and palimpsests in mutable share files
4685zooko@zooko.com**20110912222655
4686 Ignore-this: a20782fa423779ee851ea086901e1507
4687 * storage server ignores requests to extend shares by sending a new_length
4688 * storage server fills exposed holes (created by sending a write vector whose offset begins after the end of the current data) with 0 to avoid "palimpsest" exposure of previous contents
4689 * storage server zeroes out lease info at the old location when moving it to a new location
4690 ref. #1528
4691]
4692[storage: test that the storage server ignores requests to extend shares by sending a new_length, and that the storage server fills exposed holes with 0 to avoid "palimpsest" exposure of previous contents
4693zooko@zooko.com**20110912222554
4694 Ignore-this: 61ebd7b11250963efdf5b1734a35271
4695 ref. #1528
4696]
4697[immutable: prevent clients from reading past the end of share data, which would allow them to learn the cancellation secret
4698zooko@zooko.com**20110912222458
4699 Ignore-this: da1ebd31433ea052087b75b2e3480c25
4700 Declare explicitly that we prevent this problem in the server's version dict.
4701 fixes #1528 (there are two patches that are each a sufficient fix to #1528 and this is one of them)
4702]
4703[storage: remove the storage server's "remote_cancel_lease" function
4704zooko@zooko.com**20110912222331
4705 Ignore-this: 1c32dee50e0981408576daffad648c50
4706 We're removing this function because it is currently unused, because it is dangerous, and because the bug described in #1528 leaks the cancellation secret, which allows anyone who knows a file's storage index to abuse this function to delete shares of that file.
4707 fixes #1528 (there are two patches that are each a sufficient fix to #1528 and this is one of them)
4708]
4709[storage: test that the storage server does *not* have a "remote_cancel_lease" function
4710zooko@zooko.com**20110912222324
4711 Ignore-this: 21c652009704652d35f34651f98dd403
4712 We're removing this function because it is currently unused, because it is dangerous, and because the bug described in #1528 leaks the cancellation secret, which allows anyone who knows a file's storage index to abuse this function to delete shares of that file.
4713 ref. #1528
4714]
4715[immutable: test whether the server allows clients to read past the end of share data, which would allow them to learn the cancellation secret
4716zooko@zooko.com**20110912221201
4717 Ignore-this: 376e47b346c713d37096531491176349
4718 Also test whether the server explicitly declares that it prevents this problem.
4719 ref #1528
4720]
4721[Retrieve._activate_enough_peers: rewrite Verify logic
4722Brian Warner <warner@lothar.com>**20110909181150
4723 Ignore-this: 9367c11e1eacbf025f75ce034030d717
4724]
4725[Retrieve: implement/test stopProducing
4726Brian Warner <warner@lothar.com>**20110909181150
4727 Ignore-this: 47b2c3df7dc69835e0a066ca12e3c178
4728]
4729[move DownloadStopped from download.common to interfaces
4730Brian Warner <warner@lothar.com>**20110909181150
4731 Ignore-this: 8572acd3bb16e50341dbed8eb1d90a50
4732]
4733[retrieve.py: remove vestigal self._validated_readers
4734Brian Warner <warner@lothar.com>**20110909181150
4735 Ignore-this: faab2ec14e314a53a2ffb714de626e2d
4736]
4737[Retrieve: rewrite flow-control: use a top-level loop() to catch all errors
4738Brian Warner <warner@lothar.com>**20110909181150
4739 Ignore-this: e162d2cd53b3d3144fc6bc757e2c7714
4740 
4741 This ought to close the potential for dropped errors and hanging downloads.
4742 Verify needs to be examined, I may have broken it, although all tests pass.
4743]
4744[Retrieve: merge _validate_active_prefixes into _add_active_peers
4745Brian Warner <warner@lothar.com>**20110909181150
4746 Ignore-this: d3ead31e17e69394ae7058eeb5beaf4c
4747]
4748[Retrieve: remove the initial prefix-is-still-good check
4749Brian Warner <warner@lothar.com>**20110909181150
4750 Ignore-this: da66ee51c894eaa4e862e2dffb458acc
4751 
4752 This check needs to be done with each fetch from the storage server, to
4753 detect when someone has changed the share (i.e. our servermap goes stale).
4754 Doing it just once at the beginning of retrieve isn't enough: a write might
4755 occur after the first segment but before the second, etc.
4756 
4757 _try_to_validate_prefix() was not removed: it will be used by the future
4758 check-with-each-fetch code.
4759 
4760 test_mutable.Roundtrip.test_corrupt_all_seqnum_late was disabled, since it
4761 fails until this check is brought back. (the corruption it applies only
4762 touches the prefix, not the block data, so the check-less retrieve actually
4763 tolerates it). Don't forget to re-enable it once the check is brought back.
4764]
4765[MDMFSlotReadProxy: remove the queue
4766Brian Warner <warner@lothar.com>**20110909181150
4767 Ignore-this: 96673cb8dda7a87a423de2f4897d66d2
4768 
4769 This is a neat trick to reduce Foolscap overhead, but the need for an
4770 explicit flush() complicates the Retrieve path and makes it prone to
4771 lost-progress bugs.
4772 
4773 Also change test_mutable.FakeStorageServer to tolerate multiple reads of the
4774 same share in a row, a limitation exposed by turning off the queue.
4775]
4776[rearrange Retrieve: first step, shouldn't change order of execution
4777Brian Warner <warner@lothar.com>**20110909181149
4778 Ignore-this: e3006368bfd2802b82ea45c52409e8d6
4779]
4780[CLI: test_cli.py -- remove an unnecessary call in test_mkdir_mutable_type. refs #1527
4781david-sarah@jacaranda.org**20110906183730
4782 Ignore-this: 122e2ffbee84861c32eda766a57759cf
4783]
4784[CLI: improve test for 'tahoe mkdir --mutable-type='. refs #1527
4785david-sarah@jacaranda.org**20110906183020
4786 Ignore-this: f1d4598e6c536f0a2b15050b3bc0ef9d
4787]
4788[CLI: make the --mutable-type option value for 'tahoe put' and 'tahoe mkdir' case-insensitive, and change --help for these commands accordingly. fixes #1527
4789david-sarah@jacaranda.org**20110905020922
4790 Ignore-this: 75a6df0a2df9c467d8c010579e9a024e
4791]
4792[cli: make --mutable-type imply --mutable in 'tahoe put'
4793Kevan Carstensen <kevan@isnotajoke.com>**20110903190920
4794 Ignore-this: 23336d3c43b2a9554e40c2a11c675e93
4795]
4796[SFTP: add a comment about a subtle interaction between OverwriteableFileConsumer and GeneralSFTPFile, and test the case it is commenting on.
4797david-sarah@jacaranda.org**20110903222304
4798 Ignore-this: 980c61d4dd0119337f1463a69aeebaf0
4799]
4800[improve the storage/mutable.py asserts even more
4801warner@lothar.com**20110901160543
4802 Ignore-this: 5b2b13c49bc4034f96e6e3aaaa9a9946
4803]
4804[storage/mutable.py: special characters in struct.foo arguments indicate standard as opposed to native sizes, we should be using these characters in these asserts
4805wilcoxjg@gmail.com**20110901084144
4806 Ignore-this: 28ace2b2678642e4d7269ddab8c67f30
4807]
4808[docs/write_coordination.rst: fix formatting and add more specific warning about access via sshfs.
4809david-sarah@jacaranda.org**20110831232148
4810 Ignore-this: cd9c851d3eb4e0a1e088f337c291586c
4811]
4812[test_mutable.Version: consolidate some tests, reduce runtime from 19s to 15s
4813warner@lothar.com**20110831050451
4814 Ignore-this: 64815284d9e536f8f3798b5f44cf580c
4815]
4816[mutable/retrieve: handle the case where self._read_length is 0.
4817Kevan Carstensen <kevan@isnotajoke.com>**20110830210141
4818 Ignore-this: fceafbe485851ca53f2774e5a4fd8d30
4819 
4820 Note that the downloader will still fetch a segment for a zero-length
4821 read, which is wasteful. Fixing that isn't specifically required to fix
4822 #1512, but it should probably be fixed before 1.9.
4823]
4824[NEWS: added summary of all changes since 1.8.2. Needs editing.
4825Brian Warner <warner@lothar.com>**20110830163205
4826 Ignore-this: 273899b37a899fc6919b74572454b8b2
4827]
4828[test_mutable.Update: only upload the files needed for each test. refs #1500
4829Brian Warner <warner@lothar.com>**20110829072717
4830 Ignore-this: 4d2ab4c7523af9054af7ecca9c3d9dc7
4831 
4832 This first step shaves 15% off the runtime: from 139s to 119s on my laptop.
4833 It also fixes a couple of places where a Deferred was being dropped, which
4834 would cause two tests to run in parallel and also confuse error reporting.
4835]
4836[Let Uploader retain History instead of passing it into upload(). Fixes #1079.
4837Brian Warner <warner@lothar.com>**20110829063246
4838 Ignore-this: 3902c58ec12bd4b2d876806248e19f17
4839 
4840 This consistently records all immutable uploads in the Recent Uploads And
4841 Downloads page, regardless of code path. Previously, certain webapi upload
4842 operations (like PUT /uri/$DIRCAP/newchildname) failed to pass the History
4843 object and were left out.
4844]
4845[Fix mutable publish/retrieve timing status displays. Fixes #1505.
4846Brian Warner <warner@lothar.com>**20110828232221
4847 Ignore-this: 4080ce065cf481b2180fd711c9772dd6
4848 
4849 publish:
4850 * encrypt and encode times are cumulative, not just current-segment
4851 
4852 retrieve:
4853 * same for decrypt and decode times
4854 * update "current status" to include segment number
4855 * set status to Finished/Failed when download is complete
4856 * set progress to 1.0 when complete
4857 
4858 More improvements to consider:
4859 * progress is currently 0% or 100%: should calculate how many segments are
4860   involved (remembering retrieve can be less than the whole file) and set it
4861   to a fraction
4862 * "fetch" time is fuzzy: what we want is to know how much of the delay is not
4863   our own fault, but since we do decode/decrypt work while waiting for more
4864   shares, it's not straightforward
4865]
4866[Teach 'tahoe debug catalog-shares about MDMF. Closes #1507.
4867Brian Warner <warner@lothar.com>**20110828080931
4868 Ignore-this: 56ef2951db1a648353d7daac6a04c7d1
4869]
4870[debug.py: remove some dead comments
4871Brian Warner <warner@lothar.com>**20110828074556
4872 Ignore-this: 40e74040dd4d14fd2f4e4baaae506b31
4873]
4874[hush pyflakes
4875Brian Warner <warner@lothar.com>**20110828074254
4876 Ignore-this: bef9d537a969fa82fe4decc4ba2acb09
4877]
4878[MutableFileNode.set_downloader_hints: never depend upon order of dict.values()
4879Brian Warner <warner@lothar.com>**20110828074103
4880 Ignore-this: caaf1aa518dbdde4d797b7f335230faa
4881 
4882 The old code was calculating the "extension parameters" (a list) from the
4883 downloader hints (a dictionary) with hints.values(), which is not stable, and
4884 would result in corrupted filecaps (with the 'k' and 'segsize' hints
4885 occasionally swapped). The new code always uses [k,segsize].
4886]
4887[layout.py: fix MDMF share layout documentation
4888Brian Warner <warner@lothar.com>**20110828073921
4889 Ignore-this: 3f13366fed75b5e31b51ae895450a225
4890]
4891[teach 'tahoe debug dump-share' about MDMF and offsets. refs #1507
4892Brian Warner <warner@lothar.com>**20110828073834
4893 Ignore-this: 3a9d2ef9c47a72bf1506ba41199a1dea
4894]
4895[test_mutable.Version.test_debug: use splitlines() to fix buildslaves
4896Brian Warner <warner@lothar.com>**20110828064728
4897 Ignore-this: c7f6245426fc80b9d1ae901d5218246a
4898 
4899 Any slave running in a directory with spaces in the name was miscounting
4900 shares, causing the test to fail.
4901]
4902[test_mutable.Version: exercise 'tahoe debug find-shares' on MDMF. refs #1507
4903Brian Warner <warner@lothar.com>**20110828005542
4904 Ignore-this: cb20bea1c28bfa50a72317d70e109672
4905 
4906 Also changes NoNetworkGrid to put shares in storage/shares/ .
4907]
4908[test_mutable.py: oops, missed a .todo
4909Brian Warner <warner@lothar.com>**20110828002118
4910 Ignore-this: fda09ae86481352b7a627c278d2a3940
4911]
4912[test_mutable: merge davidsarah's patch with my Version refactorings
4913warner@lothar.com**20110827235707
4914 Ignore-this: b5aaf481c90d99e33827273b5d118fd0
4915]
4916[Make the immutable/read-only constraint checking for MDMF URIs identical to that for SSK URIs. refs #393
4917david-sarah@jacaranda.org**20110823012720
4918 Ignore-this: e1f59d7ff2007c81dbef2aeb14abd721
4919]
4920[Additional tests for MDMF URIs and for zero-length files. refs #393
4921david-sarah@jacaranda.org**20110823011532
4922 Ignore-this: a7cc0c09d1d2d72413f9cd227c47a9d5
4923]
4924[Additional tests for zero-length partial reads and updates to mutable versions. refs #393
4925david-sarah@jacaranda.org**20110822014111
4926 Ignore-this: 5fc6f4d06e11910124e4a277ec8a43ea
4927]
4928[test_mutable.Version: factor out some expensive uploads, save 25% runtime
4929Brian Warner <warner@lothar.com>**20110827232737
4930 Ignore-this: ea37383eb85ea0894b254fe4dfb45544
4931]
4932[SDMF: update filenode with correct k/N after Retrieve. Fixes #1510.
4933Brian Warner <warner@lothar.com>**20110827225031
4934 Ignore-this: b50ae6e1045818c400079f118b4ef48
4935 
4936 Without this, we get a regression when modifying a mutable file that was
4937 created with more shares (larger N) than our current tahoe.cfg . The
4938 modification attempt creates new versions of the (0,1,..,newN-1) shares, but
4939 leaves the old versions of the (newN,..,oldN-1) shares alone (and throws a
4940 assertion error in SDMFSlotWriteProxy.finish_publishing in the process).
4941 
4942 The mixed versions that result (some shares with e.g. N=10, some with N=20,
4943 such that both versions are recoverable) cause problems for the Publish code,
4944 even before MDMF landed. Might be related to refs #1390 and refs #1042.
4945]
4946[layout.py: annotate assertion to figure out 'tahoe backup' failure
4947Brian Warner <warner@lothar.com>**20110827195253
4948 Ignore-this: 9b92b954e3ed0d0f80154fff1ff674e5
4949]
4950[Add 'tahoe debug dump-cap' support for MDMF, DIR2-CHK, DIR2-MDMF. refs #1507.
4951Brian Warner <warner@lothar.com>**20110827195048
4952 Ignore-this: 61c6af5e33fc88e0251e697a50addb2c
4953 
4954 This also adds tests for all those cases, and fixes an omission in uri.py
4955 that broke parsing of DIR2-MDMF-Verifier and DIR2-CHK-Verifier.
4956]
4957[MDMF: more writable/writeable consistentifications
4958warner@lothar.com**20110827190602
4959 Ignore-this: 22492a9e20c1819ddb12091062888b55
4960]
4961[MDMF: s/Writable/Writeable/g, for consistency with existing SDMF code
4962warner@lothar.com**20110827183357
4963 Ignore-this: 9dd312acedbdb2fc2f7bef0d0fb17c0b
4964]
4965[setup.cfg: remove no-longer-supported test_mac_diskimage alias. refs #1479
4966david-sarah@jacaranda.org**20110826230345
4967 Ignore-this: 40e908b8937322a290fb8012bfcad02a
4968]
4969[test_mutable.Update: increase timeout from 120s to 400s, slaves are failing
4970Brian Warner <warner@lothar.com>**20110825230140
4971 Ignore-this: 101b1924a30cdbda9b2e419e95ca15ec
4972]
4973[tests: fix check_memory test
4974zooko@zooko.com**20110825201116
4975 Ignore-this: 4d66299fa8cb61d2ca04b3f45344d835
4976 fixes #1503
4977]
4978[TAG allmydata-tahoe-1.9.0a1
4979warner@lothar.com**20110825161122
4980 Ignore-this: 3cbf49f00dbda58189f893c427f65605
4981]
4982Patch bundle hash:
49834859218357a7434f768d619e8e0213c9879886e8