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

File pluggable-backends-davidsarah-v4.darcs.patch, 288.5 KB (added by davidsarah, at 2011-09-19T23:38:51Z)

Rerecording of pluggable-backends-davidsarah-v3.darcs.patch that should fix the darcs performance problem when applied to trunk.

Line 
13 patches for repository http://tahoe-lafs.org/source/tahoe/trunk:
2
3Thu Aug 25 01:32:17 BST 2011  david-sarah@jacaranda.org
4  * interfaces.py: 'which -> that' grammar cleanup.
5
6Tue Sep 20 00:29:26 BST 2011  david-sarah@jacaranda.org
7  * Pluggable backends -- new and moved files, changes to moved files. refs #999
8
9Tue Sep 20 00:32:56 BST 2011  david-sarah@jacaranda.org
10  * Pluggable backends -- all other changes. refs #999
11
12New patches:
13
14[interfaces.py: 'which -> that' grammar cleanup.
15david-sarah@jacaranda.org**20110825003217
16 Ignore-this: a3e15f3676de1b346ad78aabdfb8cac6
17] {
18hunk ./src/allmydata/interfaces.py 38
19     the StubClient. This object doesn't actually offer any services, but the
20     announcement helps the Introducer keep track of which clients are
21     subscribed (so the grid admin can keep track of things like the size of
22-    the grid and the client versions in use. This is the (empty)
23+    the grid and the client versions in use). This is the (empty)
24     RemoteInterface for the StubClient."""
25 
26 class RIBucketWriter(RemoteInterface):
27hunk ./src/allmydata/interfaces.py 276
28         (binary) storage index string, and 'shnum' is the integer share
29         number. 'reason' is a human-readable explanation of the problem,
30         probably including some expected hash values and the computed ones
31-        which did not match. Corruption advisories for mutable shares should
32+        that did not match. Corruption advisories for mutable shares should
33         include a hash of the public key (the same value that appears in the
34         mutable-file verify-cap), since the current share format does not
35         store that on disk.
36hunk ./src/allmydata/interfaces.py 413
37           remote_host: the IAddress, if connected, otherwise None
38 
39         This method is intended for monitoring interfaces, such as a web page
40-        which describes connecting and connected peers.
41+        that describes connecting and connected peers.
42         """
43 
44     def get_all_peerids():
45hunk ./src/allmydata/interfaces.py 515
46 
47     # TODO: rename to get_read_cap()
48     def get_readonly():
49-        """Return another IURI instance, which represents a read-only form of
50+        """Return another IURI instance that represents a read-only form of
51         this one. If is_readonly() is True, this returns self."""
52 
53     def get_verify_cap():
54hunk ./src/allmydata/interfaces.py 542
55         passing into init_from_string."""
56 
57 class IDirnodeURI(Interface):
58-    """I am a URI which represents a dirnode."""
59+    """I am a URI that represents a dirnode."""
60 
61 class IFileURI(Interface):
62hunk ./src/allmydata/interfaces.py 545
63-    """I am a URI which represents a filenode."""
64+    """I am a URI that represents a filenode."""
65     def get_size():
66         """Return the length (in bytes) of the file that I represent."""
67 
68hunk ./src/allmydata/interfaces.py 553
69     pass
70 
71 class IMutableFileURI(Interface):
72-    """I am a URI which represents a mutable filenode."""
73+    """I am a URI that represents a mutable filenode."""
74     def get_extension_params():
75         """Return the extension parameters in the URI"""
76 
77hunk ./src/allmydata/interfaces.py 856
78         """
79 
80 class IFileNode(IFilesystemNode):
81-    """I am a node which represents a file: a sequence of bytes. I am not a
82+    """I am a node that represents a file: a sequence of bytes. I am not a
83     container, like IDirectoryNode."""
84     def get_best_readable_version():
85         """Return a Deferred that fires with an IReadable for the 'best'
86hunk ./src/allmydata/interfaces.py 905
87     multiple versions of a file present in the grid, some of which might be
88     unrecoverable (i.e. have fewer than 'k' shares). These versions are
89     loosely ordered: each has a sequence number and a hash, and any version
90-    with seqnum=N was uploaded by a node which has seen at least one version
91+    with seqnum=N was uploaded by a node that has seen at least one version
92     with seqnum=N-1.
93 
94     The 'servermap' (an instance of IMutableFileServerMap) is used to
95hunk ./src/allmydata/interfaces.py 1014
96         as a guide to where the shares are located.
97 
98         I return a Deferred that fires with the requested contents, or
99-        errbacks with UnrecoverableFileError. Note that a servermap which was
100+        errbacks with UnrecoverableFileError. Note that a servermap that was
101         updated with MODE_ANYTHING or MODE_READ may not know about shares for
102         all versions (those modes stop querying servers as soon as they can
103         fulfil their goals), so you may want to use MODE_CHECK (which checks
104hunk ./src/allmydata/interfaces.py 1073
105     """Upload was unable to satisfy 'servers_of_happiness'"""
106 
107 class UnableToFetchCriticalDownloadDataError(Exception):
108-    """I was unable to fetch some piece of critical data which is supposed to
109+    """I was unable to fetch some piece of critical data that is supposed to
110     be identically present in all shares."""
111 
112 class NoServersError(Exception):
113hunk ./src/allmydata/interfaces.py 1085
114     exists, and overwrite= was set to False."""
115 
116 class NoSuchChildError(Exception):
117-    """A directory node was asked to fetch a child which does not exist."""
118+    """A directory node was asked to fetch a child that does not exist."""
119 
120 class ChildOfWrongTypeError(Exception):
121     """An operation was attempted on a child of the wrong type (file or directory)."""
122hunk ./src/allmydata/interfaces.py 1403
123         if you initially thought you were going to use 10 peers, started
124         encoding, and then two of the peers dropped out: you could use
125         desired_share_ids= to skip the work (both memory and CPU) of
126-        producing shares for the peers which are no longer available.
127+        producing shares for the peers that are no longer available.
128 
129         """
130 
131hunk ./src/allmydata/interfaces.py 1478
132         if you initially thought you were going to use 10 peers, started
133         encoding, and then two of the peers dropped out: you could use
134         desired_share_ids= to skip the work (both memory and CPU) of
135-        producing shares for the peers which are no longer available.
136+        producing shares for the peers that are no longer available.
137 
138         For each call, encode() will return a Deferred that fires with two
139         lists, one containing shares and the other containing the shareids.
140hunk ./src/allmydata/interfaces.py 1535
141         required to be of the same length.  The i'th element of their_shareids
142         is required to be the shareid of the i'th buffer in some_shares.
143 
144-        This returns a Deferred which fires with a sequence of buffers. This
145+        This returns a Deferred that fires with a sequence of buffers. This
146         sequence will contain all of the segments of the original data, in
147         order. The sum of the lengths of all of the buffers will be the
148         'data_size' value passed into the original ICodecEncode.set_params()
149hunk ./src/allmydata/interfaces.py 1582
150         Encoding parameters can be set in three ways. 1: The Encoder class
151         provides defaults (3/7/10). 2: the Encoder can be constructed with
152         an 'options' dictionary, in which the
153-        needed_and_happy_and_total_shares' key can be a (k,d,n) tuple. 3:
154+        'needed_and_happy_and_total_shares' key can be a (k,d,n) tuple. 3:
155         set_params((k,d,n)) can be called.
156 
157         If you intend to use set_params(), you must call it before
158hunk ./src/allmydata/interfaces.py 1780
159         produced, so that the segment hashes can be generated with only a
160         single pass.
161 
162-        This returns a Deferred which fires with a sequence of hashes, using:
163+        This returns a Deferred that fires with a sequence of hashes, using:
164 
165          tuple(segment_hashes[first:last])
166 
167hunk ./src/allmydata/interfaces.py 1796
168     def get_plaintext_hash():
169         """OBSOLETE; Get the hash of the whole plaintext.
170 
171-        This returns a Deferred which fires with a tagged SHA-256 hash of the
172+        This returns a Deferred that fires with a tagged SHA-256 hash of the
173         whole plaintext, obtained from hashutil.plaintext_hash(data).
174         """
175 
176hunk ./src/allmydata/interfaces.py 1856
177         be used to encrypt the data. The key will also be hashed to derive
178         the StorageIndex.
179 
180-        Uploadables which want to achieve convergence should hash their file
181+        Uploadables that want to achieve convergence should hash their file
182         contents and the serialized_encoding_parameters to form the key
183         (which of course requires a full pass over the data). Uploadables can
184         use the upload.ConvergentUploadMixin class to achieve this
185hunk ./src/allmydata/interfaces.py 1862
186         automatically.
187 
188-        Uploadables which do not care about convergence (or do not wish to
189+        Uploadables that do not care about convergence (or do not wish to
190         make multiple passes over the data) can simply return a
191         strongly-random 16 byte string.
192 
193hunk ./src/allmydata/interfaces.py 1872
194 
195     def read(length):
196         """Return a Deferred that fires with a list of strings (perhaps with
197-        only a single element) which, when concatenated together, contain the
198+        only a single element) that, when concatenated together, contain the
199         next 'length' bytes of data. If EOF is near, this may provide fewer
200         than 'length' bytes. The total number of bytes provided by read()
201         before it signals EOF must equal the size provided by get_size().
202hunk ./src/allmydata/interfaces.py 1919
203 
204     def read(length):
205         """
206-        Returns a list of strings which, when concatenated, are the next
207+        Returns a list of strings that, when concatenated, are the next
208         length bytes of the file, or fewer if there are fewer bytes
209         between the current location and the end of the file.
210         """
211hunk ./src/allmydata/interfaces.py 1932
212 
213 class IUploadResults(Interface):
214     """I am returned by upload() methods. I contain a number of public
215-    attributes which can be read to determine the results of the upload. Some
216+    attributes that can be read to determine the results of the upload. Some
217     of these are functional, some are timing information. All of these may be
218     None.
219 
220hunk ./src/allmydata/interfaces.py 1965
221 
222 class IDownloadResults(Interface):
223     """I am created internally by download() methods. I contain a number of
224-    public attributes which contain details about the download process.::
225+    public attributes that contain details about the download process.::
226 
227      .file_size : the size of the file, in bytes
228      .servers_used : set of server peerids that were used during download
229hunk ./src/allmydata/interfaces.py 1991
230 class IUploader(Interface):
231     def upload(uploadable):
232         """Upload the file. 'uploadable' must impement IUploadable. This
233-        returns a Deferred which fires with an IUploadResults instance, from
234+        returns a Deferred that fires with an IUploadResults instance, from
235         which the URI of the file can be obtained as results.uri ."""
236 
237     def upload_ssk(write_capability, new_version, uploadable):
238hunk ./src/allmydata/interfaces.py 2041
239         kind of lease that is obtained (which account number to claim, etc).
240 
241         TODO: any problems seen during checking will be reported to the
242-        health-manager.furl, a centralized object which is responsible for
243+        health-manager.furl, a centralized object that is responsible for
244         figuring out why files are unhealthy so corrective action can be
245         taken.
246         """
247hunk ./src/allmydata/interfaces.py 2056
248         will be put in the check-and-repair results. The Deferred will not
249         fire until the repair is complete.
250 
251-        This returns a Deferred which fires with an instance of
252+        This returns a Deferred that fires with an instance of
253         ICheckAndRepairResults."""
254 
255 class IDeepCheckable(Interface):
256hunk ./src/allmydata/interfaces.py 2141
257                               that was found to be corrupt. Each share
258                               locator is a list of (serverid, storage_index,
259                               sharenum).
260-         count-incompatible-shares: the number of shares which are of a share
261+         count-incompatible-shares: the number of shares that are of a share
262                                     format unknown to this checker
263          list-incompatible-shares: a list of 'share locators', one for each
264                                    share that was found to be of an unknown
265hunk ./src/allmydata/interfaces.py 2148
266                                    format. Each share locator is a list of
267                                    (serverid, storage_index, sharenum).
268          servers-responding: list of (binary) storage server identifiers,
269-                             one for each server which responded to the share
270+                             one for each server that responded to the share
271                              query (even if they said they didn't have
272                              shares, and even if they said they did have
273                              shares but then didn't send them when asked, or
274hunk ./src/allmydata/interfaces.py 2345
275         will use the data in the checker results to guide the repair process,
276         such as which servers provided bad data and should therefore be
277         avoided. The ICheckResults object is inside the
278-        ICheckAndRepairResults object, which is returned by the
279+        ICheckAndRepairResults object that is returned by the
280         ICheckable.check() method::
281 
282          d = filenode.check(repair=False)
283hunk ./src/allmydata/interfaces.py 2436
284         methods to create new objects. I return synchronously."""
285 
286     def create_mutable_file(contents=None, keysize=None):
287-        """I create a new mutable file, and return a Deferred which will fire
288+        """I create a new mutable file, and return a Deferred that will fire
289         with the IMutableFileNode instance when it is ready. If contents= is
290         provided (a bytestring), it will be used as the initial contents of
291         the new file, otherwise the file will contain zero bytes. keysize= is
292hunk ./src/allmydata/interfaces.py 2444
293         usual."""
294 
295     def create_new_mutable_directory(initial_children={}):
296-        """I create a new mutable directory, and return a Deferred which will
297+        """I create a new mutable directory, and return a Deferred that will
298         fire with the IDirectoryNode instance when it is ready. If
299         initial_children= is provided (a dict mapping unicode child name to
300         (childnode, metadata_dict) tuples), the directory will be populated
301hunk ./src/allmydata/interfaces.py 2452
302 
303 class IClientStatus(Interface):
304     def list_all_uploads():
305-        """Return a list of uploader objects, one for each upload which
306+        """Return a list of uploader objects, one for each upload that
307         currently has an object available (tracked with weakrefs). This is
308         intended for debugging purposes."""
309     def list_active_uploads():
310hunk ./src/allmydata/interfaces.py 2462
311         started uploads."""
312 
313     def list_all_downloads():
314-        """Return a list of downloader objects, one for each download which
315+        """Return a list of downloader objects, one for each download that
316         currently has an object available (tracked with weakrefs). This is
317         intended for debugging purposes."""
318     def list_active_downloads():
319hunk ./src/allmydata/interfaces.py 2689
320 
321     def provide(provider=RIStatsProvider, nickname=str):
322         """
323-        @param provider: a stats collector instance which should be polled
324+        @param provider: a stats collector instance that should be polled
325                          periodically by the gatherer to collect stats.
326         @param nickname: a name useful to identify the provided client
327         """
328hunk ./src/allmydata/interfaces.py 2722
329 
330 class IValidatedThingProxy(Interface):
331     def start():
332-        """ Acquire a thing and validate it. Return a deferred which is
333+        """ Acquire a thing and validate it. Return a deferred that is
334         eventually fired with self if the thing is valid or errbacked if it
335         can't be acquired or validated."""
336 
337}
338[Pluggable backends -- new and moved files, changes to moved files. refs #999
339david-sarah@jacaranda.org**20110919232926
340 Ignore-this: ec5d2d1362a092d919e84327d3092424
341] {
342adddir ./src/allmydata/storage/backends
343adddir ./src/allmydata/storage/backends/disk
344move ./src/allmydata/storage/immutable.py ./src/allmydata/storage/backends/disk/immutable.py
345move ./src/allmydata/storage/mutable.py ./src/allmydata/storage/backends/disk/mutable.py
346adddir ./src/allmydata/storage/backends/null
347addfile ./src/allmydata/storage/backends/__init__.py
348addfile ./src/allmydata/storage/backends/base.py
349hunk ./src/allmydata/storage/backends/base.py 1
350+
351+from twisted.application import service
352+
353+from allmydata.storage.common import si_b2a
354+from allmydata.storage.lease import LeaseInfo
355+from allmydata.storage.bucket import BucketReader
356+
357+
358+class Backend(service.MultiService):
359+    def __init__(self):
360+        service.MultiService.__init__(self)
361+
362+
363+class ShareSet(object):
364+    """
365+    This class implements shareset logic that could work for all backends, but
366+    might be useful to override for efficiency.
367+    """
368+
369+    def __init__(self, storageindex):
370+        self.storageindex = storageindex
371+
372+    def get_storage_index(self):
373+        return self.storageindex
374+
375+    def get_storage_index_string(self):
376+        return si_b2a(self.storageindex)
377+
378+    def renew_lease(self, renew_secret, new_expiration_time):
379+        found_shares = False
380+        for share in self.get_shares():
381+            found_shares = True
382+            share.renew_lease(renew_secret, new_expiration_time)
383+
384+        if not found_shares:
385+            raise IndexError("no such lease to renew")
386+
387+    def get_leases(self):
388+        # Since all shares get the same lease data, we just grab the leases
389+        # from the first share.
390+        try:
391+            sf = self.get_shares().next()
392+            return sf.get_leases()
393+        except StopIteration:
394+            return iter([])
395+
396+    def add_or_renew_lease(self, lease_info):
397+        # This implementation assumes that lease data is duplicated in
398+        # all shares of a shareset, which might not be true for all backends.
399+        for share in self.get_shares():
400+            share.add_or_renew_lease(lease_info)
401+
402+    def make_bucket_reader(self, storageserver, share):
403+        return BucketReader(storageserver, share)
404+
405+    def testv_and_readv_and_writev(self, storageserver, secrets,
406+                                   test_and_write_vectors, read_vector,
407+                                   expiration_time):
408+        # The implementation here depends on the following helper methods,
409+        # which must be provided by subclasses:
410+        #
411+        # def _clean_up_after_unlink(self):
412+        #     """clean up resources associated with the shareset after some
413+        #     shares might have been deleted"""
414+        #
415+        # def _create_mutable_share(self, storageserver, shnum, write_enabler):
416+        #     """create a mutable share with the given shnum and write_enabler"""
417+
418+        # secrets might be a triple with cancel_secret in secrets[2], but if
419+        # so we ignore the cancel_secret.
420+        write_enabler = secrets[0]
421+        renew_secret = secrets[1]
422+
423+        si_s = self.get_storage_index_string()
424+        shares = {}
425+        for share in self.get_shares():
426+            # XXX is it correct to ignore immutable shares? Maybe get_shares should
427+            # have a parameter saying what type it's expecting.
428+            if share.sharetype == "mutable":
429+                share.check_write_enabler(write_enabler, si_s)
430+                shares[share.get_shnum()] = share
431+
432+        # write_enabler is good for all existing shares
433+
434+        # now evaluate test vectors
435+        testv_is_good = True
436+        for sharenum in test_and_write_vectors:
437+            (testv, datav, new_length) = test_and_write_vectors[sharenum]
438+            if sharenum in shares:
439+                if not shares[sharenum].check_testv(testv):
440+                    self.log("testv failed: [%d]: %r" % (sharenum, testv))
441+                    testv_is_good = False
442+                    break
443+            else:
444+                # compare the vectors against an empty share, in which all
445+                # reads return empty strings
446+                if not EmptyShare().check_testv(testv):
447+                    self.log("testv failed (empty): [%d] %r" % (sharenum,
448+                                                                testv))
449+                    testv_is_good = False
450+                    break
451+
452+        # gather the read vectors, before we do any writes
453+        read_data = {}
454+        for shnum, share in shares.items():
455+            read_data[shnum] = share.readv(read_vector)
456+
457+        ownerid = 1 # TODO
458+        lease_info = LeaseInfo(ownerid, renew_secret,
459+                               expiration_time, storageserver.get_serverid())
460+
461+        if testv_is_good:
462+            # now apply the write vectors
463+            for shnum in test_and_write_vectors:
464+                (testv, datav, new_length) = test_and_write_vectors[shnum]
465+                if new_length == 0:
466+                    if shnum in shares:
467+                        shares[shnum].unlink()
468+                else:
469+                    if shnum not in shares:
470+                        # allocate a new share
471+                        share = self._create_mutable_share(storageserver, shnum, write_enabler)
472+                        shares[shnum] = share
473+                    shares[shnum].writev(datav, new_length)
474+                    # and update the lease
475+                    shares[shnum].add_or_renew_lease(lease_info)
476+
477+            if new_length == 0:
478+                self._clean_up_after_unlink()
479+
480+        return (testv_is_good, read_data)
481+
482+    def readv(self, wanted_shnums, read_vector):
483+        """
484+        Read a vector from the numbered shares in this shareset. An empty
485+        shares list means to return data from all known shares.
486+
487+        @param wanted_shnums=ListOf(int)
488+        @param read_vector=ReadVector
489+        @return DictOf(int, ReadData): shnum -> results, with one key per share
490+        """
491+        datavs = {}
492+        for share in self.get_shares():
493+            shnum = share.get_shnum()
494+            if not wanted_shnums or shnum in wanted_shnums:
495+                datavs[shnum] = share.readv(read_vector)
496+
497+        return datavs
498+
499+
500+def testv_compare(a, op, b):
501+    assert op in ("lt", "le", "eq", "ne", "ge", "gt")
502+    if op == "lt":
503+        return a < b
504+    if op == "le":
505+        return a <= b
506+    if op == "eq":
507+        return a == b
508+    if op == "ne":
509+        return a != b
510+    if op == "ge":
511+        return a >= b
512+    if op == "gt":
513+        return a > b
514+    # never reached
515+
516+
517+class EmptyShare:
518+    def check_testv(self, testv):
519+        test_good = True
520+        for (offset, length, operator, specimen) in testv:
521+            data = ""
522+            if not testv_compare(data, operator, specimen):
523+                test_good = False
524+                break
525+        return test_good
526+
527addfile ./src/allmydata/storage/backends/disk/__init__.py
528addfile ./src/allmydata/storage/backends/disk/disk_backend.py
529hunk ./src/allmydata/storage/backends/disk/disk_backend.py 1
530+
531+import re
532+
533+from twisted.python.filepath import UnlistableError
534+
535+from zope.interface import implements
536+from allmydata.interfaces import IStorageBackend, IShareSet
537+from allmydata.util import fileutil, log, time_format
538+from allmydata.storage.common import si_b2a, si_a2b
539+from allmydata.storage.bucket import BucketWriter
540+from allmydata.storage.backends.base import Backend, ShareSet
541+from allmydata.storage.backends.disk.immutable import ImmutableDiskShare
542+from allmydata.storage.backends.disk.mutable import MutableDiskShare, create_mutable_disk_share
543+
544+# storage/
545+# storage/shares/incoming
546+#   incoming/ holds temp dirs named $START/$STORAGEINDEX/$SHARENUM which will
547+#   be moved to storage/shares/$START/$STORAGEINDEX/$SHARENUM upon success
548+# storage/shares/$START/$STORAGEINDEX
549+# storage/shares/$START/$STORAGEINDEX/$SHARENUM
550+
551+# Where "$START" denotes the first 10 bits worth of $STORAGEINDEX (that's 2
552+# base-32 chars).
553+# $SHARENUM matches this regex:
554+NUM_RE=re.compile("^[0-9]+$")
555+
556+
557+def si_si2dir(startfp, storageindex):
558+    sia = si_b2a(storageindex)
559+    newfp = startfp.child(sia[:2])
560+    return newfp.child(sia)
561+
562+
563+def get_share(fp):
564+    f = fp.open('rb')
565+    try:
566+        prefix = f.read(32)
567+    finally:
568+        f.close()
569+
570+    if prefix == MutableDiskShare.MAGIC:
571+        return MutableDiskShare(fp)
572+    else:
573+        # assume it's immutable
574+        return ImmutableDiskShare(fp)
575+
576+
577+class DiskBackend(Backend):
578+    implements(IStorageBackend)
579+
580+    def __init__(self, storedir, readonly=False, reserved_space=0, discard_storage=False):
581+        Backend.__init__(self)
582+        self._setup_storage(storedir, readonly, reserved_space, discard_storage)
583+        self._setup_corruption_advisory()
584+
585+    def _setup_storage(self, storedir, readonly, reserved_space, discard_storage):
586+        self._storedir = storedir
587+        self._readonly = readonly
588+        self._reserved_space = int(reserved_space)
589+        self._discard_storage = discard_storage
590+        self._sharedir = self._storedir.child("shares")
591+        fileutil.fp_make_dirs(self._sharedir)
592+        self._incomingdir = self._sharedir.child('incoming')
593+        self._clean_incomplete()
594+        if self._reserved_space and (self.get_available_space() is None):
595+            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",
596+                    umid="0wZ27w", level=log.UNUSUAL)
597+
598+    def _clean_incomplete(self):
599+        fileutil.fp_remove(self._incomingdir)
600+        fileutil.fp_make_dirs(self._incomingdir)
601+
602+    def _setup_corruption_advisory(self):
603+        # we don't actually create the corruption-advisory dir until necessary
604+        self._corruption_advisory_dir = self._storedir.child("corruption-advisories")
605+
606+    def _make_shareset(self, sharehomedir):
607+        return self.get_shareset(si_a2b(sharehomedir.basename()))
608+
609+    def get_sharesets_for_prefix(self, prefix):
610+        prefixfp = self._sharedir.child(prefix)
611+        try:
612+            sharesets = map(self._make_shareset, prefixfp.children())
613+            def _by_base32si(b):
614+                return b.get_storage_index_string()
615+            sharesets.sort(key=_by_base32si)
616+        except EnvironmentError:
617+            sharesets = []
618+        return sharesets
619+
620+    def get_shareset(self, storageindex):
621+        sharehomedir = si_si2dir(self._sharedir, storageindex)
622+        incominghomedir = si_si2dir(self._incomingdir, storageindex)
623+        return DiskShareSet(storageindex, sharehomedir, incominghomedir, discard_storage=self._discard_storage)
624+
625+    def fill_in_space_stats(self, stats):
626+        stats['storage_server.reserved_space'] = self._reserved_space
627+        try:
628+            disk = fileutil.get_disk_stats(self._sharedir, self._reserved_space)
629+            writeable = disk['avail'] > 0
630+
631+            # spacetime predictors should use disk_avail / (d(disk_used)/dt)
632+            stats['storage_server.disk_total'] = disk['total']
633+            stats['storage_server.disk_used'] = disk['used']
634+            stats['storage_server.disk_free_for_root'] = disk['free_for_root']
635+            stats['storage_server.disk_free_for_nonroot'] = disk['free_for_nonroot']
636+            stats['storage_server.disk_avail'] = disk['avail']
637+        except AttributeError:
638+            writeable = True
639+        except EnvironmentError:
640+            log.msg("OS call to get disk statistics failed", level=log.UNUSUAL)
641+            writeable = False
642+
643+        if self._readonly:
644+            stats['storage_server.disk_avail'] = 0
645+            writeable = False
646+
647+        stats['storage_server.accepting_immutable_shares'] = int(writeable)
648+
649+    def get_available_space(self):
650+        if self._readonly:
651+            return 0
652+        return fileutil.get_available_space(self._sharedir, self._reserved_space)
653+
654+    def advise_corrupt_share(self, sharetype, storageindex, shnum, reason):
655+        fileutil.fp_make_dirs(self._corruption_advisory_dir)
656+        now = time_format.iso_utc(sep="T")
657+        si_s = si_b2a(storageindex)
658+
659+        # Windows can't handle colons in the filename.
660+        name = ("%s--%s-%d" % (now, si_s, shnum)).replace(":", "")
661+        f = self._corruption_advisory_dir.child(name).open("w")
662+        try:
663+            f.write("report: Share Corruption\n")
664+            f.write("type: %s\n" % sharetype)
665+            f.write("storage_index: %s\n" % si_s)
666+            f.write("share_number: %d\n" % shnum)
667+            f.write("\n")
668+            f.write(reason)
669+            f.write("\n")
670+        finally:
671+            f.close()
672+
673+        log.msg(format=("client claims corruption in (%(share_type)s) " +
674+                        "%(si)s-%(shnum)d: %(reason)s"),
675+                share_type=sharetype, si=si_s, shnum=shnum, reason=reason,
676+                level=log.SCARY, umid="SGx2fA")
677+
678+
679+class DiskShareSet(ShareSet):
680+    implements(IShareSet)
681+
682+    def __init__(self, storageindex, sharehomedir, incominghomedir=None, discard_storage=False):
683+        ShareSet.__init__(self, storageindex)
684+        self._sharehomedir = sharehomedir
685+        self._incominghomedir = incominghomedir
686+        self._discard_storage = discard_storage
687+
688+    def get_overhead(self):
689+        return (fileutil.get_disk_usage(self._sharehomedir) +
690+                fileutil.get_disk_usage(self._incominghomedir))
691+
692+    def get_shares(self):
693+        """
694+        Generate IStorageBackendShare objects for shares we have for this storage index.
695+        ("Shares we have" means completed ones, excluding incoming ones.)
696+        """
697+        try:
698+            for fp in self._sharehomedir.children():
699+                shnumstr = fp.basename()
700+                if not NUM_RE.match(shnumstr):
701+                    continue
702+                sharehome = self._sharehomedir.child(shnumstr)
703+                yield self.get_share(sharehome)
704+        except UnlistableError:
705+            # There is no shares directory at all.
706+            pass
707+
708+    def has_incoming(self, shnum):
709+        if self._incominghomedir is None:
710+            return False
711+        return self._incominghomedir.child(str(shnum)).exists()
712+
713+    def make_bucket_writer(self, storageserver, shnum, max_space_per_bucket, lease_info, canary):
714+        sharehome = self._sharehomedir.child(str(shnum))
715+        incominghome = self._incominghomedir.child(str(shnum))
716+        immsh = ImmutableDiskShare(self.get_storage_index(), shnum, sharehome, incominghome,
717+                                   max_size=max_space_per_bucket, create=True)
718+        bw = BucketWriter(storageserver, immsh, max_space_per_bucket, lease_info, canary)
719+        if self._discard_storage:
720+            bw.throw_out_all_data = True
721+        return bw
722+
723+    def _create_mutable_share(self, storageserver, shnum, write_enabler):
724+        fileutil.fp_make_dirs(self._sharehomedir)
725+        sharehome = self._sharehomedir.child(str(shnum))
726+        serverid = storageserver.get_serverid()
727+        return create_mutable_disk_share(sharehome, serverid, write_enabler, storageserver)
728+
729+    def _clean_up_after_unlink(self):
730+        fileutil.fp_rmdir_if_empty(self._sharehomedir)
731+
732hunk ./src/allmydata/storage/backends/disk/immutable.py 1
733-import os, stat, struct, time
734 
735hunk ./src/allmydata/storage/backends/disk/immutable.py 2
736-from foolscap.api import Referenceable
737+import struct
738 
739 from zope.interface import implements
740hunk ./src/allmydata/storage/backends/disk/immutable.py 5
741-from allmydata.interfaces import RIBucketWriter, RIBucketReader
742-from allmydata.util import base32, fileutil, log
743+
744+from allmydata.interfaces import IStoredShare
745+from allmydata.util import fileutil
746 from allmydata.util.assertutil import precondition
747hunk ./src/allmydata/storage/backends/disk/immutable.py 9
748+from allmydata.util.fileutil import fp_make_dirs
749 from allmydata.util.hashutil import constant_time_compare
750hunk ./src/allmydata/storage/backends/disk/immutable.py 11
751+from allmydata.util.encodingutil import quote_filepath
752+from allmydata.storage.common import si_b2a, UnknownImmutableContainerVersionError, DataTooLargeError
753 from allmydata.storage.lease import LeaseInfo
754hunk ./src/allmydata/storage/backends/disk/immutable.py 14
755-from allmydata.storage.common import UnknownImmutableContainerVersionError, \
756-     DataTooLargeError
757+
758 
759 # each share file (in storage/shares/$SI/$SHNUM) contains lease information
760 # and share data. The share data is accessed by RIBucketWriter.write and
761hunk ./src/allmydata/storage/backends/disk/immutable.py 41
762 # then the value stored in this field will be the actual share data length
763 # modulo 2**32.
764 
765-class ShareFile:
766-    LEASE_SIZE = struct.calcsize(">L32s32sL")
767+class ImmutableDiskShare(object):
768+    implements(IStoredShare)
769+
770     sharetype = "immutable"
771hunk ./src/allmydata/storage/backends/disk/immutable.py 45
772+    LEASE_SIZE = struct.calcsize(">L32s32sL")
773+
774 
775hunk ./src/allmydata/storage/backends/disk/immutable.py 48
776-    def __init__(self, filename, max_size=None, create=False):
777-        """ 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. """
778+    def __init__(self, storageindex, shnum, finalhome=None, incominghome=None, max_size=None, create=False):
779+        """ If max_size is not None then I won't allow more than
780+        max_size to be written to me. If create=True then max_size
781+        must not be None. """
782         precondition((max_size is not None) or (not create), max_size, create)
783hunk ./src/allmydata/storage/backends/disk/immutable.py 53
784-        self.home = filename
785+        self._storageindex = storageindex
786         self._max_size = max_size
787hunk ./src/allmydata/storage/backends/disk/immutable.py 55
788+        self._incominghome = incominghome
789+        self._home = finalhome
790+        self._shnum = shnum
791         if create:
792             # touch the file, so later callers will see that we're working on
793             # it. Also construct the metadata.
794hunk ./src/allmydata/storage/backends/disk/immutable.py 61
795-            assert not os.path.exists(self.home)
796-            fileutil.make_dirs(os.path.dirname(self.home))
797-            f = open(self.home, 'wb')
798+            assert not finalhome.exists()
799+            fp_make_dirs(self._incominghome.parent())
800             # The second field -- the four-byte share data length -- is no
801             # longer used as of Tahoe v1.3.0, but we continue to write it in
802             # there in case someone downgrades a storage server from >=
803hunk ./src/allmydata/storage/backends/disk/immutable.py 72
804             # the largest length that can fit into the field. That way, even
805             # if this does happen, the old < v1.3.0 server will still allow
806             # clients to read the first part of the share.
807-            f.write(struct.pack(">LLL", 1, min(2**32-1, max_size), 0))
808-            f.close()
809+            self._incominghome.setContent(struct.pack(">LLL", 1, min(2**32-1, max_size), 0) )
810             self._lease_offset = max_size + 0x0c
811             self._num_leases = 0
812         else:
813hunk ./src/allmydata/storage/backends/disk/immutable.py 76
814-            f = open(self.home, 'rb')
815-            filesize = os.path.getsize(self.home)
816-            (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
817-            f.close()
818+            f = self._home.open(mode='rb')
819+            try:
820+                (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
821+            finally:
822+                f.close()
823+            filesize = self._home.getsize()
824             if version != 1:
825                 msg = "sharefile %s had version %d but we wanted 1" % \
826hunk ./src/allmydata/storage/backends/disk/immutable.py 84
827-                      (filename, version)
828+                      (self._home, version)
829                 raise UnknownImmutableContainerVersionError(msg)
830             self._num_leases = num_leases
831             self._lease_offset = filesize - (num_leases * self.LEASE_SIZE)
832hunk ./src/allmydata/storage/backends/disk/immutable.py 90
833         self._data_offset = 0xc
834 
835+    def __repr__(self):
836+        return ("<ImmutableDiskShare %s:%r at %s>"
837+                % (si_b2a(self._storageindex), self._shnum, quote_filepath(self._home)))
838+
839+    def close(self):
840+        fileutil.fp_make_dirs(self._home.parent())
841+        self._incominghome.moveTo(self._home)
842+        try:
843+            # self._incominghome is like storage/shares/incoming/ab/abcde/4 .
844+            # We try to delete the parent (.../ab/abcde) to avoid leaving
845+            # these directories lying around forever, but the delete might
846+            # fail if we're working on another share for the same storage
847+            # index (like ab/abcde/5). The alternative approach would be to
848+            # use a hierarchy of objects (PrefixHolder, BucketHolder,
849+            # ShareWriter), each of which is responsible for a single
850+            # directory on disk, and have them use reference counting of
851+            # their children to know when they should do the rmdir. This
852+            # approach is simpler, but relies on os.rmdir refusing to delete
853+            # a non-empty directory. Do *not* use fileutil.fp_remove() here!
854+            fileutil.fp_rmdir_if_empty(self._incominghome.parent())
855+            # we also delete the grandparent (prefix) directory, .../ab ,
856+            # again to avoid leaving directories lying around. This might
857+            # fail if there is another bucket open that shares a prefix (like
858+            # ab/abfff).
859+            fileutil.fp_rmdir_if_empty(self._incominghome.parent().parent())
860+            # we leave the great-grandparent (incoming/) directory in place.
861+        except EnvironmentError:
862+            # ignore the "can't rmdir because the directory is not empty"
863+            # exceptions, those are normal consequences of the
864+            # above-mentioned conditions.
865+            pass
866+        pass
867+
868+    def get_used_space(self):
869+        return (fileutil.get_used_space(self._home) +
870+                fileutil.get_used_space(self._incominghome))
871+
872+    def get_storage_index(self):
873+        return self._storageindex
874+
875+    def get_shnum(self):
876+        return self._shnum
877+
878     def unlink(self):
879hunk ./src/allmydata/storage/backends/disk/immutable.py 134
880-        os.unlink(self.home)
881+        self._home.remove()
882+
883+    def get_size(self):
884+        return self._home.getsize()
885+
886+    def get_data_length(self):
887+        return self._lease_offset - self._data_offset
888+
889+    #def readv(self, read_vector):
890+    #    ...
891 
892     def read_share_data(self, offset, length):
893         precondition(offset >= 0)
894hunk ./src/allmydata/storage/backends/disk/immutable.py 147
895-        # reads beyond the end of the data are truncated. Reads that start
896+
897+        # Reads beyond the end of the data are truncated. Reads that start
898         # beyond the end of the data return an empty string.
899         seekpos = self._data_offset+offset
900         actuallength = max(0, min(length, self._lease_offset-seekpos))
901hunk ./src/allmydata/storage/backends/disk/immutable.py 154
902         if actuallength == 0:
903             return ""
904-        f = open(self.home, 'rb')
905-        f.seek(seekpos)
906-        return f.read(actuallength)
907+        f = self._home.open(mode='rb')
908+        try:
909+            f.seek(seekpos)
910+            sharedata = f.read(actuallength)
911+        finally:
912+            f.close()
913+        return sharedata
914 
915     def write_share_data(self, offset, data):
916         length = len(data)
917hunk ./src/allmydata/storage/backends/disk/immutable.py 167
918         precondition(offset >= 0, offset)
919         if self._max_size is not None and offset+length > self._max_size:
920             raise DataTooLargeError(self._max_size, offset, length)
921-        f = open(self.home, 'rb+')
922-        real_offset = self._data_offset+offset
923-        f.seek(real_offset)
924-        assert f.tell() == real_offset
925-        f.write(data)
926-        f.close()
927+        f = self._incominghome.open(mode='rb+')
928+        try:
929+            real_offset = self._data_offset+offset
930+            f.seek(real_offset)
931+            assert f.tell() == real_offset
932+            f.write(data)
933+        finally:
934+            f.close()
935 
936     def _write_lease_record(self, f, lease_number, lease_info):
937         offset = self._lease_offset + lease_number * self.LEASE_SIZE
938hunk ./src/allmydata/storage/backends/disk/immutable.py 184
939 
940     def _read_num_leases(self, f):
941         f.seek(0x08)
942-        (num_leases,) = struct.unpack(">L", f.read(4))
943+        ro = f.read(4)
944+        (num_leases,) = struct.unpack(">L", ro)
945         return num_leases
946 
947     def _write_num_leases(self, f, num_leases):
948hunk ./src/allmydata/storage/backends/disk/immutable.py 195
949     def _truncate_leases(self, f, num_leases):
950         f.truncate(self._lease_offset + num_leases * self.LEASE_SIZE)
951 
952+    # These lease operations are intended for use by disk_backend.py.
953+    # Other clients should not depend on the fact that the disk backend
954+    # stores leases in share files.
955+
956     def get_leases(self):
957         """Yields a LeaseInfo instance for all leases."""
958hunk ./src/allmydata/storage/backends/disk/immutable.py 201
959-        f = open(self.home, 'rb')
960-        (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
961-        f.seek(self._lease_offset)
962-        for i in range(num_leases):
963-            data = f.read(self.LEASE_SIZE)
964-            if data:
965-                yield LeaseInfo().from_immutable_data(data)
966+        f = self._home.open(mode='rb')
967+        try:
968+            (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
969+            f.seek(self._lease_offset)
970+            for i in range(num_leases):
971+                data = f.read(self.LEASE_SIZE)
972+                if data:
973+                    yield LeaseInfo().from_immutable_data(data)
974+        finally:
975+            f.close()
976 
977     def add_lease(self, lease_info):
978hunk ./src/allmydata/storage/backends/disk/immutable.py 213
979-        f = open(self.home, 'rb+')
980-        num_leases = self._read_num_leases(f)
981-        self._write_lease_record(f, num_leases, lease_info)
982-        self._write_num_leases(f, num_leases+1)
983-        f.close()
984+        f = self._incominghome.open(mode='rb')
985+        try:
986+            num_leases = self._read_num_leases(f)
987+        finally:
988+            f.close()
989+        f = self._home.open(mode='wb+')
990+        try:
991+            self._write_lease_record(f, num_leases, lease_info)
992+            self._write_num_leases(f, num_leases+1)
993+        finally:
994+            f.close()
995 
996     def renew_lease(self, renew_secret, new_expire_time):
997hunk ./src/allmydata/storage/backends/disk/immutable.py 226
998-        for i,lease in enumerate(self.get_leases()):
999-            if constant_time_compare(lease.renew_secret, renew_secret):
1000-                # yup. See if we need to update the owner time.
1001-                if new_expire_time > lease.expiration_time:
1002-                    # yes
1003-                    lease.expiration_time = new_expire_time
1004-                    f = open(self.home, 'rb+')
1005-                    self._write_lease_record(f, i, lease)
1006-                    f.close()
1007-                return
1008+        try:
1009+            for i, lease in enumerate(self.get_leases()):
1010+                if constant_time_compare(lease.renew_secret, renew_secret):
1011+                    # yup. See if we need to update the owner time.
1012+                    if new_expire_time > lease.expiration_time:
1013+                        # yes
1014+                        lease.expiration_time = new_expire_time
1015+                        f = self._home.open('rb+')
1016+                        try:
1017+                            self._write_lease_record(f, i, lease)
1018+                        finally:
1019+                            f.close()
1020+                    return
1021+        except IndexError, e:
1022+            raise Exception("IndexError: %s" % (e,))
1023         raise IndexError("unable to renew non-existent lease")
1024 
1025     def add_or_renew_lease(self, lease_info):
1026hunk ./src/allmydata/storage/backends/disk/immutable.py 249
1027                              lease_info.expiration_time)
1028         except IndexError:
1029             self.add_lease(lease_info)
1030-
1031-
1032-    def cancel_lease(self, cancel_secret):
1033-        """Remove a lease with the given cancel_secret. If the last lease is
1034-        cancelled, the file will be removed. Return the number of bytes that
1035-        were freed (by truncating the list of leases, and possibly by
1036-        deleting the file. Raise IndexError if there was no lease with the
1037-        given cancel_secret.
1038-        """
1039-
1040-        leases = list(self.get_leases())
1041-        num_leases_removed = 0
1042-        for i,lease in enumerate(leases):
1043-            if constant_time_compare(lease.cancel_secret, cancel_secret):
1044-                leases[i] = None
1045-                num_leases_removed += 1
1046-        if not num_leases_removed:
1047-            raise IndexError("unable to find matching lease to cancel")
1048-        if num_leases_removed:
1049-            # pack and write out the remaining leases. We write these out in
1050-            # the same order as they were added, so that if we crash while
1051-            # doing this, we won't lose any non-cancelled leases.
1052-            leases = [l for l in leases if l] # remove the cancelled leases
1053-            f = open(self.home, 'rb+')
1054-            for i,lease in enumerate(leases):
1055-                self._write_lease_record(f, i, lease)
1056-            self._write_num_leases(f, len(leases))
1057-            self._truncate_leases(f, len(leases))
1058-            f.close()
1059-        space_freed = self.LEASE_SIZE * num_leases_removed
1060-        if not len(leases):
1061-            space_freed += os.stat(self.home)[stat.ST_SIZE]
1062-            self.unlink()
1063-        return space_freed
1064-
1065-
1066-class BucketWriter(Referenceable):
1067-    implements(RIBucketWriter)
1068-
1069-    def __init__(self, ss, incominghome, finalhome, max_size, lease_info, canary):
1070-        self.ss = ss
1071-        self.incominghome = incominghome
1072-        self.finalhome = finalhome
1073-        self._max_size = max_size # don't allow the client to write more than this
1074-        self._canary = canary
1075-        self._disconnect_marker = canary.notifyOnDisconnect(self._disconnected)
1076-        self.closed = False
1077-        self.throw_out_all_data = False
1078-        self._sharefile = ShareFile(incominghome, create=True, max_size=max_size)
1079-        # also, add our lease to the file now, so that other ones can be
1080-        # added by simultaneous uploaders
1081-        self._sharefile.add_lease(lease_info)
1082-
1083-    def allocated_size(self):
1084-        return self._max_size
1085-
1086-    def remote_write(self, offset, data):
1087-        start = time.time()
1088-        precondition(not self.closed)
1089-        if self.throw_out_all_data:
1090-            return
1091-        self._sharefile.write_share_data(offset, data)
1092-        self.ss.add_latency("write", time.time() - start)
1093-        self.ss.count("write")
1094-
1095-    def remote_close(self):
1096-        precondition(not self.closed)
1097-        start = time.time()
1098-
1099-        fileutil.make_dirs(os.path.dirname(self.finalhome))
1100-        fileutil.rename(self.incominghome, self.finalhome)
1101-        try:
1102-            # self.incominghome is like storage/shares/incoming/ab/abcde/4 .
1103-            # We try to delete the parent (.../ab/abcde) to avoid leaving
1104-            # these directories lying around forever, but the delete might
1105-            # fail if we're working on another share for the same storage
1106-            # index (like ab/abcde/5). The alternative approach would be to
1107-            # use a hierarchy of objects (PrefixHolder, BucketHolder,
1108-            # ShareWriter), each of which is responsible for a single
1109-            # directory on disk, and have them use reference counting of
1110-            # their children to know when they should do the rmdir. This
1111-            # approach is simpler, but relies on os.rmdir refusing to delete
1112-            # a non-empty directory. Do *not* use fileutil.rm_dir() here!
1113-            os.rmdir(os.path.dirname(self.incominghome))
1114-            # we also delete the grandparent (prefix) directory, .../ab ,
1115-            # again to avoid leaving directories lying around. This might
1116-            # fail if there is another bucket open that shares a prefix (like
1117-            # ab/abfff).
1118-            os.rmdir(os.path.dirname(os.path.dirname(self.incominghome)))
1119-            # we leave the great-grandparent (incoming/) directory in place.
1120-        except EnvironmentError:
1121-            # ignore the "can't rmdir because the directory is not empty"
1122-            # exceptions, those are normal consequences of the
1123-            # above-mentioned conditions.
1124-            pass
1125-        self._sharefile = None
1126-        self.closed = True
1127-        self._canary.dontNotifyOnDisconnect(self._disconnect_marker)
1128-
1129-        filelen = os.stat(self.finalhome)[stat.ST_SIZE]
1130-        self.ss.bucket_writer_closed(self, filelen)
1131-        self.ss.add_latency("close", time.time() - start)
1132-        self.ss.count("close")
1133-
1134-    def _disconnected(self):
1135-        if not self.closed:
1136-            self._abort()
1137-
1138-    def remote_abort(self):
1139-        log.msg("storage: aborting sharefile %s" % self.incominghome,
1140-                facility="tahoe.storage", level=log.UNUSUAL)
1141-        if not self.closed:
1142-            self._canary.dontNotifyOnDisconnect(self._disconnect_marker)
1143-        self._abort()
1144-        self.ss.count("abort")
1145-
1146-    def _abort(self):
1147-        if self.closed:
1148-            return
1149-
1150-        os.remove(self.incominghome)
1151-        # if we were the last share to be moved, remove the incoming/
1152-        # directory that was our parent
1153-        parentdir = os.path.split(self.incominghome)[0]
1154-        if not os.listdir(parentdir):
1155-            os.rmdir(parentdir)
1156-        self._sharefile = None
1157-
1158-        # We are now considered closed for further writing. We must tell
1159-        # the storage server about this so that it stops expecting us to
1160-        # use the space it allocated for us earlier.
1161-        self.closed = True
1162-        self.ss.bucket_writer_closed(self, 0)
1163-
1164-
1165-class BucketReader(Referenceable):
1166-    implements(RIBucketReader)
1167-
1168-    def __init__(self, ss, sharefname, storage_index=None, shnum=None):
1169-        self.ss = ss
1170-        self._share_file = ShareFile(sharefname)
1171-        self.storage_index = storage_index
1172-        self.shnum = shnum
1173-
1174-    def __repr__(self):
1175-        return "<%s %s %s>" % (self.__class__.__name__,
1176-                               base32.b2a_l(self.storage_index[:8], 60),
1177-                               self.shnum)
1178-
1179-    def remote_read(self, offset, length):
1180-        start = time.time()
1181-        data = self._share_file.read_share_data(offset, length)
1182-        self.ss.add_latency("read", time.time() - start)
1183-        self.ss.count("read")
1184-        return data
1185-
1186-    def remote_advise_corrupt_share(self, reason):
1187-        return self.ss.remote_advise_corrupt_share("immutable",
1188-                                                   self.storage_index,
1189-                                                   self.shnum,
1190-                                                   reason)
1191hunk ./src/allmydata/storage/backends/disk/mutable.py 1
1192-import os, stat, struct
1193 
1194hunk ./src/allmydata/storage/backends/disk/mutable.py 2
1195-from allmydata.interfaces import BadWriteEnablerError
1196-from allmydata.util import idlib, log
1197+import struct
1198+
1199+from zope.interface import implements
1200+
1201+from allmydata.interfaces import IStoredMutableShare, BadWriteEnablerError
1202+from allmydata.util import fileutil, idlib, log
1203 from allmydata.util.assertutil import precondition
1204 from allmydata.util.hashutil import constant_time_compare
1205hunk ./src/allmydata/storage/backends/disk/mutable.py 10
1206-from allmydata.storage.lease import LeaseInfo
1207-from allmydata.storage.common import UnknownMutableContainerVersionError, \
1208+from allmydata.util.encodingutil import quote_filepath
1209+from allmydata.storage.common import si_b2a, UnknownMutableContainerVersionError, \
1210      DataTooLargeError
1211hunk ./src/allmydata/storage/backends/disk/mutable.py 13
1212+from allmydata.storage.lease import LeaseInfo
1213+from allmydata.storage.backends.base import testv_compare
1214 
1215hunk ./src/allmydata/storage/backends/disk/mutable.py 16
1216-# the MutableShareFile is like the ShareFile, but used for mutable data. It
1217-# has a different layout. See docs/mutable.txt for more details.
1218+
1219+# The MutableDiskShare is like the ImmutableDiskShare, but used for mutable data.
1220+# It has a different layout. See docs/mutable.rst for more details.
1221 
1222 # #   offset    size    name
1223 # 1   0         32      magic verstr "tahoe mutable container v1" plus binary
1224hunk ./src/allmydata/storage/backends/disk/mutable.py 31
1225 #                        4    4   expiration timestamp
1226 #                        8   32   renewal token
1227 #                        40  32   cancel token
1228-#                        72  20   nodeid which accepted the tokens
1229+#                        72  20   nodeid that accepted the tokens
1230 # 7   468       (a)     data
1231 # 8   ??        4       count of extra leases
1232 # 9   ??        n*92    extra leases
1233hunk ./src/allmydata/storage/backends/disk/mutable.py 37
1234 
1235 
1236-# The struct module doc says that L's are 4 bytes in size., and that Q's are
1237+# The struct module doc says that L's are 4 bytes in size, and that Q's are
1238 # 8 bytes in size. Since compatibility depends upon this, double-check it.
1239 assert struct.calcsize(">L") == 4, struct.calcsize(">L")
1240 assert struct.calcsize(">Q") == 8, struct.calcsize(">Q")
1241hunk ./src/allmydata/storage/backends/disk/mutable.py 42
1242 
1243-class MutableShareFile:
1244+
1245+class MutableDiskShare(object):
1246+    implements(IStoredMutableShare)
1247 
1248     sharetype = "mutable"
1249     DATA_LENGTH_OFFSET = struct.calcsize(">32s20s32s")
1250hunk ./src/allmydata/storage/backends/disk/mutable.py 54
1251     assert LEASE_SIZE == 92
1252     DATA_OFFSET = HEADER_SIZE + 4*LEASE_SIZE
1253     assert DATA_OFFSET == 468, DATA_OFFSET
1254+
1255     # our sharefiles share with a recognizable string, plus some random
1256     # binary data to reduce the chance that a regular text file will look
1257     # like a sharefile.
1258hunk ./src/allmydata/storage/backends/disk/mutable.py 63
1259     MAX_SIZE = 2*1000*1000*1000 # 2GB, kind of arbitrary
1260     # TODO: decide upon a policy for max share size
1261 
1262-    def __init__(self, filename, parent=None):
1263-        self.home = filename
1264-        if os.path.exists(self.home):
1265+    def __init__(self, storageindex, shnum, home, parent=None):
1266+        self._storageindex = storageindex
1267+        self._shnum = shnum
1268+        self._home = home
1269+        if self._home.exists():
1270             # we don't cache anything, just check the magic
1271hunk ./src/allmydata/storage/backends/disk/mutable.py 69
1272-            f = open(self.home, 'rb')
1273-            data = f.read(self.HEADER_SIZE)
1274-            (magic,
1275-             write_enabler_nodeid, write_enabler,
1276-             data_length, extra_least_offset) = \
1277-             struct.unpack(">32s20s32sQQ", data)
1278-            if magic != self.MAGIC:
1279-                msg = "sharefile %s had magic '%r' but we wanted '%r'" % \
1280-                      (filename, magic, self.MAGIC)
1281-                raise UnknownMutableContainerVersionError(msg)
1282+            f = self._home.open('rb')
1283+            try:
1284+                data = f.read(self.HEADER_SIZE)
1285+                (magic,
1286+                 write_enabler_nodeid, write_enabler,
1287+                 data_length, extra_least_offset) = \
1288+                 struct.unpack(">32s20s32sQQ", data)
1289+                if magic != self.MAGIC:
1290+                    msg = "sharefile %s had magic '%r' but we wanted '%r'" % \
1291+                          (quote_filepath(self._home), magic, self.MAGIC)
1292+                    raise UnknownMutableContainerVersionError(msg)
1293+            finally:
1294+                f.close()
1295         self.parent = parent # for logging
1296 
1297     def log(self, *args, **kwargs):
1298hunk ./src/allmydata/storage/backends/disk/mutable.py 87
1299         return self.parent.log(*args, **kwargs)
1300 
1301-    def create(self, my_nodeid, write_enabler):
1302-        assert not os.path.exists(self.home)
1303+    def create(self, serverid, write_enabler):
1304+        assert not self._home.exists()
1305         data_length = 0
1306         extra_lease_offset = (self.HEADER_SIZE
1307                               + 4 * self.LEASE_SIZE
1308hunk ./src/allmydata/storage/backends/disk/mutable.py 95
1309                               + data_length)
1310         assert extra_lease_offset == self.DATA_OFFSET # true at creation
1311         num_extra_leases = 0
1312-        f = open(self.home, 'wb')
1313-        header = struct.pack(">32s20s32sQQ",
1314-                             self.MAGIC, my_nodeid, write_enabler,
1315-                             data_length, extra_lease_offset,
1316-                             )
1317-        leases = ("\x00"*self.LEASE_SIZE) * 4
1318-        f.write(header + leases)
1319-        # data goes here, empty after creation
1320-        f.write(struct.pack(">L", num_extra_leases))
1321-        # extra leases go here, none at creation
1322-        f.close()
1323+        f = self._home.open('wb')
1324+        try:
1325+            header = struct.pack(">32s20s32sQQ",
1326+                                 self.MAGIC, serverid, write_enabler,
1327+                                 data_length, extra_lease_offset,
1328+                                 )
1329+            leases = ("\x00"*self.LEASE_SIZE) * 4
1330+            f.write(header + leases)
1331+            # data goes here, empty after creation
1332+            f.write(struct.pack(">L", num_extra_leases))
1333+            # extra leases go here, none at creation
1334+        finally:
1335+            f.close()
1336+
1337+    def __repr__(self):
1338+        return ("<MutableDiskShare %s:%r at %s>"
1339+                % (si_b2a(self._storageindex), self._shnum, quote_filepath(self._home)))
1340+
1341+    def get_used_space(self):
1342+        return fileutil.get_used_space(self._home)
1343+
1344+    def get_storage_index(self):
1345+        return self._storageindex
1346+
1347+    def get_shnum(self):
1348+        return self._shnum
1349 
1350     def unlink(self):
1351hunk ./src/allmydata/storage/backends/disk/mutable.py 123
1352-        os.unlink(self.home)
1353+        self._home.remove()
1354 
1355     def _read_data_length(self, f):
1356         f.seek(self.DATA_LENGTH_OFFSET)
1357hunk ./src/allmydata/storage/backends/disk/mutable.py 291
1358 
1359     def get_leases(self):
1360         """Yields a LeaseInfo instance for all leases."""
1361-        f = open(self.home, 'rb')
1362-        for i, lease in self._enumerate_leases(f):
1363-            yield lease
1364-        f.close()
1365+        f = self._home.open('rb')
1366+        try:
1367+            for i, lease in self._enumerate_leases(f):
1368+                yield lease
1369+        finally:
1370+            f.close()
1371 
1372     def _enumerate_leases(self, f):
1373         for i in range(self._get_num_lease_slots(f)):
1374hunk ./src/allmydata/storage/backends/disk/mutable.py 303
1375             try:
1376                 data = self._read_lease_record(f, i)
1377                 if data is not None:
1378-                    yield i,data
1379+                    yield i, data
1380             except IndexError:
1381                 return
1382 
1383hunk ./src/allmydata/storage/backends/disk/mutable.py 307
1384+    # These lease operations are intended for use by disk_backend.py.
1385+    # Other non-test clients should not depend on the fact that the disk
1386+    # backend stores leases in share files.
1387+
1388     def add_lease(self, lease_info):
1389         precondition(lease_info.owner_num != 0) # 0 means "no lease here"
1390hunk ./src/allmydata/storage/backends/disk/mutable.py 313
1391-        f = open(self.home, 'rb+')
1392-        num_lease_slots = self._get_num_lease_slots(f)
1393-        empty_slot = self._get_first_empty_lease_slot(f)
1394-        if empty_slot is not None:
1395-            self._write_lease_record(f, empty_slot, lease_info)
1396-        else:
1397-            self._write_lease_record(f, num_lease_slots, lease_info)
1398-        f.close()
1399+        f = self._home.open('rb+')
1400+        try:
1401+            num_lease_slots = self._get_num_lease_slots(f)
1402+            empty_slot = self._get_first_empty_lease_slot(f)
1403+            if empty_slot is not None:
1404+                self._write_lease_record(f, empty_slot, lease_info)
1405+            else:
1406+                self._write_lease_record(f, num_lease_slots, lease_info)
1407+        finally:
1408+            f.close()
1409 
1410     def renew_lease(self, renew_secret, new_expire_time):
1411         accepting_nodeids = set()
1412hunk ./src/allmydata/storage/backends/disk/mutable.py 326
1413-        f = open(self.home, 'rb+')
1414-        for (leasenum,lease) in self._enumerate_leases(f):
1415-            if constant_time_compare(lease.renew_secret, renew_secret):
1416-                # yup. See if we need to update the owner time.
1417-                if new_expire_time > lease.expiration_time:
1418-                    # yes
1419-                    lease.expiration_time = new_expire_time
1420-                    self._write_lease_record(f, leasenum, lease)
1421-                f.close()
1422-                return
1423-            accepting_nodeids.add(lease.nodeid)
1424-        f.close()
1425+        f = self._home.open('rb+')
1426+        try:
1427+            for (leasenum, lease) in self._enumerate_leases(f):
1428+                if constant_time_compare(lease.renew_secret, renew_secret):
1429+                    # yup. See if we need to update the owner time.
1430+                    if new_expire_time > lease.expiration_time:
1431+                        # yes
1432+                        lease.expiration_time = new_expire_time
1433+                        self._write_lease_record(f, leasenum, lease)
1434+                    return
1435+                accepting_nodeids.add(lease.nodeid)
1436+        finally:
1437+            f.close()
1438         # Return the accepting_nodeids set, to give the client a chance to
1439hunk ./src/allmydata/storage/backends/disk/mutable.py 340
1440-        # update the leases on a share which has been migrated from its
1441+        # update the leases on a share that has been migrated from its
1442         # original server to a new one.
1443         msg = ("Unable to renew non-existent lease. I have leases accepted by"
1444                " nodeids: ")
1445hunk ./src/allmydata/storage/backends/disk/mutable.py 357
1446         except IndexError:
1447             self.add_lease(lease_info)
1448 
1449-    def cancel_lease(self, cancel_secret):
1450-        """Remove any leases with the given cancel_secret. If the last lease
1451-        is cancelled, the file will be removed. Return the number of bytes
1452-        that were freed (by truncating the list of leases, and possibly by
1453-        deleting the file. Raise IndexError if there was no lease with the
1454-        given cancel_secret."""
1455-
1456-        accepting_nodeids = set()
1457-        modified = 0
1458-        remaining = 0
1459-        blank_lease = LeaseInfo(owner_num=0,
1460-                                renew_secret="\x00"*32,
1461-                                cancel_secret="\x00"*32,
1462-                                expiration_time=0,
1463-                                nodeid="\x00"*20)
1464-        f = open(self.home, 'rb+')
1465-        for (leasenum,lease) in self._enumerate_leases(f):
1466-            accepting_nodeids.add(lease.nodeid)
1467-            if constant_time_compare(lease.cancel_secret, cancel_secret):
1468-                self._write_lease_record(f, leasenum, blank_lease)
1469-                modified += 1
1470-            else:
1471-                remaining += 1
1472-        if modified:
1473-            freed_space = self._pack_leases(f)
1474-            f.close()
1475-            if not remaining:
1476-                freed_space += os.stat(self.home)[stat.ST_SIZE]
1477-                self.unlink()
1478-            return freed_space
1479-
1480-        msg = ("Unable to cancel non-existent lease. I have leases "
1481-               "accepted by nodeids: ")
1482-        msg += ",".join([("'%s'" % idlib.nodeid_b2a(anid))
1483-                         for anid in accepting_nodeids])
1484-        msg += " ."
1485-        raise IndexError(msg)
1486-
1487-    def _pack_leases(self, f):
1488-        # TODO: reclaim space from cancelled leases
1489-        return 0
1490-
1491     def _read_write_enabler_and_nodeid(self, f):
1492         f.seek(0)
1493         data = f.read(self.HEADER_SIZE)
1494hunk ./src/allmydata/storage/backends/disk/mutable.py 369
1495 
1496     def readv(self, readv):
1497         datav = []
1498-        f = open(self.home, 'rb')
1499-        for (offset, length) in readv:
1500-            datav.append(self._read_share_data(f, offset, length))
1501-        f.close()
1502+        f = self._home.open('rb')
1503+        try:
1504+            for (offset, length) in readv:
1505+                datav.append(self._read_share_data(f, offset, length))
1506+        finally:
1507+            f.close()
1508         return datav
1509 
1510hunk ./src/allmydata/storage/backends/disk/mutable.py 377
1511-#    def remote_get_length(self):
1512-#        f = open(self.home, 'rb')
1513-#        data_length = self._read_data_length(f)
1514-#        f.close()
1515-#        return data_length
1516+    def get_size(self):
1517+        return self._home.getsize()
1518+
1519+    def get_data_length(self):
1520+        f = self._home.open('rb')
1521+        try:
1522+            data_length = self._read_data_length(f)
1523+        finally:
1524+            f.close()
1525+        return data_length
1526 
1527     def check_write_enabler(self, write_enabler, si_s):
1528hunk ./src/allmydata/storage/backends/disk/mutable.py 389
1529-        f = open(self.home, 'rb+')
1530-        (real_write_enabler, write_enabler_nodeid) = \
1531-                             self._read_write_enabler_and_nodeid(f)
1532-        f.close()
1533+        f = self._home.open('rb+')
1534+        try:
1535+            (real_write_enabler, write_enabler_nodeid) = self._read_write_enabler_and_nodeid(f)
1536+        finally:
1537+            f.close()
1538         # avoid a timing attack
1539         #if write_enabler != real_write_enabler:
1540         if not constant_time_compare(write_enabler, real_write_enabler):
1541hunk ./src/allmydata/storage/backends/disk/mutable.py 410
1542 
1543     def check_testv(self, testv):
1544         test_good = True
1545-        f = open(self.home, 'rb+')
1546-        for (offset, length, operator, specimen) in testv:
1547-            data = self._read_share_data(f, offset, length)
1548-            if not testv_compare(data, operator, specimen):
1549-                test_good = False
1550-                break
1551-        f.close()
1552+        f = self._home.open('rb+')
1553+        try:
1554+            for (offset, length, operator, specimen) in testv:
1555+                data = self._read_share_data(f, offset, length)
1556+                if not testv_compare(data, operator, specimen):
1557+                    test_good = False
1558+                    break
1559+        finally:
1560+            f.close()
1561         return test_good
1562 
1563     def writev(self, datav, new_length):
1564hunk ./src/allmydata/storage/backends/disk/mutable.py 422
1565-        f = open(self.home, 'rb+')
1566-        for (offset, data) in datav:
1567-            self._write_share_data(f, offset, data)
1568-        if new_length is not None:
1569-            cur_length = self._read_data_length(f)
1570-            if new_length < cur_length:
1571-                self._write_data_length(f, new_length)
1572-                # TODO: if we're going to shrink the share file when the
1573-                # share data has shrunk, then call
1574-                # self._change_container_size() here.
1575-        f.close()
1576-
1577-def testv_compare(a, op, b):
1578-    assert op in ("lt", "le", "eq", "ne", "ge", "gt")
1579-    if op == "lt":
1580-        return a < b
1581-    if op == "le":
1582-        return a <= b
1583-    if op == "eq":
1584-        return a == b
1585-    if op == "ne":
1586-        return a != b
1587-    if op == "ge":
1588-        return a >= b
1589-    if op == "gt":
1590-        return a > b
1591-    # never reached
1592+        f = self._home.open('rb+')
1593+        try:
1594+            for (offset, data) in datav:
1595+                self._write_share_data(f, offset, data)
1596+            if new_length is not None:
1597+                cur_length = self._read_data_length(f)
1598+                if new_length < cur_length:
1599+                    self._write_data_length(f, new_length)
1600+                    # TODO: if we're going to shrink the share file when the
1601+                    # share data has shrunk, then call
1602+                    # self._change_container_size() here.
1603+        finally:
1604+            f.close()
1605 
1606hunk ./src/allmydata/storage/backends/disk/mutable.py 436
1607-class EmptyShare:
1608+    def close(self):
1609+        pass
1610 
1611hunk ./src/allmydata/storage/backends/disk/mutable.py 439
1612-    def check_testv(self, testv):
1613-        test_good = True
1614-        for (offset, length, operator, specimen) in testv:
1615-            data = ""
1616-            if not testv_compare(data, operator, specimen):
1617-                test_good = False
1618-                break
1619-        return test_good
1620 
1621hunk ./src/allmydata/storage/backends/disk/mutable.py 440
1622-def create_mutable_sharefile(filename, my_nodeid, write_enabler, parent):
1623-    ms = MutableShareFile(filename, parent)
1624-    ms.create(my_nodeid, write_enabler)
1625+def create_mutable_disk_share(fp, serverid, write_enabler, parent):
1626+    ms = MutableDiskShare(fp, parent)
1627+    ms.create(serverid, write_enabler)
1628     del ms
1629hunk ./src/allmydata/storage/backends/disk/mutable.py 444
1630-    return MutableShareFile(filename, parent)
1631-
1632+    return MutableDiskShare(fp, parent)
1633addfile ./src/allmydata/storage/backends/null/__init__.py
1634addfile ./src/allmydata/storage/backends/null/null_backend.py
1635hunk ./src/allmydata/storage/backends/null/null_backend.py 2
1636 
1637+import os, struct
1638+
1639+from zope.interface import implements
1640+
1641+from allmydata.interfaces import IStorageBackend, IShareSet, IStoredShare, IStoredMutableShare
1642+from allmydata.util.assertutil import precondition
1643+from allmydata.util.hashutil import constant_time_compare
1644+from allmydata.storage.backends.base import Backend, ShareSet
1645+from allmydata.storage.bucket import BucketWriter
1646+from allmydata.storage.common import si_b2a
1647+from allmydata.storage.lease import LeaseInfo
1648+
1649+
1650+class NullBackend(Backend):
1651+    implements(IStorageBackend)
1652+
1653+    def __init__(self):
1654+        Backend.__init__(self)
1655+
1656+    def get_available_space(self, reserved_space):
1657+        return None
1658+
1659+    def get_sharesets_for_prefix(self, prefix):
1660+        pass
1661+
1662+    def get_shareset(self, storageindex):
1663+        return NullShareSet(storageindex)
1664+
1665+    def fill_in_space_stats(self, stats):
1666+        pass
1667+
1668+    def set_storage_server(self, ss):
1669+        self.ss = ss
1670+
1671+    def advise_corrupt_share(self, sharetype, storageindex, shnum, reason):
1672+        pass
1673+
1674+
1675+class NullShareSet(ShareSet):
1676+    implements(IShareSet)
1677+
1678+    def __init__(self, storageindex):
1679+        self.storageindex = storageindex
1680+
1681+    def get_overhead(self):
1682+        return 0
1683+
1684+    def get_incoming_shnums(self):
1685+        return frozenset()
1686+
1687+    def get_shares(self):
1688+        pass
1689+
1690+    def get_share(self, shnum):
1691+        return None
1692+
1693+    def get_storage_index(self):
1694+        return self.storageindex
1695+
1696+    def get_storage_index_string(self):
1697+        return si_b2a(self.storageindex)
1698+
1699+    def make_bucket_writer(self, storageserver, shnum, max_space_per_bucket, lease_info, canary):
1700+        immutableshare = ImmutableNullShare()
1701+        return BucketWriter(self.ss, immutableshare, max_space_per_bucket, lease_info, canary)
1702+
1703+    def _create_mutable_share(self, storageserver, shnum, write_enabler):
1704+        return MutableNullShare()
1705+
1706+    def _clean_up_after_unlink(self):
1707+        pass
1708+
1709+
1710+class ImmutableNullShare:
1711+    implements(IStoredShare)
1712+    sharetype = "immutable"
1713+
1714+    def __init__(self):
1715+        """ If max_size is not None then I won't allow more than
1716+        max_size to be written to me. If create=True then max_size
1717+        must not be None. """
1718+        pass
1719+
1720+    def get_shnum(self):
1721+        return self.shnum
1722+
1723+    def unlink(self):
1724+        os.unlink(self.fname)
1725+
1726+    def read_share_data(self, offset, length):
1727+        precondition(offset >= 0)
1728+        # Reads beyond the end of the data are truncated. Reads that start
1729+        # beyond the end of the data return an empty string.
1730+        seekpos = self._data_offset+offset
1731+        fsize = os.path.getsize(self.fname)
1732+        actuallength = max(0, min(length, fsize-seekpos)) # XXX #1528
1733+        if actuallength == 0:
1734+            return ""
1735+        f = open(self.fname, 'rb')
1736+        f.seek(seekpos)
1737+        return f.read(actuallength)
1738+
1739+    def write_share_data(self, offset, data):
1740+        pass
1741+
1742+    def _write_lease_record(self, f, lease_number, lease_info):
1743+        offset = self._lease_offset + lease_number * self.LEASE_SIZE
1744+        f.seek(offset)
1745+        assert f.tell() == offset
1746+        f.write(lease_info.to_immutable_data())
1747+
1748+    def _read_num_leases(self, f):
1749+        f.seek(0x08)
1750+        (num_leases,) = struct.unpack(">L", f.read(4))
1751+        return num_leases
1752+
1753+    def _write_num_leases(self, f, num_leases):
1754+        f.seek(0x08)
1755+        f.write(struct.pack(">L", num_leases))
1756+
1757+    def _truncate_leases(self, f, num_leases):
1758+        f.truncate(self._lease_offset + num_leases * self.LEASE_SIZE)
1759+
1760+    def get_leases(self):
1761+        """Yields a LeaseInfo instance for all leases."""
1762+        f = open(self.fname, 'rb')
1763+        (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1764+        f.seek(self._lease_offset)
1765+        for i in range(num_leases):
1766+            data = f.read(self.LEASE_SIZE)
1767+            if data:
1768+                yield LeaseInfo().from_immutable_data(data)
1769+
1770+    def add_lease(self, lease):
1771+        pass
1772+
1773+    def renew_lease(self, renew_secret, new_expire_time):
1774+        for i,lease in enumerate(self.get_leases()):
1775+            if constant_time_compare(lease.renew_secret, renew_secret):
1776+                # yup. See if we need to update the owner time.
1777+                if new_expire_time > lease.expiration_time:
1778+                    # yes
1779+                    lease.expiration_time = new_expire_time
1780+                    f = open(self.fname, 'rb+')
1781+                    self._write_lease_record(f, i, lease)
1782+                    f.close()
1783+                return
1784+        raise IndexError("unable to renew non-existent lease")
1785+
1786+    def add_or_renew_lease(self, lease_info):
1787+        try:
1788+            self.renew_lease(lease_info.renew_secret,
1789+                             lease_info.expiration_time)
1790+        except IndexError:
1791+            self.add_lease(lease_info)
1792+
1793+
1794+class MutableNullShare:
1795+    implements(IStoredMutableShare)
1796+    sharetype = "mutable"
1797+
1798+    """ XXX: TODO """
1799addfile ./src/allmydata/storage/bucket.py
1800hunk ./src/allmydata/storage/bucket.py 1
1801+
1802+import time
1803+
1804+from foolscap.api import Referenceable
1805+
1806+from zope.interface import implements
1807+from allmydata.interfaces import RIBucketWriter, RIBucketReader
1808+from allmydata.util import base32, log
1809+from allmydata.util.assertutil import precondition
1810+
1811+
1812+class BucketWriter(Referenceable):
1813+    implements(RIBucketWriter)
1814+
1815+    def __init__(self, ss, immutableshare, max_size, lease_info, canary):
1816+        self.ss = ss
1817+        self._max_size = max_size # don't allow the client to write more than this
1818+        self._canary = canary
1819+        self._disconnect_marker = canary.notifyOnDisconnect(self._disconnected)
1820+        self.closed = False
1821+        self.throw_out_all_data = False
1822+        self._share = immutableshare
1823+        # also, add our lease to the file now, so that other ones can be
1824+        # added by simultaneous uploaders
1825+        self._share.add_lease(lease_info)
1826+
1827+    def allocated_size(self):
1828+        return self._max_size
1829+
1830+    def remote_write(self, offset, data):
1831+        start = time.time()
1832+        precondition(not self.closed)
1833+        if self.throw_out_all_data:
1834+            return
1835+        self._share.write_share_data(offset, data)
1836+        self.ss.add_latency("write", time.time() - start)
1837+        self.ss.count("write")
1838+
1839+    def remote_close(self):
1840+        precondition(not self.closed)
1841+        start = time.time()
1842+
1843+        self._share.close()
1844+        filelen = self._share.stat()
1845+        self._share = None
1846+
1847+        self.closed = True
1848+        self._canary.dontNotifyOnDisconnect(self._disconnect_marker)
1849+
1850+        self.ss.bucket_writer_closed(self, filelen)
1851+        self.ss.add_latency("close", time.time() - start)
1852+        self.ss.count("close")
1853+
1854+    def _disconnected(self):
1855+        if not self.closed:
1856+            self._abort()
1857+
1858+    def remote_abort(self):
1859+        log.msg("storage: aborting write to share %r" % self._share,
1860+                facility="tahoe.storage", level=log.UNUSUAL)
1861+        if not self.closed:
1862+            self._canary.dontNotifyOnDisconnect(self._disconnect_marker)
1863+        self._abort()
1864+        self.ss.count("abort")
1865+
1866+    def _abort(self):
1867+        if self.closed:
1868+            return
1869+        self._share.unlink()
1870+        self._share = None
1871+
1872+        # We are now considered closed for further writing. We must tell
1873+        # the storage server about this so that it stops expecting us to
1874+        # use the space it allocated for us earlier.
1875+        self.closed = True
1876+        self.ss.bucket_writer_closed(self, 0)
1877+
1878+
1879+class BucketReader(Referenceable):
1880+    implements(RIBucketReader)
1881+
1882+    def __init__(self, ss, share):
1883+        self.ss = ss
1884+        self._share = share
1885+        self.storageindex = share.storageindex
1886+        self.shnum = share.shnum
1887+
1888+    def __repr__(self):
1889+        return "<%s %s %s>" % (self.__class__.__name__,
1890+                               base32.b2a_l(self.storageindex[:8], 60),
1891+                               self.shnum)
1892+
1893+    def remote_read(self, offset, length):
1894+        start = time.time()
1895+        data = self._share.read_share_data(offset, length)
1896+        self.ss.add_latency("read", time.time() - start)
1897+        self.ss.count("read")
1898+        return data
1899+
1900+    def remote_advise_corrupt_share(self, reason):
1901+        return self.ss.remote_advise_corrupt_share("immutable",
1902+                                                   self.storageindex,
1903+                                                   self.shnum,
1904+                                                   reason)
1905addfile ./src/allmydata/test/test_backends.py
1906hunk ./src/allmydata/test/test_backends.py 1
1907+import os, stat
1908+from twisted.trial import unittest
1909+from allmydata.util.log import msg
1910+from allmydata.test.common_util import ReallyEqualMixin
1911+import mock
1912+
1913+# This is the code that we're going to be testing.
1914+from allmydata.storage.server import StorageServer
1915+from allmydata.storage.backends.disk.disk_backend import DiskBackend, si_si2dir
1916+from allmydata.storage.backends.null.null_backend import NullBackend
1917+
1918+# The following share file content was generated with
1919+# storage.immutable.ShareFile from Tahoe-LAFS v1.8.2
1920+# with share data == 'a'. The total size of this input
1921+# is 85 bytes.
1922+shareversionnumber = '\x00\x00\x00\x01'
1923+sharedatalength = '\x00\x00\x00\x01'
1924+numberofleases = '\x00\x00\x00\x01'
1925+shareinputdata = 'a'
1926+ownernumber = '\x00\x00\x00\x00'
1927+renewsecret  = 'x'*32
1928+cancelsecret = 'y'*32
1929+expirationtime = '\x00(\xde\x80'
1930+nextlease = ''
1931+containerdata = shareversionnumber + sharedatalength + numberofleases
1932+client_data = shareinputdata + ownernumber + renewsecret + \
1933+    cancelsecret + expirationtime + nextlease
1934+share_data = containerdata + client_data
1935+testnodeid = 'testnodeidxxxxxxxxxx'
1936+
1937+
1938+class MockFileSystem(unittest.TestCase):
1939+    """ I simulate a filesystem that the code under test can use. I simulate
1940+    just the parts of the filesystem that the current implementation of Disk
1941+    backend needs. """
1942+    def setUp(self):
1943+        # Make patcher, patch, and effects for disk-using functions.
1944+        msg( "%s.setUp()" % (self,))
1945+        self.mockedfilepaths = {}
1946+        # keys are pathnames, values are MockFilePath objects. This is necessary because
1947+        # MockFilePath behavior sometimes depends on the filesystem. Where it does,
1948+        # self.mockedfilepaths has the relevant information.
1949+        self.storedir = MockFilePath('teststoredir', self.mockedfilepaths)
1950+        self.basedir = self.storedir.child('shares')
1951+        self.baseincdir = self.basedir.child('incoming')
1952+        self.sharedirfinalname = self.basedir.child('or').child('orsxg5dtorxxeylhmvpws3temv4a')
1953+        self.sharedirincomingname = self.baseincdir.child('or').child('orsxg5dtorxxeylhmvpws3temv4a')
1954+        self.shareincomingname = self.sharedirincomingname.child('0')
1955+        self.sharefinalname = self.sharedirfinalname.child('0')
1956+
1957+        # FIXME: these patches won't work; disk_backend no longer imports FilePath, BucketCountingCrawler,
1958+        # or LeaseCheckingCrawler.
1959+
1960+        self.FilePathFake = mock.patch('allmydata.storage.backends.disk.disk_backend.FilePath', new = MockFilePath)
1961+        self.FilePathFake.__enter__()
1962+
1963+        self.BCountingCrawler = mock.patch('allmydata.storage.backends.disk.disk_backend.BucketCountingCrawler')
1964+        FakeBCC = self.BCountingCrawler.__enter__()
1965+        FakeBCC.side_effect = self.call_FakeBCC
1966+
1967+        self.LeaseCheckingCrawler = mock.patch('allmydata.storage.backends.disk.disk_backend.LeaseCheckingCrawler')
1968+        FakeLCC = self.LeaseCheckingCrawler.__enter__()
1969+        FakeLCC.side_effect = self.call_FakeLCC
1970+
1971+        self.get_available_space = mock.patch('allmydata.util.fileutil.get_available_space')
1972+        GetSpace = self.get_available_space.__enter__()
1973+        GetSpace.side_effect = self.call_get_available_space
1974+
1975+        self.statforsize = mock.patch('allmydata.storage.backends.disk.core.filepath.stat')
1976+        getsize = self.statforsize.__enter__()
1977+        getsize.side_effect = self.call_statforsize
1978+
1979+    def call_FakeBCC(self, StateFile):
1980+        return MockBCC()
1981+
1982+    def call_FakeLCC(self, StateFile, HistoryFile, ExpirationPolicy):
1983+        return MockLCC()
1984+
1985+    def call_get_available_space(self, storedir, reservedspace):
1986+        # The input vector has an input size of 85.
1987+        return 85 - reservedspace
1988+
1989+    def call_statforsize(self, fakefpname):
1990+        return self.mockedfilepaths[fakefpname].fileobject.size()
1991+
1992+    def tearDown(self):
1993+        msg( "%s.tearDown()" % (self,))
1994+        self.FilePathFake.__exit__()
1995+        self.mockedfilepaths = {}
1996+
1997+
1998+class MockFilePath:
1999+    def __init__(self, pathstring, ffpathsenvironment, existence=False):
2000+        #  I can't just make the values MockFileObjects because they may be directories.
2001+        self.mockedfilepaths = ffpathsenvironment
2002+        self.path = pathstring
2003+        self.existence = existence
2004+        if not self.mockedfilepaths.has_key(self.path):
2005+            #  The first MockFilePath object is special
2006+            self.mockedfilepaths[self.path] = self
2007+            self.fileobject = None
2008+        else:
2009+            self.fileobject = self.mockedfilepaths[self.path].fileobject
2010+        self.spawn = {}
2011+        self.antecedent = os.path.dirname(self.path)
2012+
2013+    def setContent(self, contentstring):
2014+        # This method rewrites the data in the file that corresponds to its path
2015+        # name whether it preexisted or not.
2016+        self.fileobject = MockFileObject(contentstring)
2017+        self.existence = True
2018+        self.mockedfilepaths[self.path].fileobject = self.fileobject
2019+        self.mockedfilepaths[self.path].existence = self.existence
2020+        self.setparents()
2021+
2022+    def create(self):
2023+        # This method chokes if there's a pre-existing file!
2024+        if self.mockedfilepaths[self.path].fileobject:
2025+            raise OSError
2026+        else:
2027+            self.existence = True
2028+            self.mockedfilepaths[self.path].fileobject = self.fileobject
2029+            self.mockedfilepaths[self.path].existence = self.existence
2030+            self.setparents()
2031+
2032+    def open(self, mode='r'):
2033+        # XXX Makes no use of mode.
2034+        if not self.mockedfilepaths[self.path].fileobject:
2035+            # If there's no fileobject there already then make one and put it there.
2036+            self.fileobject = MockFileObject()
2037+            self.existence = True
2038+            self.mockedfilepaths[self.path].fileobject = self.fileobject
2039+            self.mockedfilepaths[self.path].existence = self.existence
2040+        else:
2041+            # Otherwise get a ref to it.
2042+            self.fileobject = self.mockedfilepaths[self.path].fileobject
2043+            self.existence = self.mockedfilepaths[self.path].existence
2044+        return self.fileobject.open(mode)
2045+
2046+    def child(self, childstring):
2047+        arg2child = os.path.join(self.path, childstring)
2048+        child = MockFilePath(arg2child, self.mockedfilepaths)
2049+        return child
2050+
2051+    def children(self):
2052+        childrenfromffs = [ffp for ffp in self.mockedfilepaths.values() if ffp.path.startswith(self.path)]
2053+        childrenfromffs = [ffp for ffp in childrenfromffs if not ffp.path.endswith(self.path)]
2054+        childrenfromffs = [ffp for ffp in childrenfromffs if ffp.exists()]
2055+        self.spawn = frozenset(childrenfromffs)
2056+        return self.spawn
2057+
2058+    def parent(self):
2059+        if self.mockedfilepaths.has_key(self.antecedent):
2060+            parent = self.mockedfilepaths[self.antecedent]
2061+        else:
2062+            parent = MockFilePath(self.antecedent, self.mockedfilepaths)
2063+        return parent
2064+
2065+    def parents(self):
2066+        antecedents = []
2067+        def f(fps, antecedents):
2068+            newfps = os.path.split(fps)[0]
2069+            if newfps:
2070+                antecedents.append(newfps)
2071+                f(newfps, antecedents)
2072+        f(self.path, antecedents)
2073+        return antecedents
2074+
2075+    def setparents(self):
2076+        for fps in self.parents():
2077+            if not self.mockedfilepaths.has_key(fps):
2078+                self.mockedfilepaths[fps] = MockFilePath(fps, self.mockedfilepaths, exists=True)
2079+
2080+    def basename(self):
2081+        return os.path.split(self.path)[1]
2082+
2083+    def moveTo(self, newffp):
2084+        #  XXX Makes no distinction between file and directory arguments, this is deviation from filepath.moveTo
2085+        if self.mockedfilepaths[newffp.path].exists():
2086+            raise OSError
2087+        else:
2088+            self.mockedfilepaths[newffp.path] = self
2089+            self.path = newffp.path
2090+
2091+    def getsize(self):
2092+        return self.fileobject.getsize()
2093+
2094+    def exists(self):
2095+        return self.existence
2096+
2097+    def isdir(self):
2098+        return True
2099+
2100+    def makedirs(self):
2101+        # XXX These methods assume that fp_<FOO> functions in fileutil will be tested elsewhere!
2102+        pass
2103+
2104+    def remove(self):
2105+        pass
2106+
2107+
2108+class MockFileObject:
2109+    def __init__(self, contentstring=''):
2110+        self.buffer = contentstring
2111+        self.pos = 0
2112+    def open(self, mode='r'):
2113+        return self
2114+    def write(self, instring):
2115+        begin = self.pos
2116+        padlen = begin - len(self.buffer)
2117+        if padlen > 0:
2118+            self.buffer += '\x00' * padlen
2119+        end = self.pos + len(instring)
2120+        self.buffer = self.buffer[:begin]+instring+self.buffer[end:]
2121+        self.pos = end
2122+    def close(self):
2123+        self.pos = 0
2124+    def seek(self, pos):
2125+        self.pos = pos
2126+    def read(self, numberbytes):
2127+        return self.buffer[self.pos:self.pos+numberbytes]
2128+    def tell(self):
2129+        return self.pos
2130+    def size(self):
2131+        # XXX This method A: Is not to be found in a real file B: Is part of a wild-mung-up of filepath.stat!
2132+        # XXX Finally we shall hopefully use a getsize method soon, must consult first though.
2133+        # Hmmm...  perhaps we need to sometimes stat the address when there's not a mockfileobject present?
2134+        return {stat.ST_SIZE:len(self.buffer)}
2135+    def getsize(self):
2136+        return len(self.buffer)
2137+
2138+class MockBCC:
2139+    def setServiceParent(self, Parent):
2140+        pass
2141+
2142+
2143+class MockLCC:
2144+    def setServiceParent(self, Parent):
2145+        pass
2146+
2147+
2148+class TestServerWithNullBackend(unittest.TestCase, ReallyEqualMixin):
2149+    """ NullBackend is just for testing and executable documentation, so
2150+    this test is actually a test of StorageServer in which we're using
2151+    NullBackend as helper code for the test, rather than a test of
2152+    NullBackend. """
2153+    def setUp(self):
2154+        self.ss = StorageServer(testnodeid, NullBackend())
2155+
2156+    @mock.patch('os.mkdir')
2157+    @mock.patch('__builtin__.open')
2158+    @mock.patch('os.listdir')
2159+    @mock.patch('os.path.isdir')
2160+    def test_write_share(self, mockisdir, mocklistdir, mockopen, mockmkdir):
2161+        """
2162+        Write a new share. This tests that StorageServer's remote_allocate_buckets
2163+        generates the correct return types when given test-vector arguments. That
2164+        bs is of the correct type is verified by attempting to invoke remote_write
2165+        on bs[0].
2166+        """
2167+        alreadygot, bs = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
2168+        bs[0].remote_write(0, 'a')
2169+        self.failIf(mockisdir.called)
2170+        self.failIf(mocklistdir.called)
2171+        self.failIf(mockopen.called)
2172+        self.failIf(mockmkdir.called)
2173+
2174+
2175+class TestServerConstruction(MockFileSystem, ReallyEqualMixin):
2176+    def test_create_server_disk_backend(self):
2177+        """ This tests whether a server instance can be constructed with a
2178+        filesystem backend. To pass the test, it mustn't use the filesystem
2179+        outside of its configured storedir. """
2180+        StorageServer(testnodeid, DiskBackend(self.storedir))
2181+
2182+
2183+class TestServerAndDiskBackend(MockFileSystem, ReallyEqualMixin):
2184+    """ This tests both the StorageServer and the Disk backend together. """
2185+    def setUp(self):
2186+        MockFileSystem.setUp(self)
2187+        try:
2188+            self.backend = DiskBackend(self.storedir)
2189+            self.ss = StorageServer(testnodeid, self.backend)
2190+
2191+            self.backendwithreserve = DiskBackend(self.storedir, reserved_space = 1)
2192+            self.sswithreserve = StorageServer(testnodeid, self.backendwithreserve)
2193+        except:
2194+            MockFileSystem.tearDown(self)
2195+            raise
2196+
2197+    @mock.patch('time.time')
2198+    @mock.patch('allmydata.util.fileutil.get_available_space')
2199+    def test_out_of_space(self, mockget_available_space, mocktime):
2200+        mocktime.return_value = 0
2201+
2202+        def call_get_available_space(dir, reserve):
2203+            return 0
2204+
2205+        mockget_available_space.side_effect = call_get_available_space
2206+        alreadygotc, bsc = self.sswithreserve.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
2207+        self.failUnlessReallyEqual(bsc, {})
2208+
2209+    @mock.patch('time.time')
2210+    def test_write_and_read_share(self, mocktime):
2211+        """
2212+        Write a new share, read it, and test the server's (and disk backend's)
2213+        handling of simultaneous and successive attempts to write the same
2214+        share.
2215+        """
2216+        mocktime.return_value = 0
2217+        # Inspect incoming and fail unless it's empty.
2218+        incomingset = self.ss.backend.get_incoming_shnums('teststorage_index')
2219+
2220+        self.failUnlessReallyEqual(incomingset, frozenset())
2221+
2222+        # Populate incoming with the sharenum: 0.
2223+        alreadygot, bs = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, frozenset((0,)), 1, mock.Mock())
2224+
2225+        # This is a transparent-box test: Inspect incoming and fail unless the sharenum: 0 is listed there.
2226+        self.failUnlessReallyEqual(self.ss.backend.get_incoming_shnums('teststorage_index'), frozenset((0,)))
2227+
2228+
2229+
2230+        # Attempt to create a second share writer with the same sharenum.
2231+        alreadygota, bsa = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, frozenset((0,)), 1, mock.Mock())
2232+
2233+        # Show that no sharewriter results from a remote_allocate_buckets
2234+        # with the same si and sharenum, until BucketWriter.remote_close()
2235+        # has been called.
2236+        self.failIf(bsa)
2237+
2238+        # Test allocated size.
2239+        spaceint = self.ss.allocated_size()
2240+        self.failUnlessReallyEqual(spaceint, 1)
2241+
2242+        # Write 'a' to shnum 0. Only tested together with close and read.
2243+        bs[0].remote_write(0, 'a')
2244+
2245+        # Preclose: Inspect final, failUnless nothing there.
2246+        self.failUnlessReallyEqual(len(list(self.backend.get_shares('teststorage_index'))), 0)
2247+        bs[0].remote_close()
2248+
2249+        # Postclose: (Omnibus) failUnless written data is in final.
2250+        sharesinfinal = list(self.backend.get_shares('teststorage_index'))
2251+        self.failUnlessReallyEqual(len(sharesinfinal), 1)
2252+        contents = sharesinfinal[0].read_share_data(0, 73)
2253+        self.failUnlessReallyEqual(contents, client_data)
2254+
2255+        # Exercise the case that the share we're asking to allocate is
2256+        # already (completely) uploaded.
2257+        self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
2258+
2259+
2260+    def test_read_old_share(self):
2261+        """ This tests whether the code correctly finds and reads
2262+        shares written out by old (Tahoe-LAFS <= v1.8.2)
2263+        servers. There is a similar test in test_download, but that one
2264+        is from the perspective of the client and exercises a deeper
2265+        stack of code. This one is for exercising just the
2266+        StorageServer object. """
2267+        # Contruct a file with the appropriate contents in the mockfilesystem.
2268+        datalen = len(share_data)
2269+        finalhome = si_si2dir(self.basedir, 'teststorage_index').child(str(0))
2270+        finalhome.setContent(share_data)
2271+
2272+        # Now begin the test.
2273+        bs = self.ss.remote_get_buckets('teststorage_index')
2274+
2275+        self.failUnlessEqual(len(bs), 1)
2276+        b = bs['0']
2277+        # These should match by definition, the next two cases cover cases without (completely) unambiguous behaviors.
2278+        self.failUnlessReallyEqual(b.remote_read(0, datalen), client_data)
2279+        # If you try to read past the end you get the as much data as is there.
2280+        self.failUnlessReallyEqual(b.remote_read(0, datalen+20), client_data)
2281+        # If you start reading past the end of the file you get the empty string.
2282+        self.failUnlessReallyEqual(b.remote_read(datalen+1, 3), '')
2283}
2284[Pluggable backends -- all other changes. refs #999
2285david-sarah@jacaranda.org**20110919233256
2286 Ignore-this: 1a77b6b5d178b32a9b914b699ba7e957
2287] {
2288hunk ./src/allmydata/client.py 245
2289             sharetypes.append("immutable")
2290         if self.get_config("storage", "expire.mutable", True, boolean=True):
2291             sharetypes.append("mutable")
2292-        expiration_sharetypes = tuple(sharetypes)
2293 
2294hunk ./src/allmydata/client.py 246
2295+        expiration_policy = {
2296+            'enabled': expire,
2297+            'mode': mode,
2298+            'override_lease_duration': o_l_d,
2299+            'cutoff_date': cutoff_date,
2300+            'sharetypes': tuple(sharetypes),
2301+        }
2302         ss = StorageServer(storedir, self.nodeid,
2303                            reserved_space=reserved,
2304                            discard_storage=discard,
2305hunk ./src/allmydata/client.py 258
2306                            readonly_storage=readonly,
2307                            stats_provider=self.stats_provider,
2308-                           expiration_enabled=expire,
2309-                           expiration_mode=mode,
2310-                           expiration_override_lease_duration=o_l_d,
2311-                           expiration_cutoff_date=cutoff_date,
2312-                           expiration_sharetypes=expiration_sharetypes)
2313+                           expiration_policy=expiration_policy)
2314         self.add_service(ss)
2315 
2316         d = self.when_tub_ready()
2317hunk ./src/allmydata/immutable/offloaded.py 306
2318         if os.path.exists(self._encoding_file):
2319             self.log("ciphertext already present, bypassing fetch",
2320                      level=log.UNUSUAL)
2321+            # XXX the following comment is probably stale, since
2322+            # LocalCiphertextReader.get_plaintext_hashtree_leaves does not exist.
2323+            #
2324             # we'll still need the plaintext hashes (when
2325             # LocalCiphertextReader.get_plaintext_hashtree_leaves() is
2326             # called), and currently the easiest way to get them is to ask
2327hunk ./src/allmydata/immutable/upload.py 765
2328             self._status.set_progress(1, progress)
2329         return cryptdata
2330 
2331-
2332     def get_plaintext_hashtree_leaves(self, first, last, num_segments):
2333hunk ./src/allmydata/immutable/upload.py 766
2334+        """OBSOLETE; Get the leaf nodes of a merkle hash tree over the
2335+        plaintext segments, i.e. get the tagged hashes of the given segments.
2336+        The segment size is expected to be generated by the
2337+        IEncryptedUploadable before any plaintext is read or ciphertext
2338+        produced, so that the segment hashes can be generated with only a
2339+        single pass.
2340+
2341+        This returns a Deferred that fires with a sequence of hashes, using:
2342+
2343+         tuple(segment_hashes[first:last])
2344+
2345+        'num_segments' is used to assert that the number of segments that the
2346+        IEncryptedUploadable handled matches the number of segments that the
2347+        encoder was expecting.
2348+
2349+        This method must not be called until the final byte has been read
2350+        from read_encrypted(). Once this method is called, read_encrypted()
2351+        can never be called again.
2352+        """
2353         # this is currently unused, but will live again when we fix #453
2354         if len(self._plaintext_segment_hashes) < num_segments:
2355             # close out the last one
2356hunk ./src/allmydata/immutable/upload.py 803
2357         return defer.succeed(tuple(self._plaintext_segment_hashes[first:last]))
2358 
2359     def get_plaintext_hash(self):
2360+        """OBSOLETE; Get the hash of the whole plaintext.
2361+
2362+        This returns a Deferred that fires with a tagged SHA-256 hash of the
2363+        whole plaintext, obtained from hashutil.plaintext_hash(data).
2364+        """
2365+        # this is currently unused, but will live again when we fix #453
2366         h = self._plaintext_hasher.digest()
2367         return defer.succeed(h)
2368 
2369hunk ./src/allmydata/interfaces.py 29
2370 Number = IntegerConstraint(8) # 2**(8*8) == 16EiB ~= 18e18 ~= 18 exabytes
2371 Offset = Number
2372 ReadSize = int # the 'int' constraint is 2**31 == 2Gib -- large files are processed in not-so-large increments
2373-WriteEnablerSecret = Hash # used to protect mutable bucket modifications
2374-LeaseRenewSecret = Hash # used to protect bucket lease renewal requests
2375-LeaseCancelSecret = Hash # used to protect bucket lease cancellation requests
2376+WriteEnablerSecret = Hash # used to protect mutable share modifications
2377+LeaseRenewSecret = Hash # used to protect lease renewal requests
2378+LeaseCancelSecret = Hash # used to protect lease cancellation requests
2379 
2380 class RIStubClient(RemoteInterface):
2381     """Each client publishes a service announcement for a dummy object called
2382hunk ./src/allmydata/interfaces.py 106
2383                          sharenums=SetOf(int, maxLength=MAX_BUCKETS),
2384                          allocated_size=Offset, canary=Referenceable):
2385         """
2386-        @param storage_index: the index of the bucket to be created or
2387+        @param storage_index: the index of the shareset to be created or
2388                               increfed.
2389         @param sharenums: these are the share numbers (probably between 0 and
2390                           99) that the sender is proposing to store on this
2391hunk ./src/allmydata/interfaces.py 111
2392                           server.
2393-        @param renew_secret: This is the secret used to protect bucket refresh
2394+        @param renew_secret: This is the secret used to protect lease renewal.
2395                              This secret is generated by the client and
2396                              stored for later comparison by the server. Each
2397                              server is given a different secret.
2398hunk ./src/allmydata/interfaces.py 115
2399-        @param cancel_secret: Like renew_secret, but protects bucket decref.
2400-        @param canary: If the canary is lost before close(), the bucket is
2401+        @param cancel_secret: ignored
2402+        @param canary: If the canary is lost before close(), the allocation is
2403                        deleted.
2404         @return: tuple of (alreadygot, allocated), where alreadygot is what we
2405                  already have and allocated is what we hereby agree to accept.
2406hunk ./src/allmydata/interfaces.py 129
2407                   renew_secret=LeaseRenewSecret,
2408                   cancel_secret=LeaseCancelSecret):
2409         """
2410-        Add a new lease on the given bucket. If the renew_secret matches an
2411+        Add a new lease on the given shareset. If the renew_secret matches an
2412         existing lease, that lease will be renewed instead. If there is no
2413hunk ./src/allmydata/interfaces.py 131
2414-        bucket for the given storage_index, return silently. (note that in
2415+        shareset for the given storage_index, return silently. (Note that in
2416         tahoe-1.3.0 and earlier, IndexError was raised if there was no
2417hunk ./src/allmydata/interfaces.py 133
2418-        bucket)
2419+        shareset.)
2420         """
2421         return Any() # returns None now, but future versions might change
2422 
2423hunk ./src/allmydata/interfaces.py 139
2424     def renew_lease(storage_index=StorageIndex, renew_secret=LeaseRenewSecret):
2425         """
2426-        Renew the lease on a given bucket, resetting the timer to 31 days.
2427-        Some networks will use this, some will not. If there is no bucket for
2428+        Renew the lease on a given shareset, resetting the timer to 31 days.
2429+        Some networks will use this, some will not. If there is no shareset for
2430         the given storage_index, IndexError will be raised.
2431 
2432         For mutable shares, if the given renew_secret does not match an
2433hunk ./src/allmydata/interfaces.py 146
2434         existing lease, IndexError will be raised with a note listing the
2435         server-nodeids on the existing leases, so leases on migrated shares
2436-        can be renewed or cancelled. For immutable shares, IndexError
2437-        (without the note) will be raised.
2438+        can be renewed. For immutable shares, IndexError (without the note)
2439+        will be raised.
2440         """
2441         return Any()
2442 
2443hunk ./src/allmydata/interfaces.py 154
2444     def get_buckets(storage_index=StorageIndex):
2445         return DictOf(int, RIBucketReader, maxKeys=MAX_BUCKETS)
2446 
2447-
2448-
2449     def slot_readv(storage_index=StorageIndex,
2450                    shares=ListOf(int), readv=ReadVector):
2451         """Read a vector from the numbered shares associated with the given
2452hunk ./src/allmydata/interfaces.py 163
2453 
2454     def slot_testv_and_readv_and_writev(storage_index=StorageIndex,
2455                                         secrets=TupleOf(WriteEnablerSecret,
2456-                                                        LeaseRenewSecret,
2457-                                                        LeaseCancelSecret),
2458+                                                        LeaseRenewSecret),
2459                                         tw_vectors=TestAndWriteVectorsForShares,
2460                                         r_vector=ReadVector,
2461                                         ):
2462hunk ./src/allmydata/interfaces.py 167
2463-        """General-purpose test-and-set operation for mutable slots. Perform
2464-        a bunch of comparisons against the existing shares. If they all pass,
2465-        then apply a bunch of write vectors to those shares. Then use the
2466-        read vectors to extract data from all the shares and return the data.
2467+        """
2468+        General-purpose atomic test-read-and-set operation for mutable slots.
2469+        Perform a bunch of comparisons against the existing shares. If they
2470+        all pass: use the read vectors to extract data from all the shares,
2471+        then apply a bunch of write vectors to those shares. Return the read
2472+        data, which does not include any modifications made by the writes.
2473 
2474         This method is, um, large. The goal is to allow clients to update all
2475         the shares associated with a mutable file in a single round trip.
2476hunk ./src/allmydata/interfaces.py 177
2477 
2478-        @param storage_index: the index of the bucket to be created or
2479+        @param storage_index: the index of the shareset to be created or
2480                               increfed.
2481         @param write_enabler: a secret that is stored along with the slot.
2482                               Writes are accepted from any caller who can
2483hunk ./src/allmydata/interfaces.py 183
2484                               present the matching secret. A different secret
2485                               should be used for each slot*server pair.
2486-        @param renew_secret: This is the secret used to protect bucket refresh
2487+        @param renew_secret: This is the secret used to protect lease renewal.
2488                              This secret is generated by the client and
2489                              stored for later comparison by the server. Each
2490                              server is given a different secret.
2491hunk ./src/allmydata/interfaces.py 187
2492-        @param cancel_secret: Like renew_secret, but protects bucket decref.
2493+        @param cancel_secret: ignored
2494 
2495hunk ./src/allmydata/interfaces.py 189
2496-        The 'secrets' argument is a tuple of (write_enabler, renew_secret,
2497-        cancel_secret). The first is required to perform any write. The
2498-        latter two are used when allocating new shares. To simply acquire a
2499-        new lease on existing shares, use an empty testv and an empty writev.
2500+        The 'secrets' argument is a tuple with (write_enabler, renew_secret).
2501+        The write_enabler is required to perform any write. The renew_secret
2502+        is used when allocating new shares.
2503 
2504         Each share can have a separate test vector (i.e. a list of
2505         comparisons to perform). If all vectors for all shares pass, then all
2506hunk ./src/allmydata/interfaces.py 280
2507         store that on disk.
2508         """
2509 
2510-class IStorageBucketWriter(Interface):
2511+
2512+class IStorageBackend(Interface):
2513     """
2514hunk ./src/allmydata/interfaces.py 283
2515-    Objects of this kind live on the client side.
2516+    Objects of this kind live on the server side and are used by the
2517+    storage server object.
2518     """
2519hunk ./src/allmydata/interfaces.py 286
2520-    def put_block(segmentnum=int, data=ShareData):
2521-        """@param data: For most segments, this data will be 'blocksize'
2522-        bytes in length. The last segment might be shorter.
2523-        @return: a Deferred that fires (with None) when the operation completes
2524+    def get_available_space():
2525+        """
2526+        Returns available space for share storage in bytes, or
2527+        None if this information is not available or if the available
2528+        space is unlimited.
2529+
2530+        If the backend is configured for read-only mode then this will
2531+        return 0.
2532+        """
2533+
2534+    def get_sharesets_for_prefix(prefix):
2535+        """
2536+        Generates IShareSet objects for all storage indices matching the
2537+        given prefix for which this backend holds shares.
2538+        """
2539+
2540+    def get_shareset(storageindex):
2541+        """
2542+        Get an IShareSet object for the given storage index.
2543+        """
2544+
2545+    def advise_corrupt_share(storageindex, sharetype, shnum, reason):
2546+        """
2547+        Clients who discover hash failures in shares that they have
2548+        downloaded from me will use this method to inform me about the
2549+        failures. I will record their concern so that my operator can
2550+        manually inspect the shares in question.
2551+
2552+        'sharetype' is either 'mutable' or 'immutable'. 'shnum' is the integer
2553+        share number. 'reason' is a human-readable explanation of the problem,
2554+        probably including some expected hash values and the computed ones
2555+        that did not match. Corruption advisories for mutable shares should
2556+        include a hash of the public key (the same value that appears in the
2557+        mutable-file verify-cap), since the current share format does not
2558+        store that on disk.
2559+
2560+        @param storageindex=str
2561+        @param sharetype=str
2562+        @param shnum=int
2563+        @param reason=str
2564+        """
2565+
2566+
2567+class IShareSet(Interface):
2568+    def get_storage_index():
2569+        """
2570+        Returns the storage index for this shareset.
2571+        """
2572+
2573+    def get_storage_index_string():
2574+        """
2575+        Returns the base32-encoded storage index for this shareset.
2576+        """
2577+
2578+    def get_overhead():
2579+        """
2580+        Returns the storage overhead, in bytes, of this shareset (exclusive
2581+        of the space used by its shares).
2582+        """
2583+
2584+    def get_shares():
2585+        """
2586+        Generates the IStoredShare objects held in this shareset.
2587+        """
2588+
2589+    def has_incoming(shnum):
2590+        """
2591+        Returns True if this shareset has an incoming (partial) share with this number, otherwise False.
2592+        """
2593+
2594+    def make_bucket_writer(storageserver, shnum, max_space_per_bucket, lease_info, canary):
2595+        """
2596+        Create a bucket writer that can be used to write data to a given share.
2597+
2598+        @param storageserver=RIStorageServer
2599+        @param shnum=int: A share number in this shareset
2600+        @param max_space_per_bucket=int: The maximum space allocated for the
2601+                 share, in bytes
2602+        @param lease_info=LeaseInfo: The initial lease information
2603+        @param canary=Referenceable: If the canary is lost before close(), the
2604+                 bucket is deleted.
2605+        @return an IStorageBucketWriter for the given share
2606+        """
2607+
2608+    def make_bucket_reader(storageserver, share):
2609+        """
2610+        Create a bucket reader that can be used to read data from a given share.
2611+
2612+        @param storageserver=RIStorageServer
2613+        @param share=IStoredShare
2614+        @return an IStorageBucketReader for the given share
2615+        """
2616+
2617+    def readv(wanted_shnums, read_vector):
2618+        """
2619+        Read a vector from the numbered shares in this shareset. An empty
2620+        wanted_shnums list means to return data from all known shares.
2621+
2622+        @param wanted_shnums=ListOf(int)
2623+        @param read_vector=ReadVector
2624+        @return DictOf(int, ReadData): shnum -> results, with one key per share
2625+        """
2626+
2627+    def testv_and_readv_and_writev(storageserver, secrets, test_and_write_vectors, read_vector, expiration_time):
2628+        """
2629+        General-purpose atomic test-read-and-set operation for mutable slots.
2630+        Perform a bunch of comparisons against the existing shares in this
2631+        shareset. If they all pass: use the read vectors to extract data from
2632+        all the shares, then apply a bunch of write vectors to those shares.
2633+        Return the read data, which does not include any modifications made by
2634+        the writes.
2635+
2636+        See the similar method in RIStorageServer for more detail.
2637+
2638+        @param storageserver=RIStorageServer
2639+        @param secrets=TupleOf(WriteEnablerSecret, LeaseRenewSecret[, ...])
2640+        @param test_and_write_vectors=TestAndWriteVectorsForShares
2641+        @param read_vector=ReadVector
2642+        @param expiration_time=int
2643+        @return TupleOf(bool, DictOf(int, ReadData))
2644+        """
2645+
2646+    def add_or_renew_lease(lease_info):
2647+        """
2648+        Add a new lease on the shares in this shareset. If the renew_secret
2649+        matches an existing lease, that lease will be renewed instead. If
2650+        there are no shares in this shareset, return silently.
2651+
2652+        @param lease_info=LeaseInfo
2653+        """
2654+
2655+    def renew_lease(renew_secret, new_expiration_time):
2656+        """
2657+        Renew a lease on the shares in this shareset, resetting the timer
2658+        to 31 days. Some grids will use this, some will not. If there are no
2659+        shares in this shareset, IndexError will be raised.
2660+
2661+        For mutable shares, if the given renew_secret does not match an
2662+        existing lease, IndexError will be raised with a note listing the
2663+        server-nodeids on the existing leases, so leases on migrated shares
2664+        can be renewed. For immutable shares, IndexError (without the note)
2665+        will be raised.
2666+
2667+        @param renew_secret=LeaseRenewSecret
2668+        """
2669+
2670+
2671+class IStoredShare(Interface):
2672+    """
2673+    This object contains as much as all of the share data.  It is intended
2674+    for lazy evaluation, such that in many use cases substantially less than
2675+    all of the share data will be accessed.
2676+    """
2677+    def close():
2678+        """
2679+        Complete writing to this share.
2680+        """
2681+
2682+    def get_storage_index():
2683+        """
2684+        Returns the storage index.
2685+        """
2686+
2687+    def get_shnum():
2688+        """
2689+        Returns the share number.
2690+        """
2691+
2692+    def get_data_length():
2693+        """
2694+        Returns the data length in bytes.
2695+        """
2696+
2697+    def get_size():
2698+        """
2699+        Returns the size of the share in bytes.
2700+        """
2701+
2702+    def get_used_space():
2703+        """
2704+        Returns the amount of backend storage including overhead, in bytes, used
2705+        by this share.
2706+        """
2707+
2708+    def unlink():
2709+        """
2710+        Signal that this share can be removed from the backend storage. This does
2711+        not guarantee that the share data will be immediately inaccessible, or
2712+        that it will be securely erased.
2713+        """
2714+
2715+    def readv(read_vector):
2716+        """
2717+        XXX
2718+        """
2719+
2720+
2721+class IStoredMutableShare(IStoredShare):
2722+    def check_write_enabler(write_enabler, si_s):
2723+        """
2724+        XXX
2725         """
2726 
2727hunk ./src/allmydata/interfaces.py 489
2728-    def put_plaintext_hashes(hashes=ListOf(Hash)):
2729+    def check_testv(test_vector):
2730+        """
2731+        XXX
2732+        """
2733+
2734+    def writev(datav, new_length):
2735+        """
2736+        XXX
2737+        """
2738+
2739+
2740+class IStorageBucketWriter(Interface):
2741+    """
2742+    Objects of this kind live on the client side.
2743+    """
2744+    def put_block(segmentnum, data):
2745         """
2746hunk ./src/allmydata/interfaces.py 506
2747+        @param segmentnum=int
2748+        @param data=ShareData: For most segments, this data will be 'blocksize'
2749+        bytes in length. The last segment might be shorter.
2750         @return: a Deferred that fires (with None) when the operation completes
2751         """
2752 
2753hunk ./src/allmydata/interfaces.py 512
2754-    def put_crypttext_hashes(hashes=ListOf(Hash)):
2755+    def put_crypttext_hashes(hashes):
2756         """
2757hunk ./src/allmydata/interfaces.py 514
2758+        @param hashes=ListOf(Hash)
2759         @return: a Deferred that fires (with None) when the operation completes
2760         """
2761 
2762hunk ./src/allmydata/interfaces.py 518
2763-    def put_block_hashes(blockhashes=ListOf(Hash)):
2764+    def put_block_hashes(blockhashes):
2765         """
2766hunk ./src/allmydata/interfaces.py 520
2767+        @param blockhashes=ListOf(Hash)
2768         @return: a Deferred that fires (with None) when the operation completes
2769         """
2770 
2771hunk ./src/allmydata/interfaces.py 524
2772-    def put_share_hashes(sharehashes=ListOf(TupleOf(int, Hash))):
2773+    def put_share_hashes(sharehashes):
2774         """
2775hunk ./src/allmydata/interfaces.py 526
2776+        @param sharehashes=ListOf(TupleOf(int, Hash))
2777         @return: a Deferred that fires (with None) when the operation completes
2778         """
2779 
2780hunk ./src/allmydata/interfaces.py 530
2781-    def put_uri_extension(data=URIExtensionData):
2782+    def put_uri_extension(data):
2783         """This block of data contains integrity-checking information (hashes
2784         of plaintext, crypttext, and shares), as well as encoding parameters
2785         that are necessary to recover the data. This is a serialized dict
2786hunk ./src/allmydata/interfaces.py 535
2787         mapping strings to other strings. The hash of this data is kept in
2788-        the URI and verified before any of the data is used. All buckets for
2789-        a given file contain identical copies of this data.
2790+        the URI and verified before any of the data is used. All share
2791+        containers for a given file contain identical copies of this data.
2792 
2793         The serialization format is specified with the following pseudocode:
2794         for k in sorted(dict.keys()):
2795hunk ./src/allmydata/interfaces.py 543
2796             assert re.match(r'^[a-zA-Z_\-]+$', k)
2797             write(k + ':' + netstring(dict[k]))
2798 
2799+        @param data=URIExtensionData
2800         @return: a Deferred that fires (with None) when the operation completes
2801         """
2802 
2803hunk ./src/allmydata/interfaces.py 558
2804 
2805 class IStorageBucketReader(Interface):
2806 
2807-    def get_block_data(blocknum=int, blocksize=int, size=int):
2808+    def get_block_data(blocknum, blocksize, size):
2809         """Most blocks will be the same size. The last block might be shorter
2810         than the others.
2811 
2812hunk ./src/allmydata/interfaces.py 562
2813+        @param blocknum=int
2814+        @param blocksize=int
2815+        @param size=int
2816         @return: ShareData
2817         """
2818 
2819hunk ./src/allmydata/interfaces.py 573
2820         @return: ListOf(Hash)
2821         """
2822 
2823-    def get_block_hashes(at_least_these=SetOf(int)):
2824+    def get_block_hashes(at_least_these=()):
2825         """
2826hunk ./src/allmydata/interfaces.py 575
2827+        @param at_least_these=SetOf(int)
2828         @return: ListOf(Hash)
2829         """
2830 
2831hunk ./src/allmydata/interfaces.py 579
2832-    def get_share_hashes(at_least_these=SetOf(int)):
2833+    def get_share_hashes():
2834         """
2835         @return: ListOf(TupleOf(int, Hash))
2836         """
2837hunk ./src/allmydata/interfaces.py 611
2838         @return: unicode nickname, or None
2839         """
2840 
2841-    # methods moved from IntroducerClient, need review
2842-    def get_all_connections():
2843-        """Return a frozenset of (nodeid, service_name, rref) tuples, one for
2844-        each active connection we've established to a remote service. This is
2845-        mostly useful for unit tests that need to wait until a certain number
2846-        of connections have been made."""
2847-
2848-    def get_all_connectors():
2849-        """Return a dict that maps from (nodeid, service_name) to a
2850-        RemoteServiceConnector instance for all services that we are actively
2851-        trying to connect to. Each RemoteServiceConnector has the following
2852-        public attributes::
2853-
2854-          service_name: the type of service provided, like 'storage'
2855-          announcement_time: when we first heard about this service
2856-          last_connect_time: when we last established a connection
2857-          last_loss_time: when we last lost a connection
2858-
2859-          version: the peer's version, from the most recent connection
2860-          oldest_supported: the peer's oldest supported version, same
2861-
2862-          rref: the RemoteReference, if connected, otherwise None
2863-          remote_host: the IAddress, if connected, otherwise None
2864-
2865-        This method is intended for monitoring interfaces, such as a web page
2866-        that describes connecting and connected peers.
2867-        """
2868-
2869-    def get_all_peerids():
2870-        """Return a frozenset of all peerids to whom we have a connection (to
2871-        one or more services) established. Mostly useful for unit tests."""
2872-
2873-    def get_all_connections_for(service_name):
2874-        """Return a frozenset of (nodeid, service_name, rref) tuples, one
2875-        for each active connection that provides the given SERVICE_NAME."""
2876-
2877-    def get_permuted_peers(service_name, key):
2878-        """Returns an ordered list of (peerid, rref) tuples, selecting from
2879-        the connections that provide SERVICE_NAME, using a hash-based
2880-        permutation keyed by KEY. This randomizes the service list in a
2881-        repeatable way, to distribute load over many peers.
2882-        """
2883-
2884 
2885 class IMutableSlotWriter(Interface):
2886     """
2887hunk ./src/allmydata/interfaces.py 616
2888     The interface for a writer around a mutable slot on a remote server.
2889     """
2890-    def set_checkstring(checkstring, *args):
2891+    def set_checkstring(seqnum_or_checkstring, root_hash=None, salt=None):
2892         """
2893         Set the checkstring that I will pass to the remote server when
2894         writing.
2895hunk ./src/allmydata/interfaces.py 640
2896         Add a block and salt to the share.
2897         """
2898 
2899-    def put_encprivey(encprivkey):
2900+    def put_encprivkey(encprivkey):
2901         """
2902         Add the encrypted private key to the share.
2903         """
2904hunk ./src/allmydata/interfaces.py 645
2905 
2906-    def put_blockhashes(blockhashes=list):
2907+    def put_blockhashes(blockhashes):
2908         """
2909hunk ./src/allmydata/interfaces.py 647
2910+        @param blockhashes=list
2911         Add the block hash tree to the share.
2912         """
2913 
2914hunk ./src/allmydata/interfaces.py 651
2915-    def put_sharehashes(sharehashes=dict):
2916+    def put_sharehashes(sharehashes):
2917         """
2918hunk ./src/allmydata/interfaces.py 653
2919+        @param sharehashes=dict
2920         Add the share hash chain to the share.
2921         """
2922 
2923hunk ./src/allmydata/interfaces.py 739
2924     def get_extension_params():
2925         """Return the extension parameters in the URI"""
2926 
2927-    def set_extension_params():
2928+    def set_extension_params(params):
2929         """Set the extension parameters that should be in the URI"""
2930 
2931 class IDirectoryURI(Interface):
2932hunk ./src/allmydata/interfaces.py 879
2933         writer-visible data using this writekey.
2934         """
2935 
2936-    # TODO: Can this be overwrite instead of replace?
2937-    def replace(new_contents):
2938-        """Replace the contents of the mutable file, provided that no other
2939+    def overwrite(new_contents):
2940+        """Overwrite the contents of the mutable file, provided that no other
2941         node has published (or is attempting to publish, concurrently) a
2942         newer version of the file than this one.
2943 
2944hunk ./src/allmydata/interfaces.py 1346
2945         is empty, the metadata will be an empty dictionary.
2946         """
2947 
2948-    def set_uri(name, writecap, readcap=None, metadata=None, overwrite=True):
2949+    def set_uri(name, writecap, readcap, metadata=None, overwrite=True):
2950         """I add a child (by writecap+readcap) at the specific name. I return
2951         a Deferred that fires when the operation finishes. If overwrite= is
2952         True, I will replace any existing child of the same name, otherwise
2953hunk ./src/allmydata/interfaces.py 1745
2954     Block Hash, and the encoding parameters, both of which must be included
2955     in the URI.
2956 
2957-    I do not choose shareholders, that is left to the IUploader. I must be
2958-    given a dict of RemoteReferences to storage buckets that are ready and
2959-    willing to receive data.
2960+    I do not choose shareholders, that is left to the IUploader.
2961     """
2962 
2963     def set_size(size):
2964hunk ./src/allmydata/interfaces.py 1752
2965         """Specify the number of bytes that will be encoded. This must be
2966         peformed before get_serialized_params() can be called.
2967         """
2968+
2969     def set_params(params):
2970         """Override the default encoding parameters. 'params' is a tuple of
2971         (k,d,n), where 'k' is the number of required shares, 'd' is the
2972hunk ./src/allmydata/interfaces.py 1848
2973     download, validate, decode, and decrypt data from them, writing the
2974     results to an output file.
2975 
2976-    I do not locate the shareholders, that is left to the IDownloader. I must
2977-    be given a dict of RemoteReferences to storage buckets that are ready to
2978-    send data.
2979+    I do not locate the shareholders, that is left to the IDownloader.
2980     """
2981 
2982     def setup(outfile):
2983hunk ./src/allmydata/interfaces.py 1950
2984         resuming an interrupted upload (where we need to compute the
2985         plaintext hashes, but don't need the redundant encrypted data)."""
2986 
2987-    def get_plaintext_hashtree_leaves(first, last, num_segments):
2988-        """OBSOLETE; Get the leaf nodes of a merkle hash tree over the
2989-        plaintext segments, i.e. get the tagged hashes of the given segments.
2990-        The segment size is expected to be generated by the
2991-        IEncryptedUploadable before any plaintext is read or ciphertext
2992-        produced, so that the segment hashes can be generated with only a
2993-        single pass.
2994-
2995-        This returns a Deferred that fires with a sequence of hashes, using:
2996-
2997-         tuple(segment_hashes[first:last])
2998-
2999-        'num_segments' is used to assert that the number of segments that the
3000-        IEncryptedUploadable handled matches the number of segments that the
3001-        encoder was expecting.
3002-
3003-        This method must not be called until the final byte has been read
3004-        from read_encrypted(). Once this method is called, read_encrypted()
3005-        can never be called again.
3006-        """
3007-
3008-    def get_plaintext_hash():
3009-        """OBSOLETE; Get the hash of the whole plaintext.
3010-
3011-        This returns a Deferred that fires with a tagged SHA-256 hash of the
3012-        whole plaintext, obtained from hashutil.plaintext_hash(data).
3013-        """
3014-
3015     def close():
3016         """Just like IUploadable.close()."""
3017 
3018hunk ./src/allmydata/interfaces.py 2144
3019         returns a Deferred that fires with an IUploadResults instance, from
3020         which the URI of the file can be obtained as results.uri ."""
3021 
3022-    def upload_ssk(write_capability, new_version, uploadable):
3023-        """TODO: how should this work?"""
3024-
3025 class ICheckable(Interface):
3026     def check(monitor, verify=False, add_lease=False):
3027         """Check up on my health, optionally repairing any problems.
3028hunk ./src/allmydata/interfaces.py 2505
3029 
3030 class IRepairResults(Interface):
3031     """I contain the results of a repair operation."""
3032-    def get_successful(self):
3033+    def get_successful():
3034         """Returns a boolean: True if the repair made the file healthy, False
3035         if not. Repair failure generally indicates a file that has been
3036         damaged beyond repair."""
3037hunk ./src/allmydata/interfaces.py 2577
3038     Tahoe process will typically have a single NodeMaker, but unit tests may
3039     create simplified/mocked forms for testing purposes.
3040     """
3041-    def create_from_cap(writecap, readcap=None, **kwargs):
3042+    def create_from_cap(writecap, readcap=None, deep_immutable=False, name=u"<unknown name>"):
3043         """I create an IFilesystemNode from the given writecap/readcap. I can
3044         only provide nodes for existing file/directory objects: use my other
3045         methods to create new objects. I return synchronously."""
3046hunk ./src/allmydata/monitor.py 30
3047 
3048     # the following methods are provided for the operation code
3049 
3050-    def is_cancelled(self):
3051+    def is_cancelled():
3052         """Returns True if the operation has been cancelled. If True,
3053         operation code should stop creating new work, and attempt to stop any
3054         work already in progress."""
3055hunk ./src/allmydata/monitor.py 35
3056 
3057-    def raise_if_cancelled(self):
3058+    def raise_if_cancelled():
3059         """Raise OperationCancelledError if the operation has been cancelled.
3060         Operation code that has a robust error-handling path can simply call
3061         this periodically."""
3062hunk ./src/allmydata/monitor.py 40
3063 
3064-    def set_status(self, status):
3065+    def set_status(status):
3066         """Sets the Monitor's 'status' object to an arbitrary value.
3067         Different operations will store different sorts of status information
3068         here. Operation code should use get+modify+set sequences to update
3069hunk ./src/allmydata/monitor.py 46
3070         this."""
3071 
3072-    def get_status(self):
3073+    def get_status():
3074         """Return the status object. If the operation failed, this will be a
3075         Failure instance."""
3076 
3077hunk ./src/allmydata/monitor.py 50
3078-    def finish(self, status):
3079+    def finish(status):
3080         """Call this when the operation is done, successful or not. The
3081         Monitor's lifetime is influenced by the completion of the operation
3082         it is monitoring. The Monitor's 'status' value will be set with the
3083hunk ./src/allmydata/monitor.py 63
3084 
3085     # the following methods are provided for the initiator of the operation
3086 
3087-    def is_finished(self):
3088+    def is_finished():
3089         """Return a boolean, True if the operation is done (whether
3090         successful or failed), False if it is still running."""
3091 
3092hunk ./src/allmydata/monitor.py 67
3093-    def when_done(self):
3094+    def when_done():
3095         """Return a Deferred that fires when the operation is complete. It
3096         will fire with the operation status, the same value as returned by
3097         get_status()."""
3098hunk ./src/allmydata/monitor.py 72
3099 
3100-    def cancel(self):
3101+    def cancel():
3102         """Cancel the operation as soon as possible. is_cancelled() will
3103         start returning True after this is called."""
3104 
3105hunk ./src/allmydata/mutable/filenode.py 753
3106         self._writekey = writekey
3107         self._serializer = defer.succeed(None)
3108 
3109-
3110     def get_sequence_number(self):
3111         """
3112         Get the sequence number of the mutable version that I represent.
3113hunk ./src/allmydata/mutable/filenode.py 759
3114         """
3115         return self._version[0] # verinfo[0] == the sequence number
3116 
3117+    def get_servermap(self):
3118+        return self._servermap
3119 
3120hunk ./src/allmydata/mutable/filenode.py 762
3121-    # TODO: Terminology?
3122     def get_writekey(self):
3123         """
3124         I return a writekey or None if I don't have a writekey.
3125hunk ./src/allmydata/mutable/filenode.py 768
3126         """
3127         return self._writekey
3128 
3129-
3130     def set_downloader_hints(self, hints):
3131         """
3132         I set the downloader hints.
3133hunk ./src/allmydata/mutable/filenode.py 776
3134 
3135         self._downloader_hints = hints
3136 
3137-
3138     def get_downloader_hints(self):
3139         """
3140         I return the downloader hints.
3141hunk ./src/allmydata/mutable/filenode.py 782
3142         """
3143         return self._downloader_hints
3144 
3145-
3146     def overwrite(self, new_contents):
3147         """
3148         I overwrite the contents of this mutable file version with the
3149hunk ./src/allmydata/mutable/filenode.py 791
3150 
3151         return self._do_serialized(self._overwrite, new_contents)
3152 
3153-
3154     def _overwrite(self, new_contents):
3155         assert IMutableUploadable.providedBy(new_contents)
3156         assert self._servermap.last_update_mode == MODE_WRITE
3157hunk ./src/allmydata/mutable/filenode.py 797
3158 
3159         return self._upload(new_contents)
3160 
3161-
3162     def modify(self, modifier, backoffer=None):
3163         """I use a modifier callback to apply a change to the mutable file.
3164         I implement the following pseudocode::
3165hunk ./src/allmydata/mutable/filenode.py 841
3166 
3167         return self._do_serialized(self._modify, modifier, backoffer)
3168 
3169-
3170     def _modify(self, modifier, backoffer):
3171         if backoffer is None:
3172             backoffer = BackoffAgent().delay
3173hunk ./src/allmydata/mutable/filenode.py 846
3174         return self._modify_and_retry(modifier, backoffer, True)
3175 
3176-
3177     def _modify_and_retry(self, modifier, backoffer, first_time):
3178         """
3179         I try to apply modifier to the contents of this version of the
3180hunk ./src/allmydata/mutable/filenode.py 878
3181         d.addErrback(_retry)
3182         return d
3183 
3184-
3185     def _modify_once(self, modifier, first_time):
3186         """
3187         I attempt to apply a modifier to the contents of the mutable
3188hunk ./src/allmydata/mutable/filenode.py 913
3189         d.addCallback(_apply)
3190         return d
3191 
3192-
3193     def is_readonly(self):
3194         """
3195         I return True if this MutableFileVersion provides no write
3196hunk ./src/allmydata/mutable/filenode.py 921
3197         """
3198         return self._writekey is None
3199 
3200-
3201     def is_mutable(self):
3202         """
3203         I return True, since mutable files are always mutable by
3204hunk ./src/allmydata/mutable/filenode.py 928
3205         """
3206         return True
3207 
3208-
3209     def get_storage_index(self):
3210         """
3211         I return the storage index of the reference that I encapsulate.
3212hunk ./src/allmydata/mutable/filenode.py 934
3213         """
3214         return self._storage_index
3215 
3216-
3217     def get_size(self):
3218         """
3219         I return the length, in bytes, of this readable object.
3220hunk ./src/allmydata/mutable/filenode.py 940
3221         """
3222         return self._servermap.size_of_version(self._version)
3223 
3224-
3225     def download_to_data(self, fetch_privkey=False):
3226         """
3227         I return a Deferred that fires with the contents of this
3228hunk ./src/allmydata/mutable/filenode.py 951
3229         d.addCallback(lambda mc: "".join(mc.chunks))
3230         return d
3231 
3232-
3233     def _try_to_download_data(self):
3234         """
3235         I am an unserialized cousin of download_to_data; I am called
3236hunk ./src/allmydata/mutable/filenode.py 963
3237         d.addCallback(lambda mc: "".join(mc.chunks))
3238         return d
3239 
3240-
3241     def read(self, consumer, offset=0, size=None, fetch_privkey=False):
3242         """
3243         I read a portion (possibly all) of the mutable file that I
3244hunk ./src/allmydata/mutable/filenode.py 971
3245         return self._do_serialized(self._read, consumer, offset, size,
3246                                    fetch_privkey)
3247 
3248-
3249     def _read(self, consumer, offset=0, size=None, fetch_privkey=False):
3250         """
3251         I am the serialized companion of read.
3252hunk ./src/allmydata/mutable/filenode.py 981
3253         d = r.download(consumer, offset, size)
3254         return d
3255 
3256-
3257     def _do_serialized(self, cb, *args, **kwargs):
3258         # note: to avoid deadlock, this callable is *not* allowed to invoke
3259         # other serialized methods within this (or any other)
3260hunk ./src/allmydata/mutable/filenode.py 999
3261         self._serializer.addErrback(log.err)
3262         return d
3263 
3264-
3265     def _upload(self, new_contents):
3266         #assert self._pubkey, "update_servermap must be called before publish"
3267         p = Publish(self._node, self._storage_broker, self._servermap)
3268hunk ./src/allmydata/mutable/filenode.py 1009
3269         d.addCallback(self._did_upload, new_contents.get_size())
3270         return d
3271 
3272-
3273     def _did_upload(self, res, size):
3274         self._most_recent_size = size
3275         return res
3276hunk ./src/allmydata/mutable/filenode.py 1029
3277         """
3278         return self._do_serialized(self._update, data, offset)
3279 
3280-
3281     def _update(self, data, offset):
3282         """
3283         I update the mutable file version represented by this particular
3284hunk ./src/allmydata/mutable/filenode.py 1058
3285         d.addCallback(self._build_uploadable_and_finish, data, offset)
3286         return d
3287 
3288-
3289     def _do_modify_update(self, data, offset):
3290         """
3291         I perform a file update by modifying the contents of the file
3292hunk ./src/allmydata/mutable/filenode.py 1073
3293             return new
3294         return self._modify(m, None)
3295 
3296-
3297     def _do_update_update(self, data, offset):
3298         """
3299         I start the Servermap update that gets us the data we need to
3300hunk ./src/allmydata/mutable/filenode.py 1108
3301         return self._update_servermap(update_range=(start_segment,
3302                                                     end_segment))
3303 
3304-
3305     def _decode_and_decrypt_segments(self, ignored, data, offset):
3306         """
3307         After the servermap update, I take the encrypted and encoded
3308hunk ./src/allmydata/mutable/filenode.py 1148
3309         d3 = defer.succeed(blockhashes)
3310         return deferredutil.gatherResults([d1, d2, d3])
3311 
3312-
3313     def _build_uploadable_and_finish(self, segments_and_bht, data, offset):
3314         """
3315         After the process has the plaintext segments, I build the
3316hunk ./src/allmydata/mutable/filenode.py 1163
3317         p = Publish(self._node, self._storage_broker, self._servermap)
3318         return p.update(u, offset, segments_and_bht[2], self._version)
3319 
3320-
3321     def _update_servermap(self, mode=MODE_WRITE, update_range=None):
3322         """
3323         I update the servermap. I return a Deferred that fires when the
3324hunk ./src/allmydata/storage/common.py 1
3325-
3326-import os.path
3327 from allmydata.util import base32
3328 
3329 class DataTooLargeError(Exception):
3330hunk ./src/allmydata/storage/common.py 5
3331     pass
3332+
3333 class UnknownMutableContainerVersionError(Exception):
3334     pass
3335hunk ./src/allmydata/storage/common.py 8
3336+
3337 class UnknownImmutableContainerVersionError(Exception):
3338     pass
3339 
3340hunk ./src/allmydata/storage/common.py 18
3341 
3342 def si_a2b(ascii_storageindex):
3343     return base32.a2b(ascii_storageindex)
3344-
3345-def storage_index_to_dir(storageindex):
3346-    sia = si_b2a(storageindex)
3347-    return os.path.join(sia[:2], sia)
3348hunk ./src/allmydata/storage/crawler.py 2
3349 
3350-import os, time, struct
3351+import time, struct
3352 import cPickle as pickle
3353 from twisted.internet import reactor
3354 from twisted.application import service
3355hunk ./src/allmydata/storage/crawler.py 6
3356+
3357+from allmydata.util.assertutil import precondition
3358+from allmydata.interfaces import IStorageBackend
3359 from allmydata.storage.common import si_b2a
3360hunk ./src/allmydata/storage/crawler.py 10
3361-from allmydata.util import fileutil
3362+
3363 
3364 class TimeSliceExceeded(Exception):
3365     pass
3366hunk ./src/allmydata/storage/crawler.py 15
3367 
3368+
3369 class ShareCrawler(service.MultiService):
3370hunk ./src/allmydata/storage/crawler.py 17
3371-    """A ShareCrawler subclass is attached to a StorageServer, and
3372-    periodically walks all of its shares, processing each one in some
3373-    fashion. This crawl is rate-limited, to reduce the IO burden on the host,
3374-    since large servers can easily have a terabyte of shares, in several
3375-    million files, which can take hours or days to read.
3376+    """
3377+    An instance of a subclass of ShareCrawler is attached to a storage
3378+    backend, and periodically walks the backend's shares, processing them
3379+    in some fashion. This crawl is rate-limited to reduce the I/O burden on
3380+    the host, since large servers can easily have a terabyte of shares in
3381+    several million files, which can take hours or days to read.
3382 
3383     Once the crawler starts a cycle, it will proceed at a rate limited by the
3384     allowed_cpu_percentage= and cpu_slice= parameters: yielding the reactor
3385hunk ./src/allmydata/storage/crawler.py 33
3386     long enough to ensure that 'minimum_cycle_time' elapses between the start
3387     of two consecutive cycles.
3388 
3389-    We assume that the normal upload/download/get_buckets traffic of a tahoe
3390+    We assume that the normal upload/download/DYHB traffic of a Tahoe-LAFS
3391     grid will cause the prefixdir contents to be mostly cached in the kernel,
3392hunk ./src/allmydata/storage/crawler.py 35
3393-    or that the number of buckets in each prefixdir will be small enough to
3394-    load quickly. A 1TB allmydata.com server was measured to have 2.56M
3395-    buckets, spread into the 1024 prefixdirs, with about 2500 buckets per
3396+    or that the number of sharesets in each prefixdir will be small enough to
3397+    load quickly. A 1TB allmydata.com server was measured to have 2.56 million
3398+    sharesets, spread into the 1024 prefixdirs, with about 2500 sharesets per
3399     prefix. On this server, each prefixdir took 130ms-200ms to list the first
3400     time, and 17ms to list the second time.
3401 
3402hunk ./src/allmydata/storage/crawler.py 41
3403-    To use a crawler, create a subclass which implements the process_bucket()
3404-    method. It will be called with a prefixdir and a base32 storage index
3405-    string. process_bucket() must run synchronously. Any keys added to
3406-    self.state will be preserved. Override add_initial_state() to set up
3407-    initial state keys. Override finished_cycle() to perform additional
3408-    processing when the cycle is complete. Any status that the crawler
3409-    produces should be put in the self.state dictionary. Status renderers
3410-    (like a web page which describes the accomplishments of your crawler)
3411-    will use crawler.get_state() to retrieve this dictionary; they can
3412-    present the contents as they see fit.
3413+    To implement a crawler, create a subclass that implements the
3414+    process_shareset() method. It will be called with a prefixdir and an
3415+    object providing the IShareSet interface. process_shareset() must run
3416+    synchronously. Any keys added to self.state will be preserved. Override
3417+    add_initial_state() to set up initial state keys. Override
3418+    finished_cycle() to perform additional processing when the cycle is
3419+    complete. Any status that the crawler produces should be put in the
3420+    self.state dictionary. Status renderers (like a web page describing the
3421+    accomplishments of your crawler) will use crawler.get_state() to retrieve
3422+    this dictionary; they can present the contents as they see fit.
3423 
3424hunk ./src/allmydata/storage/crawler.py 52
3425-    Then create an instance, with a reference to a StorageServer and a
3426-    filename where it can store persistent state. The statefile is used to
3427-    keep track of how far around the ring the process has travelled, as well
3428-    as timing history to allow the pace to be predicted and controlled. The
3429-    statefile will be updated and written to disk after each time slice (just
3430-    before the crawler yields to the reactor), and also after each cycle is
3431-    finished, and also when stopService() is called. Note that this means
3432-    that a crawler which is interrupted with SIGKILL while it is in the
3433-    middle of a time slice will lose progress: the next time the node is
3434-    started, the crawler will repeat some unknown amount of work.
3435+    Then create an instance, with a reference to a backend object providing
3436+    the IStorageBackend interface, and a filename where it can store
3437+    persistent state. The statefile is used to keep track of how far around
3438+    the ring the process has travelled, as well as timing history to allow
3439+    the pace to be predicted and controlled. The statefile will be updated
3440+    and written to disk after each time slice (just before the crawler yields
3441+    to the reactor), and also after each cycle is finished, and also when
3442+    stopService() is called. Note that this means that a crawler that is
3443+    interrupted with SIGKILL while it is in the middle of a time slice will
3444+    lose progress: the next time the node is started, the crawler will repeat
3445+    some unknown amount of work.
3446 
3447     The crawler instance must be started with startService() before it will
3448hunk ./src/allmydata/storage/crawler.py 65
3449-    do any work. To make it stop doing work, call stopService().
3450+    do any work. To make it stop doing work, call stopService(). A crawler
3451+    is usually a child service of a StorageServer, although it should not
3452+    depend on that.
3453+
3454+    For historical reasons, some dictionary key names use the term "bucket"
3455+    for what is now preferably called a "shareset" (the set of shares that a
3456+    server holds under a given storage index).
3457     """
3458 
3459     slow_start = 300 # don't start crawling for 5 minutes after startup
3460hunk ./src/allmydata/storage/crawler.py 80
3461     cpu_slice = 1.0 # use up to 1.0 seconds before yielding
3462     minimum_cycle_time = 300 # don't run a cycle faster than this
3463 
3464-    def __init__(self, server, statefile, allowed_cpu_percentage=None):
3465+    def __init__(self, backend, statefp, allowed_cpu_percentage=None):
3466+        precondition(IStorageBackend.providedBy(backend), backend)
3467         service.MultiService.__init__(self)
3468hunk ./src/allmydata/storage/crawler.py 83
3469+        self.backend = backend
3470+        self.statefp = statefp
3471         if allowed_cpu_percentage is not None:
3472             self.allowed_cpu_percentage = allowed_cpu_percentage
3473hunk ./src/allmydata/storage/crawler.py 87
3474-        self.server = server
3475-        self.sharedir = server.sharedir
3476-        self.statefile = statefile
3477         self.prefixes = [si_b2a(struct.pack(">H", i << (16-10)))[:2]
3478                          for i in range(2**10)]
3479         self.prefixes.sort()
3480hunk ./src/allmydata/storage/crawler.py 91
3481         self.timer = None
3482-        self.bucket_cache = (None, [])
3483+        self.shareset_cache = (None, [])
3484         self.current_sleep_time = None
3485         self.next_wake_time = None
3486         self.last_prefix_finished_time = None
3487hunk ./src/allmydata/storage/crawler.py 154
3488                 left = len(self.prefixes) - self.last_complete_prefix_index
3489                 remaining = left * self.last_prefix_elapsed_time
3490                 # TODO: remainder of this prefix: we need to estimate the
3491-                # per-bucket time, probably by measuring the time spent on
3492-                # this prefix so far, divided by the number of buckets we've
3493+                # per-shareset time, probably by measuring the time spent on
3494+                # this prefix so far, divided by the number of sharesets we've
3495                 # processed.
3496             d["estimated-cycle-complete-time-left"] = remaining
3497             # it's possible to call get_progress() from inside a crawler's
3498hunk ./src/allmydata/storage/crawler.py 175
3499         state dictionary.
3500 
3501         If we are not currently sleeping (i.e. get_state() was called from
3502-        inside the process_prefixdir, process_bucket, or finished_cycle()
3503+        inside the process_prefixdir, process_shareset, or finished_cycle()
3504         methods, or if startService has not yet been called on this crawler),
3505         these two keys will be None.
3506 
3507hunk ./src/allmydata/storage/crawler.py 188
3508     def load_state(self):
3509         # we use this to store state for both the crawler's internals and
3510         # anything the subclass-specific code needs. The state is stored
3511-        # after each bucket is processed, after each prefixdir is processed,
3512+        # after each shareset is processed, after each prefixdir is processed,
3513         # and after a cycle is complete. The internal keys we use are:
3514         #  ["version"]: int, always 1
3515         #  ["last-cycle-finished"]: int, or None if we have not yet finished
3516hunk ./src/allmydata/storage/crawler.py 202
3517         #                            are sleeping between cycles, or if we
3518         #                            have not yet finished any prefixdir since
3519         #                            a cycle was started
3520-        #  ["last-complete-bucket"]: str, base32 storage index bucket name
3521-        #                            of the last bucket to be processed, or
3522-        #                            None if we are sleeping between cycles
3523+        #  ["last-complete-bucket"]: str, base32 storage index of the last
3524+        #                            shareset to be processed, or None if we
3525+        #                            are sleeping between cycles
3526         try:
3527hunk ./src/allmydata/storage/crawler.py 206
3528-            f = open(self.statefile, "rb")
3529-            state = pickle.load(f)
3530-            f.close()
3531+            state = pickle.loads(self.statefp.getContent())
3532         except EnvironmentError:
3533             state = {"version": 1,
3534                      "last-cycle-finished": None,
3535hunk ./src/allmydata/storage/crawler.py 242
3536         else:
3537             last_complete_prefix = self.prefixes[lcpi]
3538         self.state["last-complete-prefix"] = last_complete_prefix
3539-        tmpfile = self.statefile + ".tmp"
3540-        f = open(tmpfile, "wb")
3541-        pickle.dump(self.state, f)
3542-        f.close()
3543-        fileutil.move_into_place(tmpfile, self.statefile)
3544+        self.statefp.setContent(pickle.dumps(self.state))
3545 
3546     def startService(self):
3547         # arrange things to look like we were just sleeping, so
3548hunk ./src/allmydata/storage/crawler.py 284
3549         sleep_time = (this_slice / self.allowed_cpu_percentage) - this_slice
3550         # if the math gets weird, or a timequake happens, don't sleep
3551         # forever. Note that this means that, while a cycle is running, we
3552-        # will process at least one bucket every 5 minutes, no matter how
3553-        # long that bucket takes.
3554+        # will process at least one shareset every 5 minutes, no matter how
3555+        # long that shareset takes.
3556         sleep_time = max(0.0, min(sleep_time, 299))
3557         if finished_cycle:
3558             # how long should we sleep between cycles? Don't run faster than
3559hunk ./src/allmydata/storage/crawler.py 315
3560         for i in range(self.last_complete_prefix_index+1, len(self.prefixes)):
3561             # if we want to yield earlier, just raise TimeSliceExceeded()
3562             prefix = self.prefixes[i]
3563-            prefixdir = os.path.join(self.sharedir, prefix)
3564-            if i == self.bucket_cache[0]:
3565-                buckets = self.bucket_cache[1]
3566+            if i == self.shareset_cache[0]:
3567+                sharesets = self.shareset_cache[1]
3568             else:
3569hunk ./src/allmydata/storage/crawler.py 318
3570-                try:
3571-                    buckets = os.listdir(prefixdir)
3572-                    buckets.sort()
3573-                except EnvironmentError:
3574-                    buckets = []
3575-                self.bucket_cache = (i, buckets)
3576-            self.process_prefixdir(cycle, prefix, prefixdir,
3577-                                   buckets, start_slice)
3578+                sharesets = self.backend.get_sharesets_for_prefix(prefix)
3579+                self.shareset_cache = (i, sharesets)
3580+            self.process_prefixdir(cycle, prefix, sharesets, start_slice)
3581             self.last_complete_prefix_index = i
3582 
3583             now = time.time()
3584hunk ./src/allmydata/storage/crawler.py 345
3585         self.finished_cycle(cycle)
3586         self.save_state()
3587 
3588-    def process_prefixdir(self, cycle, prefix, prefixdir, buckets, start_slice):
3589-        """This gets a list of bucket names (i.e. storage index strings,
3590+    def process_prefixdir(self, cycle, prefix, sharesets, start_slice):
3591+        """
3592+        This gets a list of shareset names (i.e. storage index strings,
3593         base32-encoded) in sorted order.
3594 
3595         You can override this if your crawler doesn't care about the actual
3596hunk ./src/allmydata/storage/crawler.py 352
3597         shares, for example a crawler which merely keeps track of how many
3598-        buckets are being managed by this server.
3599+        sharesets are being managed by this server.
3600 
3601hunk ./src/allmydata/storage/crawler.py 354
3602-        Subclasses which *do* care about actual bucket should leave this
3603-        method along, and implement process_bucket() instead.
3604+        Subclasses which *do* care about actual shareset should leave this
3605+        method alone, and implement process_shareset() instead.
3606         """
3607 
3608hunk ./src/allmydata/storage/crawler.py 358
3609-        for bucket in buckets:
3610-            if bucket <= self.state["last-complete-bucket"]:
3611+        for shareset in sharesets:
3612+            base32si = shareset.get_storage_index_string()
3613+            if base32si <= self.state["last-complete-bucket"]:
3614                 continue
3615hunk ./src/allmydata/storage/crawler.py 362
3616-            self.process_bucket(cycle, prefix, prefixdir, bucket)
3617-            self.state["last-complete-bucket"] = bucket
3618+            self.process_shareset(cycle, prefix, shareset)
3619+            self.state["last-complete-bucket"] = base32si
3620             if time.time() >= start_slice + self.cpu_slice:
3621                 raise TimeSliceExceeded()
3622 
3623hunk ./src/allmydata/storage/crawler.py 370
3624     # the remaining methods are explictly for subclasses to implement.
3625 
3626     def started_cycle(self, cycle):
3627-        """Notify a subclass that the crawler is about to start a cycle.
3628+        """
3629+        Notify a subclass that the crawler is about to start a cycle.
3630 
3631         This method is for subclasses to override. No upcall is necessary.
3632         """
3633hunk ./src/allmydata/storage/crawler.py 377
3634         pass
3635 
3636-    def process_bucket(self, cycle, prefix, prefixdir, storage_index_b32):
3637-        """Examine a single bucket. Subclasses should do whatever they want
3638+    def process_shareset(self, cycle, prefix, shareset):
3639+        """
3640+        Examine a single shareset. Subclasses should do whatever they want
3641         to do to the shares therein, then update self.state as necessary.
3642 
3643         If the crawler is never interrupted by SIGKILL, this method will be
3644hunk ./src/allmydata/storage/crawler.py 383
3645-        called exactly once per share (per cycle). If it *is* interrupted,
3646+        called exactly once per shareset (per cycle). If it *is* interrupted,
3647         then the next time the node is started, some amount of work will be
3648         duplicated, according to when self.save_state() was last called. By
3649         default, save_state() is called at the end of each timeslice, and
3650hunk ./src/allmydata/storage/crawler.py 391
3651 
3652         To reduce the chance of duplicate work (i.e. to avoid adding multiple
3653         records to a database), you can call save_state() at the end of your
3654-        process_bucket() method. This will reduce the maximum duplicated work
3655-        to one bucket per SIGKILL. It will also add overhead, probably 1-20ms
3656-        per bucket (and some disk writes), which will count against your
3657-        allowed_cpu_percentage, and which may be considerable if
3658-        process_bucket() runs quickly.
3659+        process_shareset() method. This will reduce the maximum duplicated
3660+        work to one shareset per SIGKILL. It will also add overhead, probably
3661+        1-20ms per shareset (and some disk writes), which will count against
3662+        your allowed_cpu_percentage, and which may be considerable if
3663+        process_shareset() runs quickly.
3664 
3665         This method is for subclasses to override. No upcall is necessary.
3666         """
3667hunk ./src/allmydata/storage/crawler.py 402
3668         pass
3669 
3670     def finished_prefix(self, cycle, prefix):
3671-        """Notify a subclass that the crawler has just finished processing a
3672-        prefix directory (all buckets with the same two-character/10bit
3673+        """
3674+        Notify a subclass that the crawler has just finished processing a
3675+        prefix directory (all sharesets with the same two-character/10-bit
3676         prefix). To impose a limit on how much work might be duplicated by a
3677         SIGKILL that occurs during a timeslice, you can call
3678         self.save_state() here, but be aware that it may represent a
3679hunk ./src/allmydata/storage/crawler.py 415
3680         pass
3681 
3682     def finished_cycle(self, cycle):
3683-        """Notify subclass that a cycle (one complete traversal of all
3684+        """
3685+        Notify subclass that a cycle (one complete traversal of all
3686         prefixdirs) has just finished. 'cycle' is the number of the cycle
3687         that just finished. This method should perform summary work and
3688         update self.state to publish information to status displays.
3689hunk ./src/allmydata/storage/crawler.py 433
3690         pass
3691 
3692     def yielding(self, sleep_time):
3693-        """The crawler is about to sleep for 'sleep_time' seconds. This
3694+        """
3695+        The crawler is about to sleep for 'sleep_time' seconds. This
3696         method is mostly for the convenience of unit tests.
3697 
3698         This method is for subclasses to override. No upcall is necessary.
3699hunk ./src/allmydata/storage/crawler.py 443
3700 
3701 
3702 class BucketCountingCrawler(ShareCrawler):
3703-    """I keep track of how many buckets are being managed by this server.
3704-    This is equivalent to the number of distributed files and directories for
3705-    which I am providing storage. The actual number of files+directories in
3706-    the full grid is probably higher (especially when there are more servers
3707-    than 'N', the number of generated shares), because some files+directories
3708-    will have shares on other servers instead of me. Also note that the
3709-    number of buckets will differ from the number of shares in small grids,
3710-    when more than one share is placed on a single server.
3711+    """
3712+    I keep track of how many sharesets, each corresponding to a storage index,
3713+    are being managed by this server. This is equivalent to the number of
3714+    distributed files and directories for which I am providing storage. The
3715+    actual number of files and directories in the full grid is probably higher
3716+    (especially when there are more servers than 'N', the number of generated
3717+    shares), because some files and directories will have shares on other
3718+    servers instead of me. Also note that the number of sharesets will differ
3719+    from the number of shares in small grids, when more than one share is
3720+    placed on a single server.
3721     """
3722 
3723     minimum_cycle_time = 60*60 # we don't need this more than once an hour
3724hunk ./src/allmydata/storage/crawler.py 457
3725 
3726-    def __init__(self, server, statefile, num_sample_prefixes=1):
3727-        ShareCrawler.__init__(self, server, statefile)
3728+    def __init__(self, backend, statefp, num_sample_prefixes=1):
3729+        ShareCrawler.__init__(self, backend, statefp)
3730         self.num_sample_prefixes = num_sample_prefixes
3731 
3732     def add_initial_state(self):
3733hunk ./src/allmydata/storage/crawler.py 471
3734         self.state.setdefault("last-complete-bucket-count", None)
3735         self.state.setdefault("storage-index-samples", {})
3736 
3737-    def process_prefixdir(self, cycle, prefix, prefixdir, buckets, start_slice):
3738+    def process_prefixdir(self, cycle, prefix, sharesets, start_slice):
3739         # we override process_prefixdir() because we don't want to look at
3740hunk ./src/allmydata/storage/crawler.py 473
3741-        # the individual buckets. We'll save state after each one. On my
3742+        # the individual sharesets. We'll save state after each one. On my
3743         # laptop, a mostly-empty storage server can process about 70
3744         # prefixdirs in a 1.0s slice.
3745         if cycle not in self.state["bucket-counts"]:
3746hunk ./src/allmydata/storage/crawler.py 478
3747             self.state["bucket-counts"][cycle] = {}
3748-        self.state["bucket-counts"][cycle][prefix] = len(buckets)
3749+        self.state["bucket-counts"][cycle][prefix] = len(sharesets)
3750         if prefix in self.prefixes[:self.num_sample_prefixes]:
3751hunk ./src/allmydata/storage/crawler.py 480
3752-            self.state["storage-index-samples"][prefix] = (cycle, buckets)
3753+            self.state["storage-index-samples"][prefix] = (cycle, sharesets)
3754 
3755     def finished_cycle(self, cycle):
3756         last_counts = self.state["bucket-counts"].get(cycle, [])
3757hunk ./src/allmydata/storage/crawler.py 486
3758         if len(last_counts) == len(self.prefixes):
3759             # great, we have a whole cycle.
3760-            num_buckets = sum(last_counts.values())
3761-            self.state["last-complete-bucket-count"] = num_buckets
3762+            num_sharesets = sum(last_counts.values())
3763+            self.state["last-complete-bucket-count"] = num_sharesets
3764             # get rid of old counts
3765             for old_cycle in list(self.state["bucket-counts"].keys()):
3766                 if old_cycle != cycle:
3767hunk ./src/allmydata/storage/crawler.py 494
3768                     del self.state["bucket-counts"][old_cycle]
3769         # get rid of old samples too
3770         for prefix in list(self.state["storage-index-samples"].keys()):
3771-            old_cycle,buckets = self.state["storage-index-samples"][prefix]
3772+            old_cycle, storage_indices = self.state["storage-index-samples"][prefix]
3773             if old_cycle != cycle:
3774                 del self.state["storage-index-samples"][prefix]
3775hunk ./src/allmydata/storage/crawler.py 497
3776-
3777hunk ./src/allmydata/storage/expirer.py 1
3778-import time, os, pickle, struct
3779+
3780+import time, pickle, struct
3781+from twisted.python import log as twlog
3782+
3783 from allmydata.storage.crawler import ShareCrawler
3784hunk ./src/allmydata/storage/expirer.py 6
3785-from allmydata.storage.shares import get_share_file
3786-from allmydata.storage.common import UnknownMutableContainerVersionError, \
3787+from allmydata.storage.common import si_b2a, UnknownMutableContainerVersionError, \
3788      UnknownImmutableContainerVersionError
3789hunk ./src/allmydata/storage/expirer.py 8
3790-from twisted.python import log as twlog
3791+
3792 
3793 class LeaseCheckingCrawler(ShareCrawler):
3794     """I examine the leases on all shares, determining which are still valid
3795hunk ./src/allmydata/storage/expirer.py 17
3796     removed.
3797 
3798     I collect statistics on the leases and make these available to a web
3799-    status page, including::
3800+    status page, including:
3801 
3802     Space recovered during this cycle-so-far:
3803      actual (only if expiration_enabled=True):
3804hunk ./src/allmydata/storage/expirer.py 21
3805-      num-buckets, num-shares, sum of share sizes, real disk usage
3806+      num-storage-indices, num-shares, sum of share sizes, real disk usage
3807       ('real disk usage' means we use stat(fn).st_blocks*512 and include any
3808        space used by the directory)
3809      what it would have been with the original lease expiration time
3810hunk ./src/allmydata/storage/expirer.py 32
3811 
3812     Space recovered during the last 10 cycles  <-- saved in separate pickle
3813 
3814-    Shares/buckets examined:
3815+    Shares/storage-indices examined:
3816      this cycle-so-far
3817      prediction of rest of cycle
3818      during last 10 cycles <-- separate pickle
3819hunk ./src/allmydata/storage/expirer.py 42
3820     Histogram of leases-per-share:
3821      this-cycle-to-date
3822      last 10 cycles <-- separate pickle
3823-    Histogram of lease ages, buckets = 1day
3824+    Histogram of lease ages, storage-indices over 1 day
3825      cycle-to-date
3826      last 10 cycles <-- separate pickle
3827 
3828hunk ./src/allmydata/storage/expirer.py 53
3829     slow_start = 360 # wait 6 minutes after startup
3830     minimum_cycle_time = 12*60*60 # not more than twice per day
3831 
3832-    def __init__(self, server, statefile, historyfile,
3833-                 expiration_enabled, mode,
3834-                 override_lease_duration, # used if expiration_mode=="age"
3835-                 cutoff_date, # used if expiration_mode=="cutoff-date"
3836-                 sharetypes):
3837-        self.historyfile = historyfile
3838-        self.expiration_enabled = expiration_enabled
3839-        self.mode = mode
3840+    def __init__(self, backend, statefp, historyfp, expiration_policy):
3841+        # ShareCrawler.__init__ will call add_initial_state, so self.historyfp has to be set first.
3842+        self.historyfp = historyfp
3843+        ShareCrawler.__init__(self, backend, statefp)
3844+
3845+        self.expiration_enabled = expiration_policy['enabled']
3846+        self.mode = expiration_policy['mode']
3847         self.override_lease_duration = None
3848         self.cutoff_date = None
3849         if self.mode == "age":
3850hunk ./src/allmydata/storage/expirer.py 63
3851-            assert isinstance(override_lease_duration, (int, type(None)))
3852-            self.override_lease_duration = override_lease_duration # seconds
3853+            assert isinstance(expiration_policy['override_lease_duration'], (int, type(None)))
3854+            self.override_lease_duration = expiration_policy['override_lease_duration'] # seconds
3855         elif self.mode == "cutoff-date":
3856hunk ./src/allmydata/storage/expirer.py 66
3857-            assert isinstance(cutoff_date, int) # seconds-since-epoch
3858-            assert cutoff_date is not None
3859-            self.cutoff_date = cutoff_date
3860+            assert isinstance(expiration_policy['cutoff_date'], int) # seconds-since-epoch
3861+            self.cutoff_date = expiration_policy['cutoff_date']
3862         else:
3863hunk ./src/allmydata/storage/expirer.py 69
3864-            raise ValueError("GC mode '%s' must be 'age' or 'cutoff-date'" % mode)
3865-        self.sharetypes_to_expire = sharetypes
3866-        ShareCrawler.__init__(self, server, statefile)
3867+            raise ValueError("GC mode '%s' must be 'age' or 'cutoff-date'" % expiration_policy['mode'])
3868+        self.sharetypes_to_expire = expiration_policy['sharetypes']
3869 
3870     def add_initial_state(self):
3871         # we fill ["cycle-to-date"] here (even though they will be reset in
3872hunk ./src/allmydata/storage/expirer.py 84
3873             self.state["cycle-to-date"].setdefault(k, so_far[k])
3874 
3875         # initialize history
3876-        if not os.path.exists(self.historyfile):
3877+        if not self.historyfp.exists():
3878             history = {} # cyclenum -> dict
3879hunk ./src/allmydata/storage/expirer.py 86
3880-            f = open(self.historyfile, "wb")
3881-            pickle.dump(history, f)
3882-            f.close()
3883+            self.historyfp.setContent(pickle.dumps(history))
3884 
3885     def create_empty_cycle_dict(self):
3886         recovered = self.create_empty_recovered_dict()
3887hunk ./src/allmydata/storage/expirer.py 99
3888 
3889     def create_empty_recovered_dict(self):
3890         recovered = {}
3891+        # "buckets" is ambiguous; here it means the number of sharesets (one per storage index per server)
3892         for a in ("actual", "original", "configured", "examined"):
3893             for b in ("buckets", "shares", "sharebytes", "diskbytes"):
3894                 recovered[a+"-"+b] = 0
3895hunk ./src/allmydata/storage/expirer.py 110
3896     def started_cycle(self, cycle):
3897         self.state["cycle-to-date"] = self.create_empty_cycle_dict()
3898 
3899-    def stat(self, fn):
3900-        return os.stat(fn)
3901-
3902-    def process_bucket(self, cycle, prefix, prefixdir, storage_index_b32):
3903-        bucketdir = os.path.join(prefixdir, storage_index_b32)
3904-        s = self.stat(bucketdir)
3905+    def process_storage_index(self, cycle, prefix, container):
3906         would_keep_shares = []
3907         wks = None
3908hunk ./src/allmydata/storage/expirer.py 113
3909+        sharetype = None
3910 
3911hunk ./src/allmydata/storage/expirer.py 115
3912-        for fn in os.listdir(bucketdir):
3913-            try:
3914-                shnum = int(fn)
3915-            except ValueError:
3916-                continue # non-numeric means not a sharefile
3917-            sharefile = os.path.join(bucketdir, fn)
3918+        for share in container.get_shares():
3919+            sharetype = share.sharetype
3920             try:
3921hunk ./src/allmydata/storage/expirer.py 118
3922-                wks = self.process_share(sharefile)
3923+                wks = self.process_share(share)
3924             except (UnknownMutableContainerVersionError,
3925                     UnknownImmutableContainerVersionError,
3926                     struct.error):
3927hunk ./src/allmydata/storage/expirer.py 122
3928-                twlog.msg("lease-checker error processing %s" % sharefile)
3929+                twlog.msg("lease-checker error processing %r" % (share,))
3930                 twlog.err()
3931hunk ./src/allmydata/storage/expirer.py 124
3932-                which = (storage_index_b32, shnum)
3933+                which = (si_b2a(share.storageindex), share.get_shnum())
3934                 self.state["cycle-to-date"]["corrupt-shares"].append(which)
3935                 wks = (1, 1, 1, "unknown")
3936             would_keep_shares.append(wks)
3937hunk ./src/allmydata/storage/expirer.py 129
3938 
3939-        sharetype = None
3940+        container_type = None
3941         if wks:
3942hunk ./src/allmydata/storage/expirer.py 131
3943-            # use the last share's sharetype as the buckettype
3944-            sharetype = wks[3]
3945+            # use the last share's sharetype as the container type
3946+            container_type = wks[3]
3947         rec = self.state["cycle-to-date"]["space-recovered"]
3948         self.increment(rec, "examined-buckets", 1)
3949         if sharetype:
3950hunk ./src/allmydata/storage/expirer.py 136
3951-            self.increment(rec, "examined-buckets-"+sharetype, 1)
3952+            self.increment(rec, "examined-buckets-"+container_type, 1)
3953+
3954+        container_diskbytes = container.get_overhead()
3955 
3956hunk ./src/allmydata/storage/expirer.py 140
3957-        try:
3958-            bucket_diskbytes = s.st_blocks * 512
3959-        except AttributeError:
3960-            bucket_diskbytes = 0 # no stat().st_blocks on windows
3961         if sum([wks[0] for wks in would_keep_shares]) == 0:
3962hunk ./src/allmydata/storage/expirer.py 141
3963-            self.increment_bucketspace("original", bucket_diskbytes, sharetype)
3964+            self.increment_container_space("original", container_diskbytes, sharetype)
3965         if sum([wks[1] for wks in would_keep_shares]) == 0:
3966hunk ./src/allmydata/storage/expirer.py 143
3967-            self.increment_bucketspace("configured", bucket_diskbytes, sharetype)
3968+            self.increment_container_space("configured", container_diskbytes, sharetype)
3969         if sum([wks[2] for wks in would_keep_shares]) == 0:
3970hunk ./src/allmydata/storage/expirer.py 145
3971-            self.increment_bucketspace("actual", bucket_diskbytes, sharetype)
3972+            self.increment_container_space("actual", container_diskbytes, sharetype)
3973 
3974hunk ./src/allmydata/storage/expirer.py 147
3975-    def process_share(self, sharefilename):
3976-        # first, find out what kind of a share it is
3977-        sf = get_share_file(sharefilename)
3978-        sharetype = sf.sharetype
3979+    def process_share(self, share):
3980+        sharetype = share.sharetype
3981         now = time.time()
3982hunk ./src/allmydata/storage/expirer.py 150
3983-        s = self.stat(sharefilename)
3984+        sharebytes = share.get_size()
3985+        diskbytes = share.get_used_space()
3986 
3987         num_leases = 0
3988         num_valid_leases_original = 0
3989hunk ./src/allmydata/storage/expirer.py 158
3990         num_valid_leases_configured = 0
3991         expired_leases_configured = []
3992 
3993-        for li in sf.get_leases():
3994+        for li in share.get_leases():
3995             num_leases += 1
3996             original_expiration_time = li.get_expiration_time()
3997             grant_renew_time = li.get_grant_renew_time_time()
3998hunk ./src/allmydata/storage/expirer.py 171
3999 
4000             #  expired-or-not according to our configured age limit
4001             expired = False
4002-            if self.mode == "age":
4003-                age_limit = original_expiration_time
4004-                if self.override_lease_duration is not None:
4005-                    age_limit = self.override_lease_duration
4006-                if age > age_limit:
4007-                    expired = True
4008-            else:
4009-                assert self.mode == "cutoff-date"
4010-                if grant_renew_time < self.cutoff_date:
4011-                    expired = True
4012-            if sharetype not in self.sharetypes_to_expire:
4013-                expired = False
4014+            if sharetype in self.sharetypes_to_expire:
4015+                if self.mode == "age":
4016+                    age_limit = original_expiration_time
4017+                    if self.override_lease_duration is not None:
4018+                        age_limit = self.override_lease_duration
4019+                    if age > age_limit:
4020+                        expired = True
4021+                else:
4022+                    assert self.mode == "cutoff-date"
4023+                    if grant_renew_time < self.cutoff_date:
4024+                        expired = True
4025 
4026             if expired:
4027                 expired_leases_configured.append(li)
4028hunk ./src/allmydata/storage/expirer.py 190
4029 
4030         so_far = self.state["cycle-to-date"]
4031         self.increment(so_far["leases-per-share-histogram"], num_leases, 1)
4032-        self.increment_space("examined", s, sharetype)
4033+        self.increment_space("examined", diskbytes, sharetype)
4034 
4035         would_keep_share = [1, 1, 1, sharetype]
4036 
4037hunk ./src/allmydata/storage/expirer.py 196
4038         if self.expiration_enabled:
4039             for li in expired_leases_configured:
4040-                sf.cancel_lease(li.cancel_secret)
4041+                share.cancel_lease(li.cancel_secret)
4042 
4043         if num_valid_leases_original == 0:
4044             would_keep_share[0] = 0
4045hunk ./src/allmydata/storage/expirer.py 200
4046-            self.increment_space("original", s, sharetype)
4047+            self.increment_space("original", sharebytes, diskbytes, sharetype)
4048 
4049         if num_valid_leases_configured == 0:
4050             would_keep_share[1] = 0
4051hunk ./src/allmydata/storage/expirer.py 204
4052-            self.increment_space("configured", s, sharetype)
4053+            self.increment_space("configured", sharebytes, diskbytes, sharetype)
4054             if self.expiration_enabled:
4055                 would_keep_share[2] = 0
4056hunk ./src/allmydata/storage/expirer.py 207
4057-                self.increment_space("actual", s, sharetype)
4058+                self.increment_space("actual", sharebytes, diskbytes, sharetype)
4059 
4060         return would_keep_share
4061 
4062hunk ./src/allmydata/storage/expirer.py 211
4063-    def increment_space(self, a, s, sharetype):
4064-        sharebytes = s.st_size
4065-        try:
4066-            # note that stat(2) says that st_blocks is 512 bytes, and that
4067-            # st_blksize is "optimal file sys I/O ops blocksize", which is
4068-            # independent of the block-size that st_blocks uses.
4069-            diskbytes = s.st_blocks * 512
4070-        except AttributeError:
4071-            # the docs say that st_blocks is only on linux. I also see it on
4072-            # MacOS. But it isn't available on windows.
4073-            diskbytes = sharebytes
4074+    def increment_space(self, a, sharebytes, diskbytes, sharetype):
4075         so_far_sr = self.state["cycle-to-date"]["space-recovered"]
4076         self.increment(so_far_sr, a+"-shares", 1)
4077         self.increment(so_far_sr, a+"-sharebytes", sharebytes)
4078hunk ./src/allmydata/storage/expirer.py 221
4079             self.increment(so_far_sr, a+"-sharebytes-"+sharetype, sharebytes)
4080             self.increment(so_far_sr, a+"-diskbytes-"+sharetype, diskbytes)
4081 
4082-    def increment_bucketspace(self, a, bucket_diskbytes, sharetype):
4083+    def increment_container_space(self, a, container_diskbytes, container_type):
4084         rec = self.state["cycle-to-date"]["space-recovered"]
4085hunk ./src/allmydata/storage/expirer.py 223
4086-        self.increment(rec, a+"-diskbytes", bucket_diskbytes)
4087+        self.increment(rec, a+"-diskbytes", container_diskbytes)
4088         self.increment(rec, a+"-buckets", 1)
4089hunk ./src/allmydata/storage/expirer.py 225
4090-        if sharetype:
4091-            self.increment(rec, a+"-diskbytes-"+sharetype, bucket_diskbytes)
4092-            self.increment(rec, a+"-buckets-"+sharetype, 1)
4093+        if container_type:
4094+            self.increment(rec, a+"-diskbytes-"+container_type, container_diskbytes)
4095+            self.increment(rec, a+"-buckets-"+container_type, 1)
4096 
4097     def increment(self, d, k, delta=1):
4098         if k not in d:
4099hunk ./src/allmydata/storage/expirer.py 281
4100         # copy() needs to become a deepcopy
4101         h["space-recovered"] = s["space-recovered"].copy()
4102 
4103-        history = pickle.load(open(self.historyfile, "rb"))
4104+        history = pickle.load(self.historyfp.getContent())
4105         history[cycle] = h
4106         while len(history) > 10:
4107             oldcycles = sorted(history.keys())
4108hunk ./src/allmydata/storage/expirer.py 286
4109             del history[oldcycles[0]]
4110-        f = open(self.historyfile, "wb")
4111-        pickle.dump(history, f)
4112-        f.close()
4113+        self.historyfp.setContent(pickle.dumps(history))
4114 
4115     def get_state(self):
4116         """In addition to the crawler state described in
4117hunk ./src/allmydata/storage/expirer.py 355
4118         progress = self.get_progress()
4119 
4120         state = ShareCrawler.get_state(self) # does a shallow copy
4121-        history = pickle.load(open(self.historyfile, "rb"))
4122+        history = pickle.load(self.historyfp.getContent())
4123         state["history"] = history
4124 
4125         if not progress["cycle-in-progress"]:
4126hunk ./src/allmydata/storage/lease.py 3
4127 import struct, time
4128 
4129+
4130+class NonExistentLeaseError(Exception):
4131+    pass
4132+
4133 class LeaseInfo:
4134     def __init__(self, owner_num=None, renew_secret=None, cancel_secret=None,
4135                  expiration_time=None, nodeid=None):
4136hunk ./src/allmydata/storage/lease.py 21
4137 
4138     def get_expiration_time(self):
4139         return self.expiration_time
4140+
4141     def get_grant_renew_time_time(self):
4142         # hack, based upon fixed 31day expiration period
4143         return self.expiration_time - 31*24*60*60
4144hunk ./src/allmydata/storage/lease.py 25
4145+
4146     def get_age(self):
4147         return time.time() - self.get_grant_renew_time_time()
4148 
4149hunk ./src/allmydata/storage/lease.py 36
4150          self.expiration_time) = struct.unpack(">L32s32sL", data)
4151         self.nodeid = None
4152         return self
4153+
4154     def to_immutable_data(self):
4155         return struct.pack(">L32s32sL",
4156                            self.owner_num,
4157hunk ./src/allmydata/storage/lease.py 49
4158                            int(self.expiration_time),
4159                            self.renew_secret, self.cancel_secret,
4160                            self.nodeid)
4161+
4162     def from_mutable_data(self, data):
4163         (self.owner_num,
4164          self.expiration_time,
4165hunk ./src/allmydata/storage/server.py 1
4166-import os, re, weakref, struct, time
4167+import weakref, time
4168 
4169 from foolscap.api import Referenceable
4170 from twisted.application import service
4171hunk ./src/allmydata/storage/server.py 7
4172 
4173 from zope.interface import implements
4174-from allmydata.interfaces import RIStorageServer, IStatsProducer
4175-from allmydata.util import fileutil, idlib, log, time_format
4176+from allmydata.interfaces import RIStorageServer, IStatsProducer, IStorageBackend
4177+from allmydata.util.assertutil import precondition
4178+from allmydata.util import idlib, log
4179 import allmydata # for __full_version__
4180 
4181hunk ./src/allmydata/storage/server.py 12
4182-from allmydata.storage.common import si_b2a, si_a2b, storage_index_to_dir
4183-_pyflakes_hush = [si_b2a, si_a2b, storage_index_to_dir] # re-exported
4184+from allmydata.storage.common import si_a2b, si_b2a
4185+[si_a2b]  # hush pyflakes
4186 from allmydata.storage.lease import LeaseInfo
4187hunk ./src/allmydata/storage/server.py 15
4188-from allmydata.storage.mutable import MutableShareFile, EmptyShare, \
4189-     create_mutable_sharefile
4190-from allmydata.storage.immutable import ShareFile, BucketWriter, BucketReader
4191-from allmydata.storage.crawler import BucketCountingCrawler
4192 from allmydata.storage.expirer import LeaseCheckingCrawler
4193hunk ./src/allmydata/storage/server.py 16
4194-
4195-# storage/
4196-# storage/shares/incoming
4197-#   incoming/ holds temp dirs named $START/$STORAGEINDEX/$SHARENUM which will
4198-#   be moved to storage/shares/$START/$STORAGEINDEX/$SHARENUM upon success
4199-# storage/shares/$START/$STORAGEINDEX
4200-# storage/shares/$START/$STORAGEINDEX/$SHARENUM
4201-
4202-# Where "$START" denotes the first 10 bits worth of $STORAGEINDEX (that's 2
4203-# base-32 chars).
4204-
4205-# $SHARENUM matches this regex:
4206-NUM_RE=re.compile("^[0-9]+$")
4207-
4208+from allmydata.storage.crawler import BucketCountingCrawler
4209 
4210 
4211 class StorageServer(service.MultiService, Referenceable):
4212hunk ./src/allmydata/storage/server.py 21
4213     implements(RIStorageServer, IStatsProducer)
4214+
4215     name = 'storage'
4216     LeaseCheckerClass = LeaseCheckingCrawler
4217hunk ./src/allmydata/storage/server.py 24
4218+    DEFAULT_EXPIRATION_POLICY = {
4219+        'enabled': False,
4220+        'mode': 'age',
4221+        'override_lease_duration': None,
4222+        'cutoff_date': None,
4223+        'sharetypes': ('mutable', 'immutable'),
4224+    }
4225 
4226hunk ./src/allmydata/storage/server.py 32
4227-    def __init__(self, storedir, nodeid, reserved_space=0,
4228-                 discard_storage=False, readonly_storage=False,
4229+    def __init__(self, serverid, backend, statedir,
4230                  stats_provider=None,
4231hunk ./src/allmydata/storage/server.py 34
4232-                 expiration_enabled=False,
4233-                 expiration_mode="age",
4234-                 expiration_override_lease_duration=None,
4235-                 expiration_cutoff_date=None,
4236-                 expiration_sharetypes=("mutable", "immutable")):
4237+                 expiration_policy=None):
4238         service.MultiService.__init__(self)
4239hunk ./src/allmydata/storage/server.py 36
4240-        assert isinstance(nodeid, str)
4241-        assert len(nodeid) == 20
4242-        self.my_nodeid = nodeid
4243-        self.storedir = storedir
4244-        sharedir = os.path.join(storedir, "shares")
4245-        fileutil.make_dirs(sharedir)
4246-        self.sharedir = sharedir
4247-        # we don't actually create the corruption-advisory dir until necessary
4248-        self.corruption_advisory_dir = os.path.join(storedir,
4249-                                                    "corruption-advisories")
4250-        self.reserved_space = int(reserved_space)
4251-        self.no_storage = discard_storage
4252-        self.readonly_storage = readonly_storage
4253+        precondition(IStorageBackend.providedBy(backend), backend)
4254+        precondition(isinstance(serverid, str), serverid)
4255+        precondition(len(serverid) == 20, serverid)
4256+
4257+        self._serverid = serverid
4258         self.stats_provider = stats_provider
4259         if self.stats_provider:
4260             self.stats_provider.register_producer(self)
4261hunk ./src/allmydata/storage/server.py 44
4262-        self.incomingdir = os.path.join(sharedir, 'incoming')
4263-        self._clean_incomplete()
4264-        fileutil.make_dirs(self.incomingdir)
4265         self._active_writers = weakref.WeakKeyDictionary()
4266hunk ./src/allmydata/storage/server.py 45
4267+        self.backend = backend
4268+        self.backend.setServiceParent(self)
4269+        self._statedir = statedir
4270         log.msg("StorageServer created", facility="tahoe.storage")
4271 
4272hunk ./src/allmydata/storage/server.py 50
4273-        if reserved_space:
4274-            if self.get_available_space() is None:
4275-                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",
4276-                        umin="0wZ27w", level=log.UNUSUAL)
4277-
4278         self.latencies = {"allocate": [], # immutable
4279                           "write": [],
4280                           "close": [],
4281hunk ./src/allmydata/storage/server.py 61
4282                           "renew": [],
4283                           "cancel": [],
4284                           }
4285-        self.add_bucket_counter()
4286-
4287-        statefile = os.path.join(self.storedir, "lease_checker.state")
4288-        historyfile = os.path.join(self.storedir, "lease_checker.history")
4289-        klass = self.LeaseCheckerClass
4290-        self.lease_checker = klass(self, statefile, historyfile,
4291-                                   expiration_enabled, expiration_mode,
4292-                                   expiration_override_lease_duration,
4293-                                   expiration_cutoff_date,
4294-                                   expiration_sharetypes)
4295-        self.lease_checker.setServiceParent(self)
4296+        self._setup_bucket_counter()
4297+        self._setup_lease_checker(expiration_policy or self.DEFAULT_EXPIRATION_POLICY)
4298 
4299     def __repr__(self):
4300hunk ./src/allmydata/storage/server.py 65
4301-        return "<StorageServer %s>" % (idlib.shortnodeid_b2a(self.my_nodeid),)
4302+        return "<StorageServer %s>" % (idlib.shortnodeid_b2a(self._serverid),)
4303 
4304hunk ./src/allmydata/storage/server.py 67
4305-    def add_bucket_counter(self):
4306-        statefile = os.path.join(self.storedir, "bucket_counter.state")
4307-        self.bucket_counter = BucketCountingCrawler(self, statefile)
4308+    def _setup_bucket_counter(self):
4309+        statefp = self._statedir.child("bucket_counter.state")
4310+        self.bucket_counter = BucketCountingCrawler(self.backend, statefp)
4311         self.bucket_counter.setServiceParent(self)
4312 
4313hunk ./src/allmydata/storage/server.py 72
4314+    def _setup_lease_checker(self, expiration_policy):
4315+        statefp = self._statedir.child("lease_checker.state")
4316+        historyfp = self._statedir.child("lease_checker.history")
4317+        self.lease_checker = self.LeaseCheckerClass(self.backend, statefp, historyfp, expiration_policy)
4318+        self.lease_checker.setServiceParent(self)
4319+
4320     def count(self, name, delta=1):
4321         if self.stats_provider:
4322             self.stats_provider.count("storage_server." + name, delta)
4323hunk ./src/allmydata/storage/server.py 92
4324         """Return a dict, indexed by category, that contains a dict of
4325         latency numbers for each category. If there are sufficient samples
4326         for unambiguous interpretation, each dict will contain the
4327-        following keys: mean, 01_0_percentile, 10_0_percentile,
4328+        following keys: samplesize, mean, 01_0_percentile, 10_0_percentile,
4329         50_0_percentile (median), 90_0_percentile, 95_0_percentile,
4330         99_0_percentile, 99_9_percentile.  If there are insufficient
4331         samples for a given percentile to be interpreted unambiguously
4332hunk ./src/allmydata/storage/server.py 114
4333             else:
4334                 stats["mean"] = None
4335 
4336-            orderstatlist = [(0.01, "01_0_percentile", 100), (0.1, "10_0_percentile", 10),\
4337-                             (0.50, "50_0_percentile", 10), (0.90, "90_0_percentile", 10),\
4338-                             (0.95, "95_0_percentile", 20), (0.99, "99_0_percentile", 100),\
4339+            orderstatlist = [(0.1, "10_0_percentile", 10), (0.5, "50_0_percentile", 10), \
4340+                             (0.9, "90_0_percentile", 10), (0.95, "95_0_percentile", 20), \
4341+                             (0.01, "01_0_percentile", 100),  (0.99, "99_0_percentile", 100),\
4342                              (0.999, "99_9_percentile", 1000)]
4343 
4344             for percentile, percentilestring, minnumtoobserve in orderstatlist:
4345hunk ./src/allmydata/storage/server.py 133
4346             kwargs["facility"] = "tahoe.storage"
4347         return log.msg(*args, **kwargs)
4348 
4349-    def _clean_incomplete(self):
4350-        fileutil.rm_dir(self.incomingdir)
4351+    def get_serverid(self):
4352+        return self._serverid
4353 
4354     def get_stats(self):
4355         # remember: RIStatsProvider requires that our return dict
4356hunk ./src/allmydata/storage/server.py 138
4357-        # contains numeric values.
4358+        # contains numeric, or None values.
4359         stats = { 'storage_server.allocated': self.allocated_size(), }
4360hunk ./src/allmydata/storage/server.py 140
4361-        stats['storage_server.reserved_space'] = self.reserved_space
4362         for category,ld in self.get_latencies().items():
4363             for name,v in ld.items():
4364                 stats['storage_server.latencies.%s.%s' % (category, name)] = v
4365hunk ./src/allmydata/storage/server.py 144
4366 
4367-        try:
4368-            disk = fileutil.get_disk_stats(self.sharedir, self.reserved_space)
4369-            writeable = disk['avail'] > 0
4370-
4371-            # spacetime predictors should use disk_avail / (d(disk_used)/dt)
4372-            stats['storage_server.disk_total'] = disk['total']
4373-            stats['storage_server.disk_used'] = disk['used']
4374-            stats['storage_server.disk_free_for_root'] = disk['free_for_root']
4375-            stats['storage_server.disk_free_for_nonroot'] = disk['free_for_nonroot']
4376-            stats['storage_server.disk_avail'] = disk['avail']
4377-        except AttributeError:
4378-            writeable = True
4379-        except EnvironmentError:
4380-            log.msg("OS call to get disk statistics failed", level=log.UNUSUAL)
4381-            writeable = False
4382-
4383-        if self.readonly_storage:
4384-            stats['storage_server.disk_avail'] = 0
4385-            writeable = False
4386+        self.backend.fill_in_space_stats(stats)
4387 
4388hunk ./src/allmydata/storage/server.py 146
4389-        stats['storage_server.accepting_immutable_shares'] = int(writeable)
4390         s = self.bucket_counter.get_state()
4391         bucket_count = s.get("last-complete-bucket-count")
4392         if bucket_count:
4393hunk ./src/allmydata/storage/server.py 153
4394         return stats
4395 
4396     def get_available_space(self):
4397-        """Returns available space for share storage in bytes, or None if no
4398-        API to get this information is available."""
4399-
4400-        if self.readonly_storage:
4401-            return 0
4402-        return fileutil.get_available_space(self.sharedir, self.reserved_space)
4403+        return self.backend.get_available_space()
4404 
4405     def allocated_size(self):
4406         space = 0
4407hunk ./src/allmydata/storage/server.py 162
4408         return space
4409 
4410     def remote_get_version(self):
4411-        remaining_space = self.get_available_space()
4412+        remaining_space = self.backend.get_available_space()
4413         if remaining_space is None:
4414             # We're on a platform that has no API to get disk stats.
4415             remaining_space = 2**64
4416hunk ./src/allmydata/storage/server.py 178
4417                     }
4418         return version
4419 
4420-    def remote_allocate_buckets(self, storage_index,
4421+    def remote_allocate_buckets(self, storageindex,
4422                                 renew_secret, cancel_secret,
4423                                 sharenums, allocated_size,
4424                                 canary, owner_num=0):
4425hunk ./src/allmydata/storage/server.py 182
4426+        # cancel_secret is no longer used.
4427         # owner_num is not for clients to set, but rather it should be
4428hunk ./src/allmydata/storage/server.py 184
4429-        # curried into the PersonalStorageServer instance that is dedicated
4430-        # to a particular owner.
4431+        # curried into a StorageServer instance dedicated to a particular
4432+        # owner.
4433         start = time.time()
4434         self.count("allocate")
4435hunk ./src/allmydata/storage/server.py 188
4436-        alreadygot = set()
4437         bucketwriters = {} # k: shnum, v: BucketWriter
4438hunk ./src/allmydata/storage/server.py 189
4439-        si_dir = storage_index_to_dir(storage_index)
4440-        si_s = si_b2a(storage_index)
4441 
4442hunk ./src/allmydata/storage/server.py 190
4443+        si_s = si_b2a(storageindex)
4444         log.msg("storage: allocate_buckets %s" % si_s)
4445 
4446hunk ./src/allmydata/storage/server.py 193
4447-        # in this implementation, the lease information (including secrets)
4448-        # goes into the share files themselves. It could also be put into a
4449-        # separate database. Note that the lease should not be added until
4450-        # the BucketWriter has been closed.
4451+        # Note that the lease should not be added until the BucketWriter
4452+        # has been closed.
4453         expire_time = time.time() + 31*24*60*60
4454hunk ./src/allmydata/storage/server.py 196
4455-        lease_info = LeaseInfo(owner_num,
4456-                               renew_secret, cancel_secret,
4457-                               expire_time, self.my_nodeid)
4458+        lease_info = LeaseInfo(owner_num, renew_secret,
4459+                               expire_time, self._serverid)
4460 
4461         max_space_per_bucket = allocated_size
4462 
4463hunk ./src/allmydata/storage/server.py 201
4464-        remaining_space = self.get_available_space()
4465+        remaining_space = self.backend.get_available_space()
4466         limited = remaining_space is not None
4467         if limited:
4468hunk ./src/allmydata/storage/server.py 204
4469-            # this is a bit conservative, since some of this allocated_size()
4470-            # has already been written to disk, where it will show up in
4471+            # This is a bit conservative, since some of this allocated_size()
4472+            # has already been written to the backend, where it will show up in
4473             # get_available_space.
4474             remaining_space -= self.allocated_size()
4475hunk ./src/allmydata/storage/server.py 208
4476-        # self.readonly_storage causes remaining_space <= 0
4477+            # If the backend is read-only, remaining_space will be <= 0.
4478+
4479+        shareset = self.backend.get_shareset(storageindex)
4480 
4481hunk ./src/allmydata/storage/server.py 212
4482-        # fill alreadygot with all shares that we have, not just the ones
4483+        # Fill alreadygot with all shares that we have, not just the ones
4484         # they asked about: this will save them a lot of work. Add or update
4485         # leases for all of them: if they want us to hold shares for this
4486hunk ./src/allmydata/storage/server.py 215
4487-        # file, they'll want us to hold leases for this file.
4488-        for (shnum, fn) in self._get_bucket_shares(storage_index):
4489-            alreadygot.add(shnum)
4490-            sf = ShareFile(fn)
4491-            sf.add_or_renew_lease(lease_info)
4492+        # file, they'll want us to hold leases for all the shares of it.
4493+        #
4494+        # XXX should we be making the assumption here that lease info is
4495+        # duplicated in all shares?
4496+        alreadygot = set()
4497+        for share in shareset.get_shares():
4498+            share.add_or_renew_lease(lease_info)
4499+            alreadygot.add(share.shnum)
4500 
4501hunk ./src/allmydata/storage/server.py 224
4502-        for shnum in sharenums:
4503-            incominghome = os.path.join(self.incomingdir, si_dir, "%d" % shnum)
4504-            finalhome = os.path.join(self.sharedir, si_dir, "%d" % shnum)
4505-            if os.path.exists(finalhome):
4506-                # great! we already have it. easy.
4507-                pass
4508-            elif os.path.exists(incominghome):
4509+        for shnum in sharenums - alreadygot:
4510+            if shareset.has_incoming(shnum):
4511                 # Note that we don't create BucketWriters for shnums that
4512                 # have a partial share (in incoming/), so if a second upload
4513                 # occurs while the first is still in progress, the second
4514hunk ./src/allmydata/storage/server.py 232
4515                 # uploader will use different storage servers.
4516                 pass
4517             elif (not limited) or (remaining_space >= max_space_per_bucket):
4518-                # ok! we need to create the new share file.
4519-                bw = BucketWriter(self, incominghome, finalhome,
4520-                                  max_space_per_bucket, lease_info, canary)
4521-                if self.no_storage:
4522-                    bw.throw_out_all_data = True
4523+                bw = shareset.make_bucket_writer(self, shnum, max_space_per_bucket,
4524+                                                 lease_info, canary)
4525                 bucketwriters[shnum] = bw
4526                 self._active_writers[bw] = 1
4527                 if limited:
4528hunk ./src/allmydata/storage/server.py 239
4529                     remaining_space -= max_space_per_bucket
4530             else:
4531-                # bummer! not enough space to accept this bucket
4532+                # Bummer not enough space to accept this share.
4533                 pass
4534 
4535hunk ./src/allmydata/storage/server.py 242
4536-        if bucketwriters:
4537-            fileutil.make_dirs(os.path.join(self.sharedir, si_dir))
4538-
4539         self.add_latency("allocate", time.time() - start)
4540         return alreadygot, bucketwriters
4541 
4542hunk ./src/allmydata/storage/server.py 245
4543-    def _iter_share_files(self, storage_index):
4544-        for shnum, filename in self._get_bucket_shares(storage_index):
4545-            f = open(filename, 'rb')
4546-            header = f.read(32)
4547-            f.close()
4548-            if header[:32] == MutableShareFile.MAGIC:
4549-                sf = MutableShareFile(filename, self)
4550-                # note: if the share has been migrated, the renew_lease()
4551-                # call will throw an exception, with information to help the
4552-                # client update the lease.
4553-            elif header[:4] == struct.pack(">L", 1):
4554-                sf = ShareFile(filename)
4555-            else:
4556-                continue # non-sharefile
4557-            yield sf
4558-
4559-    def remote_add_lease(self, storage_index, renew_secret, cancel_secret,
4560+    def remote_add_lease(self, storageindex, renew_secret, cancel_secret,
4561                          owner_num=1):
4562hunk ./src/allmydata/storage/server.py 247
4563+        # cancel_secret is no longer used.
4564         start = time.time()
4565         self.count("add-lease")
4566         new_expire_time = time.time() + 31*24*60*60
4567hunk ./src/allmydata/storage/server.py 251
4568-        lease_info = LeaseInfo(owner_num,
4569-                               renew_secret, cancel_secret,
4570-                               new_expire_time, self.my_nodeid)
4571-        for sf in self._iter_share_files(storage_index):
4572-            sf.add_or_renew_lease(lease_info)
4573-        self.add_latency("add-lease", time.time() - start)
4574-        return None
4575+        lease_info = LeaseInfo(owner_num, renew_secret,
4576+                               new_expire_time, self._serverid)
4577 
4578hunk ./src/allmydata/storage/server.py 254
4579-    def remote_renew_lease(self, storage_index, renew_secret):
4580+        try:
4581+            self.backend.add_or_renew_lease(lease_info)
4582+        finally:
4583+            self.add_latency("add-lease", time.time() - start)
4584+
4585+    def remote_renew_lease(self, storageindex, renew_secret):
4586         start = time.time()
4587         self.count("renew")
4588hunk ./src/allmydata/storage/server.py 262
4589-        new_expire_time = time.time() + 31*24*60*60
4590-        found_buckets = False
4591-        for sf in self._iter_share_files(storage_index):
4592-            found_buckets = True
4593-            sf.renew_lease(renew_secret, new_expire_time)
4594-        self.add_latency("renew", time.time() - start)
4595-        if not found_buckets:
4596-            raise IndexError("no such lease to renew")
4597+
4598+        try:
4599+            shareset = self.backend.get_shareset(storageindex)
4600+            new_expiration_time = start + 31*24*60*60   # one month from now
4601+            shareset.renew_lease(renew_secret, new_expiration_time)
4602+        finally:
4603+            self.add_latency("renew", time.time() - start)
4604 
4605     def bucket_writer_closed(self, bw, consumed_size):
4606         if self.stats_provider:
4607hunk ./src/allmydata/storage/server.py 275
4608             self.stats_provider.count('storage_server.bytes_added', consumed_size)
4609         del self._active_writers[bw]
4610 
4611-    def _get_bucket_shares(self, storage_index):
4612-        """Return a list of (shnum, pathname) tuples for files that hold
4613-        shares for this storage_index. In each tuple, 'shnum' will always be
4614-        the integer form of the last component of 'pathname'."""
4615-        storagedir = os.path.join(self.sharedir, storage_index_to_dir(storage_index))
4616-        try:
4617-            for f in os.listdir(storagedir):
4618-                if NUM_RE.match(f):
4619-                    filename = os.path.join(storagedir, f)
4620-                    yield (int(f), filename)
4621-        except OSError:
4622-            # Commonly caused by there being no buckets at all.
4623-            pass
4624-
4625-    def remote_get_buckets(self, storage_index):
4626+    def remote_get_buckets(self, storageindex):
4627         start = time.time()
4628         self.count("get")
4629hunk ./src/allmydata/storage/server.py 278
4630-        si_s = si_b2a(storage_index)
4631+        si_s = si_b2a(storageindex)
4632         log.msg("storage: get_buckets %s" % si_s)
4633         bucketreaders = {} # k: sharenum, v: BucketReader
4634hunk ./src/allmydata/storage/server.py 281
4635-        for shnum, filename in self._get_bucket_shares(storage_index):
4636-            bucketreaders[shnum] = BucketReader(self, filename,
4637-                                                storage_index, shnum)
4638-        self.add_latency("get", time.time() - start)
4639-        return bucketreaders
4640 
4641hunk ./src/allmydata/storage/server.py 282
4642-    def get_leases(self, storage_index):
4643-        """Provide an iterator that yields all of the leases attached to this
4644-        bucket. Each lease is returned as a LeaseInfo instance.
4645+        try:
4646+            shareset = self.backend.get_shareset(storageindex)
4647+            for share in shareset.get_shares():
4648+                bucketreaders[share.get_shnum()] = shareset.make_bucket_reader(self, share)
4649+            return bucketreaders
4650+        finally:
4651+            self.add_latency("get", time.time() - start)
4652 
4653hunk ./src/allmydata/storage/server.py 290
4654-        This method is not for client use.
4655+    def get_leases(self, storageindex):
4656         """
4657hunk ./src/allmydata/storage/server.py 292
4658+        Provide an iterator that yields all of the leases attached to this
4659+        bucket. Each lease is returned as a LeaseInfo instance.
4660 
4661hunk ./src/allmydata/storage/server.py 295
4662-        # since all shares get the same lease data, we just grab the leases
4663-        # from the first share
4664-        try:
4665-            shnum, filename = self._get_bucket_shares(storage_index).next()
4666-            sf = ShareFile(filename)
4667-            return sf.get_leases()
4668-        except StopIteration:
4669-            return iter([])
4670+        This method is not for client use. XXX do we need it at all?
4671+        """
4672+        return self.backend.get_shareset(storageindex).get_leases()
4673 
4674hunk ./src/allmydata/storage/server.py 299
4675-    def remote_slot_testv_and_readv_and_writev(self, storage_index,
4676+    def remote_slot_testv_and_readv_and_writev(self, storageindex,
4677                                                secrets,
4678                                                test_and_write_vectors,
4679                                                read_vector):
4680hunk ./src/allmydata/storage/server.py 305
4681         start = time.time()
4682         self.count("writev")
4683-        si_s = si_b2a(storage_index)
4684+        si_s = si_b2a(storageindex)
4685         log.msg("storage: slot_writev %s" % si_s)
4686hunk ./src/allmydata/storage/server.py 307
4687-        si_dir = storage_index_to_dir(storage_index)
4688-        (write_enabler, renew_secret, cancel_secret) = secrets
4689-        # shares exist if there is a file for them
4690-        bucketdir = os.path.join(self.sharedir, si_dir)
4691-        shares = {}
4692-        if os.path.isdir(bucketdir):
4693-            for sharenum_s in os.listdir(bucketdir):
4694-                try:
4695-                    sharenum = int(sharenum_s)
4696-                except ValueError:
4697-                    continue
4698-                filename = os.path.join(bucketdir, sharenum_s)
4699-                msf = MutableShareFile(filename, self)
4700-                msf.check_write_enabler(write_enabler, si_s)
4701-                shares[sharenum] = msf
4702-        # write_enabler is good for all existing shares.
4703-
4704-        # Now evaluate test vectors.
4705-        testv_is_good = True
4706-        for sharenum in test_and_write_vectors:
4707-            (testv, datav, new_length) = test_and_write_vectors[sharenum]
4708-            if sharenum in shares:
4709-                if not shares[sharenum].check_testv(testv):
4710-                    self.log("testv failed: [%d]: %r" % (sharenum, testv))
4711-                    testv_is_good = False
4712-                    break
4713-            else:
4714-                # compare the vectors against an empty share, in which all
4715-                # reads return empty strings.
4716-                if not EmptyShare().check_testv(testv):
4717-                    self.log("testv failed (empty): [%d] %r" % (sharenum,
4718-                                                                testv))
4719-                    testv_is_good = False
4720-                    break
4721-
4722-        # now gather the read vectors, before we do any writes
4723-        read_data = {}
4724-        for sharenum, share in shares.items():
4725-            read_data[sharenum] = share.readv(read_vector)
4726-
4727-        ownerid = 1 # TODO
4728-        expire_time = time.time() + 31*24*60*60   # one month
4729-        lease_info = LeaseInfo(ownerid,
4730-                               renew_secret, cancel_secret,
4731-                               expire_time, self.my_nodeid)
4732-
4733-        if testv_is_good:
4734-            # now apply the write vectors
4735-            for sharenum in test_and_write_vectors:
4736-                (testv, datav, new_length) = test_and_write_vectors[sharenum]
4737-                if new_length == 0:
4738-                    if sharenum in shares:
4739-                        shares[sharenum].unlink()
4740-                else:
4741-                    if sharenum not in shares:
4742-                        # allocate a new share
4743-                        allocated_size = 2000 # arbitrary, really
4744-                        share = self._allocate_slot_share(bucketdir, secrets,
4745-                                                          sharenum,
4746-                                                          allocated_size,
4747-                                                          owner_num=0)
4748-                        shares[sharenum] = share
4749-                    shares[sharenum].writev(datav, new_length)
4750-                    # and update the lease
4751-                    shares[sharenum].add_or_renew_lease(lease_info)
4752-
4753-            if new_length == 0:
4754-                # delete empty bucket directories
4755-                if not os.listdir(bucketdir):
4756-                    os.rmdir(bucketdir)
4757 
4758hunk ./src/allmydata/storage/server.py 308
4759+        try:
4760+            shareset = self.backend.get_shareset(storageindex)
4761+            expiration_time = start + 31*24*60*60   # one month from now
4762+            return shareset.testv_and_readv_and_writev(self, secrets, test_and_write_vectors,
4763+                                                       read_vector, expiration_time)
4764+        finally:
4765+            self.add_latency("writev", time.time() - start)
4766 
4767hunk ./src/allmydata/storage/server.py 316
4768-        # all done
4769-        self.add_latency("writev", time.time() - start)
4770-        return (testv_is_good, read_data)
4771-
4772-    def _allocate_slot_share(self, bucketdir, secrets, sharenum,
4773-                             allocated_size, owner_num=0):
4774-        (write_enabler, renew_secret, cancel_secret) = secrets
4775-        my_nodeid = self.my_nodeid
4776-        fileutil.make_dirs(bucketdir)
4777-        filename = os.path.join(bucketdir, "%d" % sharenum)
4778-        share = create_mutable_sharefile(filename, my_nodeid, write_enabler,
4779-                                         self)
4780-        return share
4781-
4782-    def remote_slot_readv(self, storage_index, shares, readv):
4783+    def remote_slot_readv(self, storageindex, shares, readv):
4784         start = time.time()
4785         self.count("readv")
4786hunk ./src/allmydata/storage/server.py 319
4787-        si_s = si_b2a(storage_index)
4788-        lp = log.msg("storage: slot_readv %s %s" % (si_s, shares),
4789-                     facility="tahoe.storage", level=log.OPERATIONAL)
4790-        si_dir = storage_index_to_dir(storage_index)
4791-        # shares exist if there is a file for them
4792-        bucketdir = os.path.join(self.sharedir, si_dir)
4793-        if not os.path.isdir(bucketdir):
4794+        si_s = si_b2a(storageindex)
4795+        log.msg("storage: slot_readv %s %s" % (si_s, shares),
4796+                facility="tahoe.storage", level=log.OPERATIONAL)
4797+
4798+        try:
4799+            shareset = self.backend.get_shareset(storageindex)
4800+            return shareset.readv(self, shares, readv)
4801+        finally:
4802             self.add_latency("readv", time.time() - start)
4803hunk ./src/allmydata/storage/server.py 328
4804-            return {}
4805-        datavs = {}
4806-        for sharenum_s in os.listdir(bucketdir):
4807-            try:
4808-                sharenum = int(sharenum_s)
4809-            except ValueError:
4810-                continue
4811-            if sharenum in shares or not shares:
4812-                filename = os.path.join(bucketdir, sharenum_s)
4813-                msf = MutableShareFile(filename, self)
4814-                datavs[sharenum] = msf.readv(readv)
4815-        log.msg("returning shares %s" % (datavs.keys(),),
4816-                facility="tahoe.storage", level=log.NOISY, parent=lp)
4817-        self.add_latency("readv", time.time() - start)
4818-        return datavs
4819 
4820hunk ./src/allmydata/storage/server.py 329
4821-    def remote_advise_corrupt_share(self, share_type, storage_index, shnum,
4822-                                    reason):
4823-        fileutil.make_dirs(self.corruption_advisory_dir)
4824-        now = time_format.iso_utc(sep="T")
4825-        si_s = si_b2a(storage_index)
4826-        # windows can't handle colons in the filename
4827-        fn = os.path.join(self.corruption_advisory_dir,
4828-                          "%s--%s-%d" % (now, si_s, shnum)).replace(":","")
4829-        f = open(fn, "w")
4830-        f.write("report: Share Corruption\n")
4831-        f.write("type: %s\n" % share_type)
4832-        f.write("storage_index: %s\n" % si_s)
4833-        f.write("share_number: %d\n" % shnum)
4834-        f.write("\n")
4835-        f.write(reason)
4836-        f.write("\n")
4837-        f.close()
4838-        log.msg(format=("client claims corruption in (%(share_type)s) " +
4839-                        "%(si)s-%(shnum)d: %(reason)s"),
4840-                share_type=share_type, si=si_s, shnum=shnum, reason=reason,
4841-                level=log.SCARY, umid="SGx2fA")
4842-        return None
4843+    def remote_advise_corrupt_share(self, share_type, storage_index, shnum, reason):
4844+        self.backend.advise_corrupt_share(share_type, storage_index, shnum, reason)
4845hunk ./src/allmydata/test/common.py 20
4846 from allmydata.mutable.common import CorruptShareError
4847 from allmydata.mutable.layout import unpack_header
4848 from allmydata.mutable.publish import MutableData
4849-from allmydata.storage.mutable import MutableShareFile
4850+from allmydata.storage.backends.disk.mutable import MutableDiskShare
4851 from allmydata.util import hashutil, log, fileutil, pollmixin
4852 from allmydata.util.assertutil import precondition
4853 from allmydata.util.consumer import download_to_data
4854hunk ./src/allmydata/test/common.py 1297
4855 
4856 def _corrupt_mutable_share_data(data, debug=False):
4857     prefix = data[:32]
4858-    assert prefix == MutableShareFile.MAGIC, "This function is designed to corrupt mutable shares of v1, and the magic number doesn't look right: %r vs %r" % (prefix, MutableShareFile.MAGIC)
4859-    data_offset = MutableShareFile.DATA_OFFSET
4860+    assert prefix == MutableDiskShare.MAGIC, "This function is designed to corrupt mutable shares of v1, and the magic number doesn't look right: %r vs %r" % (prefix, MutableDiskShare.MAGIC)
4861+    data_offset = MutableDiskShare.DATA_OFFSET
4862     sharetype = data[data_offset:data_offset+1]
4863     assert sharetype == "\x00", "non-SDMF mutable shares not supported"
4864     (version, ig_seqnum, ig_roothash, ig_IV, ig_k, ig_N, ig_segsize,
4865hunk ./src/allmydata/test/no_network.py 21
4866 from twisted.application import service
4867 from twisted.internet import defer, reactor
4868 from twisted.python.failure import Failure
4869+from twisted.python.filepath import FilePath
4870 from foolscap.api import Referenceable, fireEventually, RemoteException
4871 from base64 import b32encode
4872hunk ./src/allmydata/test/no_network.py 24
4873+
4874 from allmydata import uri as tahoe_uri
4875 from allmydata.client import Client
4876hunk ./src/allmydata/test/no_network.py 27
4877-from allmydata.storage.server import StorageServer, storage_index_to_dir
4878+from allmydata.storage.server import StorageServer
4879+from allmydata.storage.backends.disk.disk_backend import DiskBackend
4880 from allmydata.util import fileutil, idlib, hashutil
4881 from allmydata.util.hashutil import sha1
4882 from allmydata.test.common_web import HTTPClientGETFactory
4883hunk ./src/allmydata/test/no_network.py 155
4884             seed = server.get_permutation_seed()
4885             return sha1(peer_selection_index + seed).digest()
4886         return sorted(self.get_connected_servers(), key=_permuted)
4887+
4888     def get_connected_servers(self):
4889         return self.client._servers
4890hunk ./src/allmydata/test/no_network.py 158
4891+
4892     def get_nickname_for_serverid(self, serverid):
4893         return None
4894 
4895hunk ./src/allmydata/test/no_network.py 162
4896+    def get_known_servers(self):
4897+        return self.get_connected_servers()
4898+
4899+    def get_all_serverids(self):
4900+        return self.client.get_all_serverids()
4901+
4902+
4903 class NoNetworkClient(Client):
4904     def create_tub(self):
4905         pass
4906hunk ./src/allmydata/test/no_network.py 262
4907 
4908     def make_server(self, i, readonly=False):
4909         serverid = hashutil.tagged_hash("serverid", str(i))[:20]
4910-        serverdir = os.path.join(self.basedir, "servers",
4911-                                 idlib.shortnodeid_b2a(serverid), "storage")
4912-        fileutil.make_dirs(serverdir)
4913-        ss = StorageServer(serverdir, serverid, stats_provider=SimpleStats(),
4914-                           readonly_storage=readonly)
4915+        storagedir = FilePath(self.basedir).child("servers").child(idlib.shortnodeid_b2a(serverid)).child("storage")
4916+
4917+        # The backend will make the storage directory and any necessary parents.
4918+        backend = DiskBackend(storagedir, readonly=readonly)
4919+        ss = StorageServer(serverid, backend, storagedir, stats_provider=SimpleStats())
4920         ss._no_network_server_number = i
4921         return ss
4922 
4923hunk ./src/allmydata/test/no_network.py 276
4924         middleman = service.MultiService()
4925         middleman.setServiceParent(self)
4926         ss.setServiceParent(middleman)
4927-        serverid = ss.my_nodeid
4928+        serverid = ss.get_serverid()
4929         self.servers_by_number[i] = ss
4930         wrapper = wrap_storage_server(ss)
4931         self.wrappers_by_id[serverid] = wrapper
4932hunk ./src/allmydata/test/no_network.py 295
4933         # it's enough to remove the server from c._servers (we don't actually
4934         # have to detach and stopService it)
4935         for i,ss in self.servers_by_number.items():
4936-            if ss.my_nodeid == serverid:
4937+            if ss.get_serverid() == serverid:
4938                 del self.servers_by_number[i]
4939                 break
4940         del self.wrappers_by_id[serverid]
4941hunk ./src/allmydata/test/no_network.py 345
4942     def get_clientdir(self, i=0):
4943         return self.g.clients[i].basedir
4944 
4945+    def get_server(self, i):
4946+        return self.g.servers_by_number[i]
4947+
4948     def get_serverdir(self, i):
4949hunk ./src/allmydata/test/no_network.py 349
4950-        return self.g.servers_by_number[i].storedir
4951+        return self.g.servers_by_number[i].backend.storedir
4952+
4953+    def remove_server(self, i):
4954+        self.g.remove_server(self.g.servers_by_number[i].get_serverid())
4955 
4956     def iterate_servers(self):
4957         for i in sorted(self.g.servers_by_number.keys()):
4958hunk ./src/allmydata/test/no_network.py 357
4959             ss = self.g.servers_by_number[i]
4960-            yield (i, ss, ss.storedir)
4961+            yield (i, ss, ss.backend.storedir)
4962 
4963     def find_uri_shares(self, uri):
4964         si = tahoe_uri.from_string(uri).get_storage_index()
4965hunk ./src/allmydata/test/no_network.py 361
4966-        prefixdir = storage_index_to_dir(si)
4967         shares = []
4968         for i,ss in self.g.servers_by_number.items():
4969hunk ./src/allmydata/test/no_network.py 363
4970-            serverid = ss.my_nodeid
4971-            basedir = os.path.join(ss.sharedir, prefixdir)
4972-            if not os.path.exists(basedir):
4973-                continue
4974-            for f in os.listdir(basedir):
4975-                try:
4976-                    shnum = int(f)
4977-                    shares.append((shnum, serverid, os.path.join(basedir, f)))
4978-                except ValueError:
4979-                    pass
4980+            for share in ss.backend.get_shareset(si).get_shares():
4981+                shares.append((share.get_shnum(), ss.get_serverid(), share._home))
4982         return sorted(shares)
4983 
4984hunk ./src/allmydata/test/no_network.py 367
4985+    def count_leases(self, uri):
4986+        """Return (filename, leasecount) pairs in arbitrary order."""
4987+        si = tahoe_uri.from_string(uri).get_storage_index()
4988+        lease_counts = []
4989+        for i,ss in self.g.servers_by_number.items():
4990+            for share in ss.backend.get_shareset(si).get_shares():
4991+                num_leases = len(list(share.get_leases()))
4992+                lease_counts.append( (share._home.path, num_leases) )
4993+        return lease_counts
4994+
4995     def copy_shares(self, uri):
4996         shares = {}
4997hunk ./src/allmydata/test/no_network.py 379
4998-        for (shnum, serverid, sharefile) in self.find_uri_shares(uri):
4999-            shares[sharefile] = open(sharefile, "rb").read()
5000+        for (shnum, serverid, sharefp) in self.find_uri_shares(uri):
5001+            shares[sharefp.path] = sharefp.getContent()
5002         return shares
5003 
5004hunk ./src/allmydata/test/no_network.py 383
5005+    def copy_share(self, from_share, uri, to_server):
5006+        si = uri.from_string(self.uri).get_storage_index()
5007+        (i_shnum, i_serverid, i_sharefp) = from_share
5008+        shares_dir = to_server.backend.get_shareset(si)._sharehomedir
5009+        i_sharefp.copyTo(shares_dir.child(str(i_shnum)))
5010+
5011     def restore_all_shares(self, shares):
5012hunk ./src/allmydata/test/no_network.py 390
5013-        for sharefile, data in shares.items():
5014-            open(sharefile, "wb").write(data)
5015+        for share, data in shares.items():
5016+            share.home.setContent(data)
5017 
5018hunk ./src/allmydata/test/no_network.py 393
5019-    def delete_share(self, (shnum, serverid, sharefile)):
5020-        os.unlink(sharefile)
5021+    def delete_share(self, (shnum, serverid, sharefp)):
5022+        sharefp.remove()
5023 
5024     def delete_shares_numbered(self, uri, shnums):
5025hunk ./src/allmydata/test/no_network.py 397
5026-        for (i_shnum, i_serverid, i_sharefile) in self.find_uri_shares(uri):
5027+        for (i_shnum, i_serverid, i_sharefp) in self.find_uri_shares(uri):
5028             if i_shnum in shnums:
5029hunk ./src/allmydata/test/no_network.py 399
5030-                os.unlink(i_sharefile)
5031+                i_sharefp.remove()
5032 
5033hunk ./src/allmydata/test/no_network.py 401
5034-    def corrupt_share(self, (shnum, serverid, sharefile), corruptor_function):
5035-        sharedata = open(sharefile, "rb").read()
5036-        corruptdata = corruptor_function(sharedata)
5037-        open(sharefile, "wb").write(corruptdata)
5038+    def corrupt_share(self, (shnum, serverid, sharefp), corruptor_function, debug=False):
5039+        sharedata = sharefp.getContent()
5040+        corruptdata = corruptor_function(sharedata, debug=debug)
5041+        sharefp.setContent(corruptdata)
5042 
5043     def corrupt_shares_numbered(self, uri, shnums, corruptor, debug=False):
5044hunk ./src/allmydata/test/no_network.py 407
5045-        for (i_shnum, i_serverid, i_sharefile) in self.find_uri_shares(uri):
5046+        for (i_shnum, i_serverid, i_sharefp) in self.find_uri_shares(uri):
5047             if i_shnum in shnums:
5048hunk ./src/allmydata/test/no_network.py 409
5049-                sharedata = open(i_sharefile, "rb").read()
5050-                corruptdata = corruptor(sharedata, debug=debug)
5051-                open(i_sharefile, "wb").write(corruptdata)
5052+                self.corrupt_share((i_shnum, i_serverid, i_sharefp), corruptor, debug=debug)
5053 
5054     def corrupt_all_shares(self, uri, corruptor, debug=False):
5055hunk ./src/allmydata/test/no_network.py 412
5056-        for (i_shnum, i_serverid, i_sharefile) in self.find_uri_shares(uri):
5057-            sharedata = open(i_sharefile, "rb").read()
5058-            corruptdata = corruptor(sharedata, debug=debug)
5059-            open(i_sharefile, "wb").write(corruptdata)
5060+        for (i_shnum, i_serverid, i_sharefp) in self.find_uri_shares(uri):
5061+            self.corrupt_share((i_shnum, i_serverid, i_sharefp), corruptor, debug=debug)
5062 
5063     def GET(self, urlpath, followRedirect=False, return_response=False,
5064             method="GET", clientnum=0, **kwargs):
5065hunk ./src/allmydata/test/test_download.py 6
5066 # a previous run. This asserts that the current code is capable of decoding
5067 # shares from a previous version.
5068 
5069-import os
5070 from twisted.trial import unittest
5071 from twisted.internet import defer, reactor
5072 from allmydata import uri
5073hunk ./src/allmydata/test/test_download.py 9
5074-from allmydata.storage.server import storage_index_to_dir
5075 from allmydata.util import base32, fileutil, spans, log, hashutil
5076 from allmydata.util.consumer import download_to_data, MemoryConsumer
5077 from allmydata.immutable import upload, layout
5078hunk ./src/allmydata/test/test_download.py 85
5079         u = upload.Data(plaintext, None)
5080         d = self.c0.upload(u)
5081         f = open("stored_shares.py", "w")
5082-        def _created_immutable(ur):
5083-            # write the generated shares and URI to a file, which can then be
5084-            # incorporated into this one next time.
5085-            f.write('immutable_uri = "%s"\n' % ur.uri)
5086-            f.write('immutable_shares = {\n')
5087-            si = uri.from_string(ur.uri).get_storage_index()
5088-            si_dir = storage_index_to_dir(si)
5089+
5090+        def _write_py(uri):
5091+            si = uri.from_string(uri).get_storage_index()
5092             for (i,ss,ssdir) in self.iterate_servers():
5093hunk ./src/allmydata/test/test_download.py 89
5094-                sharedir = os.path.join(ssdir, "shares", si_dir)
5095                 shares = {}
5096hunk ./src/allmydata/test/test_download.py 90
5097-                for fn in os.listdir(sharedir):
5098-                    shnum = int(fn)
5099-                    sharedata = open(os.path.join(sharedir, fn), "rb").read()
5100-                    shares[shnum] = sharedata
5101-                fileutil.rm_dir(sharedir)
5102+                shareset = ss.backend.get_shareset(si)
5103+                for share in shareset.get_shares():
5104+                    sharedata = share._home.getContent()
5105+                    shares[share.get_shnum()] = sharedata
5106+
5107+                fileutil.fp_remove(shareset._sharehomedir)
5108                 if shares:
5109                     f.write(' %d: { # client[%d]\n' % (i, i))
5110                     for shnum in sorted(shares.keys()):
5111hunk ./src/allmydata/test/test_download.py 103
5112                                 (shnum, base32.b2a(shares[shnum])))
5113                     f.write('    },\n')
5114             f.write('}\n')
5115-            f.write('\n')
5116 
5117hunk ./src/allmydata/test/test_download.py 104
5118+        def _created_immutable(ur):
5119+            # write the generated shares and URI to a file, which can then be
5120+            # incorporated into this one next time.
5121+            f.write('immutable_uri = "%s"\n' % ur.uri)
5122+            f.write('immutable_shares = {\n')
5123+            _write_py(ur.uri)
5124+            f.write('\n')
5125         d.addCallback(_created_immutable)
5126 
5127         d.addCallback(lambda ignored:
5128hunk ./src/allmydata/test/test_download.py 118
5129         def _created_mutable(n):
5130             f.write('mutable_uri = "%s"\n' % n.get_uri())
5131             f.write('mutable_shares = {\n')
5132-            si = uri.from_string(n.get_uri()).get_storage_index()
5133-            si_dir = storage_index_to_dir(si)
5134-            for (i,ss,ssdir) in self.iterate_servers():
5135-                sharedir = os.path.join(ssdir, "shares", si_dir)
5136-                shares = {}
5137-                for fn in os.listdir(sharedir):
5138-                    shnum = int(fn)
5139-                    sharedata = open(os.path.join(sharedir, fn), "rb").read()
5140-                    shares[shnum] = sharedata
5141-                fileutil.rm_dir(sharedir)
5142-                if shares:
5143-                    f.write(' %d: { # client[%d]\n' % (i, i))
5144-                    for shnum in sorted(shares.keys()):
5145-                        f.write('  %d: base32.a2b("%s"),\n' %
5146-                                (shnum, base32.b2a(shares[shnum])))
5147-                    f.write('    },\n')
5148-            f.write('}\n')
5149-
5150-            f.close()
5151+            _write_py(n.get_uri())
5152         d.addCallback(_created_mutable)
5153 
5154         def _done(ignored):
5155hunk ./src/allmydata/test/test_download.py 123
5156             f.close()
5157-        d.addCallback(_done)
5158+        d.addBoth(_done)
5159 
5160         return d
5161 
5162hunk ./src/allmydata/test/test_download.py 127
5163+    def _write_shares(self, uri, shares):
5164+        si = uri.from_string(uri).get_storage_index()
5165+        for i in shares:
5166+            shares_for_server = shares[i]
5167+            for shnum in shares_for_server:
5168+                share_dir = self.get_server(i).backend.get_shareset(si)._sharehomedir
5169+                fileutil.fp_make_dirs(share_dir)
5170+                share_dir.child(str(shnum)).setContent(shares[shnum])
5171+
5172     def load_shares(self, ignored=None):
5173         # this uses the data generated by create_shares() to populate the
5174         # storage servers with pre-generated shares
5175hunk ./src/allmydata/test/test_download.py 139
5176-        si = uri.from_string(immutable_uri).get_storage_index()
5177-        si_dir = storage_index_to_dir(si)
5178-        for i in immutable_shares:
5179-            shares = immutable_shares[i]
5180-            for shnum in shares:
5181-                dn = os.path.join(self.get_serverdir(i), "shares", si_dir)
5182-                fileutil.make_dirs(dn)
5183-                fn = os.path.join(dn, str(shnum))
5184-                f = open(fn, "wb")
5185-                f.write(shares[shnum])
5186-                f.close()
5187-
5188-        si = uri.from_string(mutable_uri).get_storage_index()
5189-        si_dir = storage_index_to_dir(si)
5190-        for i in mutable_shares:
5191-            shares = mutable_shares[i]
5192-            for shnum in shares:
5193-                dn = os.path.join(self.get_serverdir(i), "shares", si_dir)
5194-                fileutil.make_dirs(dn)
5195-                fn = os.path.join(dn, str(shnum))
5196-                f = open(fn, "wb")
5197-                f.write(shares[shnum])
5198-                f.close()
5199+        self._write_shares(immutable_uri, immutable_shares)
5200+        self._write_shares(mutable_uri, mutable_shares)
5201 
5202     def download_immutable(self, ignored=None):
5203         n = self.c0.create_node_from_uri(immutable_uri)
5204hunk ./src/allmydata/test/test_download.py 183
5205 
5206         self.load_shares()
5207         si = uri.from_string(immutable_uri).get_storage_index()
5208-        si_dir = storage_index_to_dir(si)
5209 
5210         n = self.c0.create_node_from_uri(immutable_uri)
5211         d = download_to_data(n)
5212hunk ./src/allmydata/test/test_download.py 198
5213                 for clientnum in immutable_shares:
5214                     for shnum in immutable_shares[clientnum]:
5215                         if s._shnum == shnum:
5216-                            fn = os.path.join(self.get_serverdir(clientnum),
5217-                                              "shares", si_dir, str(shnum))
5218-                            os.unlink(fn)
5219+                            share_dir = self.get_server(clientnum).backend.get_shareset(si)._sharehomedir
5220+                            share_dir.child(str(shnum)).remove()
5221         d.addCallback(_clobber_some_shares)
5222         d.addCallback(lambda ign: download_to_data(n))
5223         d.addCallback(_got_data)
5224hunk ./src/allmydata/test/test_download.py 212
5225                 for shnum in immutable_shares[clientnum]:
5226                     if shnum == save_me:
5227                         continue
5228-                    fn = os.path.join(self.get_serverdir(clientnum),
5229-                                      "shares", si_dir, str(shnum))
5230-                    if os.path.exists(fn):
5231-                        os.unlink(fn)
5232+                    share_dir = self.get_server(clientnum).backend.get_shareset(si)._sharehomedir
5233+                    fileutil.fp_remove(share_dir.child(str(shnum)))
5234             # now the download should fail with NotEnoughSharesError
5235             return self.shouldFail(NotEnoughSharesError, "1shares", None,
5236                                    download_to_data, n)
5237hunk ./src/allmydata/test/test_download.py 223
5238             # delete the last remaining share
5239             for clientnum in immutable_shares:
5240                 for shnum in immutable_shares[clientnum]:
5241-                    fn = os.path.join(self.get_serverdir(clientnum),
5242-                                      "shares", si_dir, str(shnum))
5243-                    if os.path.exists(fn):
5244-                        os.unlink(fn)
5245+                    share_dir = self.get_server(clientnum).backend.get_shareset(si)._sharehomedir
5246+                    share_dir.child(str(shnum)).remove()
5247             # now a new download should fail with NoSharesError. We want a
5248             # new ImmutableFileNode so it will forget about the old shares.
5249             # If we merely called create_node_from_uri() without first
5250hunk ./src/allmydata/test/test_download.py 801
5251         # will report two shares, and the ShareFinder will handle the
5252         # duplicate by attaching both to the same CommonShare instance.
5253         si = uri.from_string(immutable_uri).get_storage_index()
5254-        si_dir = storage_index_to_dir(si)
5255-        sh0_file = [sharefile
5256-                    for (shnum, serverid, sharefile)
5257-                    in self.find_uri_shares(immutable_uri)
5258-                    if shnum == 0][0]
5259-        sh0_data = open(sh0_file, "rb").read()
5260+        sh0_fp = [sharefp for (shnum, serverid, sharefp)
5261+                          in self.find_uri_shares(immutable_uri)
5262+                          if shnum == 0][0]
5263+        sh0_data = sh0_fp.getContent()
5264         for clientnum in immutable_shares:
5265             if 0 in immutable_shares[clientnum]:
5266                 continue
5267hunk ./src/allmydata/test/test_download.py 808
5268-            cdir = self.get_serverdir(clientnum)
5269-            target = os.path.join(cdir, "shares", si_dir, "0")
5270-            outf = open(target, "wb")
5271-            outf.write(sh0_data)
5272-            outf.close()
5273+            cdir = self.get_server(clientnum).backend.get_shareset(si)._sharehomedir
5274+            fileutil.fp_make_dirs(cdir)
5275+            cdir.child(str(shnum)).setContent(sh0_data)
5276 
5277         d = self.download_immutable()
5278         return d
5279hunk ./src/allmydata/test/test_encode.py 134
5280         d.addCallback(_try)
5281         return d
5282 
5283-    def get_share_hashes(self, at_least_these=()):
5284+    def get_share_hashes(self):
5285         d = self._start()
5286         def _try(unused=None):
5287             if self.mode == "bad sharehash":
5288hunk ./src/allmydata/test/test_hung_server.py 3
5289 # -*- coding: utf-8 -*-
5290 
5291-import os, shutil
5292 from twisted.trial import unittest
5293 from twisted.internet import defer
5294hunk ./src/allmydata/test/test_hung_server.py 5
5295-from allmydata import uri
5296+
5297 from allmydata.util.consumer import download_to_data
5298 from allmydata.immutable import upload
5299 from allmydata.mutable.common import UnrecoverableFileError
5300hunk ./src/allmydata/test/test_hung_server.py 10
5301 from allmydata.mutable.publish import MutableData
5302-from allmydata.storage.common import storage_index_to_dir
5303 from allmydata.test.no_network import GridTestMixin
5304 from allmydata.test.common import ShouldFailMixin
5305 from allmydata.util.pollmixin import PollMixin
5306hunk ./src/allmydata/test/test_hung_server.py 18
5307 immutable_plaintext = "data" * 10000
5308 mutable_plaintext = "muta" * 10000
5309 
5310+
5311 class HungServerDownloadTest(GridTestMixin, ShouldFailMixin, PollMixin,
5312                              unittest.TestCase):
5313     # Many of these tests take around 60 seconds on François's ARM buildslave:
5314hunk ./src/allmydata/test/test_hung_server.py 31
5315     timeout = 240
5316 
5317     def _break(self, servers):
5318-        for (id, ss) in servers:
5319-            self.g.break_server(id)
5320+        for ss in servers:
5321+            self.g.break_server(ss.get_serverid())
5322 
5323     def _hang(self, servers, **kwargs):
5324hunk ./src/allmydata/test/test_hung_server.py 35
5325-        for (id, ss) in servers:
5326-            self.g.hang_server(id, **kwargs)
5327+        for ss in servers:
5328+            self.g.hang_server(ss.get_serverid(), **kwargs)
5329 
5330     def _unhang(self, servers, **kwargs):
5331hunk ./src/allmydata/test/test_hung_server.py 39
5332-        for (id, ss) in servers:
5333-            self.g.unhang_server(id, **kwargs)
5334+        for ss in servers:
5335+            self.g.unhang_server(ss.get_serverid(), **kwargs)
5336 
5337     def _hang_shares(self, shnums, **kwargs):
5338         # hang all servers who are holding the given shares
5339hunk ./src/allmydata/test/test_hung_server.py 52
5340                     hung_serverids.add(i_serverid)
5341 
5342     def _delete_all_shares_from(self, servers):
5343-        serverids = [id for (id, ss) in servers]
5344-        for (i_shnum, i_serverid, i_sharefile) in self.shares:
5345+        serverids = [ss.get_serverid() for ss in servers]
5346+        for (i_shnum, i_serverid, i_sharefp) in self.shares:
5347             if i_serverid in serverids:
5348hunk ./src/allmydata/test/test_hung_server.py 55
5349-                os.unlink(i_sharefile)
5350+                i_sharefp.remove()
5351 
5352     def _corrupt_all_shares_in(self, servers, corruptor_func):
5353hunk ./src/allmydata/test/test_hung_server.py 58
5354-        serverids = [id for (id, ss) in servers]
5355-        for (i_shnum, i_serverid, i_sharefile) in self.shares:
5356+        serverids = [ss.get_serverid() for ss in servers]
5357+        for (i_shnum, i_serverid, i_sharefp) in self.shares:
5358             if i_serverid in serverids:
5359hunk ./src/allmydata/test/test_hung_server.py 61
5360-                self._corrupt_share((i_shnum, i_sharefile), corruptor_func)
5361+                self.corrupt_share((i_shnum, i_serverid, i_sharefp), corruptor_func)
5362 
5363     def _copy_all_shares_from(self, from_servers, to_server):
5364hunk ./src/allmydata/test/test_hung_server.py 64
5365-        serverids = [id for (id, ss) in from_servers]
5366-        for (i_shnum, i_serverid, i_sharefile) in self.shares:
5367+        serverids = [ss.get_serverid() for ss in from_servers]
5368+        for (i_shnum, i_serverid, i_sharefp) in self.shares:
5369             if i_serverid in serverids:
5370hunk ./src/allmydata/test/test_hung_server.py 67
5371-                self._copy_share((i_shnum, i_sharefile), to_server)
5372+                self.copy_share((i_shnum, i_serverid, i_sharefp), self.uri, to_server)
5373 
5374hunk ./src/allmydata/test/test_hung_server.py 69
5375-    def _copy_share(self, share, to_server):
5376-        (sharenum, sharefile) = share
5377-        (id, ss) = to_server
5378-        shares_dir = os.path.join(ss.original.storedir, "shares")
5379-        si = uri.from_string(self.uri).get_storage_index()
5380-        si_dir = os.path.join(shares_dir, storage_index_to_dir(si))
5381-        if not os.path.exists(si_dir):
5382-            os.makedirs(si_dir)
5383-        new_sharefile = os.path.join(si_dir, str(sharenum))
5384-        shutil.copy(sharefile, new_sharefile)
5385         self.shares = self.find_uri_shares(self.uri)
5386hunk ./src/allmydata/test/test_hung_server.py 70
5387-        # Make sure that the storage server has the share.
5388-        self.failUnless((sharenum, ss.original.my_nodeid, new_sharefile)
5389-                        in self.shares)
5390-
5391-    def _corrupt_share(self, share, corruptor_func):
5392-        (sharenum, sharefile) = share
5393-        data = open(sharefile, "rb").read()
5394-        newdata = corruptor_func(data)
5395-        os.unlink(sharefile)
5396-        wf = open(sharefile, "wb")
5397-        wf.write(newdata)
5398-        wf.close()
5399 
5400     def _set_up(self, mutable, testdir, num_clients=1, num_servers=10):
5401         self.mutable = mutable
5402hunk ./src/allmydata/test/test_hung_server.py 82
5403 
5404         self.c0 = self.g.clients[0]
5405         nm = self.c0.nodemaker
5406-        self.servers = sorted([(s.get_serverid(), s.get_rref())
5407-                               for s in nm.storage_broker.get_connected_servers()])
5408+        unsorted = [(s.get_serverid(), s.get_rref()) for s in nm.storage_broker.get_connected_servers()]
5409+        self.servers = [ss for (id, ss) in sorted(unsorted)]
5410         self.servers = self.servers[5:] + self.servers[:5]
5411 
5412         if mutable:
5413hunk ./src/allmydata/test/test_hung_server.py 244
5414             # stuck-but-not-overdue, and 4 live requests. All 4 live requests
5415             # will retire before the download is complete and the ShareFinder
5416             # is shut off. That will leave 4 OVERDUE and 1
5417-            # stuck-but-not-overdue, for a total of 5 requests in in
5418+            # stuck-but-not-overdue, for a total of 5 requests in
5419             # _sf.pending_requests
5420             for t in self._sf.overdue_timers.values()[:4]:
5421                 t.reset(-1.0)
5422hunk ./src/allmydata/test/test_mutable.py 21
5423 from foolscap.api import eventually, fireEventually
5424 from foolscap.logging import log
5425 from allmydata.storage_client import StorageFarmBroker
5426-from allmydata.storage.common import storage_index_to_dir
5427 from allmydata.scripts import debug
5428 
5429 from allmydata.mutable.filenode import MutableFileNode, BackoffAgent
5430hunk ./src/allmydata/test/test_mutable.py 3662
5431         # Now execute each assignment by writing the storage.
5432         for (share, servernum) in assignments:
5433             sharedata = base64.b64decode(self.sdmf_old_shares[share])
5434-            storedir = self.get_serverdir(servernum)
5435-            storage_path = os.path.join(storedir, "shares",
5436-                                        storage_index_to_dir(si))
5437-            fileutil.make_dirs(storage_path)
5438-            fileutil.write(os.path.join(storage_path, "%d" % share),
5439-                           sharedata)
5440+            storage_dir = self.get_server(servernum).backend.get_shareset(si).sharehomedir
5441+            fileutil.fp_make_dirs(storage_dir)
5442+            storage_dir.child("%d" % share).setContent(sharedata)
5443         # ...and verify that the shares are there.
5444         shares = self.find_uri_shares(self.sdmf_old_cap)
5445         assert len(shares) == 10
5446hunk ./src/allmydata/test/test_provisioning.py 13
5447 from nevow import inevow
5448 from zope.interface import implements
5449 
5450-class MyRequest:
5451+class MockRequest:
5452     implements(inevow.IRequest)
5453     pass
5454 
5455hunk ./src/allmydata/test/test_provisioning.py 26
5456     def test_load(self):
5457         pt = provisioning.ProvisioningTool()
5458         self.fields = {}
5459-        #r = MyRequest()
5460+        #r = MockRequest()
5461         #r.fields = self.fields
5462         #ctx = RequestContext()
5463         #unfilled = pt.renderSynchronously(ctx)
5464hunk ./src/allmydata/test/test_repairer.py 537
5465         # happiness setting.
5466         def _delete_some_servers(ignored):
5467             for i in xrange(7):
5468-                self.g.remove_server(self.g.servers_by_number[i].my_nodeid)
5469+                self.remove_server(i)
5470 
5471             assert len(self.g.servers_by_number) == 3
5472 
5473hunk ./src/allmydata/test/test_storage.py 14
5474 from allmydata import interfaces
5475 from allmydata.util import fileutil, hashutil, base32, pollmixin, time_format
5476 from allmydata.storage.server import StorageServer
5477-from allmydata.storage.mutable import MutableShareFile
5478-from allmydata.storage.immutable import BucketWriter, BucketReader
5479-from allmydata.storage.common import DataTooLargeError, storage_index_to_dir, \
5480+from allmydata.storage.backends.disk.mutable import MutableDiskShare
5481+from allmydata.storage.bucket import BucketWriter, BucketReader
5482+from allmydata.storage.common import DataTooLargeError, \
5483      UnknownMutableContainerVersionError, UnknownImmutableContainerVersionError
5484 from allmydata.storage.lease import LeaseInfo
5485 from allmydata.storage.crawler import BucketCountingCrawler
5486hunk ./src/allmydata/test/test_storage.py 474
5487         w[0].remote_write(0, "\xff"*10)
5488         w[0].remote_close()
5489 
5490-        fn = os.path.join(ss.sharedir, storage_index_to_dir("si1"), "0")
5491-        f = open(fn, "rb+")
5492+        fp = ss.backend.get_shareset("si1").sharehomedir.child("0")
5493+        f = fp.open("rb+")
5494         f.seek(0)
5495         f.write(struct.pack(">L", 0)) # this is invalid: minimum used is v1
5496         f.close()
5497hunk ./src/allmydata/test/test_storage.py 814
5498     def test_bad_magic(self):
5499         ss = self.create("test_bad_magic")
5500         self.allocate(ss, "si1", "we1", self._lease_secret.next(), set([0]), 10)
5501-        fn = os.path.join(ss.sharedir, storage_index_to_dir("si1"), "0")
5502-        f = open(fn, "rb+")
5503+        fp = ss.backend.get_shareset("si1").sharehomedir.child("0")
5504+        f = fp.open("rb+")
5505         f.seek(0)
5506         f.write("BAD MAGIC")
5507         f.close()
5508hunk ./src/allmydata/test/test_storage.py 842
5509 
5510         # Trying to make the container too large (by sending a write vector
5511         # whose offset is too high) will raise an exception.
5512-        TOOBIG = MutableShareFile.MAX_SIZE + 10
5513+        TOOBIG = MutableDiskShare.MAX_SIZE + 10
5514         self.failUnlessRaises(DataTooLargeError,
5515                               rstaraw, "si1", secrets,
5516                               {0: ([], [(TOOBIG,data)], None)},
5517hunk ./src/allmydata/test/test_storage.py 1229
5518 
5519         # create a random non-numeric file in the bucket directory, to
5520         # exercise the code that's supposed to ignore those.
5521-        bucket_dir = os.path.join(self.workdir("test_leases"),
5522-                                  "shares", storage_index_to_dir("si1"))
5523-        f = open(os.path.join(bucket_dir, "ignore_me.txt"), "w")
5524-        f.write("you ought to be ignoring me\n")
5525-        f.close()
5526+        bucket_dir = ss.backend.get_shareset("si1").sharehomedir
5527+        bucket_dir.child("ignore_me.txt").setContent("you ought to be ignoring me\n")
5528 
5529hunk ./src/allmydata/test/test_storage.py 1232
5530-        s0 = MutableShareFile(os.path.join(bucket_dir, "0"))
5531+        s0 = MutableDiskShare(os.path.join(bucket_dir, "0"))
5532         self.failUnlessEqual(len(list(s0.get_leases())), 1)
5533 
5534         # add-lease on a missing storage index is silently ignored
5535hunk ./src/allmydata/test/test_storage.py 3118
5536         [immutable_si_0, immutable_si_1, mutable_si_2, mutable_si_3] = self.sis
5537 
5538         # add a non-sharefile to exercise another code path
5539-        fn = os.path.join(ss.sharedir,
5540-                          storage_index_to_dir(immutable_si_0),
5541-                          "not-a-share")
5542-        f = open(fn, "wb")
5543-        f.write("I am not a share.\n")
5544-        f.close()
5545+        fp = ss.backend.get_shareset(immutable_si_0).sharehomedir.child("not-a-share")
5546+        fp.setContent("I am not a share.\n")
5547 
5548         # this is before the crawl has started, so we're not in a cycle yet
5549         initial_state = lc.get_state()
5550hunk ./src/allmydata/test/test_storage.py 3282
5551     def test_expire_age(self):
5552         basedir = "storage/LeaseCrawler/expire_age"
5553         fileutil.make_dirs(basedir)
5554-        # setting expiration_time to 2000 means that any lease which is more
5555-        # than 2000s old will be expired.
5556-        ss = InstrumentedStorageServer(basedir, "\x00" * 20,
5557-                                       expiration_enabled=True,
5558-                                       expiration_mode="age",
5559-                                       expiration_override_lease_duration=2000)
5560+        # setting 'override_lease_duration' to 2000 means that any lease that
5561+        # is more than 2000 seconds old will be expired.
5562+        expiration_policy = {
5563+            'enabled': True,
5564+            'mode': 'age',
5565+            'override_lease_duration': 2000,
5566+            'sharetypes': ('mutable', 'immutable'),
5567+        }
5568+        ss = InstrumentedStorageServer(basedir, "\x00" * 20, expiration_policy)
5569         # make it start sooner than usual.
5570         lc = ss.lease_checker
5571         lc.slow_start = 0
5572hunk ./src/allmydata/test/test_storage.py 3423
5573     def test_expire_cutoff_date(self):
5574         basedir = "storage/LeaseCrawler/expire_cutoff_date"
5575         fileutil.make_dirs(basedir)
5576-        # setting cutoff-date to 2000 seconds ago means that any lease which
5577-        # is more than 2000s old will be expired.
5578+        # setting 'cutoff_date' to 2000 seconds ago means that any lease that
5579+        # is more than 2000 seconds old will be expired.
5580         now = time.time()
5581         then = int(now - 2000)
5582hunk ./src/allmydata/test/test_storage.py 3427
5583-        ss = InstrumentedStorageServer(basedir, "\x00" * 20,
5584-                                       expiration_enabled=True,
5585-                                       expiration_mode="cutoff-date",
5586-                                       expiration_cutoff_date=then)
5587+        expiration_policy = {
5588+            'enabled': True,
5589+            'mode': 'cutoff-date',
5590+            'cutoff_date': then,
5591+            'sharetypes': ('mutable', 'immutable'),
5592+        }
5593+        ss = InstrumentedStorageServer(basedir, "\x00" * 20, expiration_policy)
5594         # make it start sooner than usual.
5595         lc = ss.lease_checker
5596         lc.slow_start = 0
5597hunk ./src/allmydata/test/test_storage.py 3575
5598     def test_only_immutable(self):
5599         basedir = "storage/LeaseCrawler/only_immutable"
5600         fileutil.make_dirs(basedir)
5601+        # setting 'cutoff_date' to 2000 seconds ago means that any lease that
5602+        # is more than 2000 seconds old will be expired.
5603         now = time.time()
5604         then = int(now - 2000)
5605hunk ./src/allmydata/test/test_storage.py 3579
5606-        ss = StorageServer(basedir, "\x00" * 20,
5607-                           expiration_enabled=True,
5608-                           expiration_mode="cutoff-date",
5609-                           expiration_cutoff_date=then,
5610-                           expiration_sharetypes=("immutable",))
5611+        expiration_policy = {
5612+            'enabled': True,
5613+            'mode': 'cutoff-date',
5614+            'cutoff_date': then,
5615+            'sharetypes': ('immutable',),
5616+        }
5617+        ss = StorageServer(basedir, "\x00" * 20, expiration_policy)
5618         lc = ss.lease_checker
5619         lc.slow_start = 0
5620         webstatus = StorageStatus(ss)
5621hunk ./src/allmydata/test/test_storage.py 3636
5622     def test_only_mutable(self):
5623         basedir = "storage/LeaseCrawler/only_mutable"
5624         fileutil.make_dirs(basedir)
5625+        # setting 'cutoff_date' to 2000 seconds ago means that any lease that
5626+        # is more than 2000 seconds old will be expired.
5627         now = time.time()
5628         then = int(now - 2000)
5629hunk ./src/allmydata/test/test_storage.py 3640
5630-        ss = StorageServer(basedir, "\x00" * 20,
5631-                           expiration_enabled=True,
5632-                           expiration_mode="cutoff-date",
5633-                           expiration_cutoff_date=then,
5634-                           expiration_sharetypes=("mutable",))
5635+        expiration_policy = {
5636+            'enabled': True,
5637+            'mode': 'cutoff-date',
5638+            'cutoff_date': then,
5639+            'sharetypes': ('mutable',),
5640+        }
5641+        ss = StorageServer(basedir, "\x00" * 20, expiration_policy)
5642         lc = ss.lease_checker
5643         lc.slow_start = 0
5644         webstatus = StorageStatus(ss)
5645hunk ./src/allmydata/test/test_storage.py 3819
5646     def test_no_st_blocks(self):
5647         basedir = "storage/LeaseCrawler/no_st_blocks"
5648         fileutil.make_dirs(basedir)
5649-        ss = No_ST_BLOCKS_StorageServer(basedir, "\x00" * 20,
5650-                                        expiration_mode="age",
5651-                                        expiration_override_lease_duration=-1000)
5652-        # a negative expiration_time= means the "configured-"
5653+        # A negative 'override_lease_duration' means that the "configured-"
5654         # space-recovered counts will be non-zero, since all shares will have
5655hunk ./src/allmydata/test/test_storage.py 3821
5656-        # expired by then
5657+        # expired by then.
5658+        expiration_policy = {
5659+            'enabled': True,
5660+            'mode': 'age',
5661+            'override_lease_duration': -1000,
5662+            'sharetypes': ('mutable', 'immutable'),
5663+        }
5664+        ss = No_ST_BLOCKS_StorageServer(basedir, "\x00" * 20, expiration_policy)
5665 
5666         # make it start sooner than usual.
5667         lc = ss.lease_checker
5668hunk ./src/allmydata/test/test_storage.py 3877
5669         [immutable_si_0, immutable_si_1, mutable_si_2, mutable_si_3] = self.sis
5670         first = min(self.sis)
5671         first_b32 = base32.b2a(first)
5672-        fn = os.path.join(ss.sharedir, storage_index_to_dir(first), "0")
5673-        f = open(fn, "rb+")
5674+        fp = ss.backend.get_shareset(first).sharehomedir.child("0")
5675+        f = fp.open("rb+")
5676         f.seek(0)
5677         f.write("BAD MAGIC")
5678         f.close()
5679hunk ./src/allmydata/test/test_storage.py 3890
5680 
5681         # also create an empty bucket
5682         empty_si = base32.b2a("\x04"*16)
5683-        empty_bucket_dir = os.path.join(ss.sharedir,
5684-                                        storage_index_to_dir(empty_si))
5685-        fileutil.make_dirs(empty_bucket_dir)
5686+        empty_bucket_dir = ss.backend.get_shareset(empty_si).sharehomedir
5687+        fileutil.fp_make_dirs(empty_bucket_dir)
5688 
5689         ss.setServiceParent(self.s)
5690 
5691hunk ./src/allmydata/test/test_system.py 10
5692 
5693 import allmydata
5694 from allmydata import uri
5695-from allmydata.storage.mutable import MutableShareFile
5696+from allmydata.storage.backends.disk.mutable import MutableDiskShare
5697 from allmydata.storage.server import si_a2b
5698 from allmydata.immutable import offloaded, upload
5699 from allmydata.immutable.literal import LiteralFileNode
5700hunk ./src/allmydata/test/test_system.py 421
5701         return shares
5702 
5703     def _corrupt_mutable_share(self, filename, which):
5704-        msf = MutableShareFile(filename)
5705+        msf = MutableDiskShare(filename)
5706         datav = msf.readv([ (0, 1000000) ])
5707         final_share = datav[0]
5708         assert len(final_share) < 1000000 # ought to be truncated
5709hunk ./src/allmydata/test/test_upload.py 22
5710 from allmydata.util.happinessutil import servers_of_happiness, \
5711                                          shares_by_server, merge_servers
5712 from allmydata.storage_client import StorageFarmBroker
5713-from allmydata.storage.server import storage_index_to_dir
5714 
5715 MiB = 1024*1024
5716 
5717hunk ./src/allmydata/test/test_upload.py 821
5718 
5719     def _copy_share_to_server(self, share_number, server_number):
5720         ss = self.g.servers_by_number[server_number]
5721-        # Copy share i from the directory associated with the first
5722-        # storage server to the directory associated with this one.
5723-        assert self.g, "I tried to find a grid at self.g, but failed"
5724-        assert self.shares, "I tried to find shares at self.shares, but failed"
5725-        old_share_location = self.shares[share_number][2]
5726-        new_share_location = os.path.join(ss.storedir, "shares")
5727-        si = uri.from_string(self.uri).get_storage_index()
5728-        new_share_location = os.path.join(new_share_location,
5729-                                          storage_index_to_dir(si))
5730-        if not os.path.exists(new_share_location):
5731-            os.makedirs(new_share_location)
5732-        new_share_location = os.path.join(new_share_location,
5733-                                          str(share_number))
5734-        if old_share_location != new_share_location:
5735-            shutil.copy(old_share_location, new_share_location)
5736-        shares = self.find_uri_shares(self.uri)
5737-        # Make sure that the storage server has the share.
5738-        self.failUnless((share_number, ss.my_nodeid, new_share_location)
5739-                        in shares)
5740+        self.copy_share(self.shares[share_number], ss)
5741 
5742     def _setup_grid(self):
5743         """
5744hunk ./src/allmydata/test/test_upload.py 1103
5745                 self._copy_share_to_server(i, 2)
5746         d.addCallback(_copy_shares)
5747         # Remove the first server, and add a placeholder with share 0
5748-        d.addCallback(lambda ign:
5749-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
5750+        d.addCallback(lambda ign: self.remove_server(0))
5751         d.addCallback(lambda ign:
5752             self._add_server_with_share(server_number=4, share_number=0))
5753         # Now try uploading.
5754hunk ./src/allmydata/test/test_upload.py 1134
5755         d.addCallback(lambda ign:
5756             self._add_server(server_number=4))
5757         d.addCallback(_copy_shares)
5758-        d.addCallback(lambda ign:
5759-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
5760+        d.addCallback(lambda ign: self.remove_server(0))
5761         d.addCallback(_reset_encoding_parameters)
5762         d.addCallback(lambda client:
5763             client.upload(upload.Data("data" * 10000, convergence="")))
5764hunk ./src/allmydata/test/test_upload.py 1196
5765                 self._copy_share_to_server(i, 2)
5766         d.addCallback(_copy_shares)
5767         # Remove server 0, and add another in its place
5768-        d.addCallback(lambda ign:
5769-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
5770+        d.addCallback(lambda ign: self.remove_server(0))
5771         d.addCallback(lambda ign:
5772             self._add_server_with_share(server_number=4, share_number=0,
5773                                         readonly=True))
5774hunk ./src/allmydata/test/test_upload.py 1237
5775             for i in xrange(1, 10):
5776                 self._copy_share_to_server(i, 2)
5777         d.addCallback(_copy_shares)
5778-        d.addCallback(lambda ign:
5779-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
5780+        d.addCallback(lambda ign: self.remove_server(0))
5781         def _reset_encoding_parameters(ign, happy=4):
5782             client = self.g.clients[0]
5783             client.DEFAULT_ENCODING_PARAMETERS['happy'] = happy
5784hunk ./src/allmydata/test/test_upload.py 1273
5785         # remove the original server
5786         # (necessary to ensure that the Tahoe2ServerSelector will distribute
5787         #  all the shares)
5788-        def _remove_server(ign):
5789-            server = self.g.servers_by_number[0]
5790-            self.g.remove_server(server.my_nodeid)
5791-        d.addCallback(_remove_server)
5792+        d.addCallback(lambda ign: self.remove_server(0))
5793         # This should succeed; we still have 4 servers, and the
5794         # happiness of the upload is 4.
5795         d.addCallback(lambda ign:
5796hunk ./src/allmydata/test/test_upload.py 1285
5797         d.addCallback(lambda ign:
5798             self._setup_and_upload())
5799         d.addCallback(_do_server_setup)
5800-        d.addCallback(_remove_server)
5801+        d.addCallback(lambda ign: self.remove_server(0))
5802         d.addCallback(lambda ign:
5803             self.shouldFail(UploadUnhappinessError,
5804                             "test_dropped_servers_in_encoder",
5805hunk ./src/allmydata/test/test_upload.py 1307
5806             self._add_server_with_share(4, 7, readonly=True)
5807             self._add_server_with_share(5, 8, readonly=True)
5808         d.addCallback(_do_server_setup_2)
5809-        d.addCallback(_remove_server)
5810+        d.addCallback(lambda ign: self.remove_server(0))
5811         d.addCallback(lambda ign:
5812             self._do_upload_with_broken_servers(1))
5813         d.addCallback(_set_basedir)
5814hunk ./src/allmydata/test/test_upload.py 1314
5815         d.addCallback(lambda ign:
5816             self._setup_and_upload())
5817         d.addCallback(_do_server_setup_2)
5818-        d.addCallback(_remove_server)
5819+        d.addCallback(lambda ign: self.remove_server(0))
5820         d.addCallback(lambda ign:
5821             self.shouldFail(UploadUnhappinessError,
5822                             "test_dropped_servers_in_encoder",
5823hunk ./src/allmydata/test/test_upload.py 1528
5824             for i in xrange(1, 10):
5825                 self._copy_share_to_server(i, 1)
5826         d.addCallback(_copy_shares)
5827-        d.addCallback(lambda ign:
5828-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
5829+        d.addCallback(lambda ign: self.remove_server(0))
5830         def _prepare_client(ign):
5831             client = self.g.clients[0]
5832             client.DEFAULT_ENCODING_PARAMETERS['happy'] = 4
5833hunk ./src/allmydata/test/test_upload.py 1550
5834         def _setup(ign):
5835             for i in xrange(1, 11):
5836                 self._add_server(server_number=i)
5837-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid)
5838+            self.remove_server(0)
5839             c = self.g.clients[0]
5840             # We set happy to an unsatisfiable value so that we can check the
5841             # counting in the exception message. The same progress message
5842hunk ./src/allmydata/test/test_upload.py 1577
5843                 self._add_server(server_number=i)
5844             self._add_server(server_number=11, readonly=True)
5845             self._add_server(server_number=12, readonly=True)
5846-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid)
5847+            self.remove_server(0)
5848             c = self.g.clients[0]
5849             c.DEFAULT_ENCODING_PARAMETERS['happy'] = 45
5850             return c
5851hunk ./src/allmydata/test/test_upload.py 1605
5852             # the first one that the selector sees.
5853             for i in xrange(10):
5854                 self._copy_share_to_server(i, 9)
5855-            # Remove server 0, and its contents
5856-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid)
5857+            self.remove_server(0)
5858             # Make happiness unsatisfiable
5859             c = self.g.clients[0]
5860             c.DEFAULT_ENCODING_PARAMETERS['happy'] = 45
5861hunk ./src/allmydata/test/test_upload.py 1625
5862         def _then(ign):
5863             for i in xrange(1, 11):
5864                 self._add_server(server_number=i, readonly=True)
5865-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid)
5866+            self.remove_server(0)
5867             c = self.g.clients[0]
5868             c.DEFAULT_ENCODING_PARAMETERS['k'] = 2
5869             c.DEFAULT_ENCODING_PARAMETERS['happy'] = 4
5870hunk ./src/allmydata/test/test_upload.py 1661
5871             self._add_server(server_number=4, readonly=True))
5872         d.addCallback(lambda ign:
5873             self._add_server(server_number=5, readonly=True))
5874-        d.addCallback(lambda ign:
5875-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
5876+        d.addCallback(lambda ign: self.remove_server(0))
5877         def _reset_encoding_parameters(ign, happy=4):
5878             client = self.g.clients[0]
5879             client.DEFAULT_ENCODING_PARAMETERS['happy'] = happy
5880hunk ./src/allmydata/test/test_upload.py 1696
5881         d.addCallback(lambda ign:
5882             self._add_server(server_number=2))
5883         def _break_server_2(ign):
5884-            serverid = self.g.servers_by_number[2].my_nodeid
5885+            serverid = self.get_server(2).get_serverid()
5886             self.g.break_server(serverid)
5887         d.addCallback(_break_server_2)
5888         d.addCallback(lambda ign:
5889hunk ./src/allmydata/test/test_upload.py 1705
5890             self._add_server(server_number=4, readonly=True))
5891         d.addCallback(lambda ign:
5892             self._add_server(server_number=5, readonly=True))
5893-        d.addCallback(lambda ign:
5894-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
5895+        d.addCallback(lambda ign: self.remove_server(0))
5896         d.addCallback(_reset_encoding_parameters)
5897         d.addCallback(lambda client:
5898             self.shouldFail(UploadUnhappinessError, "test_selection_exceptions",
5899hunk ./src/allmydata/test/test_upload.py 1816
5900             # Copy shares
5901             self._copy_share_to_server(1, 1)
5902             self._copy_share_to_server(2, 1)
5903-            # Remove server 0
5904-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid)
5905+            self.remove_server(0)
5906             client = self.g.clients[0]
5907             client.DEFAULT_ENCODING_PARAMETERS['happy'] = 3
5908             return client
5909hunk ./src/allmydata/test/test_upload.py 1930
5910                                         readonly=True)
5911             self._add_server_with_share(server_number=4, share_number=3,
5912                                         readonly=True)
5913-            # Remove server 0.
5914-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid)
5915+            self.remove_server(0)
5916             # Set the client appropriately
5917             c = self.g.clients[0]
5918             c.DEFAULT_ENCODING_PARAMETERS['happy'] = 4
5919hunk ./src/allmydata/test/test_util.py 9
5920 from twisted.trial import unittest
5921 from twisted.internet import defer, reactor
5922 from twisted.python.failure import Failure
5923+from twisted.python.filepath import FilePath
5924 from twisted.python import log
5925 from pycryptopp.hash.sha256 import SHA256 as _hash
5926 
5927hunk ./src/allmydata/test/test_util.py 508
5928                 os.chdir(saved_cwd)
5929 
5930     def test_disk_stats(self):
5931-        avail = fileutil.get_available_space('.', 2**14)
5932+        avail = fileutil.get_available_space(FilePath('.'), 2**14)
5933         if avail == 0:
5934             raise unittest.SkipTest("This test will spuriously fail there is no disk space left.")
5935 
5936hunk ./src/allmydata/test/test_util.py 512
5937-        disk = fileutil.get_disk_stats('.', 2**13)
5938+        disk = fileutil.get_disk_stats(FilePath('.'), 2**13)
5939         self.failUnless(disk['total'] > 0, disk['total'])
5940         self.failUnless(disk['used'] > 0, disk['used'])
5941         self.failUnless(disk['free_for_root'] > 0, disk['free_for_root'])
5942hunk ./src/allmydata/test/test_util.py 521
5943 
5944     def test_disk_stats_avail_nonnegative(self):
5945         # This test will spuriously fail if you have more than 2^128
5946-        # bytes of available space on your filesystem.
5947-        disk = fileutil.get_disk_stats('.', 2**128)
5948+        # bytes of available space on your filesystem (lucky you).
5949+        disk = fileutil.get_disk_stats(FilePath('.'), 2**128)
5950         self.failUnlessEqual(disk['avail'], 0)
5951 
5952 class PollMixinTests(unittest.TestCase):
5953hunk ./src/allmydata/test/test_web.py 12
5954 from twisted.python import failure, log
5955 from nevow import rend
5956 from allmydata import interfaces, uri, webish, dirnode
5957-from allmydata.storage.shares import get_share_file
5958 from allmydata.storage_client import StorageFarmBroker
5959 from allmydata.immutable import upload
5960 from allmydata.immutable.downloader.status import DownloadStatus
5961hunk ./src/allmydata/test/test_web.py 4111
5962             good_shares = self.find_uri_shares(self.uris["good"])
5963             self.failUnlessReallyEqual(len(good_shares), 10)
5964             sick_shares = self.find_uri_shares(self.uris["sick"])
5965-            os.unlink(sick_shares[0][2])
5966+            sick_shares[0][2].remove()
5967             dead_shares = self.find_uri_shares(self.uris["dead"])
5968             for i in range(1, 10):
5969hunk ./src/allmydata/test/test_web.py 4114
5970-                os.unlink(dead_shares[i][2])
5971+                dead_shares[i][2].remove()
5972             c_shares = self.find_uri_shares(self.uris["corrupt"])
5973             cso = CorruptShareOptions()
5974             cso.stdout = StringIO()
5975hunk ./src/allmydata/test/test_web.py 4118
5976-            cso.parseOptions([c_shares[0][2]])
5977+            cso.parseOptions([c_shares[0][2].path])
5978             corrupt_share(cso)
5979         d.addCallback(_clobber_shares)
5980 
5981hunk ./src/allmydata/test/test_web.py 4253
5982             good_shares = self.find_uri_shares(self.uris["good"])
5983             self.failUnlessReallyEqual(len(good_shares), 10)
5984             sick_shares = self.find_uri_shares(self.uris["sick"])
5985-            os.unlink(sick_shares[0][2])
5986+            sick_shares[0][2].remove()
5987             dead_shares = self.find_uri_shares(self.uris["dead"])
5988             for i in range(1, 10):
5989hunk ./src/allmydata/test/test_web.py 4256
5990-                os.unlink(dead_shares[i][2])
5991+                dead_shares[i][2].remove()
5992             c_shares = self.find_uri_shares(self.uris["corrupt"])
5993             cso = CorruptShareOptions()
5994             cso.stdout = StringIO()
5995hunk ./src/allmydata/test/test_web.py 4260
5996-            cso.parseOptions([c_shares[0][2]])
5997+            cso.parseOptions([c_shares[0][2].path])
5998             corrupt_share(cso)
5999         d.addCallback(_clobber_shares)
6000 
6001hunk ./src/allmydata/test/test_web.py 4319
6002 
6003         def _clobber_shares(ignored):
6004             sick_shares = self.find_uri_shares(self.uris["sick"])
6005-            os.unlink(sick_shares[0][2])
6006+            sick_shares[0][2].remove()
6007         d.addCallback(_clobber_shares)
6008 
6009         d.addCallback(self.CHECK, "sick", "t=check&repair=true&output=json")
6010hunk ./src/allmydata/test/test_web.py 4811
6011             good_shares = self.find_uri_shares(self.uris["good"])
6012             self.failUnlessReallyEqual(len(good_shares), 10)
6013             sick_shares = self.find_uri_shares(self.uris["sick"])
6014-            os.unlink(sick_shares[0][2])
6015+            sick_shares[0][2].remove()
6016             #dead_shares = self.find_uri_shares(self.uris["dead"])
6017             #for i in range(1, 10):
6018hunk ./src/allmydata/test/test_web.py 4814
6019-            #    os.unlink(dead_shares[i][2])
6020+            #    dead_shares[i][2].remove()
6021 
6022             #c_shares = self.find_uri_shares(self.uris["corrupt"])
6023             #cso = CorruptShareOptions()
6024hunk ./src/allmydata/test/test_web.py 4819
6025             #cso.stdout = StringIO()
6026-            #cso.parseOptions([c_shares[0][2]])
6027+            #cso.parseOptions([c_shares[0][2].path])
6028             #corrupt_share(cso)
6029         d.addCallback(_clobber_shares)
6030 
6031hunk ./src/allmydata/test/test_web.py 4870
6032         d.addErrback(self.explain_web_error)
6033         return d
6034 
6035-    def _count_leases(self, ignored, which):
6036-        u = self.uris[which]
6037-        shares = self.find_uri_shares(u)
6038-        lease_counts = []
6039-        for shnum, serverid, fn in shares:
6040-            sf = get_share_file(fn)
6041-            num_leases = len(list(sf.get_leases()))
6042-            lease_counts.append( (fn, num_leases) )
6043-        return lease_counts
6044-
6045-    def _assert_leasecount(self, lease_counts, expected):
6046+    def _assert_leasecount(self, ignored, which, expected):
6047+        lease_counts = self.count_leases(self.uris[which])
6048         for (fn, num_leases) in lease_counts:
6049             if num_leases != expected:
6050                 self.fail("expected %d leases, have %d, on %s" %
6051hunk ./src/allmydata/test/test_web.py 4903
6052                 self.fileurls[which] = "uri/" + urllib.quote(self.uris[which])
6053         d.addCallback(_compute_fileurls)
6054 
6055-        d.addCallback(self._count_leases, "one")
6056-        d.addCallback(self._assert_leasecount, 1)
6057-        d.addCallback(self._count_leases, "two")
6058-        d.addCallback(self._assert_leasecount, 1)
6059-        d.addCallback(self._count_leases, "mutable")
6060-        d.addCallback(self._assert_leasecount, 1)
6061+        d.addCallback(self._assert_leasecount, "one", 1)
6062+        d.addCallback(self._assert_leasecount, "two", 1)
6063+        d.addCallback(self._assert_leasecount, "mutable", 1)
6064 
6065         d.addCallback(self.CHECK, "one", "t=check") # no add-lease
6066         def _got_html_good(res):
6067hunk ./src/allmydata/test/test_web.py 4913
6068             self.failIf("Not Healthy" in res, res)
6069         d.addCallback(_got_html_good)
6070 
6071-        d.addCallback(self._count_leases, "one")
6072-        d.addCallback(self._assert_leasecount, 1)
6073-        d.addCallback(self._count_leases, "two")
6074-        d.addCallback(self._assert_leasecount, 1)
6075-        d.addCallback(self._count_leases, "mutable")
6076-        d.addCallback(self._assert_leasecount, 1)
6077+        d.addCallback(self._assert_leasecount, "one", 1)
6078+        d.addCallback(self._assert_leasecount, "two", 1)
6079+        d.addCallback(self._assert_leasecount, "mutable", 1)
6080 
6081         # this CHECK uses the original client, which uses the same
6082         # lease-secrets, so it will just renew the original lease
6083hunk ./src/allmydata/test/test_web.py 4922
6084         d.addCallback(self.CHECK, "one", "t=check&add-lease=true")
6085         d.addCallback(_got_html_good)
6086 
6087-        d.addCallback(self._count_leases, "one")
6088-        d.addCallback(self._assert_leasecount, 1)
6089-        d.addCallback(self._count_leases, "two")
6090-        d.addCallback(self._assert_leasecount, 1)
6091-        d.addCallback(self._count_leases, "mutable")
6092-        d.addCallback(self._assert_leasecount, 1)
6093+        d.addCallback(self._assert_leasecount, "one", 1)
6094+        d.addCallback(self._assert_leasecount, "two", 1)
6095+        d.addCallback(self._assert_leasecount, "mutable", 1)
6096 
6097         # this CHECK uses an alternate client, which adds a second lease
6098         d.addCallback(self.CHECK, "one", "t=check&add-lease=true", clientnum=1)
6099hunk ./src/allmydata/test/test_web.py 4930
6100         d.addCallback(_got_html_good)
6101 
6102-        d.addCallback(self._count_leases, "one")
6103-        d.addCallback(self._assert_leasecount, 2)
6104-        d.addCallback(self._count_leases, "two")
6105-        d.addCallback(self._assert_leasecount, 1)
6106-        d.addCallback(self._count_leases, "mutable")
6107-        d.addCallback(self._assert_leasecount, 1)
6108+        d.addCallback(self._assert_leasecount, "one", 2)
6109+        d.addCallback(self._assert_leasecount, "two", 1)
6110+        d.addCallback(self._assert_leasecount, "mutable", 1)
6111 
6112         d.addCallback(self.CHECK, "mutable", "t=check&add-lease=true")
6113         d.addCallback(_got_html_good)
6114hunk ./src/allmydata/test/test_web.py 4937
6115 
6116-        d.addCallback(self._count_leases, "one")
6117-        d.addCallback(self._assert_leasecount, 2)
6118-        d.addCallback(self._count_leases, "two")
6119-        d.addCallback(self._assert_leasecount, 1)
6120-        d.addCallback(self._count_leases, "mutable")
6121-        d.addCallback(self._assert_leasecount, 1)
6122+        d.addCallback(self._assert_leasecount, "one", 2)
6123+        d.addCallback(self._assert_leasecount, "two", 1)
6124+        d.addCallback(self._assert_leasecount, "mutable", 1)
6125 
6126         d.addCallback(self.CHECK, "mutable", "t=check&add-lease=true",
6127                       clientnum=1)
6128hunk ./src/allmydata/test/test_web.py 4945
6129         d.addCallback(_got_html_good)
6130 
6131-        d.addCallback(self._count_leases, "one")
6132-        d.addCallback(self._assert_leasecount, 2)
6133-        d.addCallback(self._count_leases, "two")
6134-        d.addCallback(self._assert_leasecount, 1)
6135-        d.addCallback(self._count_leases, "mutable")
6136-        d.addCallback(self._assert_leasecount, 2)
6137+        d.addCallback(self._assert_leasecount, "one", 2)
6138+        d.addCallback(self._assert_leasecount, "two", 1)
6139+        d.addCallback(self._assert_leasecount, "mutable", 2)
6140 
6141         d.addErrback(self.explain_web_error)
6142         return d
6143hunk ./src/allmydata/test/test_web.py 4989
6144             self.failUnlessReallyEqual(len(units), 4+1)
6145         d.addCallback(_done)
6146 
6147-        d.addCallback(self._count_leases, "root")
6148-        d.addCallback(self._assert_leasecount, 1)
6149-        d.addCallback(self._count_leases, "one")
6150-        d.addCallback(self._assert_leasecount, 1)
6151-        d.addCallback(self._count_leases, "mutable")
6152-        d.addCallback(self._assert_leasecount, 1)
6153+        d.addCallback(self._assert_leasecount, "root", 1)
6154+        d.addCallback(self._assert_leasecount, "one", 1)
6155+        d.addCallback(self._assert_leasecount, "mutable", 1)
6156 
6157         d.addCallback(self.CHECK, "root", "t=stream-deep-check&add-lease=true")
6158         d.addCallback(_done)
6159hunk ./src/allmydata/test/test_web.py 4996
6160 
6161-        d.addCallback(self._count_leases, "root")
6162-        d.addCallback(self._assert_leasecount, 1)
6163-        d.addCallback(self._count_leases, "one")
6164-        d.addCallback(self._assert_leasecount, 1)
6165-        d.addCallback(self._count_leases, "mutable")
6166-        d.addCallback(self._assert_leasecount, 1)
6167+        d.addCallback(self._assert_leasecount, "root", 1)
6168+        d.addCallback(self._assert_leasecount, "one", 1)
6169+        d.addCallback(self._assert_leasecount, "mutable", 1)
6170 
6171         d.addCallback(self.CHECK, "root", "t=stream-deep-check&add-lease=true",
6172                       clientnum=1)
6173hunk ./src/allmydata/test/test_web.py 5004
6174         d.addCallback(_done)
6175 
6176-        d.addCallback(self._count_leases, "root")
6177-        d.addCallback(self._assert_leasecount, 2)
6178-        d.addCallback(self._count_leases, "one")
6179-        d.addCallback(self._assert_leasecount, 2)
6180-        d.addCallback(self._count_leases, "mutable")
6181-        d.addCallback(self._assert_leasecount, 2)
6182+        d.addCallback(self._assert_leasecount, "root", 2)
6183+        d.addCallback(self._assert_leasecount, "one", 2)
6184+        d.addCallback(self._assert_leasecount, "mutable", 2)
6185 
6186         d.addErrback(self.explain_web_error)
6187         return d
6188hunk ./src/allmydata/uri.py 829
6189     def is_mutable(self):
6190         return False
6191 
6192+    def is_readonly(self):
6193+        return True
6194+
6195+    def get_readonly(self):
6196+        return self
6197+
6198+
6199 class DirectoryURIVerifier(_DirectoryBaseURI):
6200     implements(IVerifierURI)
6201 
6202hunk ./src/allmydata/uri.py 855
6203     def is_mutable(self):
6204         return False
6205 
6206+    def is_readonly(self):
6207+        return True
6208+
6209+    def get_readonly(self):
6210+        return self
6211+
6212 
6213 class ImmutableDirectoryURIVerifier(DirectoryURIVerifier):
6214     implements(IVerifierURI)
6215hunk ./src/allmydata/util/encodingutil.py 221
6216 def quote_path(path, quotemarks=True):
6217     return quote_output("/".join(map(to_str, path)), quotemarks=quotemarks)
6218 
6219+def quote_filepath(fp, quotemarks=True, encoding=None):
6220+    path = fp.path
6221+    if isinstance(path, str):
6222+        try:
6223+            path = path.decode(filesystem_encoding)
6224+        except UnicodeDecodeError:
6225+            return 'b"%s"' % (ESCAPABLE_8BIT.sub(_str_escape, path),)
6226+
6227+    return quote_output(path, quotemarks=quotemarks, encoding=encoding)
6228+
6229 
6230 def unicode_platform():
6231     """
6232hunk ./src/allmydata/util/fileutil.py 5
6233 Futz with files like a pro.
6234 """
6235 
6236-import sys, exceptions, os, stat, tempfile, time, binascii
6237+import errno, sys, exceptions, os, stat, tempfile, time, binascii
6238+
6239+from allmydata.util.assertutil import precondition
6240 
6241 from twisted.python import log
6242hunk ./src/allmydata/util/fileutil.py 10
6243+from twisted.python.filepath import FilePath, UnlistableError
6244 
6245 from pycryptopp.cipher.aes import AES
6246 
6247hunk ./src/allmydata/util/fileutil.py 189
6248             raise tx
6249         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...
6250 
6251-def rm_dir(dirname):
6252+def fp_make_dirs(dirfp):
6253+    """
6254+    An idempotent version of FilePath.makedirs().  If the dir already
6255+    exists, do nothing and return without raising an exception.  If this
6256+    call creates the dir, return without raising an exception.  If there is
6257+    an error that prevents creation or if the directory gets deleted after
6258+    fp_make_dirs() creates it and before fp_make_dirs() checks that it
6259+    exists, raise an exception.
6260+    """
6261+    log.msg( "xxx 0 %s" % (dirfp,))
6262+    tx = None
6263+    try:
6264+        dirfp.makedirs()
6265+    except OSError, x:
6266+        tx = x
6267+
6268+    if not dirfp.isdir():
6269+        if tx:
6270+            raise tx
6271+        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...
6272+
6273+def fp_rmdir_if_empty(dirfp):
6274+    """ Remove the directory if it is empty. """
6275+    try:
6276+        os.rmdir(dirfp.path)
6277+    except OSError, e:
6278+        if e.errno != errno.ENOTEMPTY:
6279+            raise
6280+    else:
6281+        dirfp.changed()
6282+
6283+def rmtree(dirname):
6284     """
6285     A threadsafe and idempotent version of shutil.rmtree().  If the dir is
6286     already gone, do nothing and return without raising an exception.  If this
6287hunk ./src/allmydata/util/fileutil.py 239
6288             else:
6289                 remove(fullname)
6290         os.rmdir(dirname)
6291-    except Exception, le:
6292-        # Ignore "No such file or directory"
6293-        if (not isinstance(le, OSError)) or le.args[0] != 2:
6294+    except EnvironmentError, le:
6295+        # Ignore "No such file or directory", collect any other exception.
6296+        if (le.args[0] != 2 and le.args[0] != 3) or (le.args[0] != errno.ENOENT):
6297             excs.append(le)
6298hunk ./src/allmydata/util/fileutil.py 243
6299+    except Exception, le:
6300+        excs.append(le)
6301 
6302     # Okay, now we've recursively removed everything, ignoring any "No
6303     # such file or directory" errors, and collecting any other errors.
6304hunk ./src/allmydata/util/fileutil.py 256
6305             raise OSError, "Failed to remove dir for unknown reason."
6306         raise OSError, excs
6307 
6308+def fp_remove(fp):
6309+    """
6310+    An idempotent version of shutil.rmtree().  If the file/dir is already
6311+    gone, do nothing and return without raising an exception.  If this call
6312+    removes the file/dir, return without raising an exception.  If there is
6313+    an error that prevents removal, or if a file or directory at the same
6314+    path gets created again by someone else after this deletes it and before
6315+    this checks that it is gone, raise an exception.
6316+    """
6317+    try:
6318+        fp.remove()
6319+    except UnlistableError, e:
6320+        if e.originalException.errno != errno.ENOENT:
6321+            raise
6322+    except OSError, e:
6323+        if e.errno != errno.ENOENT:
6324+            raise
6325+
6326+def rm_dir(dirname):
6327+    # Renamed to be like shutil.rmtree and unlike rmdir.
6328+    return rmtree(dirname)
6329 
6330 def remove_if_possible(f):
6331     try:
6332hunk ./src/allmydata/util/fileutil.py 387
6333         import traceback
6334         traceback.print_exc()
6335 
6336-def get_disk_stats(whichdir, reserved_space=0):
6337+def get_disk_stats(whichdirfp, reserved_space=0):
6338     """Return disk statistics for the storage disk, in the form of a dict
6339     with the following fields.
6340       total:            total bytes on disk
6341hunk ./src/allmydata/util/fileutil.py 408
6342     you can pass how many bytes you would like to leave unused on this
6343     filesystem as reserved_space.
6344     """
6345+    precondition(isinstance(whichdirfp, FilePath), whichdirfp)
6346 
6347     if have_GetDiskFreeSpaceExW:
6348         # If this is a Windows system and GetDiskFreeSpaceExW is available, use it.
6349hunk ./src/allmydata/util/fileutil.py 419
6350         n_free_for_nonroot = c_ulonglong(0)
6351         n_total            = c_ulonglong(0)
6352         n_free_for_root    = c_ulonglong(0)
6353-        retval = GetDiskFreeSpaceExW(whichdir, byref(n_free_for_nonroot),
6354-                                               byref(n_total),
6355-                                               byref(n_free_for_root))
6356+        retval = GetDiskFreeSpaceExW(whichdirfp.path, byref(n_free_for_nonroot),
6357+                                                      byref(n_total),
6358+                                                      byref(n_free_for_root))
6359         if retval == 0:
6360             raise OSError("Windows error %d attempting to get disk statistics for %r"
6361hunk ./src/allmydata/util/fileutil.py 424
6362-                          % (GetLastError(), whichdir))
6363+                          % (GetLastError(), whichdirfp.path))
6364         free_for_nonroot = n_free_for_nonroot.value
6365         total            = n_total.value
6366         free_for_root    = n_free_for_root.value
6367hunk ./src/allmydata/util/fileutil.py 433
6368         # <http://docs.python.org/library/os.html#os.statvfs>
6369         # <http://opengroup.org/onlinepubs/7990989799/xsh/fstatvfs.html>
6370         # <http://opengroup.org/onlinepubs/7990989799/xsh/sysstatvfs.h.html>
6371-        s = os.statvfs(whichdir)
6372+        s = os.statvfs(whichdirfp.path)
6373 
6374         # on my mac laptop:
6375         #  statvfs(2) is a wrapper around statfs(2).
6376hunk ./src/allmydata/util/fileutil.py 460
6377              'avail': avail,
6378            }
6379 
6380-def get_available_space(whichdir, reserved_space):
6381+def get_available_space(whichdirfp, reserved_space):
6382     """Returns available space for share storage in bytes, or None if no
6383     API to get this information is available.
6384 
6385hunk ./src/allmydata/util/fileutil.py 472
6386     you can pass how many bytes you would like to leave unused on this
6387     filesystem as reserved_space.
6388     """
6389+    precondition(isinstance(whichdirfp, FilePath), whichdirfp)
6390     try:
6391hunk ./src/allmydata/util/fileutil.py 474
6392-        return get_disk_stats(whichdir, reserved_space)['avail']
6393+        return get_disk_stats(whichdirfp, reserved_space)['avail']
6394     except AttributeError:
6395         return None
6396hunk ./src/allmydata/util/fileutil.py 477
6397-    except EnvironmentError:
6398-        log.msg("OS call to get disk statistics failed")
6399+
6400+
6401+def get_used_space(fp):
6402+    if fp is None:
6403         return 0
6404hunk ./src/allmydata/util/fileutil.py 482
6405+    try:
6406+        s = os.stat(fp.path)
6407+    except EnvironmentError:
6408+        if not fp.exists():
6409+            return 0
6410+        raise
6411+    else:
6412+        # POSIX defines st_blocks (originally a BSDism):
6413+        #   <http://pubs.opengroup.org/onlinepubs/009695399/basedefs/sys/stat.h.html>
6414+        # but does not require stat() to give it a "meaningful value"
6415+        #   <http://pubs.opengroup.org/onlinepubs/009695399/functions/stat.html>
6416+        # and says:
6417+        #   "The unit for the st_blocks member of the stat structure is not defined
6418+        #    within IEEE Std 1003.1-2001. In some implementations it is 512 bytes.
6419+        #    It may differ on a file system basis. There is no correlation between
6420+        #    values of the st_blocks and st_blksize, and the f_bsize (from <sys/statvfs.h>)
6421+        #    structure members."
6422+        #
6423+        # The Linux docs define it as "the number of blocks allocated to the file,
6424+        # [in] 512-byte units." It is also defined that way on MacOS X. Python does
6425+        # not set the attribute on Windows.
6426+        #
6427+        # We consider platforms that define st_blocks but give it a wrong value, or
6428+        # measure it in a unit other than 512 bytes, to be broken. See also
6429+        # <http://bugs.python.org/issue12350>.
6430+
6431+        if hasattr(s, 'st_blocks'):
6432+            return s.st_blocks * 512
6433+        else:
6434+            return s.st_size
6435}
6436
6437Context:
6438
6439[Make platform-detection code tolerate linux-3.0, patch by zooko.
6440Brian Warner <warner@lothar.com>**20110915202620
6441 Ignore-this: af63cf9177ae531984dea7a1cad03762
6442 
6443 Otherwise address-autodetection can't find ifconfig. refs #1536
6444]
6445[test_web.py: fix a bug in _count_leases that was causing us to check only the lease count of one share file, not of all share files as intended.
6446david-sarah@jacaranda.org**20110915185126
6447 Ignore-this: d96632bc48d770b9b577cda1bbd8ff94
6448]
6449[docs: insert a newline at the beginning of known_issues.rst to see if this makes it render more nicely in trac
6450zooko@zooko.com**20110914064728
6451 Ignore-this: aca15190fa22083c5d4114d3965f5d65
6452]
6453[docs: remove the coding: utf-8 declaration at the to of known_issues.rst, since the trac rendering doesn't hide it
6454zooko@zooko.com**20110914055713
6455 Ignore-this: 941ed32f83ead377171aa7a6bd198fcf
6456]
6457[docs: more cleanup of known_issues.rst -- now it passes "rst2html --verbose" without comment
6458zooko@zooko.com**20110914055419
6459 Ignore-this: 5505b3d76934bd97d0312cc59ed53879
6460]
6461[docs: more formatting improvements to known_issues.rst
6462zooko@zooko.com**20110914051639
6463 Ignore-this: 9ae9230ec9a38a312cbacaf370826691
6464]
6465[docs: reformatting of known_issues.rst
6466zooko@zooko.com**20110914050240
6467 Ignore-this: b8be0375079fb478be9d07500f9aaa87
6468]
6469[docs: fix formatting error in docs/known_issues.rst
6470zooko@zooko.com**20110914045909
6471 Ignore-this: f73fe74ad2b9e655aa0c6075acced15a
6472]
6473[merge Tahoe-LAFS v1.8.3 release announcement with trunk
6474zooko@zooko.com**20110913210544
6475 Ignore-this: 163f2c3ddacca387d7308e4b9332516e
6476]
6477[docs: release notes for Tahoe-LAFS v1.8.3
6478zooko@zooko.com**20110913165826
6479 Ignore-this: 84223604985b14733a956d2fbaeb4e9f
6480]
6481[tests: bump up the timeout in this test that fails on FreeStorm's CentOS in order to see if it is just very slow
6482zooko@zooko.com**20110913024255
6483 Ignore-this: 6a86d691e878cec583722faad06fb8e4
6484]
6485[interfaces: document that the 'fills-holes-with-zero-bytes' key should be used to detect whether a storage server has that behavior. refs #1528
6486david-sarah@jacaranda.org**20110913002843
6487 Ignore-this: 1a00a6029d40f6792af48c5578c1fd69
6488]
6489[CREDITS: more CREDITS for Kevan and David-Sarah
6490zooko@zooko.com**20110912223357
6491 Ignore-this: 4ea8f0d6f2918171d2f5359c25ad1ada
6492]
6493[merge NEWS about the mutable file bounds fixes with NEWS about work-in-progress
6494zooko@zooko.com**20110913205521
6495 Ignore-this: 4289a4225f848d6ae6860dd39bc92fa8
6496]
6497[doc: add NEWS item about fixes to potential palimpsest issues in mutable files
6498zooko@zooko.com**20110912223329
6499 Ignore-this: 9d63c95ddf95c7d5453c94a1ba4d406a
6500 ref. #1528
6501]
6502[merge the NEWS about the security fix (#1528) with the work-in-progress NEWS
6503zooko@zooko.com**20110913205153
6504 Ignore-this: 88e88a2ad140238c62010cf7c66953fc
6505]
6506[doc: add NEWS entry about the issue which allows unauthorized deletion of shares
6507zooko@zooko.com**20110912223246
6508 Ignore-this: 77e06d09103d2ef6bb51ea3e5d6e80b0
6509 ref. #1528
6510]
6511[doc: add entry in known_issues.rst about the issue which allows unauthorized deletion of shares
6512zooko@zooko.com**20110912223135
6513 Ignore-this: b26c6ea96b6c8740b93da1f602b5a4cd
6514 ref. #1528
6515]
6516[storage: more paranoid handling of bounds and palimpsests in mutable share files
6517zooko@zooko.com**20110912222655
6518 Ignore-this: a20782fa423779ee851ea086901e1507
6519 * storage server ignores requests to extend shares by sending a new_length
6520 * 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
6521 * storage server zeroes out lease info at the old location when moving it to a new location
6522 ref. #1528
6523]
6524[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
6525zooko@zooko.com**20110912222554
6526 Ignore-this: 61ebd7b11250963efdf5b1734a35271
6527 ref. #1528
6528]
6529[immutable: prevent clients from reading past the end of share data, which would allow them to learn the cancellation secret
6530zooko@zooko.com**20110912222458
6531 Ignore-this: da1ebd31433ea052087b75b2e3480c25
6532 Declare explicitly that we prevent this problem in the server's version dict.
6533 fixes #1528 (there are two patches that are each a sufficient fix to #1528 and this is one of them)
6534]
6535[storage: remove the storage server's "remote_cancel_lease" function
6536zooko@zooko.com**20110912222331
6537 Ignore-this: 1c32dee50e0981408576daffad648c50
6538 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.
6539 fixes #1528 (there are two patches that are each a sufficient fix to #1528 and this is one of them)
6540]
6541[storage: test that the storage server does *not* have a "remote_cancel_lease" function
6542zooko@zooko.com**20110912222324
6543 Ignore-this: 21c652009704652d35f34651f98dd403
6544 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.
6545 ref. #1528
6546]
6547[immutable: test whether the server allows clients to read past the end of share data, which would allow them to learn the cancellation secret
6548zooko@zooko.com**20110912221201
6549 Ignore-this: 376e47b346c713d37096531491176349
6550 Also test whether the server explicitly declares that it prevents this problem.
6551 ref #1528
6552]
6553[Retrieve._activate_enough_peers: rewrite Verify logic
6554Brian Warner <warner@lothar.com>**20110909181150
6555 Ignore-this: 9367c11e1eacbf025f75ce034030d717
6556]
6557[Retrieve: implement/test stopProducing
6558Brian Warner <warner@lothar.com>**20110909181150
6559 Ignore-this: 47b2c3df7dc69835e0a066ca12e3c178
6560]
6561[move DownloadStopped from download.common to interfaces
6562Brian Warner <warner@lothar.com>**20110909181150
6563 Ignore-this: 8572acd3bb16e50341dbed8eb1d90a50
6564]
6565[retrieve.py: remove vestigal self._validated_readers
6566Brian Warner <warner@lothar.com>**20110909181150
6567 Ignore-this: faab2ec14e314a53a2ffb714de626e2d
6568]
6569[Retrieve: rewrite flow-control: use a top-level loop() to catch all errors
6570Brian Warner <warner@lothar.com>**20110909181150
6571 Ignore-this: e162d2cd53b3d3144fc6bc757e2c7714
6572 
6573 This ought to close the potential for dropped errors and hanging downloads.
6574 Verify needs to be examined, I may have broken it, although all tests pass.
6575]
6576[Retrieve: merge _validate_active_prefixes into _add_active_peers
6577Brian Warner <warner@lothar.com>**20110909181150
6578 Ignore-this: d3ead31e17e69394ae7058eeb5beaf4c
6579]
6580[Retrieve: remove the initial prefix-is-still-good check
6581Brian Warner <warner@lothar.com>**20110909181150
6582 Ignore-this: da66ee51c894eaa4e862e2dffb458acc
6583 
6584 This check needs to be done with each fetch from the storage server, to
6585 detect when someone has changed the share (i.e. our servermap goes stale).
6586 Doing it just once at the beginning of retrieve isn't enough: a write might
6587 occur after the first segment but before the second, etc.
6588 
6589 _try_to_validate_prefix() was not removed: it will be used by the future
6590 check-with-each-fetch code.
6591 
6592 test_mutable.Roundtrip.test_corrupt_all_seqnum_late was disabled, since it
6593 fails until this check is brought back. (the corruption it applies only
6594 touches the prefix, not the block data, so the check-less retrieve actually
6595 tolerates it). Don't forget to re-enable it once the check is brought back.
6596]
6597[MDMFSlotReadProxy: remove the queue
6598Brian Warner <warner@lothar.com>**20110909181150
6599 Ignore-this: 96673cb8dda7a87a423de2f4897d66d2
6600 
6601 This is a neat trick to reduce Foolscap overhead, but the need for an
6602 explicit flush() complicates the Retrieve path and makes it prone to
6603 lost-progress bugs.
6604 
6605 Also change test_mutable.FakeStorageServer to tolerate multiple reads of the
6606 same share in a row, a limitation exposed by turning off the queue.
6607]
6608[rearrange Retrieve: first step, shouldn't change order of execution
6609Brian Warner <warner@lothar.com>**20110909181149
6610 Ignore-this: e3006368bfd2802b82ea45c52409e8d6
6611]
6612[CLI: test_cli.py -- remove an unnecessary call in test_mkdir_mutable_type. refs #1527
6613david-sarah@jacaranda.org**20110906183730
6614 Ignore-this: 122e2ffbee84861c32eda766a57759cf
6615]
6616[CLI: improve test for 'tahoe mkdir --mutable-type='. refs #1527
6617david-sarah@jacaranda.org**20110906183020
6618 Ignore-this: f1d4598e6c536f0a2b15050b3bc0ef9d
6619]
6620[CLI: make the --mutable-type option value for 'tahoe put' and 'tahoe mkdir' case-insensitive, and change --help for these commands accordingly. fixes #1527
6621david-sarah@jacaranda.org**20110905020922
6622 Ignore-this: 75a6df0a2df9c467d8c010579e9a024e
6623]
6624[cli: make --mutable-type imply --mutable in 'tahoe put'
6625Kevan Carstensen <kevan@isnotajoke.com>**20110903190920
6626 Ignore-this: 23336d3c43b2a9554e40c2a11c675e93
6627]
6628[SFTP: add a comment about a subtle interaction between OverwriteableFileConsumer and GeneralSFTPFile, and test the case it is commenting on.
6629david-sarah@jacaranda.org**20110903222304
6630 Ignore-this: 980c61d4dd0119337f1463a69aeebaf0
6631]
6632[improve the storage/mutable.py asserts even more
6633warner@lothar.com**20110901160543
6634 Ignore-this: 5b2b13c49bc4034f96e6e3aaaa9a9946
6635]
6636[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
6637wilcoxjg@gmail.com**20110901084144
6638 Ignore-this: 28ace2b2678642e4d7269ddab8c67f30
6639]
6640[docs/write_coordination.rst: fix formatting and add more specific warning about access via sshfs.
6641david-sarah@jacaranda.org**20110831232148
6642 Ignore-this: cd9c851d3eb4e0a1e088f337c291586c
6643]
6644[test_mutable.Version: consolidate some tests, reduce runtime from 19s to 15s
6645warner@lothar.com**20110831050451
6646 Ignore-this: 64815284d9e536f8f3798b5f44cf580c
6647]
6648[mutable/retrieve: handle the case where self._read_length is 0.
6649Kevan Carstensen <kevan@isnotajoke.com>**20110830210141
6650 Ignore-this: fceafbe485851ca53f2774e5a4fd8d30
6651 
6652 Note that the downloader will still fetch a segment for a zero-length
6653 read, which is wasteful. Fixing that isn't specifically required to fix
6654 #1512, but it should probably be fixed before 1.9.
6655]
6656[NEWS: added summary of all changes since 1.8.2. Needs editing.
6657Brian Warner <warner@lothar.com>**20110830163205
6658 Ignore-this: 273899b37a899fc6919b74572454b8b2
6659]
6660[test_mutable.Update: only upload the files needed for each test. refs #1500
6661Brian Warner <warner@lothar.com>**20110829072717
6662 Ignore-this: 4d2ab4c7523af9054af7ecca9c3d9dc7
6663 
6664 This first step shaves 15% off the runtime: from 139s to 119s on my laptop.
6665 It also fixes a couple of places where a Deferred was being dropped, which
6666 would cause two tests to run in parallel and also confuse error reporting.
6667]
6668[Let Uploader retain History instead of passing it into upload(). Fixes #1079.
6669Brian Warner <warner@lothar.com>**20110829063246
6670 Ignore-this: 3902c58ec12bd4b2d876806248e19f17
6671 
6672 This consistently records all immutable uploads in the Recent Uploads And
6673 Downloads page, regardless of code path. Previously, certain webapi upload
6674 operations (like PUT /uri/$DIRCAP/newchildname) failed to pass the History
6675 object and were left out.
6676]
6677[Fix mutable publish/retrieve timing status displays. Fixes #1505.
6678Brian Warner <warner@lothar.com>**20110828232221
6679 Ignore-this: 4080ce065cf481b2180fd711c9772dd6
6680 
6681 publish:
6682 * encrypt and encode times are cumulative, not just current-segment
6683 
6684 retrieve:
6685 * same for decrypt and decode times
6686 * update "current status" to include segment number
6687 * set status to Finished/Failed when download is complete
6688 * set progress to 1.0 when complete
6689 
6690 More improvements to consider:
6691 * progress is currently 0% or 100%: should calculate how many segments are
6692   involved (remembering retrieve can be less than the whole file) and set it
6693   to a fraction
6694 * "fetch" time is fuzzy: what we want is to know how much of the delay is not
6695   our own fault, but since we do decode/decrypt work while waiting for more
6696   shares, it's not straightforward
6697]
6698[Teach 'tahoe debug catalog-shares about MDMF. Closes #1507.
6699Brian Warner <warner@lothar.com>**20110828080931
6700 Ignore-this: 56ef2951db1a648353d7daac6a04c7d1
6701]
6702[debug.py: remove some dead comments
6703Brian Warner <warner@lothar.com>**20110828074556
6704 Ignore-this: 40e74040dd4d14fd2f4e4baaae506b31
6705]
6706[hush pyflakes
6707Brian Warner <warner@lothar.com>**20110828074254
6708 Ignore-this: bef9d537a969fa82fe4decc4ba2acb09
6709]
6710[MutableFileNode.set_downloader_hints: never depend upon order of dict.values()
6711Brian Warner <warner@lothar.com>**20110828074103
6712 Ignore-this: caaf1aa518dbdde4d797b7f335230faa
6713 
6714 The old code was calculating the "extension parameters" (a list) from the
6715 downloader hints (a dictionary) with hints.values(), which is not stable, and
6716 would result in corrupted filecaps (with the 'k' and 'segsize' hints
6717 occasionally swapped). The new code always uses [k,segsize].
6718]
6719[layout.py: fix MDMF share layout documentation
6720Brian Warner <warner@lothar.com>**20110828073921
6721 Ignore-this: 3f13366fed75b5e31b51ae895450a225
6722]
6723[teach 'tahoe debug dump-share' about MDMF and offsets. refs #1507
6724Brian Warner <warner@lothar.com>**20110828073834
6725 Ignore-this: 3a9d2ef9c47a72bf1506ba41199a1dea
6726]
6727[test_mutable.Version.test_debug: use splitlines() to fix buildslaves
6728Brian Warner <warner@lothar.com>**20110828064728
6729 Ignore-this: c7f6245426fc80b9d1ae901d5218246a
6730 
6731 Any slave running in a directory with spaces in the name was miscounting
6732 shares, causing the test to fail.
6733]
6734[test_mutable.Version: exercise 'tahoe debug find-shares' on MDMF. refs #1507
6735Brian Warner <warner@lothar.com>**20110828005542
6736 Ignore-this: cb20bea1c28bfa50a72317d70e109672
6737 
6738 Also changes NoNetworkGrid to put shares in storage/shares/ .
6739]
6740[test_mutable.py: oops, missed a .todo
6741Brian Warner <warner@lothar.com>**20110828002118
6742 Ignore-this: fda09ae86481352b7a627c278d2a3940
6743]
6744[test_mutable: merge davidsarah's patch with my Version refactorings
6745warner@lothar.com**20110827235707
6746 Ignore-this: b5aaf481c90d99e33827273b5d118fd0
6747]
6748[Make the immutable/read-only constraint checking for MDMF URIs identical to that for SSK URIs. refs #393
6749david-sarah@jacaranda.org**20110823012720
6750 Ignore-this: e1f59d7ff2007c81dbef2aeb14abd721
6751]
6752[Additional tests for MDMF URIs and for zero-length files. refs #393
6753david-sarah@jacaranda.org**20110823011532
6754 Ignore-this: a7cc0c09d1d2d72413f9cd227c47a9d5
6755]
6756[Additional tests for zero-length partial reads and updates to mutable versions. refs #393
6757david-sarah@jacaranda.org**20110822014111
6758 Ignore-this: 5fc6f4d06e11910124e4a277ec8a43ea
6759]
6760[test_mutable.Version: factor out some expensive uploads, save 25% runtime
6761Brian Warner <warner@lothar.com>**20110827232737
6762 Ignore-this: ea37383eb85ea0894b254fe4dfb45544
6763]
6764[SDMF: update filenode with correct k/N after Retrieve. Fixes #1510.
6765Brian Warner <warner@lothar.com>**20110827225031
6766 Ignore-this: b50ae6e1045818c400079f118b4ef48
6767 
6768 Without this, we get a regression when modifying a mutable file that was
6769 created with more shares (larger N) than our current tahoe.cfg . The
6770 modification attempt creates new versions of the (0,1,..,newN-1) shares, but
6771 leaves the old versions of the (newN,..,oldN-1) shares alone (and throws a
6772 assertion error in SDMFSlotWriteProxy.finish_publishing in the process).
6773 
6774 The mixed versions that result (some shares with e.g. N=10, some with N=20,
6775 such that both versions are recoverable) cause problems for the Publish code,
6776 even before MDMF landed. Might be related to refs #1390 and refs #1042.
6777]
6778[layout.py: annotate assertion to figure out 'tahoe backup' failure
6779Brian Warner <warner@lothar.com>**20110827195253
6780 Ignore-this: 9b92b954e3ed0d0f80154fff1ff674e5
6781]
6782[Add 'tahoe debug dump-cap' support for MDMF, DIR2-CHK, DIR2-MDMF. refs #1507.
6783Brian Warner <warner@lothar.com>**20110827195048
6784 Ignore-this: 61c6af5e33fc88e0251e697a50addb2c
6785 
6786 This also adds tests for all those cases, and fixes an omission in uri.py
6787 that broke parsing of DIR2-MDMF-Verifier and DIR2-CHK-Verifier.
6788]
6789[MDMF: more writable/writeable consistentifications
6790warner@lothar.com**20110827190602
6791 Ignore-this: 22492a9e20c1819ddb12091062888b55
6792]
6793[MDMF: s/Writable/Writeable/g, for consistency with existing SDMF code
6794warner@lothar.com**20110827183357
6795 Ignore-this: 9dd312acedbdb2fc2f7bef0d0fb17c0b
6796]
6797[setup.cfg: remove no-longer-supported test_mac_diskimage alias. refs #1479
6798david-sarah@jacaranda.org**20110826230345
6799 Ignore-this: 40e908b8937322a290fb8012bfcad02a
6800]
6801[test_mutable.Update: increase timeout from 120s to 400s, slaves are failing
6802Brian Warner <warner@lothar.com>**20110825230140
6803 Ignore-this: 101b1924a30cdbda9b2e419e95ca15ec
6804]
6805[tests: fix check_memory test
6806zooko@zooko.com**20110825201116
6807 Ignore-this: 4d66299fa8cb61d2ca04b3f45344d835
6808 fixes #1503
6809]
6810[TAG allmydata-tahoe-1.9.0a1
6811warner@lothar.com**20110825161122
6812 Ignore-this: 3cbf49f00dbda58189f893c427f65605
6813]
6814Patch bundle hash:
6815ec2d886b10b87db445cb61c5e0eb6088d83a82f8