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

File pluggable-backends-davidsarah-v6.darcs.patch, 322.1 KB (added by davidsarah, at 2011-09-21T03:21:58Z)

v6. Tests are looking in much better shape now -- still some problems with path vs FilePath? and other stale assumptions in the test framework, but the disk backend basically works now.

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