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

File pluggable-backends-davidsarah-v8.darcs.patch, 375.1 KB (added by davidsarah, at 2011-09-21T22:29:14Z)

v8 snapshot. More tests pass.

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