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

File pluggable-backends-davidsarah-v11.darcs.patch, 487.1 KB (added by davidsarah, at 2011-09-23T04:20:00Z)

Includes a fix for iterating over a dict while removing entries from it in mutable/publish.py, some cosmetic changes, and a start on the S3 backend.

Line 
119 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
36Thu Sep 22 05:54:51 BST 2011  david-sarah@jacaranda.org
37  * Fix some more test failures. refs #999
38
39Thu Sep 22 19:30:08 BST 2011  david-sarah@jacaranda.org
40  * Fix most of the crawler tests. refs #999
41
42Thu Sep 22 19:33:23 BST 2011  david-sarah@jacaranda.org
43  * Reinstate the cancel_lease methods of ImmutableDiskShare and MutableDiskShare, since they are needed for lease expiry. refs #999
44
45Fri Sep 23 02:20:44 BST 2011  david-sarah@jacaranda.org
46  * Blank line cleanups.
47
48Fri Sep 23 05:08:25 BST 2011  david-sarah@jacaranda.org
49  * mutable/publish.py: elements should not be removed from a dictionary while it is being iterated over. refs #393
50
51Fri Sep 23 05:10:03 BST 2011  david-sarah@jacaranda.org
52  * A few comment cleanups. refs #999
53
54Fri Sep 23 05:11:15 BST 2011  david-sarah@jacaranda.org
55  * Move advise_corrupt_share to allmydata/storage/backends/base.py, since it will be common to the disk and S3 backends. refs #999
56
57Fri Sep 23 05:13:14 BST 2011  david-sarah@jacaranda.org
58  * Add incomplete S3 backend. refs #999
59
60New patches:
61
62[interfaces.py: 'which -> that' grammar cleanup.
63david-sarah@jacaranda.org**20110825003217
64 Ignore-this: a3e15f3676de1b346ad78aabdfb8cac6
65] {
66hunk ./src/allmydata/interfaces.py 38
67     the StubClient. This object doesn't actually offer any services, but the
68     announcement helps the Introducer keep track of which clients are
69     subscribed (so the grid admin can keep track of things like the size of
70-    the grid and the client versions in use. This is the (empty)
71+    the grid and the client versions in use). This is the (empty)
72     RemoteInterface for the StubClient."""
73 
74 class RIBucketWriter(RemoteInterface):
75hunk ./src/allmydata/interfaces.py 276
76         (binary) storage index string, and 'shnum' is the integer share
77         number. 'reason' is a human-readable explanation of the problem,
78         probably including some expected hash values and the computed ones
79-        which did not match. Corruption advisories for mutable shares should
80+        that did not match. Corruption advisories for mutable shares should
81         include a hash of the public key (the same value that appears in the
82         mutable-file verify-cap), since the current share format does not
83         store that on disk.
84hunk ./src/allmydata/interfaces.py 413
85           remote_host: the IAddress, if connected, otherwise None
86 
87         This method is intended for monitoring interfaces, such as a web page
88-        which describes connecting and connected peers.
89+        that describes connecting and connected peers.
90         """
91 
92     def get_all_peerids():
93hunk ./src/allmydata/interfaces.py 515
94 
95     # TODO: rename to get_read_cap()
96     def get_readonly():
97-        """Return another IURI instance, which represents a read-only form of
98+        """Return another IURI instance that represents a read-only form of
99         this one. If is_readonly() is True, this returns self."""
100 
101     def get_verify_cap():
102hunk ./src/allmydata/interfaces.py 542
103         passing into init_from_string."""
104 
105 class IDirnodeURI(Interface):
106-    """I am a URI which represents a dirnode."""
107+    """I am a URI that represents a dirnode."""
108 
109 class IFileURI(Interface):
110hunk ./src/allmydata/interfaces.py 545
111-    """I am a URI which represents a filenode."""
112+    """I am a URI that represents a filenode."""
113     def get_size():
114         """Return the length (in bytes) of the file that I represent."""
115 
116hunk ./src/allmydata/interfaces.py 553
117     pass
118 
119 class IMutableFileURI(Interface):
120-    """I am a URI which represents a mutable filenode."""
121+    """I am a URI that represents a mutable filenode."""
122     def get_extension_params():
123         """Return the extension parameters in the URI"""
124 
125hunk ./src/allmydata/interfaces.py 856
126         """
127 
128 class IFileNode(IFilesystemNode):
129-    """I am a node which represents a file: a sequence of bytes. I am not a
130+    """I am a node that represents a file: a sequence of bytes. I am not a
131     container, like IDirectoryNode."""
132     def get_best_readable_version():
133         """Return a Deferred that fires with an IReadable for the 'best'
134hunk ./src/allmydata/interfaces.py 905
135     multiple versions of a file present in the grid, some of which might be
136     unrecoverable (i.e. have fewer than 'k' shares). These versions are
137     loosely ordered: each has a sequence number and a hash, and any version
138-    with seqnum=N was uploaded by a node which has seen at least one version
139+    with seqnum=N was uploaded by a node that has seen at least one version
140     with seqnum=N-1.
141 
142     The 'servermap' (an instance of IMutableFileServerMap) is used to
143hunk ./src/allmydata/interfaces.py 1014
144         as a guide to where the shares are located.
145 
146         I return a Deferred that fires with the requested contents, or
147-        errbacks with UnrecoverableFileError. Note that a servermap which was
148+        errbacks with UnrecoverableFileError. Note that a servermap that was
149         updated with MODE_ANYTHING or MODE_READ may not know about shares for
150         all versions (those modes stop querying servers as soon as they can
151         fulfil their goals), so you may want to use MODE_CHECK (which checks
152hunk ./src/allmydata/interfaces.py 1073
153     """Upload was unable to satisfy 'servers_of_happiness'"""
154 
155 class UnableToFetchCriticalDownloadDataError(Exception):
156-    """I was unable to fetch some piece of critical data which is supposed to
157+    """I was unable to fetch some piece of critical data that is supposed to
158     be identically present in all shares."""
159 
160 class NoServersError(Exception):
161hunk ./src/allmydata/interfaces.py 1085
162     exists, and overwrite= was set to False."""
163 
164 class NoSuchChildError(Exception):
165-    """A directory node was asked to fetch a child which does not exist."""
166+    """A directory node was asked to fetch a child that does not exist."""
167 
168 class ChildOfWrongTypeError(Exception):
169     """An operation was attempted on a child of the wrong type (file or directory)."""
170hunk ./src/allmydata/interfaces.py 1403
171         if you initially thought you were going to use 10 peers, started
172         encoding, and then two of the peers dropped out: you could use
173         desired_share_ids= to skip the work (both memory and CPU) of
174-        producing shares for the peers which are no longer available.
175+        producing shares for the peers that are no longer available.
176 
177         """
178 
179hunk ./src/allmydata/interfaces.py 1478
180         if you initially thought you were going to use 10 peers, started
181         encoding, and then two of the peers dropped out: you could use
182         desired_share_ids= to skip the work (both memory and CPU) of
183-        producing shares for the peers which are no longer available.
184+        producing shares for the peers that are no longer available.
185 
186         For each call, encode() will return a Deferred that fires with two
187         lists, one containing shares and the other containing the shareids.
188hunk ./src/allmydata/interfaces.py 1535
189         required to be of the same length.  The i'th element of their_shareids
190         is required to be the shareid of the i'th buffer in some_shares.
191 
192-        This returns a Deferred which fires with a sequence of buffers. This
193+        This returns a Deferred that fires with a sequence of buffers. This
194         sequence will contain all of the segments of the original data, in
195         order. The sum of the lengths of all of the buffers will be the
196         'data_size' value passed into the original ICodecEncode.set_params()
197hunk ./src/allmydata/interfaces.py 1582
198         Encoding parameters can be set in three ways. 1: The Encoder class
199         provides defaults (3/7/10). 2: the Encoder can be constructed with
200         an 'options' dictionary, in which the
201-        needed_and_happy_and_total_shares' key can be a (k,d,n) tuple. 3:
202+        'needed_and_happy_and_total_shares' key can be a (k,d,n) tuple. 3:
203         set_params((k,d,n)) can be called.
204 
205         If you intend to use set_params(), you must call it before
206hunk ./src/allmydata/interfaces.py 1780
207         produced, so that the segment hashes can be generated with only a
208         single pass.
209 
210-        This returns a Deferred which fires with a sequence of hashes, using:
211+        This returns a Deferred that fires with a sequence of hashes, using:
212 
213          tuple(segment_hashes[first:last])
214 
215hunk ./src/allmydata/interfaces.py 1796
216     def get_plaintext_hash():
217         """OBSOLETE; Get the hash of the whole plaintext.
218 
219-        This returns a Deferred which fires with a tagged SHA-256 hash of the
220+        This returns a Deferred that fires with a tagged SHA-256 hash of the
221         whole plaintext, obtained from hashutil.plaintext_hash(data).
222         """
223 
224hunk ./src/allmydata/interfaces.py 1856
225         be used to encrypt the data. The key will also be hashed to derive
226         the StorageIndex.
227 
228-        Uploadables which want to achieve convergence should hash their file
229+        Uploadables that want to achieve convergence should hash their file
230         contents and the serialized_encoding_parameters to form the key
231         (which of course requires a full pass over the data). Uploadables can
232         use the upload.ConvergentUploadMixin class to achieve this
233hunk ./src/allmydata/interfaces.py 1862
234         automatically.
235 
236-        Uploadables which do not care about convergence (or do not wish to
237+        Uploadables that do not care about convergence (or do not wish to
238         make multiple passes over the data) can simply return a
239         strongly-random 16 byte string.
240 
241hunk ./src/allmydata/interfaces.py 1872
242 
243     def read(length):
244         """Return a Deferred that fires with a list of strings (perhaps with
245-        only a single element) which, when concatenated together, contain the
246+        only a single element) that, when concatenated together, contain the
247         next 'length' bytes of data. If EOF is near, this may provide fewer
248         than 'length' bytes. The total number of bytes provided by read()
249         before it signals EOF must equal the size provided by get_size().
250hunk ./src/allmydata/interfaces.py 1919
251 
252     def read(length):
253         """
254-        Returns a list of strings which, when concatenated, are the next
255+        Returns a list of strings that, when concatenated, are the next
256         length bytes of the file, or fewer if there are fewer bytes
257         between the current location and the end of the file.
258         """
259hunk ./src/allmydata/interfaces.py 1932
260 
261 class IUploadResults(Interface):
262     """I am returned by upload() methods. I contain a number of public
263-    attributes which can be read to determine the results of the upload. Some
264+    attributes that can be read to determine the results of the upload. Some
265     of these are functional, some are timing information. All of these may be
266     None.
267 
268hunk ./src/allmydata/interfaces.py 1965
269 
270 class IDownloadResults(Interface):
271     """I am created internally by download() methods. I contain a number of
272-    public attributes which contain details about the download process.::
273+    public attributes that contain details about the download process.::
274 
275      .file_size : the size of the file, in bytes
276      .servers_used : set of server peerids that were used during download
277hunk ./src/allmydata/interfaces.py 1991
278 class IUploader(Interface):
279     def upload(uploadable):
280         """Upload the file. 'uploadable' must impement IUploadable. This
281-        returns a Deferred which fires with an IUploadResults instance, from
282+        returns a Deferred that fires with an IUploadResults instance, from
283         which the URI of the file can be obtained as results.uri ."""
284 
285     def upload_ssk(write_capability, new_version, uploadable):
286hunk ./src/allmydata/interfaces.py 2041
287         kind of lease that is obtained (which account number to claim, etc).
288 
289         TODO: any problems seen during checking will be reported to the
290-        health-manager.furl, a centralized object which is responsible for
291+        health-manager.furl, a centralized object that is responsible for
292         figuring out why files are unhealthy so corrective action can be
293         taken.
294         """
295hunk ./src/allmydata/interfaces.py 2056
296         will be put in the check-and-repair results. The Deferred will not
297         fire until the repair is complete.
298 
299-        This returns a Deferred which fires with an instance of
300+        This returns a Deferred that fires with an instance of
301         ICheckAndRepairResults."""
302 
303 class IDeepCheckable(Interface):
304hunk ./src/allmydata/interfaces.py 2141
305                               that was found to be corrupt. Each share
306                               locator is a list of (serverid, storage_index,
307                               sharenum).
308-         count-incompatible-shares: the number of shares which are of a share
309+         count-incompatible-shares: the number of shares that are of a share
310                                     format unknown to this checker
311          list-incompatible-shares: a list of 'share locators', one for each
312                                    share that was found to be of an unknown
313hunk ./src/allmydata/interfaces.py 2148
314                                    format. Each share locator is a list of
315                                    (serverid, storage_index, sharenum).
316          servers-responding: list of (binary) storage server identifiers,
317-                             one for each server which responded to the share
318+                             one for each server that responded to the share
319                              query (even if they said they didn't have
320                              shares, and even if they said they did have
321                              shares but then didn't send them when asked, or
322hunk ./src/allmydata/interfaces.py 2345
323         will use the data in the checker results to guide the repair process,
324         such as which servers provided bad data and should therefore be
325         avoided. The ICheckResults object is inside the
326-        ICheckAndRepairResults object, which is returned by the
327+        ICheckAndRepairResults object that is returned by the
328         ICheckable.check() method::
329 
330          d = filenode.check(repair=False)
331hunk ./src/allmydata/interfaces.py 2436
332         methods to create new objects. I return synchronously."""
333 
334     def create_mutable_file(contents=None, keysize=None):
335-        """I create a new mutable file, and return a Deferred which will fire
336+        """I create a new mutable file, and return a Deferred that will fire
337         with the IMutableFileNode instance when it is ready. If contents= is
338         provided (a bytestring), it will be used as the initial contents of
339         the new file, otherwise the file will contain zero bytes. keysize= is
340hunk ./src/allmydata/interfaces.py 2444
341         usual."""
342 
343     def create_new_mutable_directory(initial_children={}):
344-        """I create a new mutable directory, and return a Deferred which will
345+        """I create a new mutable directory, and return a Deferred that will
346         fire with the IDirectoryNode instance when it is ready. If
347         initial_children= is provided (a dict mapping unicode child name to
348         (childnode, metadata_dict) tuples), the directory will be populated
349hunk ./src/allmydata/interfaces.py 2452
350 
351 class IClientStatus(Interface):
352     def list_all_uploads():
353-        """Return a list of uploader objects, one for each upload which
354+        """Return a list of uploader objects, one for each upload that
355         currently has an object available (tracked with weakrefs). This is
356         intended for debugging purposes."""
357     def list_active_uploads():
358hunk ./src/allmydata/interfaces.py 2462
359         started uploads."""
360 
361     def list_all_downloads():
362-        """Return a list of downloader objects, one for each download which
363+        """Return a list of downloader objects, one for each download that
364         currently has an object available (tracked with weakrefs). This is
365         intended for debugging purposes."""
366     def list_active_downloads():
367hunk ./src/allmydata/interfaces.py 2689
368 
369     def provide(provider=RIStatsProvider, nickname=str):
370         """
371-        @param provider: a stats collector instance which should be polled
372+        @param provider: a stats collector instance that should be polled
373                          periodically by the gatherer to collect stats.
374         @param nickname: a name useful to identify the provided client
375         """
376hunk ./src/allmydata/interfaces.py 2722
377 
378 class IValidatedThingProxy(Interface):
379     def start():
380-        """ Acquire a thing and validate it. Return a deferred which is
381+        """ Acquire a thing and validate it. Return a deferred that is
382         eventually fired with self if the thing is valid or errbacked if it
383         can't be acquired or validated."""
384 
385}
386[Pluggable backends -- new and moved files, changes to moved files. refs #999
387david-sarah@jacaranda.org**20110919232926
388 Ignore-this: ec5d2d1362a092d919e84327d3092424
389] {
390adddir ./src/allmydata/storage/backends
391adddir ./src/allmydata/storage/backends/disk
392move ./src/allmydata/storage/immutable.py ./src/allmydata/storage/backends/disk/immutable.py
393move ./src/allmydata/storage/mutable.py ./src/allmydata/storage/backends/disk/mutable.py
394adddir ./src/allmydata/storage/backends/null
395addfile ./src/allmydata/storage/backends/__init__.py
396addfile ./src/allmydata/storage/backends/base.py
397hunk ./src/allmydata/storage/backends/base.py 1
398+
399+from twisted.application import service
400+
401+from allmydata.storage.common import si_b2a
402+from allmydata.storage.lease import LeaseInfo
403+from allmydata.storage.bucket import BucketReader
404+
405+
406+class Backend(service.MultiService):
407+    def __init__(self):
408+        service.MultiService.__init__(self)
409+
410+
411+class ShareSet(object):
412+    """
413+    This class implements shareset logic that could work for all backends, but
414+    might be useful to override for efficiency.
415+    """
416+
417+    def __init__(self, storageindex):
418+        self.storageindex = storageindex
419+
420+    def get_storage_index(self):
421+        return self.storageindex
422+
423+    def get_storage_index_string(self):
424+        return si_b2a(self.storageindex)
425+
426+    def renew_lease(self, renew_secret, new_expiration_time):
427+        found_shares = False
428+        for share in self.get_shares():
429+            found_shares = True
430+            share.renew_lease(renew_secret, new_expiration_time)
431+
432+        if not found_shares:
433+            raise IndexError("no such lease to renew")
434+
435+    def get_leases(self):
436+        # Since all shares get the same lease data, we just grab the leases
437+        # from the first share.
438+        try:
439+            sf = self.get_shares().next()
440+            return sf.get_leases()
441+        except StopIteration:
442+            return iter([])
443+
444+    def add_or_renew_lease(self, lease_info):
445+        # This implementation assumes that lease data is duplicated in
446+        # all shares of a shareset, which might not be true for all backends.
447+        for share in self.get_shares():
448+            share.add_or_renew_lease(lease_info)
449+
450+    def make_bucket_reader(self, storageserver, share):
451+        return BucketReader(storageserver, share)
452+
453+    def testv_and_readv_and_writev(self, storageserver, secrets,
454+                                   test_and_write_vectors, read_vector,
455+                                   expiration_time):
456+        # The implementation here depends on the following helper methods,
457+        # which must be provided by subclasses:
458+        #
459+        # def _clean_up_after_unlink(self):
460+        #     """clean up resources associated with the shareset after some
461+        #     shares might have been deleted"""
462+        #
463+        # def _create_mutable_share(self, storageserver, shnum, write_enabler):
464+        #     """create a mutable share with the given shnum and write_enabler"""
465+
466+        # secrets might be a triple with cancel_secret in secrets[2], but if
467+        # so we ignore the cancel_secret.
468+        write_enabler = secrets[0]
469+        renew_secret = secrets[1]
470+
471+        si_s = self.get_storage_index_string()
472+        shares = {}
473+        for share in self.get_shares():
474+            # XXX is it correct to ignore immutable shares? Maybe get_shares should
475+            # have a parameter saying what type it's expecting.
476+            if share.sharetype == "mutable":
477+                share.check_write_enabler(write_enabler, si_s)
478+                shares[share.get_shnum()] = share
479+
480+        # write_enabler is good for all existing shares
481+
482+        # now evaluate test vectors
483+        testv_is_good = True
484+        for sharenum in test_and_write_vectors:
485+            (testv, datav, new_length) = test_and_write_vectors[sharenum]
486+            if sharenum in shares:
487+                if not shares[sharenum].check_testv(testv):
488+                    self.log("testv failed: [%d]: %r" % (sharenum, testv))
489+                    testv_is_good = False
490+                    break
491+            else:
492+                # compare the vectors against an empty share, in which all
493+                # reads return empty strings
494+                if not EmptyShare().check_testv(testv):
495+                    self.log("testv failed (empty): [%d] %r" % (sharenum,
496+                                                                testv))
497+                    testv_is_good = False
498+                    break
499+
500+        # gather the read vectors, before we do any writes
501+        read_data = {}
502+        for shnum, share in shares.items():
503+            read_data[shnum] = share.readv(read_vector)
504+
505+        ownerid = 1 # TODO
506+        lease_info = LeaseInfo(ownerid, renew_secret,
507+                               expiration_time, storageserver.get_serverid())
508+
509+        if testv_is_good:
510+            # now apply the write vectors
511+            for shnum in test_and_write_vectors:
512+                (testv, datav, new_length) = test_and_write_vectors[shnum]
513+                if new_length == 0:
514+                    if shnum in shares:
515+                        shares[shnum].unlink()
516+                else:
517+                    if shnum not in shares:
518+                        # allocate a new share
519+                        share = self._create_mutable_share(storageserver, shnum, write_enabler)
520+                        shares[shnum] = share
521+                    shares[shnum].writev(datav, new_length)
522+                    # and update the lease
523+                    shares[shnum].add_or_renew_lease(lease_info)
524+
525+            if new_length == 0:
526+                self._clean_up_after_unlink()
527+
528+        return (testv_is_good, read_data)
529+
530+    def readv(self, wanted_shnums, read_vector):
531+        """
532+        Read a vector from the numbered shares in this shareset. An empty
533+        shares list means to return data from all known shares.
534+
535+        @param wanted_shnums=ListOf(int)
536+        @param read_vector=ReadVector
537+        @return DictOf(int, ReadData): shnum -> results, with one key per share
538+        """
539+        datavs = {}
540+        for share in self.get_shares():
541+            shnum = share.get_shnum()
542+            if not wanted_shnums or shnum in wanted_shnums:
543+                datavs[shnum] = share.readv(read_vector)
544+
545+        return datavs
546+
547+
548+def testv_compare(a, op, b):
549+    assert op in ("lt", "le", "eq", "ne", "ge", "gt")
550+    if op == "lt":
551+        return a < b
552+    if op == "le":
553+        return a <= b
554+    if op == "eq":
555+        return a == b
556+    if op == "ne":
557+        return a != b
558+    if op == "ge":
559+        return a >= b
560+    if op == "gt":
561+        return a > b
562+    # never reached
563+
564+
565+class EmptyShare:
566+    def check_testv(self, testv):
567+        test_good = True
568+        for (offset, length, operator, specimen) in testv:
569+            data = ""
570+            if not testv_compare(data, operator, specimen):
571+                test_good = False
572+                break
573+        return test_good
574+
575addfile ./src/allmydata/storage/backends/disk/__init__.py
576addfile ./src/allmydata/storage/backends/disk/disk_backend.py
577hunk ./src/allmydata/storage/backends/disk/disk_backend.py 1
578+
579+import re
580+
581+from twisted.python.filepath import UnlistableError
582+
583+from zope.interface import implements
584+from allmydata.interfaces import IStorageBackend, IShareSet
585+from allmydata.util import fileutil, log, time_format
586+from allmydata.storage.common import si_b2a, si_a2b
587+from allmydata.storage.bucket import BucketWriter
588+from allmydata.storage.backends.base import Backend, ShareSet
589+from allmydata.storage.backends.disk.immutable import ImmutableDiskShare
590+from allmydata.storage.backends.disk.mutable import MutableDiskShare, create_mutable_disk_share
591+
592+# storage/
593+# storage/shares/incoming
594+#   incoming/ holds temp dirs named $START/$STORAGEINDEX/$SHARENUM which will
595+#   be moved to storage/shares/$START/$STORAGEINDEX/$SHARENUM upon success
596+# storage/shares/$START/$STORAGEINDEX
597+# storage/shares/$START/$STORAGEINDEX/$SHARENUM
598+
599+# Where "$START" denotes the first 10 bits worth of $STORAGEINDEX (that's 2
600+# base-32 chars).
601+# $SHARENUM matches this regex:
602+NUM_RE=re.compile("^[0-9]+$")
603+
604+
605+def si_si2dir(startfp, storageindex):
606+    sia = si_b2a(storageindex)
607+    newfp = startfp.child(sia[:2])
608+    return newfp.child(sia)
609+
610+
611+def get_share(fp):
612+    f = fp.open('rb')
613+    try:
614+        prefix = f.read(32)
615+    finally:
616+        f.close()
617+
618+    if prefix == MutableDiskShare.MAGIC:
619+        return MutableDiskShare(fp)
620+    else:
621+        # assume it's immutable
622+        return ImmutableDiskShare(fp)
623+
624+
625+class DiskBackend(Backend):
626+    implements(IStorageBackend)
627+
628+    def __init__(self, storedir, readonly=False, reserved_space=0, discard_storage=False):
629+        Backend.__init__(self)
630+        self._setup_storage(storedir, readonly, reserved_space, discard_storage)
631+        self._setup_corruption_advisory()
632+
633+    def _setup_storage(self, storedir, readonly, reserved_space, discard_storage):
634+        self._storedir = storedir
635+        self._readonly = readonly
636+        self._reserved_space = int(reserved_space)
637+        self._discard_storage = discard_storage
638+        self._sharedir = self._storedir.child("shares")
639+        fileutil.fp_make_dirs(self._sharedir)
640+        self._incomingdir = self._sharedir.child('incoming')
641+        self._clean_incomplete()
642+        if self._reserved_space and (self.get_available_space() is None):
643+            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",
644+                    umid="0wZ27w", level=log.UNUSUAL)
645+
646+    def _clean_incomplete(self):
647+        fileutil.fp_remove(self._incomingdir)
648+        fileutil.fp_make_dirs(self._incomingdir)
649+
650+    def _setup_corruption_advisory(self):
651+        # we don't actually create the corruption-advisory dir until necessary
652+        self._corruption_advisory_dir = self._storedir.child("corruption-advisories")
653+
654+    def _make_shareset(self, sharehomedir):
655+        return self.get_shareset(si_a2b(sharehomedir.basename()))
656+
657+    def get_sharesets_for_prefix(self, prefix):
658+        prefixfp = self._sharedir.child(prefix)
659+        try:
660+            sharesets = map(self._make_shareset, prefixfp.children())
661+            def _by_base32si(b):
662+                return b.get_storage_index_string()
663+            sharesets.sort(key=_by_base32si)
664+        except EnvironmentError:
665+            sharesets = []
666+        return sharesets
667+
668+    def get_shareset(self, storageindex):
669+        sharehomedir = si_si2dir(self._sharedir, storageindex)
670+        incominghomedir = si_si2dir(self._incomingdir, storageindex)
671+        return DiskShareSet(storageindex, sharehomedir, incominghomedir, discard_storage=self._discard_storage)
672+
673+    def fill_in_space_stats(self, stats):
674+        stats['storage_server.reserved_space'] = self._reserved_space
675+        try:
676+            disk = fileutil.get_disk_stats(self._sharedir, self._reserved_space)
677+            writeable = disk['avail'] > 0
678+
679+            # spacetime predictors should use disk_avail / (d(disk_used)/dt)
680+            stats['storage_server.disk_total'] = disk['total']
681+            stats['storage_server.disk_used'] = disk['used']
682+            stats['storage_server.disk_free_for_root'] = disk['free_for_root']
683+            stats['storage_server.disk_free_for_nonroot'] = disk['free_for_nonroot']
684+            stats['storage_server.disk_avail'] = disk['avail']
685+        except AttributeError:
686+            writeable = True
687+        except EnvironmentError:
688+            log.msg("OS call to get disk statistics failed", level=log.UNUSUAL)
689+            writeable = False
690+
691+        if self._readonly:
692+            stats['storage_server.disk_avail'] = 0
693+            writeable = False
694+
695+        stats['storage_server.accepting_immutable_shares'] = int(writeable)
696+
697+    def get_available_space(self):
698+        if self._readonly:
699+            return 0
700+        return fileutil.get_available_space(self._sharedir, self._reserved_space)
701+
702+    def advise_corrupt_share(self, sharetype, storageindex, shnum, reason):
703+        fileutil.fp_make_dirs(self._corruption_advisory_dir)
704+        now = time_format.iso_utc(sep="T")
705+        si_s = si_b2a(storageindex)
706+
707+        # Windows can't handle colons in the filename.
708+        name = ("%s--%s-%d" % (now, si_s, shnum)).replace(":", "")
709+        f = self._corruption_advisory_dir.child(name).open("w")
710+        try:
711+            f.write("report: Share Corruption\n")
712+            f.write("type: %s\n" % sharetype)
713+            f.write("storage_index: %s\n" % si_s)
714+            f.write("share_number: %d\n" % shnum)
715+            f.write("\n")
716+            f.write(reason)
717+            f.write("\n")
718+        finally:
719+            f.close()
720+
721+        log.msg(format=("client claims corruption in (%(share_type)s) " +
722+                        "%(si)s-%(shnum)d: %(reason)s"),
723+                share_type=sharetype, si=si_s, shnum=shnum, reason=reason,
724+                level=log.SCARY, umid="SGx2fA")
725+
726+
727+class DiskShareSet(ShareSet):
728+    implements(IShareSet)
729+
730+    def __init__(self, storageindex, sharehomedir, incominghomedir=None, discard_storage=False):
731+        ShareSet.__init__(self, storageindex)
732+        self._sharehomedir = sharehomedir
733+        self._incominghomedir = incominghomedir
734+        self._discard_storage = discard_storage
735+
736+    def get_overhead(self):
737+        return (fileutil.get_disk_usage(self._sharehomedir) +
738+                fileutil.get_disk_usage(self._incominghomedir))
739+
740+    def get_shares(self):
741+        """
742+        Generate IStorageBackendShare objects for shares we have for this storage index.
743+        ("Shares we have" means completed ones, excluding incoming ones.)
744+        """
745+        try:
746+            for fp in self._sharehomedir.children():
747+                shnumstr = fp.basename()
748+                if not NUM_RE.match(shnumstr):
749+                    continue
750+                sharehome = self._sharehomedir.child(shnumstr)
751+                yield self.get_share(sharehome)
752+        except UnlistableError:
753+            # There is no shares directory at all.
754+            pass
755+
756+    def has_incoming(self, shnum):
757+        if self._incominghomedir is None:
758+            return False
759+        return self._incominghomedir.child(str(shnum)).exists()
760+
761+    def make_bucket_writer(self, storageserver, shnum, max_space_per_bucket, lease_info, canary):
762+        sharehome = self._sharehomedir.child(str(shnum))
763+        incominghome = self._incominghomedir.child(str(shnum))
764+        immsh = ImmutableDiskShare(self.get_storage_index(), shnum, sharehome, incominghome,
765+                                   max_size=max_space_per_bucket, create=True)
766+        bw = BucketWriter(storageserver, immsh, max_space_per_bucket, lease_info, canary)
767+        if self._discard_storage:
768+            bw.throw_out_all_data = True
769+        return bw
770+
771+    def _create_mutable_share(self, storageserver, shnum, write_enabler):
772+        fileutil.fp_make_dirs(self._sharehomedir)
773+        sharehome = self._sharehomedir.child(str(shnum))
774+        serverid = storageserver.get_serverid()
775+        return create_mutable_disk_share(sharehome, serverid, write_enabler, storageserver)
776+
777+    def _clean_up_after_unlink(self):
778+        fileutil.fp_rmdir_if_empty(self._sharehomedir)
779+
780hunk ./src/allmydata/storage/backends/disk/immutable.py 1
781-import os, stat, struct, time
782 
783hunk ./src/allmydata/storage/backends/disk/immutable.py 2
784-from foolscap.api import Referenceable
785+import struct
786 
787 from zope.interface import implements
788hunk ./src/allmydata/storage/backends/disk/immutable.py 5
789-from allmydata.interfaces import RIBucketWriter, RIBucketReader
790-from allmydata.util import base32, fileutil, log
791+
792+from allmydata.interfaces import IStoredShare
793+from allmydata.util import fileutil
794 from allmydata.util.assertutil import precondition
795hunk ./src/allmydata/storage/backends/disk/immutable.py 9
796+from allmydata.util.fileutil import fp_make_dirs
797 from allmydata.util.hashutil import constant_time_compare
798hunk ./src/allmydata/storage/backends/disk/immutable.py 11
799+from allmydata.util.encodingutil import quote_filepath
800+from allmydata.storage.common import si_b2a, UnknownImmutableContainerVersionError, DataTooLargeError
801 from allmydata.storage.lease import LeaseInfo
802hunk ./src/allmydata/storage/backends/disk/immutable.py 14
803-from allmydata.storage.common import UnknownImmutableContainerVersionError, \
804-     DataTooLargeError
805+
806 
807 # each share file (in storage/shares/$SI/$SHNUM) contains lease information
808 # and share data. The share data is accessed by RIBucketWriter.write and
809hunk ./src/allmydata/storage/backends/disk/immutable.py 41
810 # then the value stored in this field will be the actual share data length
811 # modulo 2**32.
812 
813-class ShareFile:
814-    LEASE_SIZE = struct.calcsize(">L32s32sL")
815+class ImmutableDiskShare(object):
816+    implements(IStoredShare)
817+
818     sharetype = "immutable"
819hunk ./src/allmydata/storage/backends/disk/immutable.py 45
820+    LEASE_SIZE = struct.calcsize(">L32s32sL")
821+
822 
823hunk ./src/allmydata/storage/backends/disk/immutable.py 48
824-    def __init__(self, filename, max_size=None, create=False):
825-        """ 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. """
826+    def __init__(self, storageindex, shnum, finalhome=None, incominghome=None, max_size=None, create=False):
827+        """ If max_size is not None then I won't allow more than
828+        max_size to be written to me. If create=True then max_size
829+        must not be None. """
830         precondition((max_size is not None) or (not create), max_size, create)
831hunk ./src/allmydata/storage/backends/disk/immutable.py 53
832-        self.home = filename
833+        self._storageindex = storageindex
834         self._max_size = max_size
835hunk ./src/allmydata/storage/backends/disk/immutable.py 55
836+        self._incominghome = incominghome
837+        self._home = finalhome
838+        self._shnum = shnum
839         if create:
840             # touch the file, so later callers will see that we're working on
841             # it. Also construct the metadata.
842hunk ./src/allmydata/storage/backends/disk/immutable.py 61
843-            assert not os.path.exists(self.home)
844-            fileutil.make_dirs(os.path.dirname(self.home))
845-            f = open(self.home, 'wb')
846+            assert not finalhome.exists()
847+            fp_make_dirs(self._incominghome.parent())
848             # The second field -- the four-byte share data length -- is no
849             # longer used as of Tahoe v1.3.0, but we continue to write it in
850             # there in case someone downgrades a storage server from >=
851hunk ./src/allmydata/storage/backends/disk/immutable.py 72
852             # the largest length that can fit into the field. That way, even
853             # if this does happen, the old < v1.3.0 server will still allow
854             # clients to read the first part of the share.
855-            f.write(struct.pack(">LLL", 1, min(2**32-1, max_size), 0))
856-            f.close()
857+            self._incominghome.setContent(struct.pack(">LLL", 1, min(2**32-1, max_size), 0) )
858             self._lease_offset = max_size + 0x0c
859             self._num_leases = 0
860         else:
861hunk ./src/allmydata/storage/backends/disk/immutable.py 76
862-            f = open(self.home, 'rb')
863-            filesize = os.path.getsize(self.home)
864-            (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
865-            f.close()
866+            f = self._home.open(mode='rb')
867+            try:
868+                (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
869+            finally:
870+                f.close()
871+            filesize = self._home.getsize()
872             if version != 1:
873                 msg = "sharefile %s had version %d but we wanted 1" % \
874hunk ./src/allmydata/storage/backends/disk/immutable.py 84
875-                      (filename, version)
876+                      (self._home, version)
877                 raise UnknownImmutableContainerVersionError(msg)
878             self._num_leases = num_leases
879             self._lease_offset = filesize - (num_leases * self.LEASE_SIZE)
880hunk ./src/allmydata/storage/backends/disk/immutable.py 90
881         self._data_offset = 0xc
882 
883+    def __repr__(self):
884+        return ("<ImmutableDiskShare %s:%r at %s>"
885+                % (si_b2a(self._storageindex), self._shnum, quote_filepath(self._home)))
886+
887+    def close(self):
888+        fileutil.fp_make_dirs(self._home.parent())
889+        self._incominghome.moveTo(self._home)
890+        try:
891+            # self._incominghome is like storage/shares/incoming/ab/abcde/4 .
892+            # We try to delete the parent (.../ab/abcde) to avoid leaving
893+            # these directories lying around forever, but the delete might
894+            # fail if we're working on another share for the same storage
895+            # index (like ab/abcde/5). The alternative approach would be to
896+            # use a hierarchy of objects (PrefixHolder, BucketHolder,
897+            # ShareWriter), each of which is responsible for a single
898+            # directory on disk, and have them use reference counting of
899+            # their children to know when they should do the rmdir. This
900+            # approach is simpler, but relies on os.rmdir refusing to delete
901+            # a non-empty directory. Do *not* use fileutil.fp_remove() here!
902+            fileutil.fp_rmdir_if_empty(self._incominghome.parent())
903+            # we also delete the grandparent (prefix) directory, .../ab ,
904+            # again to avoid leaving directories lying around. This might
905+            # fail if there is another bucket open that shares a prefix (like
906+            # ab/abfff).
907+            fileutil.fp_rmdir_if_empty(self._incominghome.parent().parent())
908+            # we leave the great-grandparent (incoming/) directory in place.
909+        except EnvironmentError:
910+            # ignore the "can't rmdir because the directory is not empty"
911+            # exceptions, those are normal consequences of the
912+            # above-mentioned conditions.
913+            pass
914+        pass
915+
916+    def get_used_space(self):
917+        return (fileutil.get_used_space(self._home) +
918+                fileutil.get_used_space(self._incominghome))
919+
920+    def get_storage_index(self):
921+        return self._storageindex
922+
923+    def get_shnum(self):
924+        return self._shnum
925+
926     def unlink(self):
927hunk ./src/allmydata/storage/backends/disk/immutable.py 134
928-        os.unlink(self.home)
929+        self._home.remove()
930+
931+    def get_size(self):
932+        return self._home.getsize()
933+
934+    def get_data_length(self):
935+        return self._lease_offset - self._data_offset
936+
937+    #def readv(self, read_vector):
938+    #    ...
939 
940     def read_share_data(self, offset, length):
941         precondition(offset >= 0)
942hunk ./src/allmydata/storage/backends/disk/immutable.py 147
943-        # reads beyond the end of the data are truncated. Reads that start
944+
945+        # Reads beyond the end of the data are truncated. Reads that start
946         # beyond the end of the data return an empty string.
947         seekpos = self._data_offset+offset
948         actuallength = max(0, min(length, self._lease_offset-seekpos))
949hunk ./src/allmydata/storage/backends/disk/immutable.py 154
950         if actuallength == 0:
951             return ""
952-        f = open(self.home, 'rb')
953-        f.seek(seekpos)
954-        return f.read(actuallength)
955+        f = self._home.open(mode='rb')
956+        try:
957+            f.seek(seekpos)
958+            sharedata = f.read(actuallength)
959+        finally:
960+            f.close()
961+        return sharedata
962 
963     def write_share_data(self, offset, data):
964         length = len(data)
965hunk ./src/allmydata/storage/backends/disk/immutable.py 167
966         precondition(offset >= 0, offset)
967         if self._max_size is not None and offset+length > self._max_size:
968             raise DataTooLargeError(self._max_size, offset, length)
969-        f = open(self.home, 'rb+')
970-        real_offset = self._data_offset+offset
971-        f.seek(real_offset)
972-        assert f.tell() == real_offset
973-        f.write(data)
974-        f.close()
975+        f = self._incominghome.open(mode='rb+')
976+        try:
977+            real_offset = self._data_offset+offset
978+            f.seek(real_offset)
979+            assert f.tell() == real_offset
980+            f.write(data)
981+        finally:
982+            f.close()
983 
984     def _write_lease_record(self, f, lease_number, lease_info):
985         offset = self._lease_offset + lease_number * self.LEASE_SIZE
986hunk ./src/allmydata/storage/backends/disk/immutable.py 184
987 
988     def _read_num_leases(self, f):
989         f.seek(0x08)
990-        (num_leases,) = struct.unpack(">L", f.read(4))
991+        ro = f.read(4)
992+        (num_leases,) = struct.unpack(">L", ro)
993         return num_leases
994 
995     def _write_num_leases(self, f, num_leases):
996hunk ./src/allmydata/storage/backends/disk/immutable.py 195
997     def _truncate_leases(self, f, num_leases):
998         f.truncate(self._lease_offset + num_leases * self.LEASE_SIZE)
999 
1000+    # These lease operations are intended for use by disk_backend.py.
1001+    # Other clients should not depend on the fact that the disk backend
1002+    # stores leases in share files.
1003+
1004     def get_leases(self):
1005         """Yields a LeaseInfo instance for all leases."""
1006hunk ./src/allmydata/storage/backends/disk/immutable.py 201
1007-        f = open(self.home, 'rb')
1008-        (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1009-        f.seek(self._lease_offset)
1010-        for i in range(num_leases):
1011-            data = f.read(self.LEASE_SIZE)
1012-            if data:
1013-                yield LeaseInfo().from_immutable_data(data)
1014+        f = self._home.open(mode='rb')
1015+        try:
1016+            (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1017+            f.seek(self._lease_offset)
1018+            for i in range(num_leases):
1019+                data = f.read(self.LEASE_SIZE)
1020+                if data:
1021+                    yield LeaseInfo().from_immutable_data(data)
1022+        finally:
1023+            f.close()
1024 
1025     def add_lease(self, lease_info):
1026hunk ./src/allmydata/storage/backends/disk/immutable.py 213
1027-        f = open(self.home, 'rb+')
1028-        num_leases = self._read_num_leases(f)
1029-        self._write_lease_record(f, num_leases, lease_info)
1030-        self._write_num_leases(f, num_leases+1)
1031-        f.close()
1032+        f = self._incominghome.open(mode='rb')
1033+        try:
1034+            num_leases = self._read_num_leases(f)
1035+        finally:
1036+            f.close()
1037+        f = self._home.open(mode='wb+')
1038+        try:
1039+            self._write_lease_record(f, num_leases, lease_info)
1040+            self._write_num_leases(f, num_leases+1)
1041+        finally:
1042+            f.close()
1043 
1044     def renew_lease(self, renew_secret, new_expire_time):
1045hunk ./src/allmydata/storage/backends/disk/immutable.py 226
1046-        for i,lease in enumerate(self.get_leases()):
1047-            if constant_time_compare(lease.renew_secret, renew_secret):
1048-                # yup. See if we need to update the owner time.
1049-                if new_expire_time > lease.expiration_time:
1050-                    # yes
1051-                    lease.expiration_time = new_expire_time
1052-                    f = open(self.home, 'rb+')
1053-                    self._write_lease_record(f, i, lease)
1054-                    f.close()
1055-                return
1056+        try:
1057+            for i, lease in enumerate(self.get_leases()):
1058+                if constant_time_compare(lease.renew_secret, renew_secret):
1059+                    # yup. See if we need to update the owner time.
1060+                    if new_expire_time > lease.expiration_time:
1061+                        # yes
1062+                        lease.expiration_time = new_expire_time
1063+                        f = self._home.open('rb+')
1064+                        try:
1065+                            self._write_lease_record(f, i, lease)
1066+                        finally:
1067+                            f.close()
1068+                    return
1069+        except IndexError, e:
1070+            raise Exception("IndexError: %s" % (e,))
1071         raise IndexError("unable to renew non-existent lease")
1072 
1073     def add_or_renew_lease(self, lease_info):
1074hunk ./src/allmydata/storage/backends/disk/immutable.py 249
1075                              lease_info.expiration_time)
1076         except IndexError:
1077             self.add_lease(lease_info)
1078-
1079-
1080-    def cancel_lease(self, cancel_secret):
1081-        """Remove a lease with the given cancel_secret. If the last lease is
1082-        cancelled, the file will be removed. Return the number of bytes that
1083-        were freed (by truncating the list of leases, and possibly by
1084-        deleting the file. Raise IndexError if there was no lease with the
1085-        given cancel_secret.
1086-        """
1087-
1088-        leases = list(self.get_leases())
1089-        num_leases_removed = 0
1090-        for i,lease in enumerate(leases):
1091-            if constant_time_compare(lease.cancel_secret, cancel_secret):
1092-                leases[i] = None
1093-                num_leases_removed += 1
1094-        if not num_leases_removed:
1095-            raise IndexError("unable to find matching lease to cancel")
1096-        if num_leases_removed:
1097-            # pack and write out the remaining leases. We write these out in
1098-            # the same order as they were added, so that if we crash while
1099-            # doing this, we won't lose any non-cancelled leases.
1100-            leases = [l for l in leases if l] # remove the cancelled leases
1101-            f = open(self.home, 'rb+')
1102-            for i,lease in enumerate(leases):
1103-                self._write_lease_record(f, i, lease)
1104-            self._write_num_leases(f, len(leases))
1105-            self._truncate_leases(f, len(leases))
1106-            f.close()
1107-        space_freed = self.LEASE_SIZE * num_leases_removed
1108-        if not len(leases):
1109-            space_freed += os.stat(self.home)[stat.ST_SIZE]
1110-            self.unlink()
1111-        return space_freed
1112-
1113-
1114-class BucketWriter(Referenceable):
1115-    implements(RIBucketWriter)
1116-
1117-    def __init__(self, ss, incominghome, finalhome, max_size, lease_info, canary):
1118-        self.ss = ss
1119-        self.incominghome = incominghome
1120-        self.finalhome = finalhome
1121-        self._max_size = max_size # don't allow the client to write more than this
1122-        self._canary = canary
1123-        self._disconnect_marker = canary.notifyOnDisconnect(self._disconnected)
1124-        self.closed = False
1125-        self.throw_out_all_data = False
1126-        self._sharefile = ShareFile(incominghome, create=True, max_size=max_size)
1127-        # also, add our lease to the file now, so that other ones can be
1128-        # added by simultaneous uploaders
1129-        self._sharefile.add_lease(lease_info)
1130-
1131-    def allocated_size(self):
1132-        return self._max_size
1133-
1134-    def remote_write(self, offset, data):
1135-        start = time.time()
1136-        precondition(not self.closed)
1137-        if self.throw_out_all_data:
1138-            return
1139-        self._sharefile.write_share_data(offset, data)
1140-        self.ss.add_latency("write", time.time() - start)
1141-        self.ss.count("write")
1142-
1143-    def remote_close(self):
1144-        precondition(not self.closed)
1145-        start = time.time()
1146-
1147-        fileutil.make_dirs(os.path.dirname(self.finalhome))
1148-        fileutil.rename(self.incominghome, self.finalhome)
1149-        try:
1150-            # self.incominghome is like storage/shares/incoming/ab/abcde/4 .
1151-            # We try to delete the parent (.../ab/abcde) to avoid leaving
1152-            # these directories lying around forever, but the delete might
1153-            # fail if we're working on another share for the same storage
1154-            # index (like ab/abcde/5). The alternative approach would be to
1155-            # use a hierarchy of objects (PrefixHolder, BucketHolder,
1156-            # ShareWriter), each of which is responsible for a single
1157-            # directory on disk, and have them use reference counting of
1158-            # their children to know when they should do the rmdir. This
1159-            # approach is simpler, but relies on os.rmdir refusing to delete
1160-            # a non-empty directory. Do *not* use fileutil.rm_dir() here!
1161-            os.rmdir(os.path.dirname(self.incominghome))
1162-            # we also delete the grandparent (prefix) directory, .../ab ,
1163-            # again to avoid leaving directories lying around. This might
1164-            # fail if there is another bucket open that shares a prefix (like
1165-            # ab/abfff).
1166-            os.rmdir(os.path.dirname(os.path.dirname(self.incominghome)))
1167-            # we leave the great-grandparent (incoming/) directory in place.
1168-        except EnvironmentError:
1169-            # ignore the "can't rmdir because the directory is not empty"
1170-            # exceptions, those are normal consequences of the
1171-            # above-mentioned conditions.
1172-            pass
1173-        self._sharefile = None
1174-        self.closed = True
1175-        self._canary.dontNotifyOnDisconnect(self._disconnect_marker)
1176-
1177-        filelen = os.stat(self.finalhome)[stat.ST_SIZE]
1178-        self.ss.bucket_writer_closed(self, filelen)
1179-        self.ss.add_latency("close", time.time() - start)
1180-        self.ss.count("close")
1181-
1182-    def _disconnected(self):
1183-        if not self.closed:
1184-            self._abort()
1185-
1186-    def remote_abort(self):
1187-        log.msg("storage: aborting sharefile %s" % self.incominghome,
1188-                facility="tahoe.storage", level=log.UNUSUAL)
1189-        if not self.closed:
1190-            self._canary.dontNotifyOnDisconnect(self._disconnect_marker)
1191-        self._abort()
1192-        self.ss.count("abort")
1193-
1194-    def _abort(self):
1195-        if self.closed:
1196-            return
1197-
1198-        os.remove(self.incominghome)
1199-        # if we were the last share to be moved, remove the incoming/
1200-        # directory that was our parent
1201-        parentdir = os.path.split(self.incominghome)[0]
1202-        if not os.listdir(parentdir):
1203-            os.rmdir(parentdir)
1204-        self._sharefile = None
1205-
1206-        # We are now considered closed for further writing. We must tell
1207-        # the storage server about this so that it stops expecting us to
1208-        # use the space it allocated for us earlier.
1209-        self.closed = True
1210-        self.ss.bucket_writer_closed(self, 0)
1211-
1212-
1213-class BucketReader(Referenceable):
1214-    implements(RIBucketReader)
1215-
1216-    def __init__(self, ss, sharefname, storage_index=None, shnum=None):
1217-        self.ss = ss
1218-        self._share_file = ShareFile(sharefname)
1219-        self.storage_index = storage_index
1220-        self.shnum = shnum
1221-
1222-    def __repr__(self):
1223-        return "<%s %s %s>" % (self.__class__.__name__,
1224-                               base32.b2a_l(self.storage_index[:8], 60),
1225-                               self.shnum)
1226-
1227-    def remote_read(self, offset, length):
1228-        start = time.time()
1229-        data = self._share_file.read_share_data(offset, length)
1230-        self.ss.add_latency("read", time.time() - start)
1231-        self.ss.count("read")
1232-        return data
1233-
1234-    def remote_advise_corrupt_share(self, reason):
1235-        return self.ss.remote_advise_corrupt_share("immutable",
1236-                                                   self.storage_index,
1237-                                                   self.shnum,
1238-                                                   reason)
1239hunk ./src/allmydata/storage/backends/disk/mutable.py 1
1240-import os, stat, struct
1241 
1242hunk ./src/allmydata/storage/backends/disk/mutable.py 2
1243-from allmydata.interfaces import BadWriteEnablerError
1244-from allmydata.util import idlib, log
1245+import struct
1246+
1247+from zope.interface import implements
1248+
1249+from allmydata.interfaces import IStoredMutableShare, BadWriteEnablerError
1250+from allmydata.util import fileutil, idlib, log
1251 from allmydata.util.assertutil import precondition
1252 from allmydata.util.hashutil import constant_time_compare
1253hunk ./src/allmydata/storage/backends/disk/mutable.py 10
1254-from allmydata.storage.lease import LeaseInfo
1255-from allmydata.storage.common import UnknownMutableContainerVersionError, \
1256+from allmydata.util.encodingutil import quote_filepath
1257+from allmydata.storage.common import si_b2a, UnknownMutableContainerVersionError, \
1258      DataTooLargeError
1259hunk ./src/allmydata/storage/backends/disk/mutable.py 13
1260+from allmydata.storage.lease import LeaseInfo
1261+from allmydata.storage.backends.base import testv_compare
1262 
1263hunk ./src/allmydata/storage/backends/disk/mutable.py 16
1264-# the MutableShareFile is like the ShareFile, but used for mutable data. It
1265-# has a different layout. See docs/mutable.txt for more details.
1266+
1267+# The MutableDiskShare is like the ImmutableDiskShare, but used for mutable data.
1268+# It has a different layout. See docs/mutable.rst for more details.
1269 
1270 # #   offset    size    name
1271 # 1   0         32      magic verstr "tahoe mutable container v1" plus binary
1272hunk ./src/allmydata/storage/backends/disk/mutable.py 31
1273 #                        4    4   expiration timestamp
1274 #                        8   32   renewal token
1275 #                        40  32   cancel token
1276-#                        72  20   nodeid which accepted the tokens
1277+#                        72  20   nodeid that accepted the tokens
1278 # 7   468       (a)     data
1279 # 8   ??        4       count of extra leases
1280 # 9   ??        n*92    extra leases
1281hunk ./src/allmydata/storage/backends/disk/mutable.py 37
1282 
1283 
1284-# The struct module doc says that L's are 4 bytes in size., and that Q's are
1285+# The struct module doc says that L's are 4 bytes in size, and that Q's are
1286 # 8 bytes in size. Since compatibility depends upon this, double-check it.
1287 assert struct.calcsize(">L") == 4, struct.calcsize(">L")
1288 assert struct.calcsize(">Q") == 8, struct.calcsize(">Q")
1289hunk ./src/allmydata/storage/backends/disk/mutable.py 42
1290 
1291-class MutableShareFile:
1292+
1293+class MutableDiskShare(object):
1294+    implements(IStoredMutableShare)
1295 
1296     sharetype = "mutable"
1297     DATA_LENGTH_OFFSET = struct.calcsize(">32s20s32s")
1298hunk ./src/allmydata/storage/backends/disk/mutable.py 54
1299     assert LEASE_SIZE == 92
1300     DATA_OFFSET = HEADER_SIZE + 4*LEASE_SIZE
1301     assert DATA_OFFSET == 468, DATA_OFFSET
1302+
1303     # our sharefiles share with a recognizable string, plus some random
1304     # binary data to reduce the chance that a regular text file will look
1305     # like a sharefile.
1306hunk ./src/allmydata/storage/backends/disk/mutable.py 63
1307     MAX_SIZE = 2*1000*1000*1000 # 2GB, kind of arbitrary
1308     # TODO: decide upon a policy for max share size
1309 
1310-    def __init__(self, filename, parent=None):
1311-        self.home = filename
1312-        if os.path.exists(self.home):
1313+    def __init__(self, storageindex, shnum, home, parent=None):
1314+        self._storageindex = storageindex
1315+        self._shnum = shnum
1316+        self._home = home
1317+        if self._home.exists():
1318             # we don't cache anything, just check the magic
1319hunk ./src/allmydata/storage/backends/disk/mutable.py 69
1320-            f = open(self.home, 'rb')
1321-            data = f.read(self.HEADER_SIZE)
1322-            (magic,
1323-             write_enabler_nodeid, write_enabler,
1324-             data_length, extra_least_offset) = \
1325-             struct.unpack(">32s20s32sQQ", data)
1326-            if magic != self.MAGIC:
1327-                msg = "sharefile %s had magic '%r' but we wanted '%r'" % \
1328-                      (filename, magic, self.MAGIC)
1329-                raise UnknownMutableContainerVersionError(msg)
1330+            f = self._home.open('rb')
1331+            try:
1332+                data = f.read(self.HEADER_SIZE)
1333+                (magic,
1334+                 write_enabler_nodeid, write_enabler,
1335+                 data_length, extra_least_offset) = \
1336+                 struct.unpack(">32s20s32sQQ", data)
1337+                if magic != self.MAGIC:
1338+                    msg = "sharefile %s had magic '%r' but we wanted '%r'" % \
1339+                          (quote_filepath(self._home), magic, self.MAGIC)
1340+                    raise UnknownMutableContainerVersionError(msg)
1341+            finally:
1342+                f.close()
1343         self.parent = parent # for logging
1344 
1345     def log(self, *args, **kwargs):
1346hunk ./src/allmydata/storage/backends/disk/mutable.py 87
1347         return self.parent.log(*args, **kwargs)
1348 
1349-    def create(self, my_nodeid, write_enabler):
1350-        assert not os.path.exists(self.home)
1351+    def create(self, serverid, write_enabler):
1352+        assert not self._home.exists()
1353         data_length = 0
1354         extra_lease_offset = (self.HEADER_SIZE
1355                               + 4 * self.LEASE_SIZE
1356hunk ./src/allmydata/storage/backends/disk/mutable.py 95
1357                               + data_length)
1358         assert extra_lease_offset == self.DATA_OFFSET # true at creation
1359         num_extra_leases = 0
1360-        f = open(self.home, 'wb')
1361-        header = struct.pack(">32s20s32sQQ",
1362-                             self.MAGIC, my_nodeid, write_enabler,
1363-                             data_length, extra_lease_offset,
1364-                             )
1365-        leases = ("\x00"*self.LEASE_SIZE) * 4
1366-        f.write(header + leases)
1367-        # data goes here, empty after creation
1368-        f.write(struct.pack(">L", num_extra_leases))
1369-        # extra leases go here, none at creation
1370-        f.close()
1371+        f = self._home.open('wb')
1372+        try:
1373+            header = struct.pack(">32s20s32sQQ",
1374+                                 self.MAGIC, serverid, write_enabler,
1375+                                 data_length, extra_lease_offset,
1376+                                 )
1377+            leases = ("\x00"*self.LEASE_SIZE) * 4
1378+            f.write(header + leases)
1379+            # data goes here, empty after creation
1380+            f.write(struct.pack(">L", num_extra_leases))
1381+            # extra leases go here, none at creation
1382+        finally:
1383+            f.close()
1384+
1385+    def __repr__(self):
1386+        return ("<MutableDiskShare %s:%r at %s>"
1387+                % (si_b2a(self._storageindex), self._shnum, quote_filepath(self._home)))
1388+
1389+    def get_used_space(self):
1390+        return fileutil.get_used_space(self._home)
1391+
1392+    def get_storage_index(self):
1393+        return self._storageindex
1394+
1395+    def get_shnum(self):
1396+        return self._shnum
1397 
1398     def unlink(self):
1399hunk ./src/allmydata/storage/backends/disk/mutable.py 123
1400-        os.unlink(self.home)
1401+        self._home.remove()
1402 
1403     def _read_data_length(self, f):
1404         f.seek(self.DATA_LENGTH_OFFSET)
1405hunk ./src/allmydata/storage/backends/disk/mutable.py 291
1406 
1407     def get_leases(self):
1408         """Yields a LeaseInfo instance for all leases."""
1409-        f = open(self.home, 'rb')
1410-        for i, lease in self._enumerate_leases(f):
1411-            yield lease
1412-        f.close()
1413+        f = self._home.open('rb')
1414+        try:
1415+            for i, lease in self._enumerate_leases(f):
1416+                yield lease
1417+        finally:
1418+            f.close()
1419 
1420     def _enumerate_leases(self, f):
1421         for i in range(self._get_num_lease_slots(f)):
1422hunk ./src/allmydata/storage/backends/disk/mutable.py 303
1423             try:
1424                 data = self._read_lease_record(f, i)
1425                 if data is not None:
1426-                    yield i,data
1427+                    yield i, data
1428             except IndexError:
1429                 return
1430 
1431hunk ./src/allmydata/storage/backends/disk/mutable.py 307
1432+    # These lease operations are intended for use by disk_backend.py.
1433+    # Other non-test clients should not depend on the fact that the disk
1434+    # backend stores leases in share files.
1435+
1436     def add_lease(self, lease_info):
1437         precondition(lease_info.owner_num != 0) # 0 means "no lease here"
1438hunk ./src/allmydata/storage/backends/disk/mutable.py 313
1439-        f = open(self.home, 'rb+')
1440-        num_lease_slots = self._get_num_lease_slots(f)
1441-        empty_slot = self._get_first_empty_lease_slot(f)
1442-        if empty_slot is not None:
1443-            self._write_lease_record(f, empty_slot, lease_info)
1444-        else:
1445-            self._write_lease_record(f, num_lease_slots, lease_info)
1446-        f.close()
1447+        f = self._home.open('rb+')
1448+        try:
1449+            num_lease_slots = self._get_num_lease_slots(f)
1450+            empty_slot = self._get_first_empty_lease_slot(f)
1451+            if empty_slot is not None:
1452+                self._write_lease_record(f, empty_slot, lease_info)
1453+            else:
1454+                self._write_lease_record(f, num_lease_slots, lease_info)
1455+        finally:
1456+            f.close()
1457 
1458     def renew_lease(self, renew_secret, new_expire_time):
1459         accepting_nodeids = set()
1460hunk ./src/allmydata/storage/backends/disk/mutable.py 326
1461-        f = open(self.home, 'rb+')
1462-        for (leasenum,lease) in self._enumerate_leases(f):
1463-            if constant_time_compare(lease.renew_secret, renew_secret):
1464-                # yup. See if we need to update the owner time.
1465-                if new_expire_time > lease.expiration_time:
1466-                    # yes
1467-                    lease.expiration_time = new_expire_time
1468-                    self._write_lease_record(f, leasenum, lease)
1469-                f.close()
1470-                return
1471-            accepting_nodeids.add(lease.nodeid)
1472-        f.close()
1473+        f = self._home.open('rb+')
1474+        try:
1475+            for (leasenum, lease) in self._enumerate_leases(f):
1476+                if constant_time_compare(lease.renew_secret, renew_secret):
1477+                    # yup. See if we need to update the owner time.
1478+                    if new_expire_time > lease.expiration_time:
1479+                        # yes
1480+                        lease.expiration_time = new_expire_time
1481+                        self._write_lease_record(f, leasenum, lease)
1482+                    return
1483+                accepting_nodeids.add(lease.nodeid)
1484+        finally:
1485+            f.close()
1486         # Return the accepting_nodeids set, to give the client a chance to
1487hunk ./src/allmydata/storage/backends/disk/mutable.py 340
1488-        # update the leases on a share which has been migrated from its
1489+        # update the leases on a share that has been migrated from its
1490         # original server to a new one.
1491         msg = ("Unable to renew non-existent lease. I have leases accepted by"
1492                " nodeids: ")
1493hunk ./src/allmydata/storage/backends/disk/mutable.py 357
1494         except IndexError:
1495             self.add_lease(lease_info)
1496 
1497-    def cancel_lease(self, cancel_secret):
1498-        """Remove any leases with the given cancel_secret. If the last lease
1499-        is cancelled, the file will be removed. Return the number of bytes
1500-        that were freed (by truncating the list of leases, and possibly by
1501-        deleting the file. Raise IndexError if there was no lease with the
1502-        given cancel_secret."""
1503-
1504-        accepting_nodeids = set()
1505-        modified = 0
1506-        remaining = 0
1507-        blank_lease = LeaseInfo(owner_num=0,
1508-                                renew_secret="\x00"*32,
1509-                                cancel_secret="\x00"*32,
1510-                                expiration_time=0,
1511-                                nodeid="\x00"*20)
1512-        f = open(self.home, 'rb+')
1513-        for (leasenum,lease) in self._enumerate_leases(f):
1514-            accepting_nodeids.add(lease.nodeid)
1515-            if constant_time_compare(lease.cancel_secret, cancel_secret):
1516-                self._write_lease_record(f, leasenum, blank_lease)
1517-                modified += 1
1518-            else:
1519-                remaining += 1
1520-        if modified:
1521-            freed_space = self._pack_leases(f)
1522-            f.close()
1523-            if not remaining:
1524-                freed_space += os.stat(self.home)[stat.ST_SIZE]
1525-                self.unlink()
1526-            return freed_space
1527-
1528-        msg = ("Unable to cancel non-existent lease. I have leases "
1529-               "accepted by nodeids: ")
1530-        msg += ",".join([("'%s'" % idlib.nodeid_b2a(anid))
1531-                         for anid in accepting_nodeids])
1532-        msg += " ."
1533-        raise IndexError(msg)
1534-
1535-    def _pack_leases(self, f):
1536-        # TODO: reclaim space from cancelled leases
1537-        return 0
1538-
1539     def _read_write_enabler_and_nodeid(self, f):
1540         f.seek(0)
1541         data = f.read(self.HEADER_SIZE)
1542hunk ./src/allmydata/storage/backends/disk/mutable.py 369
1543 
1544     def readv(self, readv):
1545         datav = []
1546-        f = open(self.home, 'rb')
1547-        for (offset, length) in readv:
1548-            datav.append(self._read_share_data(f, offset, length))
1549-        f.close()
1550+        f = self._home.open('rb')
1551+        try:
1552+            for (offset, length) in readv:
1553+                datav.append(self._read_share_data(f, offset, length))
1554+        finally:
1555+            f.close()
1556         return datav
1557 
1558hunk ./src/allmydata/storage/backends/disk/mutable.py 377
1559-#    def remote_get_length(self):
1560-#        f = open(self.home, 'rb')
1561-#        data_length = self._read_data_length(f)
1562-#        f.close()
1563-#        return data_length
1564+    def get_size(self):
1565+        return self._home.getsize()
1566+
1567+    def get_data_length(self):
1568+        f = self._home.open('rb')
1569+        try:
1570+            data_length = self._read_data_length(f)
1571+        finally:
1572+            f.close()
1573+        return data_length
1574 
1575     def check_write_enabler(self, write_enabler, si_s):
1576hunk ./src/allmydata/storage/backends/disk/mutable.py 389
1577-        f = open(self.home, 'rb+')
1578-        (real_write_enabler, write_enabler_nodeid) = \
1579-                             self._read_write_enabler_and_nodeid(f)
1580-        f.close()
1581+        f = self._home.open('rb+')
1582+        try:
1583+            (real_write_enabler, write_enabler_nodeid) = self._read_write_enabler_and_nodeid(f)
1584+        finally:
1585+            f.close()
1586         # avoid a timing attack
1587         #if write_enabler != real_write_enabler:
1588         if not constant_time_compare(write_enabler, real_write_enabler):
1589hunk ./src/allmydata/storage/backends/disk/mutable.py 410
1590 
1591     def check_testv(self, testv):
1592         test_good = True
1593-        f = open(self.home, 'rb+')
1594-        for (offset, length, operator, specimen) in testv:
1595-            data = self._read_share_data(f, offset, length)
1596-            if not testv_compare(data, operator, specimen):
1597-                test_good = False
1598-                break
1599-        f.close()
1600+        f = self._home.open('rb+')
1601+        try:
1602+            for (offset, length, operator, specimen) in testv:
1603+                data = self._read_share_data(f, offset, length)
1604+                if not testv_compare(data, operator, specimen):
1605+                    test_good = False
1606+                    break
1607+        finally:
1608+            f.close()
1609         return test_good
1610 
1611     def writev(self, datav, new_length):
1612hunk ./src/allmydata/storage/backends/disk/mutable.py 422
1613-        f = open(self.home, 'rb+')
1614-        for (offset, data) in datav:
1615-            self._write_share_data(f, offset, data)
1616-        if new_length is not None:
1617-            cur_length = self._read_data_length(f)
1618-            if new_length < cur_length:
1619-                self._write_data_length(f, new_length)
1620-                # TODO: if we're going to shrink the share file when the
1621-                # share data has shrunk, then call
1622-                # self._change_container_size() here.
1623-        f.close()
1624-
1625-def testv_compare(a, op, b):
1626-    assert op in ("lt", "le", "eq", "ne", "ge", "gt")
1627-    if op == "lt":
1628-        return a < b
1629-    if op == "le":
1630-        return a <= b
1631-    if op == "eq":
1632-        return a == b
1633-    if op == "ne":
1634-        return a != b
1635-    if op == "ge":
1636-        return a >= b
1637-    if op == "gt":
1638-        return a > b
1639-    # never reached
1640+        f = self._home.open('rb+')
1641+        try:
1642+            for (offset, data) in datav:
1643+                self._write_share_data(f, offset, data)
1644+            if new_length is not None:
1645+                cur_length = self._read_data_length(f)
1646+                if new_length < cur_length:
1647+                    self._write_data_length(f, new_length)
1648+                    # TODO: if we're going to shrink the share file when the
1649+                    # share data has shrunk, then call
1650+                    # self._change_container_size() here.
1651+        finally:
1652+            f.close()
1653 
1654hunk ./src/allmydata/storage/backends/disk/mutable.py 436
1655-class EmptyShare:
1656+    def close(self):
1657+        pass
1658 
1659hunk ./src/allmydata/storage/backends/disk/mutable.py 439
1660-    def check_testv(self, testv):
1661-        test_good = True
1662-        for (offset, length, operator, specimen) in testv:
1663-            data = ""
1664-            if not testv_compare(data, operator, specimen):
1665-                test_good = False
1666-                break
1667-        return test_good
1668 
1669hunk ./src/allmydata/storage/backends/disk/mutable.py 440
1670-def create_mutable_sharefile(filename, my_nodeid, write_enabler, parent):
1671-    ms = MutableShareFile(filename, parent)
1672-    ms.create(my_nodeid, write_enabler)
1673+def create_mutable_disk_share(fp, serverid, write_enabler, parent):
1674+    ms = MutableDiskShare(fp, parent)
1675+    ms.create(serverid, write_enabler)
1676     del ms
1677hunk ./src/allmydata/storage/backends/disk/mutable.py 444
1678-    return MutableShareFile(filename, parent)
1679-
1680+    return MutableDiskShare(fp, parent)
1681addfile ./src/allmydata/storage/backends/null/__init__.py
1682addfile ./src/allmydata/storage/backends/null/null_backend.py
1683hunk ./src/allmydata/storage/backends/null/null_backend.py 2
1684 
1685+import os, struct
1686+
1687+from zope.interface import implements
1688+
1689+from allmydata.interfaces import IStorageBackend, IShareSet, IStoredShare, IStoredMutableShare
1690+from allmydata.util.assertutil import precondition
1691+from allmydata.util.hashutil import constant_time_compare
1692+from allmydata.storage.backends.base import Backend, ShareSet
1693+from allmydata.storage.bucket import BucketWriter
1694+from allmydata.storage.common import si_b2a
1695+from allmydata.storage.lease import LeaseInfo
1696+
1697+
1698+class NullBackend(Backend):
1699+    implements(IStorageBackend)
1700+
1701+    def __init__(self):
1702+        Backend.__init__(self)
1703+
1704+    def get_available_space(self, reserved_space):
1705+        return None
1706+
1707+    def get_sharesets_for_prefix(self, prefix):
1708+        pass
1709+
1710+    def get_shareset(self, storageindex):
1711+        return NullShareSet(storageindex)
1712+
1713+    def fill_in_space_stats(self, stats):
1714+        pass
1715+
1716+    def set_storage_server(self, ss):
1717+        self.ss = ss
1718+
1719+    def advise_corrupt_share(self, sharetype, storageindex, shnum, reason):
1720+        pass
1721+
1722+
1723+class NullShareSet(ShareSet):
1724+    implements(IShareSet)
1725+
1726+    def __init__(self, storageindex):
1727+        self.storageindex = storageindex
1728+
1729+    def get_overhead(self):
1730+        return 0
1731+
1732+    def get_incoming_shnums(self):
1733+        return frozenset()
1734+
1735+    def get_shares(self):
1736+        pass
1737+
1738+    def get_share(self, shnum):
1739+        return None
1740+
1741+    def get_storage_index(self):
1742+        return self.storageindex
1743+
1744+    def get_storage_index_string(self):
1745+        return si_b2a(self.storageindex)
1746+
1747+    def make_bucket_writer(self, storageserver, shnum, max_space_per_bucket, lease_info, canary):
1748+        immutableshare = ImmutableNullShare()
1749+        return BucketWriter(self.ss, immutableshare, max_space_per_bucket, lease_info, canary)
1750+
1751+    def _create_mutable_share(self, storageserver, shnum, write_enabler):
1752+        return MutableNullShare()
1753+
1754+    def _clean_up_after_unlink(self):
1755+        pass
1756+
1757+
1758+class ImmutableNullShare:
1759+    implements(IStoredShare)
1760+    sharetype = "immutable"
1761+
1762+    def __init__(self):
1763+        """ If max_size is not None then I won't allow more than
1764+        max_size to be written to me. If create=True then max_size
1765+        must not be None. """
1766+        pass
1767+
1768+    def get_shnum(self):
1769+        return self.shnum
1770+
1771+    def unlink(self):
1772+        os.unlink(self.fname)
1773+
1774+    def read_share_data(self, offset, length):
1775+        precondition(offset >= 0)
1776+        # Reads beyond the end of the data are truncated. Reads that start
1777+        # beyond the end of the data return an empty string.
1778+        seekpos = self._data_offset+offset
1779+        fsize = os.path.getsize(self.fname)
1780+        actuallength = max(0, min(length, fsize-seekpos)) # XXX #1528
1781+        if actuallength == 0:
1782+            return ""
1783+        f = open(self.fname, 'rb')
1784+        f.seek(seekpos)
1785+        return f.read(actuallength)
1786+
1787+    def write_share_data(self, offset, data):
1788+        pass
1789+
1790+    def _write_lease_record(self, f, lease_number, lease_info):
1791+        offset = self._lease_offset + lease_number * self.LEASE_SIZE
1792+        f.seek(offset)
1793+        assert f.tell() == offset
1794+        f.write(lease_info.to_immutable_data())
1795+
1796+    def _read_num_leases(self, f):
1797+        f.seek(0x08)
1798+        (num_leases,) = struct.unpack(">L", f.read(4))
1799+        return num_leases
1800+
1801+    def _write_num_leases(self, f, num_leases):
1802+        f.seek(0x08)
1803+        f.write(struct.pack(">L", num_leases))
1804+
1805+    def _truncate_leases(self, f, num_leases):
1806+        f.truncate(self._lease_offset + num_leases * self.LEASE_SIZE)
1807+
1808+    def get_leases(self):
1809+        """Yields a LeaseInfo instance for all leases."""
1810+        f = open(self.fname, 'rb')
1811+        (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1812+        f.seek(self._lease_offset)
1813+        for i in range(num_leases):
1814+            data = f.read(self.LEASE_SIZE)
1815+            if data:
1816+                yield LeaseInfo().from_immutable_data(data)
1817+
1818+    def add_lease(self, lease):
1819+        pass
1820+
1821+    def renew_lease(self, renew_secret, new_expire_time):
1822+        for i,lease in enumerate(self.get_leases()):
1823+            if constant_time_compare(lease.renew_secret, renew_secret):
1824+                # yup. See if we need to update the owner time.
1825+                if new_expire_time > lease.expiration_time:
1826+                    # yes
1827+                    lease.expiration_time = new_expire_time
1828+                    f = open(self.fname, 'rb+')
1829+                    self._write_lease_record(f, i, lease)
1830+                    f.close()
1831+                return
1832+        raise IndexError("unable to renew non-existent lease")
1833+
1834+    def add_or_renew_lease(self, lease_info):
1835+        try:
1836+            self.renew_lease(lease_info.renew_secret,
1837+                             lease_info.expiration_time)
1838+        except IndexError:
1839+            self.add_lease(lease_info)
1840+
1841+
1842+class MutableNullShare:
1843+    implements(IStoredMutableShare)
1844+    sharetype = "mutable"
1845+
1846+    """ XXX: TODO """
1847addfile ./src/allmydata/storage/bucket.py
1848hunk ./src/allmydata/storage/bucket.py 1
1849+
1850+import time
1851+
1852+from foolscap.api import Referenceable
1853+
1854+from zope.interface import implements
1855+from allmydata.interfaces import RIBucketWriter, RIBucketReader
1856+from allmydata.util import base32, log
1857+from allmydata.util.assertutil import precondition
1858+
1859+
1860+class BucketWriter(Referenceable):
1861+    implements(RIBucketWriter)
1862+
1863+    def __init__(self, ss, immutableshare, max_size, lease_info, canary):
1864+        self.ss = ss
1865+        self._max_size = max_size # don't allow the client to write more than this
1866+        self._canary = canary
1867+        self._disconnect_marker = canary.notifyOnDisconnect(self._disconnected)
1868+        self.closed = False
1869+        self.throw_out_all_data = False
1870+        self._share = immutableshare
1871+        # also, add our lease to the file now, so that other ones can be
1872+        # added by simultaneous uploaders
1873+        self._share.add_lease(lease_info)
1874+
1875+    def allocated_size(self):
1876+        return self._max_size
1877+
1878+    def remote_write(self, offset, data):
1879+        start = time.time()
1880+        precondition(not self.closed)
1881+        if self.throw_out_all_data:
1882+            return
1883+        self._share.write_share_data(offset, data)
1884+        self.ss.add_latency("write", time.time() - start)
1885+        self.ss.count("write")
1886+
1887+    def remote_close(self):
1888+        precondition(not self.closed)
1889+        start = time.time()
1890+
1891+        self._share.close()
1892+        filelen = self._share.stat()
1893+        self._share = None
1894+
1895+        self.closed = True
1896+        self._canary.dontNotifyOnDisconnect(self._disconnect_marker)
1897+
1898+        self.ss.bucket_writer_closed(self, filelen)
1899+        self.ss.add_latency("close", time.time() - start)
1900+        self.ss.count("close")
1901+
1902+    def _disconnected(self):
1903+        if not self.closed:
1904+            self._abort()
1905+
1906+    def remote_abort(self):
1907+        log.msg("storage: aborting write to share %r" % self._share,
1908+                facility="tahoe.storage", level=log.UNUSUAL)
1909+        if not self.closed:
1910+            self._canary.dontNotifyOnDisconnect(self._disconnect_marker)
1911+        self._abort()
1912+        self.ss.count("abort")
1913+
1914+    def _abort(self):
1915+        if self.closed:
1916+            return
1917+        self._share.unlink()
1918+        self._share = None
1919+
1920+        # We are now considered closed for further writing. We must tell
1921+        # the storage server about this so that it stops expecting us to
1922+        # use the space it allocated for us earlier.
1923+        self.closed = True
1924+        self.ss.bucket_writer_closed(self, 0)
1925+
1926+
1927+class BucketReader(Referenceable):
1928+    implements(RIBucketReader)
1929+
1930+    def __init__(self, ss, share):
1931+        self.ss = ss
1932+        self._share = share
1933+        self.storageindex = share.storageindex
1934+        self.shnum = share.shnum
1935+
1936+    def __repr__(self):
1937+        return "<%s %s %s>" % (self.__class__.__name__,
1938+                               base32.b2a_l(self.storageindex[:8], 60),
1939+                               self.shnum)
1940+
1941+    def remote_read(self, offset, length):
1942+        start = time.time()
1943+        data = self._share.read_share_data(offset, length)
1944+        self.ss.add_latency("read", time.time() - start)
1945+        self.ss.count("read")
1946+        return data
1947+
1948+    def remote_advise_corrupt_share(self, reason):
1949+        return self.ss.remote_advise_corrupt_share("immutable",
1950+                                                   self.storageindex,
1951+                                                   self.shnum,
1952+                                                   reason)
1953addfile ./src/allmydata/test/test_backends.py
1954hunk ./src/allmydata/test/test_backends.py 1
1955+import os, stat
1956+from twisted.trial import unittest
1957+from allmydata.util.log import msg
1958+from allmydata.test.common_util import ReallyEqualMixin
1959+import mock
1960+
1961+# This is the code that we're going to be testing.
1962+from allmydata.storage.server import StorageServer
1963+from allmydata.storage.backends.disk.disk_backend import DiskBackend, si_si2dir
1964+from allmydata.storage.backends.null.null_backend import NullBackend
1965+
1966+# The following share file content was generated with
1967+# storage.immutable.ShareFile from Tahoe-LAFS v1.8.2
1968+# with share data == 'a'. The total size of this input
1969+# is 85 bytes.
1970+shareversionnumber = '\x00\x00\x00\x01'
1971+sharedatalength = '\x00\x00\x00\x01'
1972+numberofleases = '\x00\x00\x00\x01'
1973+shareinputdata = 'a'
1974+ownernumber = '\x00\x00\x00\x00'
1975+renewsecret  = 'x'*32
1976+cancelsecret = 'y'*32
1977+expirationtime = '\x00(\xde\x80'
1978+nextlease = ''
1979+containerdata = shareversionnumber + sharedatalength + numberofleases
1980+client_data = shareinputdata + ownernumber + renewsecret + \
1981+    cancelsecret + expirationtime + nextlease
1982+share_data = containerdata + client_data
1983+testnodeid = 'testnodeidxxxxxxxxxx'
1984+
1985+
1986+class MockFileSystem(unittest.TestCase):
1987+    """ I simulate a filesystem that the code under test can use. I simulate
1988+    just the parts of the filesystem that the current implementation of Disk
1989+    backend needs. """
1990+    def setUp(self):
1991+        # Make patcher, patch, and effects for disk-using functions.
1992+        msg( "%s.setUp()" % (self,))
1993+        self.mockedfilepaths = {}
1994+        # keys are pathnames, values are MockFilePath objects. This is necessary because
1995+        # MockFilePath behavior sometimes depends on the filesystem. Where it does,
1996+        # self.mockedfilepaths has the relevant information.
1997+        self.storedir = MockFilePath('teststoredir', self.mockedfilepaths)
1998+        self.basedir = self.storedir.child('shares')
1999+        self.baseincdir = self.basedir.child('incoming')
2000+        self.sharedirfinalname = self.basedir.child('or').child('orsxg5dtorxxeylhmvpws3temv4a')
2001+        self.sharedirincomingname = self.baseincdir.child('or').child('orsxg5dtorxxeylhmvpws3temv4a')
2002+        self.shareincomingname = self.sharedirincomingname.child('0')
2003+        self.sharefinalname = self.sharedirfinalname.child('0')
2004+
2005+        # FIXME: these patches won't work; disk_backend no longer imports FilePath, BucketCountingCrawler,
2006+        # or LeaseCheckingCrawler.
2007+
2008+        self.FilePathFake = mock.patch('allmydata.storage.backends.disk.disk_backend.FilePath', new = MockFilePath)
2009+        self.FilePathFake.__enter__()
2010+
2011+        self.BCountingCrawler = mock.patch('allmydata.storage.backends.disk.disk_backend.BucketCountingCrawler')
2012+        FakeBCC = self.BCountingCrawler.__enter__()
2013+        FakeBCC.side_effect = self.call_FakeBCC
2014+
2015+        self.LeaseCheckingCrawler = mock.patch('allmydata.storage.backends.disk.disk_backend.LeaseCheckingCrawler')
2016+        FakeLCC = self.LeaseCheckingCrawler.__enter__()
2017+        FakeLCC.side_effect = self.call_FakeLCC
2018+
2019+        self.get_available_space = mock.patch('allmydata.util.fileutil.get_available_space')
2020+        GetSpace = self.get_available_space.__enter__()
2021+        GetSpace.side_effect = self.call_get_available_space
2022+
2023+        self.statforsize = mock.patch('allmydata.storage.backends.disk.core.filepath.stat')
2024+        getsize = self.statforsize.__enter__()
2025+        getsize.side_effect = self.call_statforsize
2026+
2027+    def call_FakeBCC(self, StateFile):
2028+        return MockBCC()
2029+
2030+    def call_FakeLCC(self, StateFile, HistoryFile, ExpirationPolicy):
2031+        return MockLCC()
2032+
2033+    def call_get_available_space(self, storedir, reservedspace):
2034+        # The input vector has an input size of 85.
2035+        return 85 - reservedspace
2036+
2037+    def call_statforsize(self, fakefpname):
2038+        return self.mockedfilepaths[fakefpname].fileobject.size()
2039+
2040+    def tearDown(self):
2041+        msg( "%s.tearDown()" % (self,))
2042+        self.FilePathFake.__exit__()
2043+        self.mockedfilepaths = {}
2044+
2045+
2046+class MockFilePath:
2047+    def __init__(self, pathstring, ffpathsenvironment, existence=False):
2048+        #  I can't just make the values MockFileObjects because they may be directories.
2049+        self.mockedfilepaths = ffpathsenvironment
2050+        self.path = pathstring
2051+        self.existence = existence
2052+        if not self.mockedfilepaths.has_key(self.path):
2053+            #  The first MockFilePath object is special
2054+            self.mockedfilepaths[self.path] = self
2055+            self.fileobject = None
2056+        else:
2057+            self.fileobject = self.mockedfilepaths[self.path].fileobject
2058+        self.spawn = {}
2059+        self.antecedent = os.path.dirname(self.path)
2060+
2061+    def setContent(self, contentstring):
2062+        # This method rewrites the data in the file that corresponds to its path
2063+        # name whether it preexisted or not.
2064+        self.fileobject = MockFileObject(contentstring)
2065+        self.existence = True
2066+        self.mockedfilepaths[self.path].fileobject = self.fileobject
2067+        self.mockedfilepaths[self.path].existence = self.existence
2068+        self.setparents()
2069+
2070+    def create(self):
2071+        # This method chokes if there's a pre-existing file!
2072+        if self.mockedfilepaths[self.path].fileobject:
2073+            raise OSError
2074+        else:
2075+            self.existence = True
2076+            self.mockedfilepaths[self.path].fileobject = self.fileobject
2077+            self.mockedfilepaths[self.path].existence = self.existence
2078+            self.setparents()
2079+
2080+    def open(self, mode='r'):
2081+        # XXX Makes no use of mode.
2082+        if not self.mockedfilepaths[self.path].fileobject:
2083+            # If there's no fileobject there already then make one and put it there.
2084+            self.fileobject = MockFileObject()
2085+            self.existence = True
2086+            self.mockedfilepaths[self.path].fileobject = self.fileobject
2087+            self.mockedfilepaths[self.path].existence = self.existence
2088+        else:
2089+            # Otherwise get a ref to it.
2090+            self.fileobject = self.mockedfilepaths[self.path].fileobject
2091+            self.existence = self.mockedfilepaths[self.path].existence
2092+        return self.fileobject.open(mode)
2093+
2094+    def child(self, childstring):
2095+        arg2child = os.path.join(self.path, childstring)
2096+        child = MockFilePath(arg2child, self.mockedfilepaths)
2097+        return child
2098+
2099+    def children(self):
2100+        childrenfromffs = [ffp for ffp in self.mockedfilepaths.values() if ffp.path.startswith(self.path)]
2101+        childrenfromffs = [ffp for ffp in childrenfromffs if not ffp.path.endswith(self.path)]
2102+        childrenfromffs = [ffp for ffp in childrenfromffs if ffp.exists()]
2103+        self.spawn = frozenset(childrenfromffs)
2104+        return self.spawn
2105+
2106+    def parent(self):
2107+        if self.mockedfilepaths.has_key(self.antecedent):
2108+            parent = self.mockedfilepaths[self.antecedent]
2109+        else:
2110+            parent = MockFilePath(self.antecedent, self.mockedfilepaths)
2111+        return parent
2112+
2113+    def parents(self):
2114+        antecedents = []
2115+        def f(fps, antecedents):
2116+            newfps = os.path.split(fps)[0]
2117+            if newfps:
2118+                antecedents.append(newfps)
2119+                f(newfps, antecedents)
2120+        f(self.path, antecedents)
2121+        return antecedents
2122+
2123+    def setparents(self):
2124+        for fps in self.parents():
2125+            if not self.mockedfilepaths.has_key(fps):
2126+                self.mockedfilepaths[fps] = MockFilePath(fps, self.mockedfilepaths, exists=True)
2127+
2128+    def basename(self):
2129+        return os.path.split(self.path)[1]
2130+
2131+    def moveTo(self, newffp):
2132+        #  XXX Makes no distinction between file and directory arguments, this is deviation from filepath.moveTo
2133+        if self.mockedfilepaths[newffp.path].exists():
2134+            raise OSError
2135+        else:
2136+            self.mockedfilepaths[newffp.path] = self
2137+            self.path = newffp.path
2138+
2139+    def getsize(self):
2140+        return self.fileobject.getsize()
2141+
2142+    def exists(self):
2143+        return self.existence
2144+
2145+    def isdir(self):
2146+        return True
2147+
2148+    def makedirs(self):
2149+        # XXX These methods assume that fp_<FOO> functions in fileutil will be tested elsewhere!
2150+        pass
2151+
2152+    def remove(self):
2153+        pass
2154+
2155+
2156+class MockFileObject:
2157+    def __init__(self, contentstring=''):
2158+        self.buffer = contentstring
2159+        self.pos = 0
2160+    def open(self, mode='r'):
2161+        return self
2162+    def write(self, instring):
2163+        begin = self.pos
2164+        padlen = begin - len(self.buffer)
2165+        if padlen > 0:
2166+            self.buffer += '\x00' * padlen
2167+        end = self.pos + len(instring)
2168+        self.buffer = self.buffer[:begin]+instring+self.buffer[end:]
2169+        self.pos = end
2170+    def close(self):
2171+        self.pos = 0
2172+    def seek(self, pos):
2173+        self.pos = pos
2174+    def read(self, numberbytes):
2175+        return self.buffer[self.pos:self.pos+numberbytes]
2176+    def tell(self):
2177+        return self.pos
2178+    def size(self):
2179+        # XXX This method A: Is not to be found in a real file B: Is part of a wild-mung-up of filepath.stat!
2180+        # XXX Finally we shall hopefully use a getsize method soon, must consult first though.
2181+        # Hmmm...  perhaps we need to sometimes stat the address when there's not a mockfileobject present?
2182+        return {stat.ST_SIZE:len(self.buffer)}
2183+    def getsize(self):
2184+        return len(self.buffer)
2185+
2186+class MockBCC:
2187+    def setServiceParent(self, Parent):
2188+        pass
2189+
2190+
2191+class MockLCC:
2192+    def setServiceParent(self, Parent):
2193+        pass
2194+
2195+
2196+class TestServerWithNullBackend(unittest.TestCase, ReallyEqualMixin):
2197+    """ NullBackend is just for testing and executable documentation, so
2198+    this test is actually a test of StorageServer in which we're using
2199+    NullBackend as helper code for the test, rather than a test of
2200+    NullBackend. """
2201+    def setUp(self):
2202+        self.ss = StorageServer(testnodeid, NullBackend())
2203+
2204+    @mock.patch('os.mkdir')
2205+    @mock.patch('__builtin__.open')
2206+    @mock.patch('os.listdir')
2207+    @mock.patch('os.path.isdir')
2208+    def test_write_share(self, mockisdir, mocklistdir, mockopen, mockmkdir):
2209+        """
2210+        Write a new share. This tests that StorageServer's remote_allocate_buckets
2211+        generates the correct return types when given test-vector arguments. That
2212+        bs is of the correct type is verified by attempting to invoke remote_write
2213+        on bs[0].
2214+        """
2215+        alreadygot, bs = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
2216+        bs[0].remote_write(0, 'a')
2217+        self.failIf(mockisdir.called)
2218+        self.failIf(mocklistdir.called)
2219+        self.failIf(mockopen.called)
2220+        self.failIf(mockmkdir.called)
2221+
2222+
2223+class TestServerConstruction(MockFileSystem, ReallyEqualMixin):
2224+    def test_create_server_disk_backend(self):
2225+        """ This tests whether a server instance can be constructed with a
2226+        filesystem backend. To pass the test, it mustn't use the filesystem
2227+        outside of its configured storedir. """
2228+        StorageServer(testnodeid, DiskBackend(self.storedir))
2229+
2230+
2231+class TestServerAndDiskBackend(MockFileSystem, ReallyEqualMixin):
2232+    """ This tests both the StorageServer and the Disk backend together. """
2233+    def setUp(self):
2234+        MockFileSystem.setUp(self)
2235+        try:
2236+            self.backend = DiskBackend(self.storedir)
2237+            self.ss = StorageServer(testnodeid, self.backend)
2238+
2239+            self.backendwithreserve = DiskBackend(self.storedir, reserved_space = 1)
2240+            self.sswithreserve = StorageServer(testnodeid, self.backendwithreserve)
2241+        except:
2242+            MockFileSystem.tearDown(self)
2243+            raise
2244+
2245+    @mock.patch('time.time')
2246+    @mock.patch('allmydata.util.fileutil.get_available_space')
2247+    def test_out_of_space(self, mockget_available_space, mocktime):
2248+        mocktime.return_value = 0
2249+
2250+        def call_get_available_space(dir, reserve):
2251+            return 0
2252+
2253+        mockget_available_space.side_effect = call_get_available_space
2254+        alreadygotc, bsc = self.sswithreserve.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
2255+        self.failUnlessReallyEqual(bsc, {})
2256+
2257+    @mock.patch('time.time')
2258+    def test_write_and_read_share(self, mocktime):
2259+        """
2260+        Write a new share, read it, and test the server's (and disk backend's)
2261+        handling of simultaneous and successive attempts to write the same
2262+        share.
2263+        """
2264+        mocktime.return_value = 0
2265+        # Inspect incoming and fail unless it's empty.
2266+        incomingset = self.ss.backend.get_incoming_shnums('teststorage_index')
2267+
2268+        self.failUnlessReallyEqual(incomingset, frozenset())
2269+
2270+        # Populate incoming with the sharenum: 0.
2271+        alreadygot, bs = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, frozenset((0,)), 1, mock.Mock())
2272+
2273+        # This is a transparent-box test: Inspect incoming and fail unless the sharenum: 0 is listed there.
2274+        self.failUnlessReallyEqual(self.ss.backend.get_incoming_shnums('teststorage_index'), frozenset((0,)))
2275+
2276+
2277+
2278+        # Attempt to create a second share writer with the same sharenum.
2279+        alreadygota, bsa = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, frozenset((0,)), 1, mock.Mock())
2280+
2281+        # Show that no sharewriter results from a remote_allocate_buckets
2282+        # with the same si and sharenum, until BucketWriter.remote_close()
2283+        # has been called.
2284+        self.failIf(bsa)
2285+
2286+        # Test allocated size.
2287+        spaceint = self.ss.allocated_size()
2288+        self.failUnlessReallyEqual(spaceint, 1)
2289+
2290+        # Write 'a' to shnum 0. Only tested together with close and read.
2291+        bs[0].remote_write(0, 'a')
2292+
2293+        # Preclose: Inspect final, failUnless nothing there.
2294+        self.failUnlessReallyEqual(len(list(self.backend.get_shares('teststorage_index'))), 0)
2295+        bs[0].remote_close()
2296+
2297+        # Postclose: (Omnibus) failUnless written data is in final.
2298+        sharesinfinal = list(self.backend.get_shares('teststorage_index'))
2299+        self.failUnlessReallyEqual(len(sharesinfinal), 1)
2300+        contents = sharesinfinal[0].read_share_data(0, 73)
2301+        self.failUnlessReallyEqual(contents, client_data)
2302+
2303+        # Exercise the case that the share we're asking to allocate is
2304+        # already (completely) uploaded.
2305+        self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
2306+
2307+
2308+    def test_read_old_share(self):
2309+        """ This tests whether the code correctly finds and reads
2310+        shares written out by old (Tahoe-LAFS <= v1.8.2)
2311+        servers. There is a similar test in test_download, but that one
2312+        is from the perspective of the client and exercises a deeper
2313+        stack of code. This one is for exercising just the
2314+        StorageServer object. """
2315+        # Contruct a file with the appropriate contents in the mockfilesystem.
2316+        datalen = len(share_data)
2317+        finalhome = si_si2dir(self.basedir, 'teststorage_index').child(str(0))
2318+        finalhome.setContent(share_data)
2319+
2320+        # Now begin the test.
2321+        bs = self.ss.remote_get_buckets('teststorage_index')
2322+
2323+        self.failUnlessEqual(len(bs), 1)
2324+        b = bs['0']
2325+        # These should match by definition, the next two cases cover cases without (completely) unambiguous behaviors.
2326+        self.failUnlessReallyEqual(b.remote_read(0, datalen), client_data)
2327+        # If you try to read past the end you get the as much data as is there.
2328+        self.failUnlessReallyEqual(b.remote_read(0, datalen+20), client_data)
2329+        # If you start reading past the end of the file you get the empty string.
2330+        self.failUnlessReallyEqual(b.remote_read(datalen+1, 3), '')
2331}
2332[Pluggable backends -- all other changes. refs #999
2333david-sarah@jacaranda.org**20110919233256
2334 Ignore-this: 1a77b6b5d178b32a9b914b699ba7e957
2335] {
2336hunk ./src/allmydata/client.py 245
2337             sharetypes.append("immutable")
2338         if self.get_config("storage", "expire.mutable", True, boolean=True):
2339             sharetypes.append("mutable")
2340-        expiration_sharetypes = tuple(sharetypes)
2341 
2342hunk ./src/allmydata/client.py 246
2343+        expiration_policy = {
2344+            'enabled': expire,
2345+            'mode': mode,
2346+            'override_lease_duration': o_l_d,
2347+            'cutoff_date': cutoff_date,
2348+            'sharetypes': tuple(sharetypes),
2349+        }
2350         ss = StorageServer(storedir, self.nodeid,
2351                            reserved_space=reserved,
2352                            discard_storage=discard,
2353hunk ./src/allmydata/client.py 258
2354                            readonly_storage=readonly,
2355                            stats_provider=self.stats_provider,
2356-                           expiration_enabled=expire,
2357-                           expiration_mode=mode,
2358-                           expiration_override_lease_duration=o_l_d,
2359-                           expiration_cutoff_date=cutoff_date,
2360-                           expiration_sharetypes=expiration_sharetypes)
2361+                           expiration_policy=expiration_policy)
2362         self.add_service(ss)
2363 
2364         d = self.when_tub_ready()
2365hunk ./src/allmydata/immutable/offloaded.py 306
2366         if os.path.exists(self._encoding_file):
2367             self.log("ciphertext already present, bypassing fetch",
2368                      level=log.UNUSUAL)
2369+            # XXX the following comment is probably stale, since
2370+            # LocalCiphertextReader.get_plaintext_hashtree_leaves does not exist.
2371+            #
2372             # we'll still need the plaintext hashes (when
2373             # LocalCiphertextReader.get_plaintext_hashtree_leaves() is
2374             # called), and currently the easiest way to get them is to ask
2375hunk ./src/allmydata/immutable/upload.py 765
2376             self._status.set_progress(1, progress)
2377         return cryptdata
2378 
2379-
2380     def get_plaintext_hashtree_leaves(self, first, last, num_segments):
2381hunk ./src/allmydata/immutable/upload.py 766
2382+        """OBSOLETE; Get the leaf nodes of a merkle hash tree over the
2383+        plaintext segments, i.e. get the tagged hashes of the given segments.
2384+        The segment size is expected to be generated by the
2385+        IEncryptedUploadable before any plaintext is read or ciphertext
2386+        produced, so that the segment hashes can be generated with only a
2387+        single pass.
2388+
2389+        This returns a Deferred that fires with a sequence of hashes, using:
2390+
2391+         tuple(segment_hashes[first:last])
2392+
2393+        'num_segments' is used to assert that the number of segments that the
2394+        IEncryptedUploadable handled matches the number of segments that the
2395+        encoder was expecting.
2396+
2397+        This method must not be called until the final byte has been read
2398+        from read_encrypted(). Once this method is called, read_encrypted()
2399+        can never be called again.
2400+        """
2401         # this is currently unused, but will live again when we fix #453
2402         if len(self._plaintext_segment_hashes) < num_segments:
2403             # close out the last one
2404hunk ./src/allmydata/immutable/upload.py 803
2405         return defer.succeed(tuple(self._plaintext_segment_hashes[first:last]))
2406 
2407     def get_plaintext_hash(self):
2408+        """OBSOLETE; Get the hash of the whole plaintext.
2409+
2410+        This returns a Deferred that fires with a tagged SHA-256 hash of the
2411+        whole plaintext, obtained from hashutil.plaintext_hash(data).
2412+        """
2413+        # this is currently unused, but will live again when we fix #453
2414         h = self._plaintext_hasher.digest()
2415         return defer.succeed(h)
2416 
2417hunk ./src/allmydata/interfaces.py 29
2418 Number = IntegerConstraint(8) # 2**(8*8) == 16EiB ~= 18e18 ~= 18 exabytes
2419 Offset = Number
2420 ReadSize = int # the 'int' constraint is 2**31 == 2Gib -- large files are processed in not-so-large increments
2421-WriteEnablerSecret = Hash # used to protect mutable bucket modifications
2422-LeaseRenewSecret = Hash # used to protect bucket lease renewal requests
2423-LeaseCancelSecret = Hash # used to protect bucket lease cancellation requests
2424+WriteEnablerSecret = Hash # used to protect mutable share modifications
2425+LeaseRenewSecret = Hash # used to protect lease renewal requests
2426+LeaseCancelSecret = Hash # used to protect lease cancellation requests
2427 
2428 class RIStubClient(RemoteInterface):
2429     """Each client publishes a service announcement for a dummy object called
2430hunk ./src/allmydata/interfaces.py 106
2431                          sharenums=SetOf(int, maxLength=MAX_BUCKETS),
2432                          allocated_size=Offset, canary=Referenceable):
2433         """
2434-        @param storage_index: the index of the bucket to be created or
2435+        @param storage_index: the index of the shareset to be created or
2436                               increfed.
2437         @param sharenums: these are the share numbers (probably between 0 and
2438                           99) that the sender is proposing to store on this
2439hunk ./src/allmydata/interfaces.py 111
2440                           server.
2441-        @param renew_secret: This is the secret used to protect bucket refresh
2442+        @param renew_secret: This is the secret used to protect lease renewal.
2443                              This secret is generated by the client and
2444                              stored for later comparison by the server. Each
2445                              server is given a different secret.
2446hunk ./src/allmydata/interfaces.py 115
2447-        @param cancel_secret: Like renew_secret, but protects bucket decref.
2448-        @param canary: If the canary is lost before close(), the bucket is
2449+        @param cancel_secret: ignored
2450+        @param canary: If the canary is lost before close(), the allocation is
2451                        deleted.
2452         @return: tuple of (alreadygot, allocated), where alreadygot is what we
2453                  already have and allocated is what we hereby agree to accept.
2454hunk ./src/allmydata/interfaces.py 129
2455                   renew_secret=LeaseRenewSecret,
2456                   cancel_secret=LeaseCancelSecret):
2457         """
2458-        Add a new lease on the given bucket. If the renew_secret matches an
2459+        Add a new lease on the given shareset. If the renew_secret matches an
2460         existing lease, that lease will be renewed instead. If there is no
2461hunk ./src/allmydata/interfaces.py 131
2462-        bucket for the given storage_index, return silently. (note that in
2463+        shareset for the given storage_index, return silently. (Note that in
2464         tahoe-1.3.0 and earlier, IndexError was raised if there was no
2465hunk ./src/allmydata/interfaces.py 133
2466-        bucket)
2467+        shareset.)
2468         """
2469         return Any() # returns None now, but future versions might change
2470 
2471hunk ./src/allmydata/interfaces.py 139
2472     def renew_lease(storage_index=StorageIndex, renew_secret=LeaseRenewSecret):
2473         """
2474-        Renew the lease on a given bucket, resetting the timer to 31 days.
2475-        Some networks will use this, some will not. If there is no bucket for
2476+        Renew the lease on a given shareset, resetting the timer to 31 days.
2477+        Some networks will use this, some will not. If there is no shareset for
2478         the given storage_index, IndexError will be raised.
2479 
2480         For mutable shares, if the given renew_secret does not match an
2481hunk ./src/allmydata/interfaces.py 146
2482         existing lease, IndexError will be raised with a note listing the
2483         server-nodeids on the existing leases, so leases on migrated shares
2484-        can be renewed or cancelled. For immutable shares, IndexError
2485-        (without the note) will be raised.
2486+        can be renewed. For immutable shares, IndexError (without the note)
2487+        will be raised.
2488         """
2489         return Any()
2490 
2491hunk ./src/allmydata/interfaces.py 154
2492     def get_buckets(storage_index=StorageIndex):
2493         return DictOf(int, RIBucketReader, maxKeys=MAX_BUCKETS)
2494 
2495-
2496-
2497     def slot_readv(storage_index=StorageIndex,
2498                    shares=ListOf(int), readv=ReadVector):
2499         """Read a vector from the numbered shares associated with the given
2500hunk ./src/allmydata/interfaces.py 163
2501 
2502     def slot_testv_and_readv_and_writev(storage_index=StorageIndex,
2503                                         secrets=TupleOf(WriteEnablerSecret,
2504-                                                        LeaseRenewSecret,
2505-                                                        LeaseCancelSecret),
2506+                                                        LeaseRenewSecret),
2507                                         tw_vectors=TestAndWriteVectorsForShares,
2508                                         r_vector=ReadVector,
2509                                         ):
2510hunk ./src/allmydata/interfaces.py 167
2511-        """General-purpose test-and-set operation for mutable slots. Perform
2512-        a bunch of comparisons against the existing shares. If they all pass,
2513-        then apply a bunch of write vectors to those shares. Then use the
2514-        read vectors to extract data from all the shares and return the data.
2515+        """
2516+        General-purpose atomic test-read-and-set operation for mutable slots.
2517+        Perform a bunch of comparisons against the existing shares. If they
2518+        all pass: use the read vectors to extract data from all the shares,
2519+        then apply a bunch of write vectors to those shares. Return the read
2520+        data, which does not include any modifications made by the writes.
2521 
2522         This method is, um, large. The goal is to allow clients to update all
2523         the shares associated with a mutable file in a single round trip.
2524hunk ./src/allmydata/interfaces.py 177
2525 
2526-        @param storage_index: the index of the bucket to be created or
2527+        @param storage_index: the index of the shareset to be created or
2528                               increfed.
2529         @param write_enabler: a secret that is stored along with the slot.
2530                               Writes are accepted from any caller who can
2531hunk ./src/allmydata/interfaces.py 183
2532                               present the matching secret. A different secret
2533                               should be used for each slot*server pair.
2534-        @param renew_secret: This is the secret used to protect bucket refresh
2535+        @param renew_secret: This is the secret used to protect lease renewal.
2536                              This secret is generated by the client and
2537                              stored for later comparison by the server. Each
2538                              server is given a different secret.
2539hunk ./src/allmydata/interfaces.py 187
2540-        @param cancel_secret: Like renew_secret, but protects bucket decref.
2541+        @param cancel_secret: ignored
2542 
2543hunk ./src/allmydata/interfaces.py 189
2544-        The 'secrets' argument is a tuple of (write_enabler, renew_secret,
2545-        cancel_secret). The first is required to perform any write. The
2546-        latter two are used when allocating new shares. To simply acquire a
2547-        new lease on existing shares, use an empty testv and an empty writev.
2548+        The 'secrets' argument is a tuple with (write_enabler, renew_secret).
2549+        The write_enabler is required to perform any write. The renew_secret
2550+        is used when allocating new shares.
2551 
2552         Each share can have a separate test vector (i.e. a list of
2553         comparisons to perform). If all vectors for all shares pass, then all
2554hunk ./src/allmydata/interfaces.py 280
2555         store that on disk.
2556         """
2557 
2558-class IStorageBucketWriter(Interface):
2559+
2560+class IStorageBackend(Interface):
2561     """
2562hunk ./src/allmydata/interfaces.py 283
2563-    Objects of this kind live on the client side.
2564+    Objects of this kind live on the server side and are used by the
2565+    storage server object.
2566     """
2567hunk ./src/allmydata/interfaces.py 286
2568-    def put_block(segmentnum=int, data=ShareData):
2569-        """@param data: For most segments, this data will be 'blocksize'
2570-        bytes in length. The last segment might be shorter.
2571-        @return: a Deferred that fires (with None) when the operation completes
2572+    def get_available_space():
2573+        """
2574+        Returns available space for share storage in bytes, or
2575+        None if this information is not available or if the available
2576+        space is unlimited.
2577+
2578+        If the backend is configured for read-only mode then this will
2579+        return 0.
2580+        """
2581+
2582+    def get_sharesets_for_prefix(prefix):
2583+        """
2584+        Generates IShareSet objects for all storage indices matching the
2585+        given prefix for which this backend holds shares.
2586+        """
2587+
2588+    def get_shareset(storageindex):
2589+        """
2590+        Get an IShareSet object for the given storage index.
2591+        """
2592+
2593+    def advise_corrupt_share(storageindex, sharetype, shnum, reason):
2594+        """
2595+        Clients who discover hash failures in shares that they have
2596+        downloaded from me will use this method to inform me about the
2597+        failures. I will record their concern so that my operator can
2598+        manually inspect the shares in question.
2599+
2600+        'sharetype' is either 'mutable' or 'immutable'. 'shnum' is the integer
2601+        share number. 'reason' is a human-readable explanation of the problem,
2602+        probably including some expected hash values and the computed ones
2603+        that did not match. Corruption advisories for mutable shares should
2604+        include a hash of the public key (the same value that appears in the
2605+        mutable-file verify-cap), since the current share format does not
2606+        store that on disk.
2607+
2608+        @param storageindex=str
2609+        @param sharetype=str
2610+        @param shnum=int
2611+        @param reason=str
2612+        """
2613+
2614+
2615+class IShareSet(Interface):
2616+    def get_storage_index():
2617+        """
2618+        Returns the storage index for this shareset.
2619+        """
2620+
2621+    def get_storage_index_string():
2622+        """
2623+        Returns the base32-encoded storage index for this shareset.
2624+        """
2625+
2626+    def get_overhead():
2627+        """
2628+        Returns the storage overhead, in bytes, of this shareset (exclusive
2629+        of the space used by its shares).
2630+        """
2631+
2632+    def get_shares():
2633+        """
2634+        Generates the IStoredShare objects held in this shareset.
2635+        """
2636+
2637+    def has_incoming(shnum):
2638+        """
2639+        Returns True if this shareset has an incoming (partial) share with this number, otherwise False.
2640+        """
2641+
2642+    def make_bucket_writer(storageserver, shnum, max_space_per_bucket, lease_info, canary):
2643+        """
2644+        Create a bucket writer that can be used to write data to a given share.
2645+
2646+        @param storageserver=RIStorageServer
2647+        @param shnum=int: A share number in this shareset
2648+        @param max_space_per_bucket=int: The maximum space allocated for the
2649+                 share, in bytes
2650+        @param lease_info=LeaseInfo: The initial lease information
2651+        @param canary=Referenceable: If the canary is lost before close(), the
2652+                 bucket is deleted.
2653+        @return an IStorageBucketWriter for the given share
2654+        """
2655+
2656+    def make_bucket_reader(storageserver, share):
2657+        """
2658+        Create a bucket reader that can be used to read data from a given share.
2659+
2660+        @param storageserver=RIStorageServer
2661+        @param share=IStoredShare
2662+        @return an IStorageBucketReader for the given share
2663+        """
2664+
2665+    def readv(wanted_shnums, read_vector):
2666+        """
2667+        Read a vector from the numbered shares in this shareset. An empty
2668+        wanted_shnums list means to return data from all known shares.
2669+
2670+        @param wanted_shnums=ListOf(int)
2671+        @param read_vector=ReadVector
2672+        @return DictOf(int, ReadData): shnum -> results, with one key per share
2673+        """
2674+
2675+    def testv_and_readv_and_writev(storageserver, secrets, test_and_write_vectors, read_vector, expiration_time):
2676+        """
2677+        General-purpose atomic test-read-and-set operation for mutable slots.
2678+        Perform a bunch of comparisons against the existing shares in this
2679+        shareset. If they all pass: use the read vectors to extract data from
2680+        all the shares, then apply a bunch of write vectors to those shares.
2681+        Return the read data, which does not include any modifications made by
2682+        the writes.
2683+
2684+        See the similar method in RIStorageServer for more detail.
2685+
2686+        @param storageserver=RIStorageServer
2687+        @param secrets=TupleOf(WriteEnablerSecret, LeaseRenewSecret[, ...])
2688+        @param test_and_write_vectors=TestAndWriteVectorsForShares
2689+        @param read_vector=ReadVector
2690+        @param expiration_time=int
2691+        @return TupleOf(bool, DictOf(int, ReadData))
2692+        """
2693+
2694+    def add_or_renew_lease(lease_info):
2695+        """
2696+        Add a new lease on the shares in this shareset. If the renew_secret
2697+        matches an existing lease, that lease will be renewed instead. If
2698+        there are no shares in this shareset, return silently.
2699+
2700+        @param lease_info=LeaseInfo
2701+        """
2702+
2703+    def renew_lease(renew_secret, new_expiration_time):
2704+        """
2705+        Renew a lease on the shares in this shareset, resetting the timer
2706+        to 31 days. Some grids will use this, some will not. If there are no
2707+        shares in this shareset, IndexError will be raised.
2708+
2709+        For mutable shares, if the given renew_secret does not match an
2710+        existing lease, IndexError will be raised with a note listing the
2711+        server-nodeids on the existing leases, so leases on migrated shares
2712+        can be renewed. For immutable shares, IndexError (without the note)
2713+        will be raised.
2714+
2715+        @param renew_secret=LeaseRenewSecret
2716+        """
2717+
2718+
2719+class IStoredShare(Interface):
2720+    """
2721+    This object contains as much as all of the share data.  It is intended
2722+    for lazy evaluation, such that in many use cases substantially less than
2723+    all of the share data will be accessed.
2724+    """
2725+    def close():
2726+        """
2727+        Complete writing to this share.
2728+        """
2729+
2730+    def get_storage_index():
2731+        """
2732+        Returns the storage index.
2733+        """
2734+
2735+    def get_shnum():
2736+        """
2737+        Returns the share number.
2738+        """
2739+
2740+    def get_data_length():
2741+        """
2742+        Returns the data length in bytes.
2743+        """
2744+
2745+    def get_size():
2746+        """
2747+        Returns the size of the share in bytes.
2748+        """
2749+
2750+    def get_used_space():
2751+        """
2752+        Returns the amount of backend storage including overhead, in bytes, used
2753+        by this share.
2754+        """
2755+
2756+    def unlink():
2757+        """
2758+        Signal that this share can be removed from the backend storage. This does
2759+        not guarantee that the share data will be immediately inaccessible, or
2760+        that it will be securely erased.
2761+        """
2762+
2763+    def readv(read_vector):
2764+        """
2765+        XXX
2766+        """
2767+
2768+
2769+class IStoredMutableShare(IStoredShare):
2770+    def check_write_enabler(write_enabler, si_s):
2771+        """
2772+        XXX
2773         """
2774 
2775hunk ./src/allmydata/interfaces.py 489
2776-    def put_plaintext_hashes(hashes=ListOf(Hash)):
2777+    def check_testv(test_vector):
2778+        """
2779+        XXX
2780+        """
2781+
2782+    def writev(datav, new_length):
2783+        """
2784+        XXX
2785+        """
2786+
2787+
2788+class IStorageBucketWriter(Interface):
2789+    """
2790+    Objects of this kind live on the client side.
2791+    """
2792+    def put_block(segmentnum, data):
2793         """
2794hunk ./src/allmydata/interfaces.py 506
2795+        @param segmentnum=int
2796+        @param data=ShareData: For most segments, this data will be 'blocksize'
2797+        bytes in length. The last segment might be shorter.
2798         @return: a Deferred that fires (with None) when the operation completes
2799         """
2800 
2801hunk ./src/allmydata/interfaces.py 512
2802-    def put_crypttext_hashes(hashes=ListOf(Hash)):
2803+    def put_crypttext_hashes(hashes):
2804         """
2805hunk ./src/allmydata/interfaces.py 514
2806+        @param hashes=ListOf(Hash)
2807         @return: a Deferred that fires (with None) when the operation completes
2808         """
2809 
2810hunk ./src/allmydata/interfaces.py 518
2811-    def put_block_hashes(blockhashes=ListOf(Hash)):
2812+    def put_block_hashes(blockhashes):
2813         """
2814hunk ./src/allmydata/interfaces.py 520
2815+        @param blockhashes=ListOf(Hash)
2816         @return: a Deferred that fires (with None) when the operation completes
2817         """
2818 
2819hunk ./src/allmydata/interfaces.py 524
2820-    def put_share_hashes(sharehashes=ListOf(TupleOf(int, Hash))):
2821+    def put_share_hashes(sharehashes):
2822         """
2823hunk ./src/allmydata/interfaces.py 526
2824+        @param sharehashes=ListOf(TupleOf(int, Hash))
2825         @return: a Deferred that fires (with None) when the operation completes
2826         """
2827 
2828hunk ./src/allmydata/interfaces.py 530
2829-    def put_uri_extension(data=URIExtensionData):
2830+    def put_uri_extension(data):
2831         """This block of data contains integrity-checking information (hashes
2832         of plaintext, crypttext, and shares), as well as encoding parameters
2833         that are necessary to recover the data. This is a serialized dict
2834hunk ./src/allmydata/interfaces.py 535
2835         mapping strings to other strings. The hash of this data is kept in
2836-        the URI and verified before any of the data is used. All buckets for
2837-        a given file contain identical copies of this data.
2838+        the URI and verified before any of the data is used. All share
2839+        containers for a given file contain identical copies of this data.
2840 
2841         The serialization format is specified with the following pseudocode:
2842         for k in sorted(dict.keys()):
2843hunk ./src/allmydata/interfaces.py 543
2844             assert re.match(r'^[a-zA-Z_\-]+$', k)
2845             write(k + ':' + netstring(dict[k]))
2846 
2847+        @param data=URIExtensionData
2848         @return: a Deferred that fires (with None) when the operation completes
2849         """
2850 
2851hunk ./src/allmydata/interfaces.py 558
2852 
2853 class IStorageBucketReader(Interface):
2854 
2855-    def get_block_data(blocknum=int, blocksize=int, size=int):
2856+    def get_block_data(blocknum, blocksize, size):
2857         """Most blocks will be the same size. The last block might be shorter
2858         than the others.
2859 
2860hunk ./src/allmydata/interfaces.py 562
2861+        @param blocknum=int
2862+        @param blocksize=int
2863+        @param size=int
2864         @return: ShareData
2865         """
2866 
2867hunk ./src/allmydata/interfaces.py 573
2868         @return: ListOf(Hash)
2869         """
2870 
2871-    def get_block_hashes(at_least_these=SetOf(int)):
2872+    def get_block_hashes(at_least_these=()):
2873         """
2874hunk ./src/allmydata/interfaces.py 575
2875+        @param at_least_these=SetOf(int)
2876         @return: ListOf(Hash)
2877         """
2878 
2879hunk ./src/allmydata/interfaces.py 579
2880-    def get_share_hashes(at_least_these=SetOf(int)):
2881+    def get_share_hashes():
2882         """
2883         @return: ListOf(TupleOf(int, Hash))
2884         """
2885hunk ./src/allmydata/interfaces.py 611
2886         @return: unicode nickname, or None
2887         """
2888 
2889-    # methods moved from IntroducerClient, need review
2890-    def get_all_connections():
2891-        """Return a frozenset of (nodeid, service_name, rref) tuples, one for
2892-        each active connection we've established to a remote service. This is
2893-        mostly useful for unit tests that need to wait until a certain number
2894-        of connections have been made."""
2895-
2896-    def get_all_connectors():
2897-        """Return a dict that maps from (nodeid, service_name) to a
2898-        RemoteServiceConnector instance for all services that we are actively
2899-        trying to connect to. Each RemoteServiceConnector has the following
2900-        public attributes::
2901-
2902-          service_name: the type of service provided, like 'storage'
2903-          announcement_time: when we first heard about this service
2904-          last_connect_time: when we last established a connection
2905-          last_loss_time: when we last lost a connection
2906-
2907-          version: the peer's version, from the most recent connection
2908-          oldest_supported: the peer's oldest supported version, same
2909-
2910-          rref: the RemoteReference, if connected, otherwise None
2911-          remote_host: the IAddress, if connected, otherwise None
2912-
2913-        This method is intended for monitoring interfaces, such as a web page
2914-        that describes connecting and connected peers.
2915-        """
2916-
2917-    def get_all_peerids():
2918-        """Return a frozenset of all peerids to whom we have a connection (to
2919-        one or more services) established. Mostly useful for unit tests."""
2920-
2921-    def get_all_connections_for(service_name):
2922-        """Return a frozenset of (nodeid, service_name, rref) tuples, one
2923-        for each active connection that provides the given SERVICE_NAME."""
2924-
2925-    def get_permuted_peers(service_name, key):
2926-        """Returns an ordered list of (peerid, rref) tuples, selecting from
2927-        the connections that provide SERVICE_NAME, using a hash-based
2928-        permutation keyed by KEY. This randomizes the service list in a
2929-        repeatable way, to distribute load over many peers.
2930-        """
2931-
2932 
2933 class IMutableSlotWriter(Interface):
2934     """
2935hunk ./src/allmydata/interfaces.py 616
2936     The interface for a writer around a mutable slot on a remote server.
2937     """
2938-    def set_checkstring(checkstring, *args):
2939+    def set_checkstring(seqnum_or_checkstring, root_hash=None, salt=None):
2940         """
2941         Set the checkstring that I will pass to the remote server when
2942         writing.
2943hunk ./src/allmydata/interfaces.py 640
2944         Add a block and salt to the share.
2945         """
2946 
2947-    def put_encprivey(encprivkey):
2948+    def put_encprivkey(encprivkey):
2949         """
2950         Add the encrypted private key to the share.
2951         """
2952hunk ./src/allmydata/interfaces.py 645
2953 
2954-    def put_blockhashes(blockhashes=list):
2955+    def put_blockhashes(blockhashes):
2956         """
2957hunk ./src/allmydata/interfaces.py 647
2958+        @param blockhashes=list
2959         Add the block hash tree to the share.
2960         """
2961 
2962hunk ./src/allmydata/interfaces.py 651
2963-    def put_sharehashes(sharehashes=dict):
2964+    def put_sharehashes(sharehashes):
2965         """
2966hunk ./src/allmydata/interfaces.py 653
2967+        @param sharehashes=dict
2968         Add the share hash chain to the share.
2969         """
2970 
2971hunk ./src/allmydata/interfaces.py 739
2972     def get_extension_params():
2973         """Return the extension parameters in the URI"""
2974 
2975-    def set_extension_params():
2976+    def set_extension_params(params):
2977         """Set the extension parameters that should be in the URI"""
2978 
2979 class IDirectoryURI(Interface):
2980hunk ./src/allmydata/interfaces.py 879
2981         writer-visible data using this writekey.
2982         """
2983 
2984-    # TODO: Can this be overwrite instead of replace?
2985-    def replace(new_contents):
2986-        """Replace the contents of the mutable file, provided that no other
2987+    def overwrite(new_contents):
2988+        """Overwrite the contents of the mutable file, provided that no other
2989         node has published (or is attempting to publish, concurrently) a
2990         newer version of the file than this one.
2991 
2992hunk ./src/allmydata/interfaces.py 1346
2993         is empty, the metadata will be an empty dictionary.
2994         """
2995 
2996-    def set_uri(name, writecap, readcap=None, metadata=None, overwrite=True):
2997+    def set_uri(name, writecap, readcap, metadata=None, overwrite=True):
2998         """I add a child (by writecap+readcap) at the specific name. I return
2999         a Deferred that fires when the operation finishes. If overwrite= is
3000         True, I will replace any existing child of the same name, otherwise
3001hunk ./src/allmydata/interfaces.py 1745
3002     Block Hash, and the encoding parameters, both of which must be included
3003     in the URI.
3004 
3005-    I do not choose shareholders, that is left to the IUploader. I must be
3006-    given a dict of RemoteReferences to storage buckets that are ready and
3007-    willing to receive data.
3008+    I do not choose shareholders, that is left to the IUploader.
3009     """
3010 
3011     def set_size(size):
3012hunk ./src/allmydata/interfaces.py 1752
3013         """Specify the number of bytes that will be encoded. This must be
3014         peformed before get_serialized_params() can be called.
3015         """
3016+
3017     def set_params(params):
3018         """Override the default encoding parameters. 'params' is a tuple of
3019         (k,d,n), where 'k' is the number of required shares, 'd' is the
3020hunk ./src/allmydata/interfaces.py 1848
3021     download, validate, decode, and decrypt data from them, writing the
3022     results to an output file.
3023 
3024-    I do not locate the shareholders, that is left to the IDownloader. I must
3025-    be given a dict of RemoteReferences to storage buckets that are ready to
3026-    send data.
3027+    I do not locate the shareholders, that is left to the IDownloader.
3028     """
3029 
3030     def setup(outfile):
3031hunk ./src/allmydata/interfaces.py 1950
3032         resuming an interrupted upload (where we need to compute the
3033         plaintext hashes, but don't need the redundant encrypted data)."""
3034 
3035-    def get_plaintext_hashtree_leaves(first, last, num_segments):
3036-        """OBSOLETE; Get the leaf nodes of a merkle hash tree over the
3037-        plaintext segments, i.e. get the tagged hashes of the given segments.
3038-        The segment size is expected to be generated by the
3039-        IEncryptedUploadable before any plaintext is read or ciphertext
3040-        produced, so that the segment hashes can be generated with only a
3041-        single pass.
3042-
3043-        This returns a Deferred that fires with a sequence of hashes, using:
3044-
3045-         tuple(segment_hashes[first:last])
3046-
3047-        'num_segments' is used to assert that the number of segments that the
3048-        IEncryptedUploadable handled matches the number of segments that the
3049-        encoder was expecting.
3050-
3051-        This method must not be called until the final byte has been read
3052-        from read_encrypted(). Once this method is called, read_encrypted()
3053-        can never be called again.
3054-        """
3055-
3056-    def get_plaintext_hash():
3057-        """OBSOLETE; Get the hash of the whole plaintext.
3058-
3059-        This returns a Deferred that fires with a tagged SHA-256 hash of the
3060-        whole plaintext, obtained from hashutil.plaintext_hash(data).
3061-        """
3062-
3063     def close():
3064         """Just like IUploadable.close()."""
3065 
3066hunk ./src/allmydata/interfaces.py 2144
3067         returns a Deferred that fires with an IUploadResults instance, from
3068         which the URI of the file can be obtained as results.uri ."""
3069 
3070-    def upload_ssk(write_capability, new_version, uploadable):
3071-        """TODO: how should this work?"""
3072-
3073 class ICheckable(Interface):
3074     def check(monitor, verify=False, add_lease=False):
3075         """Check up on my health, optionally repairing any problems.
3076hunk ./src/allmydata/interfaces.py 2505
3077 
3078 class IRepairResults(Interface):
3079     """I contain the results of a repair operation."""
3080-    def get_successful(self):
3081+    def get_successful():
3082         """Returns a boolean: True if the repair made the file healthy, False
3083         if not. Repair failure generally indicates a file that has been
3084         damaged beyond repair."""
3085hunk ./src/allmydata/interfaces.py 2577
3086     Tahoe process will typically have a single NodeMaker, but unit tests may
3087     create simplified/mocked forms for testing purposes.
3088     """
3089-    def create_from_cap(writecap, readcap=None, **kwargs):
3090+    def create_from_cap(writecap, readcap=None, deep_immutable=False, name=u"<unknown name>"):
3091         """I create an IFilesystemNode from the given writecap/readcap. I can
3092         only provide nodes for existing file/directory objects: use my other
3093         methods to create new objects. I return synchronously."""
3094hunk ./src/allmydata/monitor.py 30
3095 
3096     # the following methods are provided for the operation code
3097 
3098-    def is_cancelled(self):
3099+    def is_cancelled():
3100         """Returns True if the operation has been cancelled. If True,
3101         operation code should stop creating new work, and attempt to stop any
3102         work already in progress."""
3103hunk ./src/allmydata/monitor.py 35
3104 
3105-    def raise_if_cancelled(self):
3106+    def raise_if_cancelled():
3107         """Raise OperationCancelledError if the operation has been cancelled.
3108         Operation code that has a robust error-handling path can simply call
3109         this periodically."""
3110hunk ./src/allmydata/monitor.py 40
3111 
3112-    def set_status(self, status):
3113+    def set_status(status):
3114         """Sets the Monitor's 'status' object to an arbitrary value.
3115         Different operations will store different sorts of status information
3116         here. Operation code should use get+modify+set sequences to update
3117hunk ./src/allmydata/monitor.py 46
3118         this."""
3119 
3120-    def get_status(self):
3121+    def get_status():
3122         """Return the status object. If the operation failed, this will be a
3123         Failure instance."""
3124 
3125hunk ./src/allmydata/monitor.py 50
3126-    def finish(self, status):
3127+    def finish(status):
3128         """Call this when the operation is done, successful or not. The
3129         Monitor's lifetime is influenced by the completion of the operation
3130         it is monitoring. The Monitor's 'status' value will be set with the
3131hunk ./src/allmydata/monitor.py 63
3132 
3133     # the following methods are provided for the initiator of the operation
3134 
3135-    def is_finished(self):
3136+    def is_finished():
3137         """Return a boolean, True if the operation is done (whether
3138         successful or failed), False if it is still running."""
3139 
3140hunk ./src/allmydata/monitor.py 67
3141-    def when_done(self):
3142+    def when_done():
3143         """Return a Deferred that fires when the operation is complete. It
3144         will fire with the operation status, the same value as returned by
3145         get_status()."""
3146hunk ./src/allmydata/monitor.py 72
3147 
3148-    def cancel(self):
3149+    def cancel():
3150         """Cancel the operation as soon as possible. is_cancelled() will
3151         start returning True after this is called."""
3152 
3153hunk ./src/allmydata/mutable/filenode.py 753
3154         self._writekey = writekey
3155         self._serializer = defer.succeed(None)
3156 
3157-
3158     def get_sequence_number(self):
3159         """
3160         Get the sequence number of the mutable version that I represent.
3161hunk ./src/allmydata/mutable/filenode.py 759
3162         """
3163         return self._version[0] # verinfo[0] == the sequence number
3164 
3165+    def get_servermap(self):
3166+        return self._servermap
3167 
3168hunk ./src/allmydata/mutable/filenode.py 762
3169-    # TODO: Terminology?
3170     def get_writekey(self):
3171         """
3172         I return a writekey or None if I don't have a writekey.
3173hunk ./src/allmydata/mutable/filenode.py 768
3174         """
3175         return self._writekey
3176 
3177-
3178     def set_downloader_hints(self, hints):
3179         """
3180         I set the downloader hints.
3181hunk ./src/allmydata/mutable/filenode.py 776
3182 
3183         self._downloader_hints = hints
3184 
3185-
3186     def get_downloader_hints(self):
3187         """
3188         I return the downloader hints.
3189hunk ./src/allmydata/mutable/filenode.py 782
3190         """
3191         return self._downloader_hints
3192 
3193-
3194     def overwrite(self, new_contents):
3195         """
3196         I overwrite the contents of this mutable file version with the
3197hunk ./src/allmydata/mutable/filenode.py 791
3198 
3199         return self._do_serialized(self._overwrite, new_contents)
3200 
3201-
3202     def _overwrite(self, new_contents):
3203         assert IMutableUploadable.providedBy(new_contents)
3204         assert self._servermap.last_update_mode == MODE_WRITE
3205hunk ./src/allmydata/mutable/filenode.py 797
3206 
3207         return self._upload(new_contents)
3208 
3209-
3210     def modify(self, modifier, backoffer=None):
3211         """I use a modifier callback to apply a change to the mutable file.
3212         I implement the following pseudocode::
3213hunk ./src/allmydata/mutable/filenode.py 841
3214 
3215         return self._do_serialized(self._modify, modifier, backoffer)
3216 
3217-
3218     def _modify(self, modifier, backoffer):
3219         if backoffer is None:
3220             backoffer = BackoffAgent().delay
3221hunk ./src/allmydata/mutable/filenode.py 846
3222         return self._modify_and_retry(modifier, backoffer, True)
3223 
3224-
3225     def _modify_and_retry(self, modifier, backoffer, first_time):
3226         """
3227         I try to apply modifier to the contents of this version of the
3228hunk ./src/allmydata/mutable/filenode.py 878
3229         d.addErrback(_retry)
3230         return d
3231 
3232-
3233     def _modify_once(self, modifier, first_time):
3234         """
3235         I attempt to apply a modifier to the contents of the mutable
3236hunk ./src/allmydata/mutable/filenode.py 913
3237         d.addCallback(_apply)
3238         return d
3239 
3240-
3241     def is_readonly(self):
3242         """
3243         I return True if this MutableFileVersion provides no write
3244hunk ./src/allmydata/mutable/filenode.py 921
3245         """
3246         return self._writekey is None
3247 
3248-
3249     def is_mutable(self):
3250         """
3251         I return True, since mutable files are always mutable by
3252hunk ./src/allmydata/mutable/filenode.py 928
3253         """
3254         return True
3255 
3256-
3257     def get_storage_index(self):
3258         """
3259         I return the storage index of the reference that I encapsulate.
3260hunk ./src/allmydata/mutable/filenode.py 934
3261         """
3262         return self._storage_index
3263 
3264-
3265     def get_size(self):
3266         """
3267         I return the length, in bytes, of this readable object.
3268hunk ./src/allmydata/mutable/filenode.py 940
3269         """
3270         return self._servermap.size_of_version(self._version)
3271 
3272-
3273     def download_to_data(self, fetch_privkey=False):
3274         """
3275         I return a Deferred that fires with the contents of this
3276hunk ./src/allmydata/mutable/filenode.py 951
3277         d.addCallback(lambda mc: "".join(mc.chunks))
3278         return d
3279 
3280-
3281     def _try_to_download_data(self):
3282         """
3283         I am an unserialized cousin of download_to_data; I am called
3284hunk ./src/allmydata/mutable/filenode.py 963
3285         d.addCallback(lambda mc: "".join(mc.chunks))
3286         return d
3287 
3288-
3289     def read(self, consumer, offset=0, size=None, fetch_privkey=False):
3290         """
3291         I read a portion (possibly all) of the mutable file that I
3292hunk ./src/allmydata/mutable/filenode.py 971
3293         return self._do_serialized(self._read, consumer, offset, size,
3294                                    fetch_privkey)
3295 
3296-
3297     def _read(self, consumer, offset=0, size=None, fetch_privkey=False):
3298         """
3299         I am the serialized companion of read.
3300hunk ./src/allmydata/mutable/filenode.py 981
3301         d = r.download(consumer, offset, size)
3302         return d
3303 
3304-
3305     def _do_serialized(self, cb, *args, **kwargs):
3306         # note: to avoid deadlock, this callable is *not* allowed to invoke
3307         # other serialized methods within this (or any other)
3308hunk ./src/allmydata/mutable/filenode.py 999
3309         self._serializer.addErrback(log.err)
3310         return d
3311 
3312-
3313     def _upload(self, new_contents):
3314         #assert self._pubkey, "update_servermap must be called before publish"
3315         p = Publish(self._node, self._storage_broker, self._servermap)
3316hunk ./src/allmydata/mutable/filenode.py 1009
3317         d.addCallback(self._did_upload, new_contents.get_size())
3318         return d
3319 
3320-
3321     def _did_upload(self, res, size):
3322         self._most_recent_size = size
3323         return res
3324hunk ./src/allmydata/mutable/filenode.py 1029
3325         """
3326         return self._do_serialized(self._update, data, offset)
3327 
3328-
3329     def _update(self, data, offset):
3330         """
3331         I update the mutable file version represented by this particular
3332hunk ./src/allmydata/mutable/filenode.py 1058
3333         d.addCallback(self._build_uploadable_and_finish, data, offset)
3334         return d
3335 
3336-
3337     def _do_modify_update(self, data, offset):
3338         """
3339         I perform a file update by modifying the contents of the file
3340hunk ./src/allmydata/mutable/filenode.py 1073
3341             return new
3342         return self._modify(m, None)
3343 
3344-
3345     def _do_update_update(self, data, offset):
3346         """
3347         I start the Servermap update that gets us the data we need to
3348hunk ./src/allmydata/mutable/filenode.py 1108
3349         return self._update_servermap(update_range=(start_segment,
3350                                                     end_segment))
3351 
3352-
3353     def _decode_and_decrypt_segments(self, ignored, data, offset):
3354         """
3355         After the servermap update, I take the encrypted and encoded
3356hunk ./src/allmydata/mutable/filenode.py 1148
3357         d3 = defer.succeed(blockhashes)
3358         return deferredutil.gatherResults([d1, d2, d3])
3359 
3360-
3361     def _build_uploadable_and_finish(self, segments_and_bht, data, offset):
3362         """
3363         After the process has the plaintext segments, I build the
3364hunk ./src/allmydata/mutable/filenode.py 1163
3365         p = Publish(self._node, self._storage_broker, self._servermap)
3366         return p.update(u, offset, segments_and_bht[2], self._version)
3367 
3368-
3369     def _update_servermap(self, mode=MODE_WRITE, update_range=None):
3370         """
3371         I update the servermap. I return a Deferred that fires when the
3372hunk ./src/allmydata/storage/common.py 1
3373-
3374-import os.path
3375 from allmydata.util import base32
3376 
3377 class DataTooLargeError(Exception):
3378hunk ./src/allmydata/storage/common.py 5
3379     pass
3380+
3381 class UnknownMutableContainerVersionError(Exception):
3382     pass
3383hunk ./src/allmydata/storage/common.py 8
3384+
3385 class UnknownImmutableContainerVersionError(Exception):
3386     pass
3387 
3388hunk ./src/allmydata/storage/common.py 18
3389 
3390 def si_a2b(ascii_storageindex):
3391     return base32.a2b(ascii_storageindex)
3392-
3393-def storage_index_to_dir(storageindex):
3394-    sia = si_b2a(storageindex)
3395-    return os.path.join(sia[:2], sia)
3396hunk ./src/allmydata/storage/crawler.py 2
3397 
3398-import os, time, struct
3399+import time, struct
3400 import cPickle as pickle
3401 from twisted.internet import reactor
3402 from twisted.application import service
3403hunk ./src/allmydata/storage/crawler.py 6
3404+
3405+from allmydata.util.assertutil import precondition
3406+from allmydata.interfaces import IStorageBackend
3407 from allmydata.storage.common import si_b2a
3408hunk ./src/allmydata/storage/crawler.py 10
3409-from allmydata.util import fileutil
3410+
3411 
3412 class TimeSliceExceeded(Exception):
3413     pass
3414hunk ./src/allmydata/storage/crawler.py 15
3415 
3416+
3417 class ShareCrawler(service.MultiService):
3418hunk ./src/allmydata/storage/crawler.py 17
3419-    """A ShareCrawler subclass is attached to a StorageServer, and
3420-    periodically walks all of its shares, processing each one in some
3421-    fashion. This crawl is rate-limited, to reduce the IO burden on the host,
3422-    since large servers can easily have a terabyte of shares, in several
3423-    million files, which can take hours or days to read.
3424+    """
3425+    An instance of a subclass of ShareCrawler is attached to a storage
3426+    backend, and periodically walks the backend's shares, processing them
3427+    in some fashion. This crawl is rate-limited to reduce the I/O burden on
3428+    the host, since large servers can easily have a terabyte of shares in
3429+    several million files, which can take hours or days to read.
3430 
3431     Once the crawler starts a cycle, it will proceed at a rate limited by the
3432     allowed_cpu_percentage= and cpu_slice= parameters: yielding the reactor
3433hunk ./src/allmydata/storage/crawler.py 33
3434     long enough to ensure that 'minimum_cycle_time' elapses between the start
3435     of two consecutive cycles.
3436 
3437-    We assume that the normal upload/download/get_buckets traffic of a tahoe
3438+    We assume that the normal upload/download/DYHB traffic of a Tahoe-LAFS
3439     grid will cause the prefixdir contents to be mostly cached in the kernel,
3440hunk ./src/allmydata/storage/crawler.py 35
3441-    or that the number of buckets in each prefixdir will be small enough to
3442-    load quickly. A 1TB allmydata.com server was measured to have 2.56M
3443-    buckets, spread into the 1024 prefixdirs, with about 2500 buckets per
3444+    or that the number of sharesets in each prefixdir will be small enough to
3445+    load quickly. A 1TB allmydata.com server was measured to have 2.56 million
3446+    sharesets, spread into the 1024 prefixdirs, with about 2500 sharesets per
3447     prefix. On this server, each prefixdir took 130ms-200ms to list the first
3448     time, and 17ms to list the second time.
3449 
3450hunk ./src/allmydata/storage/crawler.py 41
3451-    To use a crawler, create a subclass which implements the process_bucket()
3452-    method. It will be called with a prefixdir and a base32 storage index
3453-    string. process_bucket() must run synchronously. Any keys added to
3454-    self.state will be preserved. Override add_initial_state() to set up
3455-    initial state keys. Override finished_cycle() to perform additional
3456-    processing when the cycle is complete. Any status that the crawler
3457-    produces should be put in the self.state dictionary. Status renderers
3458-    (like a web page which describes the accomplishments of your crawler)
3459-    will use crawler.get_state() to retrieve this dictionary; they can
3460-    present the contents as they see fit.
3461+    To implement a crawler, create a subclass that implements the
3462+    process_shareset() method. It will be called with a prefixdir and an
3463+    object providing the IShareSet interface. process_shareset() must run
3464+    synchronously. Any keys added to self.state will be preserved. Override
3465+    add_initial_state() to set up initial state keys. Override
3466+    finished_cycle() to perform additional processing when the cycle is
3467+    complete. Any status that the crawler produces should be put in the
3468+    self.state dictionary. Status renderers (like a web page describing the
3469+    accomplishments of your crawler) will use crawler.get_state() to retrieve
3470+    this dictionary; they can present the contents as they see fit.
3471 
3472hunk ./src/allmydata/storage/crawler.py 52
3473-    Then create an instance, with a reference to a StorageServer and a
3474-    filename where it can store persistent state. The statefile is used to
3475-    keep track of how far around the ring the process has travelled, as well
3476-    as timing history to allow the pace to be predicted and controlled. The
3477-    statefile will be updated and written to disk after each time slice (just
3478-    before the crawler yields to the reactor), and also after each cycle is
3479-    finished, and also when stopService() is called. Note that this means
3480-    that a crawler which is interrupted with SIGKILL while it is in the
3481-    middle of a time slice will lose progress: the next time the node is
3482-    started, the crawler will repeat some unknown amount of work.
3483+    Then create an instance, with a reference to a backend object providing
3484+    the IStorageBackend interface, and a filename where it can store
3485+    persistent state. The statefile is used to keep track of how far around
3486+    the ring the process has travelled, as well as timing history to allow
3487+    the pace to be predicted and controlled. The statefile will be updated
3488+    and written to disk after each time slice (just before the crawler yields
3489+    to the reactor), and also after each cycle is finished, and also when
3490+    stopService() is called. Note that this means that a crawler that is
3491+    interrupted with SIGKILL while it is in the middle of a time slice will
3492+    lose progress: the next time the node is started, the crawler will repeat
3493+    some unknown amount of work.
3494 
3495     The crawler instance must be started with startService() before it will
3496hunk ./src/allmydata/storage/crawler.py 65
3497-    do any work. To make it stop doing work, call stopService().
3498+    do any work. To make it stop doing work, call stopService(). A crawler
3499+    is usually a child service of a StorageServer, although it should not
3500+    depend on that.
3501+
3502+    For historical reasons, some dictionary key names use the term "bucket"
3503+    for what is now preferably called a "shareset" (the set of shares that a
3504+    server holds under a given storage index).
3505     """
3506 
3507     slow_start = 300 # don't start crawling for 5 minutes after startup
3508hunk ./src/allmydata/storage/crawler.py 80
3509     cpu_slice = 1.0 # use up to 1.0 seconds before yielding
3510     minimum_cycle_time = 300 # don't run a cycle faster than this
3511 
3512-    def __init__(self, server, statefile, allowed_cpu_percentage=None):
3513+    def __init__(self, backend, statefp, allowed_cpu_percentage=None):
3514+        precondition(IStorageBackend.providedBy(backend), backend)
3515         service.MultiService.__init__(self)
3516hunk ./src/allmydata/storage/crawler.py 83
3517+        self.backend = backend
3518+        self.statefp = statefp
3519         if allowed_cpu_percentage is not None:
3520             self.allowed_cpu_percentage = allowed_cpu_percentage
3521hunk ./src/allmydata/storage/crawler.py 87
3522-        self.server = server
3523-        self.sharedir = server.sharedir
3524-        self.statefile = statefile
3525         self.prefixes = [si_b2a(struct.pack(">H", i << (16-10)))[:2]
3526                          for i in range(2**10)]
3527         self.prefixes.sort()
3528hunk ./src/allmydata/storage/crawler.py 91
3529         self.timer = None
3530-        self.bucket_cache = (None, [])
3531+        self.shareset_cache = (None, [])
3532         self.current_sleep_time = None
3533         self.next_wake_time = None
3534         self.last_prefix_finished_time = None
3535hunk ./src/allmydata/storage/crawler.py 154
3536                 left = len(self.prefixes) - self.last_complete_prefix_index
3537                 remaining = left * self.last_prefix_elapsed_time
3538                 # TODO: remainder of this prefix: we need to estimate the
3539-                # per-bucket time, probably by measuring the time spent on
3540-                # this prefix so far, divided by the number of buckets we've
3541+                # per-shareset time, probably by measuring the time spent on
3542+                # this prefix so far, divided by the number of sharesets we've
3543                 # processed.
3544             d["estimated-cycle-complete-time-left"] = remaining
3545             # it's possible to call get_progress() from inside a crawler's
3546hunk ./src/allmydata/storage/crawler.py 175
3547         state dictionary.
3548 
3549         If we are not currently sleeping (i.e. get_state() was called from
3550-        inside the process_prefixdir, process_bucket, or finished_cycle()
3551+        inside the process_prefixdir, process_shareset, or finished_cycle()
3552         methods, or if startService has not yet been called on this crawler),
3553         these two keys will be None.
3554 
3555hunk ./src/allmydata/storage/crawler.py 188
3556     def load_state(self):
3557         # we use this to store state for both the crawler's internals and
3558         # anything the subclass-specific code needs. The state is stored
3559-        # after each bucket is processed, after each prefixdir is processed,
3560+        # after each shareset is processed, after each prefixdir is processed,
3561         # and after a cycle is complete. The internal keys we use are:
3562         #  ["version"]: int, always 1
3563         #  ["last-cycle-finished"]: int, or None if we have not yet finished
3564hunk ./src/allmydata/storage/crawler.py 202
3565         #                            are sleeping between cycles, or if we
3566         #                            have not yet finished any prefixdir since
3567         #                            a cycle was started
3568-        #  ["last-complete-bucket"]: str, base32 storage index bucket name
3569-        #                            of the last bucket to be processed, or
3570-        #                            None if we are sleeping between cycles
3571+        #  ["last-complete-bucket"]: str, base32 storage index of the last
3572+        #                            shareset to be processed, or None if we
3573+        #                            are sleeping between cycles
3574         try:
3575hunk ./src/allmydata/storage/crawler.py 206
3576-            f = open(self.statefile, "rb")
3577-            state = pickle.load(f)
3578-            f.close()
3579+            state = pickle.loads(self.statefp.getContent())
3580         except EnvironmentError:
3581             state = {"version": 1,
3582                      "last-cycle-finished": None,
3583hunk ./src/allmydata/storage/crawler.py 242
3584         else:
3585             last_complete_prefix = self.prefixes[lcpi]
3586         self.state["last-complete-prefix"] = last_complete_prefix
3587-        tmpfile = self.statefile + ".tmp"
3588-        f = open(tmpfile, "wb")
3589-        pickle.dump(self.state, f)
3590-        f.close()
3591-        fileutil.move_into_place(tmpfile, self.statefile)
3592+        self.statefp.setContent(pickle.dumps(self.state))
3593 
3594     def startService(self):
3595         # arrange things to look like we were just sleeping, so
3596hunk ./src/allmydata/storage/crawler.py 284
3597         sleep_time = (this_slice / self.allowed_cpu_percentage) - this_slice
3598         # if the math gets weird, or a timequake happens, don't sleep
3599         # forever. Note that this means that, while a cycle is running, we
3600-        # will process at least one bucket every 5 minutes, no matter how
3601-        # long that bucket takes.
3602+        # will process at least one shareset every 5 minutes, no matter how
3603+        # long that shareset takes.
3604         sleep_time = max(0.0, min(sleep_time, 299))
3605         if finished_cycle:
3606             # how long should we sleep between cycles? Don't run faster than
3607hunk ./src/allmydata/storage/crawler.py 315
3608         for i in range(self.last_complete_prefix_index+1, len(self.prefixes)):
3609             # if we want to yield earlier, just raise TimeSliceExceeded()
3610             prefix = self.prefixes[i]
3611-            prefixdir = os.path.join(self.sharedir, prefix)
3612-            if i == self.bucket_cache[0]:
3613-                buckets = self.bucket_cache[1]
3614+            if i == self.shareset_cache[0]:
3615+                sharesets = self.shareset_cache[1]
3616             else:
3617hunk ./src/allmydata/storage/crawler.py 318
3618-                try:
3619-                    buckets = os.listdir(prefixdir)
3620-                    buckets.sort()
3621-                except EnvironmentError:
3622-                    buckets = []
3623-                self.bucket_cache = (i, buckets)
3624-            self.process_prefixdir(cycle, prefix, prefixdir,
3625-                                   buckets, start_slice)
3626+                sharesets = self.backend.get_sharesets_for_prefix(prefix)
3627+                self.shareset_cache = (i, sharesets)
3628+            self.process_prefixdir(cycle, prefix, sharesets, start_slice)
3629             self.last_complete_prefix_index = i
3630 
3631             now = time.time()
3632hunk ./src/allmydata/storage/crawler.py 345
3633         self.finished_cycle(cycle)
3634         self.save_state()
3635 
3636-    def process_prefixdir(self, cycle, prefix, prefixdir, buckets, start_slice):
3637-        """This gets a list of bucket names (i.e. storage index strings,
3638+    def process_prefixdir(self, cycle, prefix, sharesets, start_slice):
3639+        """
3640+        This gets a list of shareset names (i.e. storage index strings,
3641         base32-encoded) in sorted order.
3642 
3643         You can override this if your crawler doesn't care about the actual
3644hunk ./src/allmydata/storage/crawler.py 352
3645         shares, for example a crawler which merely keeps track of how many
3646-        buckets are being managed by this server.
3647+        sharesets are being managed by this server.
3648 
3649hunk ./src/allmydata/storage/crawler.py 354
3650-        Subclasses which *do* care about actual bucket should leave this
3651-        method along, and implement process_bucket() instead.
3652+        Subclasses which *do* care about actual shareset should leave this
3653+        method alone, and implement process_shareset() instead.
3654         """
3655 
3656hunk ./src/allmydata/storage/crawler.py 358
3657-        for bucket in buckets:
3658-            if bucket <= self.state["last-complete-bucket"]:
3659+        for shareset in sharesets:
3660+            base32si = shareset.get_storage_index_string()
3661+            if base32si <= self.state["last-complete-bucket"]:
3662                 continue
3663hunk ./src/allmydata/storage/crawler.py 362
3664-            self.process_bucket(cycle, prefix, prefixdir, bucket)
3665-            self.state["last-complete-bucket"] = bucket
3666+            self.process_shareset(cycle, prefix, shareset)
3667+            self.state["last-complete-bucket"] = base32si
3668             if time.time() >= start_slice + self.cpu_slice:
3669                 raise TimeSliceExceeded()
3670 
3671hunk ./src/allmydata/storage/crawler.py 370
3672     # the remaining methods are explictly for subclasses to implement.
3673 
3674     def started_cycle(self, cycle):
3675-        """Notify a subclass that the crawler is about to start a cycle.
3676+        """
3677+        Notify a subclass that the crawler is about to start a cycle.
3678 
3679         This method is for subclasses to override. No upcall is necessary.
3680         """
3681hunk ./src/allmydata/storage/crawler.py 377
3682         pass
3683 
3684-    def process_bucket(self, cycle, prefix, prefixdir, storage_index_b32):
3685-        """Examine a single bucket. Subclasses should do whatever they want
3686+    def process_shareset(self, cycle, prefix, shareset):
3687+        """
3688+        Examine a single shareset. Subclasses should do whatever they want
3689         to do to the shares therein, then update self.state as necessary.
3690 
3691         If the crawler is never interrupted by SIGKILL, this method will be
3692hunk ./src/allmydata/storage/crawler.py 383
3693-        called exactly once per share (per cycle). If it *is* interrupted,
3694+        called exactly once per shareset (per cycle). If it *is* interrupted,
3695         then the next time the node is started, some amount of work will be
3696         duplicated, according to when self.save_state() was last called. By
3697         default, save_state() is called at the end of each timeslice, and
3698hunk ./src/allmydata/storage/crawler.py 391
3699 
3700         To reduce the chance of duplicate work (i.e. to avoid adding multiple
3701         records to a database), you can call save_state() at the end of your
3702-        process_bucket() method. This will reduce the maximum duplicated work
3703-        to one bucket per SIGKILL. It will also add overhead, probably 1-20ms
3704-        per bucket (and some disk writes), which will count against your
3705-        allowed_cpu_percentage, and which may be considerable if
3706-        process_bucket() runs quickly.
3707+        process_shareset() method. This will reduce the maximum duplicated
3708+        work to one shareset per SIGKILL. It will also add overhead, probably
3709+        1-20ms per shareset (and some disk writes), which will count against
3710+        your allowed_cpu_percentage, and which may be considerable if
3711+        process_shareset() runs quickly.
3712 
3713         This method is for subclasses to override. No upcall is necessary.
3714         """
3715hunk ./src/allmydata/storage/crawler.py 402
3716         pass
3717 
3718     def finished_prefix(self, cycle, prefix):
3719-        """Notify a subclass that the crawler has just finished processing a
3720-        prefix directory (all buckets with the same two-character/10bit
3721+        """
3722+        Notify a subclass that the crawler has just finished processing a
3723+        prefix directory (all sharesets with the same two-character/10-bit
3724         prefix). To impose a limit on how much work might be duplicated by a
3725         SIGKILL that occurs during a timeslice, you can call
3726         self.save_state() here, but be aware that it may represent a
3727hunk ./src/allmydata/storage/crawler.py 415
3728         pass
3729 
3730     def finished_cycle(self, cycle):
3731-        """Notify subclass that a cycle (one complete traversal of all
3732+        """
3733+        Notify subclass that a cycle (one complete traversal of all
3734         prefixdirs) has just finished. 'cycle' is the number of the cycle
3735         that just finished. This method should perform summary work and
3736         update self.state to publish information to status displays.
3737hunk ./src/allmydata/storage/crawler.py 433
3738         pass
3739 
3740     def yielding(self, sleep_time):
3741-        """The crawler is about to sleep for 'sleep_time' seconds. This
3742+        """
3743+        The crawler is about to sleep for 'sleep_time' seconds. This
3744         method is mostly for the convenience of unit tests.
3745 
3746         This method is for subclasses to override. No upcall is necessary.
3747hunk ./src/allmydata/storage/crawler.py 443
3748 
3749 
3750 class BucketCountingCrawler(ShareCrawler):
3751-    """I keep track of how many buckets are being managed by this server.
3752-    This is equivalent to the number of distributed files and directories for
3753-    which I am providing storage. The actual number of files+directories in
3754-    the full grid is probably higher (especially when there are more servers
3755-    than 'N', the number of generated shares), because some files+directories
3756-    will have shares on other servers instead of me. Also note that the
3757-    number of buckets will differ from the number of shares in small grids,
3758-    when more than one share is placed on a single server.
3759+    """
3760+    I keep track of how many sharesets, each corresponding to a storage index,
3761+    are being managed by this server. This is equivalent to the number of
3762+    distributed files and directories for which I am providing storage. The
3763+    actual number of files and directories in the full grid is probably higher
3764+    (especially when there are more servers than 'N', the number of generated
3765+    shares), because some files and directories will have shares on other
3766+    servers instead of me. Also note that the number of sharesets will differ
3767+    from the number of shares in small grids, when more than one share is
3768+    placed on a single server.
3769     """
3770 
3771     minimum_cycle_time = 60*60 # we don't need this more than once an hour
3772hunk ./src/allmydata/storage/crawler.py 457
3773 
3774-    def __init__(self, server, statefile, num_sample_prefixes=1):
3775-        ShareCrawler.__init__(self, server, statefile)
3776+    def __init__(self, backend, statefp, num_sample_prefixes=1):
3777+        ShareCrawler.__init__(self, backend, statefp)
3778         self.num_sample_prefixes = num_sample_prefixes
3779 
3780     def add_initial_state(self):
3781hunk ./src/allmydata/storage/crawler.py 471
3782         self.state.setdefault("last-complete-bucket-count", None)
3783         self.state.setdefault("storage-index-samples", {})
3784 
3785-    def process_prefixdir(self, cycle, prefix, prefixdir, buckets, start_slice):
3786+    def process_prefixdir(self, cycle, prefix, sharesets, start_slice):
3787         # we override process_prefixdir() because we don't want to look at
3788hunk ./src/allmydata/storage/crawler.py 473
3789-        # the individual buckets. We'll save state after each one. On my
3790+        # the individual sharesets. We'll save state after each one. On my
3791         # laptop, a mostly-empty storage server can process about 70
3792         # prefixdirs in a 1.0s slice.
3793         if cycle not in self.state["bucket-counts"]:
3794hunk ./src/allmydata/storage/crawler.py 478
3795             self.state["bucket-counts"][cycle] = {}
3796-        self.state["bucket-counts"][cycle][prefix] = len(buckets)
3797+        self.state["bucket-counts"][cycle][prefix] = len(sharesets)
3798         if prefix in self.prefixes[:self.num_sample_prefixes]:
3799hunk ./src/allmydata/storage/crawler.py 480
3800-            self.state["storage-index-samples"][prefix] = (cycle, buckets)
3801+            self.state["storage-index-samples"][prefix] = (cycle, sharesets)
3802 
3803     def finished_cycle(self, cycle):
3804         last_counts = self.state["bucket-counts"].get(cycle, [])
3805hunk ./src/allmydata/storage/crawler.py 486
3806         if len(last_counts) == len(self.prefixes):
3807             # great, we have a whole cycle.
3808-            num_buckets = sum(last_counts.values())
3809-            self.state["last-complete-bucket-count"] = num_buckets
3810+            num_sharesets = sum(last_counts.values())
3811+            self.state["last-complete-bucket-count"] = num_sharesets
3812             # get rid of old counts
3813             for old_cycle in list(self.state["bucket-counts"].keys()):
3814                 if old_cycle != cycle:
3815hunk ./src/allmydata/storage/crawler.py 494
3816                     del self.state["bucket-counts"][old_cycle]
3817         # get rid of old samples too
3818         for prefix in list(self.state["storage-index-samples"].keys()):
3819-            old_cycle,buckets = self.state["storage-index-samples"][prefix]
3820+            old_cycle, storage_indices = self.state["storage-index-samples"][prefix]
3821             if old_cycle != cycle:
3822                 del self.state["storage-index-samples"][prefix]
3823hunk ./src/allmydata/storage/crawler.py 497
3824-
3825hunk ./src/allmydata/storage/expirer.py 1
3826-import time, os, pickle, struct
3827+
3828+import time, pickle, struct
3829+from twisted.python import log as twlog
3830+
3831 from allmydata.storage.crawler import ShareCrawler
3832hunk ./src/allmydata/storage/expirer.py 6
3833-from allmydata.storage.shares import get_share_file
3834-from allmydata.storage.common import UnknownMutableContainerVersionError, \
3835+from allmydata.storage.common import si_b2a, UnknownMutableContainerVersionError, \
3836      UnknownImmutableContainerVersionError
3837hunk ./src/allmydata/storage/expirer.py 8
3838-from twisted.python import log as twlog
3839+
3840 
3841 class LeaseCheckingCrawler(ShareCrawler):
3842     """I examine the leases on all shares, determining which are still valid
3843hunk ./src/allmydata/storage/expirer.py 17
3844     removed.
3845 
3846     I collect statistics on the leases and make these available to a web
3847-    status page, including::
3848+    status page, including:
3849 
3850     Space recovered during this cycle-so-far:
3851      actual (only if expiration_enabled=True):
3852hunk ./src/allmydata/storage/expirer.py 21
3853-      num-buckets, num-shares, sum of share sizes, real disk usage
3854+      num-storage-indices, num-shares, sum of share sizes, real disk usage
3855       ('real disk usage' means we use stat(fn).st_blocks*512 and include any
3856        space used by the directory)
3857      what it would have been with the original lease expiration time
3858hunk ./src/allmydata/storage/expirer.py 32
3859 
3860     Space recovered during the last 10 cycles  <-- saved in separate pickle
3861 
3862-    Shares/buckets examined:
3863+    Shares/storage-indices examined:
3864      this cycle-so-far
3865      prediction of rest of cycle
3866      during last 10 cycles <-- separate pickle
3867hunk ./src/allmydata/storage/expirer.py 42
3868     Histogram of leases-per-share:
3869      this-cycle-to-date
3870      last 10 cycles <-- separate pickle
3871-    Histogram of lease ages, buckets = 1day
3872+    Histogram of lease ages, storage-indices over 1 day
3873      cycle-to-date
3874      last 10 cycles <-- separate pickle
3875 
3876hunk ./src/allmydata/storage/expirer.py 53
3877     slow_start = 360 # wait 6 minutes after startup
3878     minimum_cycle_time = 12*60*60 # not more than twice per day
3879 
3880-    def __init__(self, server, statefile, historyfile,
3881-                 expiration_enabled, mode,
3882-                 override_lease_duration, # used if expiration_mode=="age"
3883-                 cutoff_date, # used if expiration_mode=="cutoff-date"
3884-                 sharetypes):
3885-        self.historyfile = historyfile
3886-        self.expiration_enabled = expiration_enabled
3887-        self.mode = mode
3888+    def __init__(self, backend, statefp, historyfp, expiration_policy):
3889+        # ShareCrawler.__init__ will call add_initial_state, so self.historyfp has to be set first.
3890+        self.historyfp = historyfp
3891+        ShareCrawler.__init__(self, backend, statefp)
3892+
3893+        self.expiration_enabled = expiration_policy['enabled']
3894+        self.mode = expiration_policy['mode']
3895         self.override_lease_duration = None
3896         self.cutoff_date = None
3897         if self.mode == "age":
3898hunk ./src/allmydata/storage/expirer.py 63
3899-            assert isinstance(override_lease_duration, (int, type(None)))
3900-            self.override_lease_duration = override_lease_duration # seconds
3901+            assert isinstance(expiration_policy['override_lease_duration'], (int, type(None)))
3902+            self.override_lease_duration = expiration_policy['override_lease_duration'] # seconds
3903         elif self.mode == "cutoff-date":
3904hunk ./src/allmydata/storage/expirer.py 66
3905-            assert isinstance(cutoff_date, int) # seconds-since-epoch
3906-            assert cutoff_date is not None
3907-            self.cutoff_date = cutoff_date
3908+            assert isinstance(expiration_policy['cutoff_date'], int) # seconds-since-epoch
3909+            self.cutoff_date = expiration_policy['cutoff_date']
3910         else:
3911hunk ./src/allmydata/storage/expirer.py 69
3912-            raise ValueError("GC mode '%s' must be 'age' or 'cutoff-date'" % mode)
3913-        self.sharetypes_to_expire = sharetypes
3914-        ShareCrawler.__init__(self, server, statefile)
3915+            raise ValueError("GC mode '%s' must be 'age' or 'cutoff-date'" % expiration_policy['mode'])
3916+        self.sharetypes_to_expire = expiration_policy['sharetypes']
3917 
3918     def add_initial_state(self):
3919         # we fill ["cycle-to-date"] here (even though they will be reset in
3920hunk ./src/allmydata/storage/expirer.py 84
3921             self.state["cycle-to-date"].setdefault(k, so_far[k])
3922 
3923         # initialize history
3924-        if not os.path.exists(self.historyfile):
3925+        if not self.historyfp.exists():
3926             history = {} # cyclenum -> dict
3927hunk ./src/allmydata/storage/expirer.py 86
3928-            f = open(self.historyfile, "wb")
3929-            pickle.dump(history, f)
3930-            f.close()
3931+            self.historyfp.setContent(pickle.dumps(history))
3932 
3933     def create_empty_cycle_dict(self):
3934         recovered = self.create_empty_recovered_dict()
3935hunk ./src/allmydata/storage/expirer.py 99
3936 
3937     def create_empty_recovered_dict(self):
3938         recovered = {}
3939+        # "buckets" is ambiguous; here it means the number of sharesets (one per storage index per server)
3940         for a in ("actual", "original", "configured", "examined"):
3941             for b in ("buckets", "shares", "sharebytes", "diskbytes"):
3942                 recovered[a+"-"+b] = 0
3943hunk ./src/allmydata/storage/expirer.py 110
3944     def started_cycle(self, cycle):
3945         self.state["cycle-to-date"] = self.create_empty_cycle_dict()
3946 
3947-    def stat(self, fn):
3948-        return os.stat(fn)
3949-
3950-    def process_bucket(self, cycle, prefix, prefixdir, storage_index_b32):
3951-        bucketdir = os.path.join(prefixdir, storage_index_b32)
3952-        s = self.stat(bucketdir)
3953+    def process_storage_index(self, cycle, prefix, container):
3954         would_keep_shares = []
3955         wks = None
3956hunk ./src/allmydata/storage/expirer.py 113
3957+        sharetype = None
3958 
3959hunk ./src/allmydata/storage/expirer.py 115
3960-        for fn in os.listdir(bucketdir):
3961-            try:
3962-                shnum = int(fn)
3963-            except ValueError:
3964-                continue # non-numeric means not a sharefile
3965-            sharefile = os.path.join(bucketdir, fn)
3966+        for share in container.get_shares():
3967+            sharetype = share.sharetype
3968             try:
3969hunk ./src/allmydata/storage/expirer.py 118
3970-                wks = self.process_share(sharefile)
3971+                wks = self.process_share(share)
3972             except (UnknownMutableContainerVersionError,
3973                     UnknownImmutableContainerVersionError,
3974                     struct.error):
3975hunk ./src/allmydata/storage/expirer.py 122
3976-                twlog.msg("lease-checker error processing %s" % sharefile)
3977+                twlog.msg("lease-checker error processing %r" % (share,))
3978                 twlog.err()
3979hunk ./src/allmydata/storage/expirer.py 124
3980-                which = (storage_index_b32, shnum)
3981+                which = (si_b2a(share.storageindex), share.get_shnum())
3982                 self.state["cycle-to-date"]["corrupt-shares"].append(which)
3983                 wks = (1, 1, 1, "unknown")
3984             would_keep_shares.append(wks)
3985hunk ./src/allmydata/storage/expirer.py 129
3986 
3987-        sharetype = None
3988+        container_type = None
3989         if wks:
3990hunk ./src/allmydata/storage/expirer.py 131
3991-            # use the last share's sharetype as the buckettype
3992-            sharetype = wks[3]
3993+            # use the last share's sharetype as the container type
3994+            container_type = wks[3]
3995         rec = self.state["cycle-to-date"]["space-recovered"]
3996         self.increment(rec, "examined-buckets", 1)
3997         if sharetype:
3998hunk ./src/allmydata/storage/expirer.py 136
3999-            self.increment(rec, "examined-buckets-"+sharetype, 1)
4000+            self.increment(rec, "examined-buckets-"+container_type, 1)
4001+
4002+        container_diskbytes = container.get_overhead()
4003 
4004hunk ./src/allmydata/storage/expirer.py 140
4005-        try:
4006-            bucket_diskbytes = s.st_blocks * 512
4007-        except AttributeError:
4008-            bucket_diskbytes = 0 # no stat().st_blocks on windows
4009         if sum([wks[0] for wks in would_keep_shares]) == 0:
4010hunk ./src/allmydata/storage/expirer.py 141
4011-            self.increment_bucketspace("original", bucket_diskbytes, sharetype)
4012+            self.increment_container_space("original", container_diskbytes, sharetype)
4013         if sum([wks[1] for wks in would_keep_shares]) == 0:
4014hunk ./src/allmydata/storage/expirer.py 143
4015-            self.increment_bucketspace("configured", bucket_diskbytes, sharetype)
4016+            self.increment_container_space("configured", container_diskbytes, sharetype)
4017         if sum([wks[2] for wks in would_keep_shares]) == 0:
4018hunk ./src/allmydata/storage/expirer.py 145
4019-            self.increment_bucketspace("actual", bucket_diskbytes, sharetype)
4020+            self.increment_container_space("actual", container_diskbytes, sharetype)
4021 
4022hunk ./src/allmydata/storage/expirer.py 147
4023-    def process_share(self, sharefilename):
4024-        # first, find out what kind of a share it is
4025-        sf = get_share_file(sharefilename)
4026-        sharetype = sf.sharetype
4027+    def process_share(self, share):
4028+        sharetype = share.sharetype
4029         now = time.time()
4030hunk ./src/allmydata/storage/expirer.py 150
4031-        s = self.stat(sharefilename)
4032+        sharebytes = share.get_size()
4033+        diskbytes = share.get_used_space()
4034 
4035         num_leases = 0
4036         num_valid_leases_original = 0
4037hunk ./src/allmydata/storage/expirer.py 158
4038         num_valid_leases_configured = 0
4039         expired_leases_configured = []
4040 
4041-        for li in sf.get_leases():
4042+        for li in share.get_leases():
4043             num_leases += 1
4044             original_expiration_time = li.get_expiration_time()
4045             grant_renew_time = li.get_grant_renew_time_time()
4046hunk ./src/allmydata/storage/expirer.py 171
4047 
4048             #  expired-or-not according to our configured age limit
4049             expired = False
4050-            if self.mode == "age":
4051-                age_limit = original_expiration_time
4052-                if self.override_lease_duration is not None:
4053-                    age_limit = self.override_lease_duration
4054-                if age > age_limit:
4055-                    expired = True
4056-            else:
4057-                assert self.mode == "cutoff-date"
4058-                if grant_renew_time < self.cutoff_date:
4059-                    expired = True
4060-            if sharetype not in self.sharetypes_to_expire:
4061-                expired = False
4062+            if sharetype in self.sharetypes_to_expire:
4063+                if self.mode == "age":
4064+                    age_limit = original_expiration_time
4065+                    if self.override_lease_duration is not None:
4066+                        age_limit = self.override_lease_duration
4067+                    if age > age_limit:
4068+                        expired = True
4069+                else:
4070+                    assert self.mode == "cutoff-date"
4071+                    if grant_renew_time < self.cutoff_date:
4072+                        expired = True
4073 
4074             if expired:
4075                 expired_leases_configured.append(li)
4076hunk ./src/allmydata/storage/expirer.py 190
4077 
4078         so_far = self.state["cycle-to-date"]
4079         self.increment(so_far["leases-per-share-histogram"], num_leases, 1)
4080-        self.increment_space("examined", s, sharetype)
4081+        self.increment_space("examined", diskbytes, sharetype)
4082 
4083         would_keep_share = [1, 1, 1, sharetype]
4084 
4085hunk ./src/allmydata/storage/expirer.py 196
4086         if self.expiration_enabled:
4087             for li in expired_leases_configured:
4088-                sf.cancel_lease(li.cancel_secret)
4089+                share.cancel_lease(li.cancel_secret)
4090 
4091         if num_valid_leases_original == 0:
4092             would_keep_share[0] = 0
4093hunk ./src/allmydata/storage/expirer.py 200
4094-            self.increment_space("original", s, sharetype)
4095+            self.increment_space("original", sharebytes, diskbytes, sharetype)
4096 
4097         if num_valid_leases_configured == 0:
4098             would_keep_share[1] = 0
4099hunk ./src/allmydata/storage/expirer.py 204
4100-            self.increment_space("configured", s, sharetype)
4101+            self.increment_space("configured", sharebytes, diskbytes, sharetype)
4102             if self.expiration_enabled:
4103                 would_keep_share[2] = 0
4104hunk ./src/allmydata/storage/expirer.py 207
4105-                self.increment_space("actual", s, sharetype)
4106+                self.increment_space("actual", sharebytes, diskbytes, sharetype)
4107 
4108         return would_keep_share
4109 
4110hunk ./src/allmydata/storage/expirer.py 211
4111-    def increment_space(self, a, s, sharetype):
4112-        sharebytes = s.st_size
4113-        try:
4114-            # note that stat(2) says that st_blocks is 512 bytes, and that
4115-            # st_blksize is "optimal file sys I/O ops blocksize", which is
4116-            # independent of the block-size that st_blocks uses.
4117-            diskbytes = s.st_blocks * 512
4118-        except AttributeError:
4119-            # the docs say that st_blocks is only on linux. I also see it on
4120-            # MacOS. But it isn't available on windows.
4121-            diskbytes = sharebytes
4122+    def increment_space(self, a, sharebytes, diskbytes, sharetype):
4123         so_far_sr = self.state["cycle-to-date"]["space-recovered"]
4124         self.increment(so_far_sr, a+"-shares", 1)
4125         self.increment(so_far_sr, a+"-sharebytes", sharebytes)
4126hunk ./src/allmydata/storage/expirer.py 221
4127             self.increment(so_far_sr, a+"-sharebytes-"+sharetype, sharebytes)
4128             self.increment(so_far_sr, a+"-diskbytes-"+sharetype, diskbytes)
4129 
4130-    def increment_bucketspace(self, a, bucket_diskbytes, sharetype):
4131+    def increment_container_space(self, a, container_diskbytes, container_type):
4132         rec = self.state["cycle-to-date"]["space-recovered"]
4133hunk ./src/allmydata/storage/expirer.py 223
4134-        self.increment(rec, a+"-diskbytes", bucket_diskbytes)
4135+        self.increment(rec, a+"-diskbytes", container_diskbytes)
4136         self.increment(rec, a+"-buckets", 1)
4137hunk ./src/allmydata/storage/expirer.py 225
4138-        if sharetype:
4139-            self.increment(rec, a+"-diskbytes-"+sharetype, bucket_diskbytes)
4140-            self.increment(rec, a+"-buckets-"+sharetype, 1)
4141+        if container_type:
4142+            self.increment(rec, a+"-diskbytes-"+container_type, container_diskbytes)
4143+            self.increment(rec, a+"-buckets-"+container_type, 1)
4144 
4145     def increment(self, d, k, delta=1):
4146         if k not in d:
4147hunk ./src/allmydata/storage/expirer.py 281
4148         # copy() needs to become a deepcopy
4149         h["space-recovered"] = s["space-recovered"].copy()
4150 
4151-        history = pickle.load(open(self.historyfile, "rb"))
4152+        history = pickle.load(self.historyfp.getContent())
4153         history[cycle] = h
4154         while len(history) > 10:
4155             oldcycles = sorted(history.keys())
4156hunk ./src/allmydata/storage/expirer.py 286
4157             del history[oldcycles[0]]
4158-        f = open(self.historyfile, "wb")
4159-        pickle.dump(history, f)
4160-        f.close()
4161+        self.historyfp.setContent(pickle.dumps(history))
4162 
4163     def get_state(self):
4164         """In addition to the crawler state described in
4165hunk ./src/allmydata/storage/expirer.py 355
4166         progress = self.get_progress()
4167 
4168         state = ShareCrawler.get_state(self) # does a shallow copy
4169-        history = pickle.load(open(self.historyfile, "rb"))
4170+        history = pickle.load(self.historyfp.getContent())
4171         state["history"] = history
4172 
4173         if not progress["cycle-in-progress"]:
4174hunk ./src/allmydata/storage/lease.py 3
4175 import struct, time
4176 
4177+
4178+class NonExistentLeaseError(Exception):
4179+    pass
4180+
4181 class LeaseInfo:
4182     def __init__(self, owner_num=None, renew_secret=None, cancel_secret=None,
4183                  expiration_time=None, nodeid=None):
4184hunk ./src/allmydata/storage/lease.py 21
4185 
4186     def get_expiration_time(self):
4187         return self.expiration_time
4188+
4189     def get_grant_renew_time_time(self):
4190         # hack, based upon fixed 31day expiration period
4191         return self.expiration_time - 31*24*60*60
4192hunk ./src/allmydata/storage/lease.py 25
4193+
4194     def get_age(self):
4195         return time.time() - self.get_grant_renew_time_time()
4196 
4197hunk ./src/allmydata/storage/lease.py 36
4198          self.expiration_time) = struct.unpack(">L32s32sL", data)
4199         self.nodeid = None
4200         return self
4201+
4202     def to_immutable_data(self):
4203         return struct.pack(">L32s32sL",
4204                            self.owner_num,
4205hunk ./src/allmydata/storage/lease.py 49
4206                            int(self.expiration_time),
4207                            self.renew_secret, self.cancel_secret,
4208                            self.nodeid)
4209+
4210     def from_mutable_data(self, data):
4211         (self.owner_num,
4212          self.expiration_time,
4213hunk ./src/allmydata/storage/server.py 1
4214-import os, re, weakref, struct, time
4215+import weakref, time
4216 
4217 from foolscap.api import Referenceable
4218 from twisted.application import service
4219hunk ./src/allmydata/storage/server.py 7
4220 
4221 from zope.interface import implements
4222-from allmydata.interfaces import RIStorageServer, IStatsProducer
4223-from allmydata.util import fileutil, idlib, log, time_format
4224+from allmydata.interfaces import RIStorageServer, IStatsProducer, IStorageBackend
4225+from allmydata.util.assertutil import precondition
4226+from allmydata.util import idlib, log
4227 import allmydata # for __full_version__
4228 
4229hunk ./src/allmydata/storage/server.py 12
4230-from allmydata.storage.common import si_b2a, si_a2b, storage_index_to_dir
4231-_pyflakes_hush = [si_b2a, si_a2b, storage_index_to_dir] # re-exported
4232+from allmydata.storage.common import si_a2b, si_b2a
4233+[si_a2b]  # hush pyflakes
4234 from allmydata.storage.lease import LeaseInfo
4235hunk ./src/allmydata/storage/server.py 15
4236-from allmydata.storage.mutable import MutableShareFile, EmptyShare, \
4237-     create_mutable_sharefile
4238-from allmydata.storage.immutable import ShareFile, BucketWriter, BucketReader
4239-from allmydata.storage.crawler import BucketCountingCrawler
4240 from allmydata.storage.expirer import LeaseCheckingCrawler
4241hunk ./src/allmydata/storage/server.py 16
4242-
4243-# storage/
4244-# storage/shares/incoming
4245-#   incoming/ holds temp dirs named $START/$STORAGEINDEX/$SHARENUM which will
4246-#   be moved to storage/shares/$START/$STORAGEINDEX/$SHARENUM upon success
4247-# storage/shares/$START/$STORAGEINDEX
4248-# storage/shares/$START/$STORAGEINDEX/$SHARENUM
4249-
4250-# Where "$START" denotes the first 10 bits worth of $STORAGEINDEX (that's 2
4251-# base-32 chars).
4252-
4253-# $SHARENUM matches this regex:
4254-NUM_RE=re.compile("^[0-9]+$")
4255-
4256+from allmydata.storage.crawler import BucketCountingCrawler
4257 
4258 
4259 class StorageServer(service.MultiService, Referenceable):
4260hunk ./src/allmydata/storage/server.py 21
4261     implements(RIStorageServer, IStatsProducer)
4262+
4263     name = 'storage'
4264     LeaseCheckerClass = LeaseCheckingCrawler
4265hunk ./src/allmydata/storage/server.py 24
4266+    DEFAULT_EXPIRATION_POLICY = {
4267+        'enabled': False,
4268+        'mode': 'age',
4269+        'override_lease_duration': None,
4270+        'cutoff_date': None,
4271+        'sharetypes': ('mutable', 'immutable'),
4272+    }
4273 
4274hunk ./src/allmydata/storage/server.py 32
4275-    def __init__(self, storedir, nodeid, reserved_space=0,
4276-                 discard_storage=False, readonly_storage=False,
4277+    def __init__(self, serverid, backend, statedir,
4278                  stats_provider=None,
4279hunk ./src/allmydata/storage/server.py 34
4280-                 expiration_enabled=False,
4281-                 expiration_mode="age",
4282-                 expiration_override_lease_duration=None,
4283-                 expiration_cutoff_date=None,
4284-                 expiration_sharetypes=("mutable", "immutable")):
4285+                 expiration_policy=None):
4286         service.MultiService.__init__(self)
4287hunk ./src/allmydata/storage/server.py 36
4288-        assert isinstance(nodeid, str)
4289-        assert len(nodeid) == 20
4290-        self.my_nodeid = nodeid
4291-        self.storedir = storedir
4292-        sharedir = os.path.join(storedir, "shares")
4293-        fileutil.make_dirs(sharedir)
4294-        self.sharedir = sharedir
4295-        # we don't actually create the corruption-advisory dir until necessary
4296-        self.corruption_advisory_dir = os.path.join(storedir,
4297-                                                    "corruption-advisories")
4298-        self.reserved_space = int(reserved_space)
4299-        self.no_storage = discard_storage
4300-        self.readonly_storage = readonly_storage
4301+        precondition(IStorageBackend.providedBy(backend), backend)
4302+        precondition(isinstance(serverid, str), serverid)
4303+        precondition(len(serverid) == 20, serverid)
4304+
4305+        self._serverid = serverid
4306         self.stats_provider = stats_provider
4307         if self.stats_provider:
4308             self.stats_provider.register_producer(self)
4309hunk ./src/allmydata/storage/server.py 44
4310-        self.incomingdir = os.path.join(sharedir, 'incoming')
4311-        self._clean_incomplete()
4312-        fileutil.make_dirs(self.incomingdir)
4313         self._active_writers = weakref.WeakKeyDictionary()
4314hunk ./src/allmydata/storage/server.py 45
4315+        self.backend = backend
4316+        self.backend.setServiceParent(self)
4317+        self._statedir = statedir
4318         log.msg("StorageServer created", facility="tahoe.storage")
4319 
4320hunk ./src/allmydata/storage/server.py 50
4321-        if reserved_space:
4322-            if self.get_available_space() is None:
4323-                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",
4324-                        umin="0wZ27w", level=log.UNUSUAL)
4325-
4326         self.latencies = {"allocate": [], # immutable
4327                           "write": [],
4328                           "close": [],
4329hunk ./src/allmydata/storage/server.py 61
4330                           "renew": [],
4331                           "cancel": [],
4332                           }
4333-        self.add_bucket_counter()
4334-
4335-        statefile = os.path.join(self.storedir, "lease_checker.state")
4336-        historyfile = os.path.join(self.storedir, "lease_checker.history")
4337-        klass = self.LeaseCheckerClass
4338-        self.lease_checker = klass(self, statefile, historyfile,
4339-                                   expiration_enabled, expiration_mode,
4340-                                   expiration_override_lease_duration,
4341-                                   expiration_cutoff_date,
4342-                                   expiration_sharetypes)
4343-        self.lease_checker.setServiceParent(self)
4344+        self._setup_bucket_counter()
4345+        self._setup_lease_checker(expiration_policy or self.DEFAULT_EXPIRATION_POLICY)
4346 
4347     def __repr__(self):
4348hunk ./src/allmydata/storage/server.py 65
4349-        return "<StorageServer %s>" % (idlib.shortnodeid_b2a(self.my_nodeid),)
4350+        return "<StorageServer %s>" % (idlib.shortnodeid_b2a(self._serverid),)
4351 
4352hunk ./src/allmydata/storage/server.py 67
4353-    def add_bucket_counter(self):
4354-        statefile = os.path.join(self.storedir, "bucket_counter.state")
4355-        self.bucket_counter = BucketCountingCrawler(self, statefile)
4356+    def _setup_bucket_counter(self):
4357+        statefp = self._statedir.child("bucket_counter.state")
4358+        self.bucket_counter = BucketCountingCrawler(self.backend, statefp)
4359         self.bucket_counter.setServiceParent(self)
4360 
4361hunk ./src/allmydata/storage/server.py 72
4362+    def _setup_lease_checker(self, expiration_policy):
4363+        statefp = self._statedir.child("lease_checker.state")
4364+        historyfp = self._statedir.child("lease_checker.history")
4365+        self.lease_checker = self.LeaseCheckerClass(self.backend, statefp, historyfp, expiration_policy)
4366+        self.lease_checker.setServiceParent(self)
4367+
4368     def count(self, name, delta=1):
4369         if self.stats_provider:
4370             self.stats_provider.count("storage_server." + name, delta)
4371hunk ./src/allmydata/storage/server.py 92
4372         """Return a dict, indexed by category, that contains a dict of
4373         latency numbers for each category. If there are sufficient samples
4374         for unambiguous interpretation, each dict will contain the
4375-        following keys: mean, 01_0_percentile, 10_0_percentile,
4376+        following keys: samplesize, mean, 01_0_percentile, 10_0_percentile,
4377         50_0_percentile (median), 90_0_percentile, 95_0_percentile,
4378         99_0_percentile, 99_9_percentile.  If there are insufficient
4379         samples for a given percentile to be interpreted unambiguously
4380hunk ./src/allmydata/storage/server.py 114
4381             else:
4382                 stats["mean"] = None
4383 
4384-            orderstatlist = [(0.01, "01_0_percentile", 100), (0.1, "10_0_percentile", 10),\
4385-                             (0.50, "50_0_percentile", 10), (0.90, "90_0_percentile", 10),\
4386-                             (0.95, "95_0_percentile", 20), (0.99, "99_0_percentile", 100),\
4387+            orderstatlist = [(0.1, "10_0_percentile", 10), (0.5, "50_0_percentile", 10), \
4388+                             (0.9, "90_0_percentile", 10), (0.95, "95_0_percentile", 20), \
4389+                             (0.01, "01_0_percentile", 100),  (0.99, "99_0_percentile", 100),\
4390                              (0.999, "99_9_percentile", 1000)]
4391 
4392             for percentile, percentilestring, minnumtoobserve in orderstatlist:
4393hunk ./src/allmydata/storage/server.py 133
4394             kwargs["facility"] = "tahoe.storage"
4395         return log.msg(*args, **kwargs)
4396 
4397-    def _clean_incomplete(self):
4398-        fileutil.rm_dir(self.incomingdir)
4399+    def get_serverid(self):
4400+        return self._serverid
4401 
4402     def get_stats(self):
4403         # remember: RIStatsProvider requires that our return dict
4404hunk ./src/allmydata/storage/server.py 138
4405-        # contains numeric values.
4406+        # contains numeric, or None values.
4407         stats = { 'storage_server.allocated': self.allocated_size(), }
4408hunk ./src/allmydata/storage/server.py 140
4409-        stats['storage_server.reserved_space'] = self.reserved_space
4410         for category,ld in self.get_latencies().items():
4411             for name,v in ld.items():
4412                 stats['storage_server.latencies.%s.%s' % (category, name)] = v
4413hunk ./src/allmydata/storage/server.py 144
4414 
4415-        try:
4416-            disk = fileutil.get_disk_stats(self.sharedir, self.reserved_space)
4417-            writeable = disk['avail'] > 0
4418-
4419-            # spacetime predictors should use disk_avail / (d(disk_used)/dt)
4420-            stats['storage_server.disk_total'] = disk['total']
4421-            stats['storage_server.disk_used'] = disk['used']
4422-            stats['storage_server.disk_free_for_root'] = disk['free_for_root']
4423-            stats['storage_server.disk_free_for_nonroot'] = disk['free_for_nonroot']
4424-            stats['storage_server.disk_avail'] = disk['avail']
4425-        except AttributeError:
4426-            writeable = True
4427-        except EnvironmentError:
4428-            log.msg("OS call to get disk statistics failed", level=log.UNUSUAL)
4429-            writeable = False
4430-
4431-        if self.readonly_storage:
4432-            stats['storage_server.disk_avail'] = 0
4433-            writeable = False
4434+        self.backend.fill_in_space_stats(stats)
4435 
4436hunk ./src/allmydata/storage/server.py 146
4437-        stats['storage_server.accepting_immutable_shares'] = int(writeable)
4438         s = self.bucket_counter.get_state()
4439         bucket_count = s.get("last-complete-bucket-count")
4440         if bucket_count:
4441hunk ./src/allmydata/storage/server.py 153
4442         return stats
4443 
4444     def get_available_space(self):
4445-        """Returns available space for share storage in bytes, or None if no
4446-        API to get this information is available."""
4447-
4448-        if self.readonly_storage:
4449-            return 0
4450-        return fileutil.get_available_space(self.sharedir, self.reserved_space)
4451+        return self.backend.get_available_space()
4452 
4453     def allocated_size(self):
4454         space = 0
4455hunk ./src/allmydata/storage/server.py 162
4456         return space
4457 
4458     def remote_get_version(self):
4459-        remaining_space = self.get_available_space()
4460+        remaining_space = self.backend.get_available_space()
4461         if remaining_space is None:
4462             # We're on a platform that has no API to get disk stats.
4463             remaining_space = 2**64
4464hunk ./src/allmydata/storage/server.py 178
4465                     }
4466         return version
4467 
4468-    def remote_allocate_buckets(self, storage_index,
4469+    def remote_allocate_buckets(self, storageindex,
4470                                 renew_secret, cancel_secret,
4471                                 sharenums, allocated_size,
4472                                 canary, owner_num=0):
4473hunk ./src/allmydata/storage/server.py 182
4474+        # cancel_secret is no longer used.
4475         # owner_num is not for clients to set, but rather it should be
4476hunk ./src/allmydata/storage/server.py 184
4477-        # curried into the PersonalStorageServer instance that is dedicated
4478-        # to a particular owner.
4479+        # curried into a StorageServer instance dedicated to a particular
4480+        # owner.
4481         start = time.time()
4482         self.count("allocate")
4483hunk ./src/allmydata/storage/server.py 188
4484-        alreadygot = set()
4485         bucketwriters = {} # k: shnum, v: BucketWriter
4486hunk ./src/allmydata/storage/server.py 189
4487-        si_dir = storage_index_to_dir(storage_index)
4488-        si_s = si_b2a(storage_index)
4489 
4490hunk ./src/allmydata/storage/server.py 190
4491+        si_s = si_b2a(storageindex)
4492         log.msg("storage: allocate_buckets %s" % si_s)
4493 
4494hunk ./src/allmydata/storage/server.py 193
4495-        # in this implementation, the lease information (including secrets)
4496-        # goes into the share files themselves. It could also be put into a
4497-        # separate database. Note that the lease should not be added until
4498-        # the BucketWriter has been closed.
4499+        # Note that the lease should not be added until the BucketWriter
4500+        # has been closed.
4501         expire_time = time.time() + 31*24*60*60
4502hunk ./src/allmydata/storage/server.py 196
4503-        lease_info = LeaseInfo(owner_num,
4504-                               renew_secret, cancel_secret,
4505-                               expire_time, self.my_nodeid)
4506+        lease_info = LeaseInfo(owner_num, renew_secret,
4507+                               expire_time, self._serverid)
4508 
4509         max_space_per_bucket = allocated_size
4510 
4511hunk ./src/allmydata/storage/server.py 201
4512-        remaining_space = self.get_available_space()
4513+        remaining_space = self.backend.get_available_space()
4514         limited = remaining_space is not None
4515         if limited:
4516hunk ./src/allmydata/storage/server.py 204
4517-            # this is a bit conservative, since some of this allocated_size()
4518-            # has already been written to disk, where it will show up in
4519+            # This is a bit conservative, since some of this allocated_size()
4520+            # has already been written to the backend, where it will show up in
4521             # get_available_space.
4522             remaining_space -= self.allocated_size()
4523hunk ./src/allmydata/storage/server.py 208
4524-        # self.readonly_storage causes remaining_space <= 0
4525+            # If the backend is read-only, remaining_space will be <= 0.
4526+
4527+        shareset = self.backend.get_shareset(storageindex)
4528 
4529hunk ./src/allmydata/storage/server.py 212
4530-        # fill alreadygot with all shares that we have, not just the ones
4531+        # Fill alreadygot with all shares that we have, not just the ones
4532         # they asked about: this will save them a lot of work. Add or update
4533         # leases for all of them: if they want us to hold shares for this
4534hunk ./src/allmydata/storage/server.py 215
4535-        # file, they'll want us to hold leases for this file.
4536-        for (shnum, fn) in self._get_bucket_shares(storage_index):
4537-            alreadygot.add(shnum)
4538-            sf = ShareFile(fn)
4539-            sf.add_or_renew_lease(lease_info)
4540+        # file, they'll want us to hold leases for all the shares of it.
4541+        #
4542+        # XXX should we be making the assumption here that lease info is
4543+        # duplicated in all shares?
4544+        alreadygot = set()
4545+        for share in shareset.get_shares():
4546+            share.add_or_renew_lease(lease_info)
4547+            alreadygot.add(share.shnum)
4548 
4549hunk ./src/allmydata/storage/server.py 224
4550-        for shnum in sharenums:
4551-            incominghome = os.path.join(self.incomingdir, si_dir, "%d" % shnum)
4552-            finalhome = os.path.join(self.sharedir, si_dir, "%d" % shnum)
4553-            if os.path.exists(finalhome):
4554-                # great! we already have it. easy.
4555-                pass
4556-            elif os.path.exists(incominghome):
4557+        for shnum in sharenums - alreadygot:
4558+            if shareset.has_incoming(shnum):
4559                 # Note that we don't create BucketWriters for shnums that
4560                 # have a partial share (in incoming/), so if a second upload
4561                 # occurs while the first is still in progress, the second
4562hunk ./src/allmydata/storage/server.py 232
4563                 # uploader will use different storage servers.
4564                 pass
4565             elif (not limited) or (remaining_space >= max_space_per_bucket):
4566-                # ok! we need to create the new share file.
4567-                bw = BucketWriter(self, incominghome, finalhome,
4568-                                  max_space_per_bucket, lease_info, canary)
4569-                if self.no_storage:
4570-                    bw.throw_out_all_data = True
4571+                bw = shareset.make_bucket_writer(self, shnum, max_space_per_bucket,
4572+                                                 lease_info, canary)
4573                 bucketwriters[shnum] = bw
4574                 self._active_writers[bw] = 1
4575                 if limited:
4576hunk ./src/allmydata/storage/server.py 239
4577                     remaining_space -= max_space_per_bucket
4578             else:
4579-                # bummer! not enough space to accept this bucket
4580+                # Bummer not enough space to accept this share.
4581                 pass
4582 
4583hunk ./src/allmydata/storage/server.py 242
4584-        if bucketwriters:
4585-            fileutil.make_dirs(os.path.join(self.sharedir, si_dir))
4586-
4587         self.add_latency("allocate", time.time() - start)
4588         return alreadygot, bucketwriters
4589 
4590hunk ./src/allmydata/storage/server.py 245
4591-    def _iter_share_files(self, storage_index):
4592-        for shnum, filename in self._get_bucket_shares(storage_index):
4593-            f = open(filename, 'rb')
4594-            header = f.read(32)
4595-            f.close()
4596-            if header[:32] == MutableShareFile.MAGIC:
4597-                sf = MutableShareFile(filename, self)
4598-                # note: if the share has been migrated, the renew_lease()
4599-                # call will throw an exception, with information to help the
4600-                # client update the lease.
4601-            elif header[:4] == struct.pack(">L", 1):
4602-                sf = ShareFile(filename)
4603-            else:
4604-                continue # non-sharefile
4605-            yield sf
4606-
4607-    def remote_add_lease(self, storage_index, renew_secret, cancel_secret,
4608+    def remote_add_lease(self, storageindex, renew_secret, cancel_secret,
4609                          owner_num=1):
4610hunk ./src/allmydata/storage/server.py 247
4611+        # cancel_secret is no longer used.
4612         start = time.time()
4613         self.count("add-lease")
4614         new_expire_time = time.time() + 31*24*60*60
4615hunk ./src/allmydata/storage/server.py 251
4616-        lease_info = LeaseInfo(owner_num,
4617-                               renew_secret, cancel_secret,
4618-                               new_expire_time, self.my_nodeid)
4619-        for sf in self._iter_share_files(storage_index):
4620-            sf.add_or_renew_lease(lease_info)
4621-        self.add_latency("add-lease", time.time() - start)
4622-        return None
4623+        lease_info = LeaseInfo(owner_num, renew_secret,
4624+                               new_expire_time, self._serverid)
4625 
4626hunk ./src/allmydata/storage/server.py 254
4627-    def remote_renew_lease(self, storage_index, renew_secret):
4628+        try:
4629+            self.backend.add_or_renew_lease(lease_info)
4630+        finally:
4631+            self.add_latency("add-lease", time.time() - start)
4632+
4633+    def remote_renew_lease(self, storageindex, renew_secret):
4634         start = time.time()
4635         self.count("renew")
4636hunk ./src/allmydata/storage/server.py 262
4637-        new_expire_time = time.time() + 31*24*60*60
4638-        found_buckets = False
4639-        for sf in self._iter_share_files(storage_index):
4640-            found_buckets = True
4641-            sf.renew_lease(renew_secret, new_expire_time)
4642-        self.add_latency("renew", time.time() - start)
4643-        if not found_buckets:
4644-            raise IndexError("no such lease to renew")
4645+
4646+        try:
4647+            shareset = self.backend.get_shareset(storageindex)
4648+            new_expiration_time = start + 31*24*60*60   # one month from now
4649+            shareset.renew_lease(renew_secret, new_expiration_time)
4650+        finally:
4651+            self.add_latency("renew", time.time() - start)
4652 
4653     def bucket_writer_closed(self, bw, consumed_size):
4654         if self.stats_provider:
4655hunk ./src/allmydata/storage/server.py 275
4656             self.stats_provider.count('storage_server.bytes_added', consumed_size)
4657         del self._active_writers[bw]
4658 
4659-    def _get_bucket_shares(self, storage_index):
4660-        """Return a list of (shnum, pathname) tuples for files that hold
4661-        shares for this storage_index. In each tuple, 'shnum' will always be
4662-        the integer form of the last component of 'pathname'."""
4663-        storagedir = os.path.join(self.sharedir, storage_index_to_dir(storage_index))
4664-        try:
4665-            for f in os.listdir(storagedir):
4666-                if NUM_RE.match(f):
4667-                    filename = os.path.join(storagedir, f)
4668-                    yield (int(f), filename)
4669-        except OSError:
4670-            # Commonly caused by there being no buckets at all.
4671-            pass
4672-
4673-    def remote_get_buckets(self, storage_index):
4674+    def remote_get_buckets(self, storageindex):
4675         start = time.time()
4676         self.count("get")
4677hunk ./src/allmydata/storage/server.py 278
4678-        si_s = si_b2a(storage_index)
4679+        si_s = si_b2a(storageindex)
4680         log.msg("storage: get_buckets %s" % si_s)
4681         bucketreaders = {} # k: sharenum, v: BucketReader
4682hunk ./src/allmydata/storage/server.py 281
4683-        for shnum, filename in self._get_bucket_shares(storage_index):
4684-            bucketreaders[shnum] = BucketReader(self, filename,
4685-                                                storage_index, shnum)
4686-        self.add_latency("get", time.time() - start)
4687-        return bucketreaders
4688 
4689hunk ./src/allmydata/storage/server.py 282
4690-    def get_leases(self, storage_index):
4691-        """Provide an iterator that yields all of the leases attached to this
4692-        bucket. Each lease is returned as a LeaseInfo instance.
4693+        try:
4694+            shareset = self.backend.get_shareset(storageindex)
4695+            for share in shareset.get_shares():
4696+                bucketreaders[share.get_shnum()] = shareset.make_bucket_reader(self, share)
4697+            return bucketreaders
4698+        finally:
4699+            self.add_latency("get", time.time() - start)
4700 
4701hunk ./src/allmydata/storage/server.py 290
4702-        This method is not for client use.
4703+    def get_leases(self, storageindex):
4704         """
4705hunk ./src/allmydata/storage/server.py 292
4706+        Provide an iterator that yields all of the leases attached to this
4707+        bucket. Each lease is returned as a LeaseInfo instance.
4708 
4709hunk ./src/allmydata/storage/server.py 295
4710-        # since all shares get the same lease data, we just grab the leases
4711-        # from the first share
4712-        try:
4713-            shnum, filename = self._get_bucket_shares(storage_index).next()
4714-            sf = ShareFile(filename)
4715-            return sf.get_leases()
4716-        except StopIteration:
4717-            return iter([])
4718+        This method is not for client use. XXX do we need it at all?
4719+        """
4720+        return self.backend.get_shareset(storageindex).get_leases()
4721 
4722hunk ./src/allmydata/storage/server.py 299
4723-    def remote_slot_testv_and_readv_and_writev(self, storage_index,
4724+    def remote_slot_testv_and_readv_and_writev(self, storageindex,
4725                                                secrets,
4726                                                test_and_write_vectors,
4727                                                read_vector):
4728hunk ./src/allmydata/storage/server.py 305
4729         start = time.time()
4730         self.count("writev")
4731-        si_s = si_b2a(storage_index)
4732+        si_s = si_b2a(storageindex)
4733         log.msg("storage: slot_writev %s" % si_s)
4734hunk ./src/allmydata/storage/server.py 307
4735-        si_dir = storage_index_to_dir(storage_index)
4736-        (write_enabler, renew_secret, cancel_secret) = secrets
4737-        # shares exist if there is a file for them
4738-        bucketdir = os.path.join(self.sharedir, si_dir)
4739-        shares = {}
4740-        if os.path.isdir(bucketdir):
4741-            for sharenum_s in os.listdir(bucketdir):
4742-                try:
4743-                    sharenum = int(sharenum_s)
4744-                except ValueError:
4745-                    continue
4746-                filename = os.path.join(bucketdir, sharenum_s)
4747-                msf = MutableShareFile(filename, self)
4748-                msf.check_write_enabler(write_enabler, si_s)
4749-                shares[sharenum] = msf
4750-        # write_enabler is good for all existing shares.
4751-
4752-        # Now evaluate test vectors.
4753-        testv_is_good = True
4754-        for sharenum in test_and_write_vectors:
4755-            (testv, datav, new_length) = test_and_write_vectors[sharenum]
4756-            if sharenum in shares:
4757-                if not shares[sharenum].check_testv(testv):
4758-                    self.log("testv failed: [%d]: %r" % (sharenum, testv))
4759-                    testv_is_good = False
4760-                    break
4761-            else:
4762-                # compare the vectors against an empty share, in which all
4763-                # reads return empty strings.
4764-                if not EmptyShare().check_testv(testv):
4765-                    self.log("testv failed (empty): [%d] %r" % (sharenum,
4766-                                                                testv))
4767-                    testv_is_good = False
4768-                    break
4769-
4770-        # now gather the read vectors, before we do any writes
4771-        read_data = {}
4772-        for sharenum, share in shares.items():
4773-            read_data[sharenum] = share.readv(read_vector)
4774-
4775-        ownerid = 1 # TODO
4776-        expire_time = time.time() + 31*24*60*60   # one month
4777-        lease_info = LeaseInfo(ownerid,
4778-                               renew_secret, cancel_secret,
4779-                               expire_time, self.my_nodeid)
4780-
4781-        if testv_is_good:
4782-            # now apply the write vectors
4783-            for sharenum in test_and_write_vectors:
4784-                (testv, datav, new_length) = test_and_write_vectors[sharenum]
4785-                if new_length == 0:
4786-                    if sharenum in shares:
4787-                        shares[sharenum].unlink()
4788-                else:
4789-                    if sharenum not in shares:
4790-                        # allocate a new share
4791-                        allocated_size = 2000 # arbitrary, really
4792-                        share = self._allocate_slot_share(bucketdir, secrets,
4793-                                                          sharenum,
4794-                                                          allocated_size,
4795-                                                          owner_num=0)
4796-                        shares[sharenum] = share
4797-                    shares[sharenum].writev(datav, new_length)
4798-                    # and update the lease
4799-                    shares[sharenum].add_or_renew_lease(lease_info)
4800-
4801-            if new_length == 0:
4802-                # delete empty bucket directories
4803-                if not os.listdir(bucketdir):
4804-                    os.rmdir(bucketdir)
4805 
4806hunk ./src/allmydata/storage/server.py 308
4807+        try:
4808+            shareset = self.backend.get_shareset(storageindex)
4809+            expiration_time = start + 31*24*60*60   # one month from now
4810+            return shareset.testv_and_readv_and_writev(self, secrets, test_and_write_vectors,
4811+                                                       read_vector, expiration_time)
4812+        finally:
4813+            self.add_latency("writev", time.time() - start)
4814 
4815hunk ./src/allmydata/storage/server.py 316
4816-        # all done
4817-        self.add_latency("writev", time.time() - start)
4818-        return (testv_is_good, read_data)
4819-
4820-    def _allocate_slot_share(self, bucketdir, secrets, sharenum,
4821-                             allocated_size, owner_num=0):
4822-        (write_enabler, renew_secret, cancel_secret) = secrets
4823-        my_nodeid = self.my_nodeid
4824-        fileutil.make_dirs(bucketdir)
4825-        filename = os.path.join(bucketdir, "%d" % sharenum)
4826-        share = create_mutable_sharefile(filename, my_nodeid, write_enabler,
4827-                                         self)
4828-        return share
4829-
4830-    def remote_slot_readv(self, storage_index, shares, readv):
4831+    def remote_slot_readv(self, storageindex, shares, readv):
4832         start = time.time()
4833         self.count("readv")
4834hunk ./src/allmydata/storage/server.py 319
4835-        si_s = si_b2a(storage_index)
4836-        lp = log.msg("storage: slot_readv %s %s" % (si_s, shares),
4837-                     facility="tahoe.storage", level=log.OPERATIONAL)
4838-        si_dir = storage_index_to_dir(storage_index)
4839-        # shares exist if there is a file for them
4840-        bucketdir = os.path.join(self.sharedir, si_dir)
4841-        if not os.path.isdir(bucketdir):
4842+        si_s = si_b2a(storageindex)
4843+        log.msg("storage: slot_readv %s %s" % (si_s, shares),
4844+                facility="tahoe.storage", level=log.OPERATIONAL)
4845+
4846+        try:
4847+            shareset = self.backend.get_shareset(storageindex)
4848+            return shareset.readv(self, shares, readv)
4849+        finally:
4850             self.add_latency("readv", time.time() - start)
4851hunk ./src/allmydata/storage/server.py 328
4852-            return {}
4853-        datavs = {}
4854-        for sharenum_s in os.listdir(bucketdir):
4855-            try:
4856-                sharenum = int(sharenum_s)
4857-            except ValueError:
4858-                continue
4859-            if sharenum in shares or not shares:
4860-                filename = os.path.join(bucketdir, sharenum_s)
4861-                msf = MutableShareFile(filename, self)
4862-                datavs[sharenum] = msf.readv(readv)
4863-        log.msg("returning shares %s" % (datavs.keys(),),
4864-                facility="tahoe.storage", level=log.NOISY, parent=lp)
4865-        self.add_latency("readv", time.time() - start)
4866-        return datavs
4867 
4868hunk ./src/allmydata/storage/server.py 329
4869-    def remote_advise_corrupt_share(self, share_type, storage_index, shnum,
4870-                                    reason):
4871-        fileutil.make_dirs(self.corruption_advisory_dir)
4872-        now = time_format.iso_utc(sep="T")
4873-        si_s = si_b2a(storage_index)
4874-        # windows can't handle colons in the filename
4875-        fn = os.path.join(self.corruption_advisory_dir,
4876-                          "%s--%s-%d" % (now, si_s, shnum)).replace(":","")
4877-        f = open(fn, "w")
4878-        f.write("report: Share Corruption\n")
4879-        f.write("type: %s\n" % share_type)
4880-        f.write("storage_index: %s\n" % si_s)
4881-        f.write("share_number: %d\n" % shnum)
4882-        f.write("\n")
4883-        f.write(reason)
4884-        f.write("\n")
4885-        f.close()
4886-        log.msg(format=("client claims corruption in (%(share_type)s) " +
4887-                        "%(si)s-%(shnum)d: %(reason)s"),
4888-                share_type=share_type, si=si_s, shnum=shnum, reason=reason,
4889-                level=log.SCARY, umid="SGx2fA")
4890-        return None
4891+    def remote_advise_corrupt_share(self, share_type, storage_index, shnum, reason):
4892+        self.backend.advise_corrupt_share(share_type, storage_index, shnum, reason)
4893hunk ./src/allmydata/test/common.py 20
4894 from allmydata.mutable.common import CorruptShareError
4895 from allmydata.mutable.layout import unpack_header
4896 from allmydata.mutable.publish import MutableData
4897-from allmydata.storage.mutable import MutableShareFile
4898+from allmydata.storage.backends.disk.mutable import MutableDiskShare
4899 from allmydata.util import hashutil, log, fileutil, pollmixin
4900 from allmydata.util.assertutil import precondition
4901 from allmydata.util.consumer import download_to_data
4902hunk ./src/allmydata/test/common.py 1297
4903 
4904 def _corrupt_mutable_share_data(data, debug=False):
4905     prefix = data[:32]
4906-    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)
4907-    data_offset = MutableShareFile.DATA_OFFSET
4908+    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)
4909+    data_offset = MutableDiskShare.DATA_OFFSET
4910     sharetype = data[data_offset:data_offset+1]
4911     assert sharetype == "\x00", "non-SDMF mutable shares not supported"
4912     (version, ig_seqnum, ig_roothash, ig_IV, ig_k, ig_N, ig_segsize,
4913hunk ./src/allmydata/test/no_network.py 21
4914 from twisted.application import service
4915 from twisted.internet import defer, reactor
4916 from twisted.python.failure import Failure
4917+from twisted.python.filepath import FilePath
4918 from foolscap.api import Referenceable, fireEventually, RemoteException
4919 from base64 import b32encode
4920hunk ./src/allmydata/test/no_network.py 24
4921+
4922 from allmydata import uri as tahoe_uri
4923 from allmydata.client import Client
4924hunk ./src/allmydata/test/no_network.py 27
4925-from allmydata.storage.server import StorageServer, storage_index_to_dir
4926+from allmydata.storage.server import StorageServer
4927+from allmydata.storage.backends.disk.disk_backend import DiskBackend
4928 from allmydata.util import fileutil, idlib, hashutil
4929 from allmydata.util.hashutil import sha1
4930 from allmydata.test.common_web import HTTPClientGETFactory
4931hunk ./src/allmydata/test/no_network.py 155
4932             seed = server.get_permutation_seed()
4933             return sha1(peer_selection_index + seed).digest()
4934         return sorted(self.get_connected_servers(), key=_permuted)
4935+
4936     def get_connected_servers(self):
4937         return self.client._servers
4938hunk ./src/allmydata/test/no_network.py 158
4939+
4940     def get_nickname_for_serverid(self, serverid):
4941         return None
4942 
4943hunk ./src/allmydata/test/no_network.py 162
4944+    def get_known_servers(self):
4945+        return self.get_connected_servers()
4946+
4947+    def get_all_serverids(self):
4948+        return self.client.get_all_serverids()
4949+
4950+
4951 class NoNetworkClient(Client):
4952     def create_tub(self):
4953         pass
4954hunk ./src/allmydata/test/no_network.py 262
4955 
4956     def make_server(self, i, readonly=False):
4957         serverid = hashutil.tagged_hash("serverid", str(i))[:20]
4958-        serverdir = os.path.join(self.basedir, "servers",
4959-                                 idlib.shortnodeid_b2a(serverid), "storage")
4960-        fileutil.make_dirs(serverdir)
4961-        ss = StorageServer(serverdir, serverid, stats_provider=SimpleStats(),
4962-                           readonly_storage=readonly)
4963+        storagedir = FilePath(self.basedir).child("servers").child(idlib.shortnodeid_b2a(serverid)).child("storage")
4964+
4965+        # The backend will make the storage directory and any necessary parents.
4966+        backend = DiskBackend(storagedir, readonly=readonly)
4967+        ss = StorageServer(serverid, backend, storagedir, stats_provider=SimpleStats())
4968         ss._no_network_server_number = i
4969         return ss
4970 
4971hunk ./src/allmydata/test/no_network.py 276
4972         middleman = service.MultiService()
4973         middleman.setServiceParent(self)
4974         ss.setServiceParent(middleman)
4975-        serverid = ss.my_nodeid
4976+        serverid = ss.get_serverid()
4977         self.servers_by_number[i] = ss
4978         wrapper = wrap_storage_server(ss)
4979         self.wrappers_by_id[serverid] = wrapper
4980hunk ./src/allmydata/test/no_network.py 295
4981         # it's enough to remove the server from c._servers (we don't actually
4982         # have to detach and stopService it)
4983         for i,ss in self.servers_by_number.items():
4984-            if ss.my_nodeid == serverid:
4985+            if ss.get_serverid() == serverid:
4986                 del self.servers_by_number[i]
4987                 break
4988         del self.wrappers_by_id[serverid]
4989hunk ./src/allmydata/test/no_network.py 345
4990     def get_clientdir(self, i=0):
4991         return self.g.clients[i].basedir
4992 
4993+    def get_server(self, i):
4994+        return self.g.servers_by_number[i]
4995+
4996     def get_serverdir(self, i):
4997hunk ./src/allmydata/test/no_network.py 349
4998-        return self.g.servers_by_number[i].storedir
4999+        return self.g.servers_by_number[i].backend.storedir
5000+
5001+    def remove_server(self, i):
5002+        self.g.remove_server(self.g.servers_by_number[i].get_serverid())
5003 
5004     def iterate_servers(self):
5005         for i in sorted(self.g.servers_by_number.keys()):
5006hunk ./src/allmydata/test/no_network.py 357
5007             ss = self.g.servers_by_number[i]
5008-            yield (i, ss, ss.storedir)
5009+            yield (i, ss, ss.backend.storedir)
5010 
5011     def find_uri_shares(self, uri):
5012         si = tahoe_uri.from_string(uri).get_storage_index()
5013hunk ./src/allmydata/test/no_network.py 361
5014-        prefixdir = storage_index_to_dir(si)
5015         shares = []
5016         for i,ss in self.g.servers_by_number.items():
5017hunk ./src/allmydata/test/no_network.py 363
5018-            serverid = ss.my_nodeid
5019-            basedir = os.path.join(ss.sharedir, prefixdir)
5020-            if not os.path.exists(basedir):
5021-                continue
5022-            for f in os.listdir(basedir):
5023-                try:
5024-                    shnum = int(f)
5025-                    shares.append((shnum, serverid, os.path.join(basedir, f)))
5026-                except ValueError:
5027-                    pass
5028+            for share in ss.backend.get_shareset(si).get_shares():
5029+                shares.append((share.get_shnum(), ss.get_serverid(), share._home))
5030         return sorted(shares)
5031 
5032hunk ./src/allmydata/test/no_network.py 367
5033+    def count_leases(self, uri):
5034+        """Return (filename, leasecount) pairs in arbitrary order."""
5035+        si = tahoe_uri.from_string(uri).get_storage_index()
5036+        lease_counts = []
5037+        for i,ss in self.g.servers_by_number.items():
5038+            for share in ss.backend.get_shareset(si).get_shares():
5039+                num_leases = len(list(share.get_leases()))
5040+                lease_counts.append( (share._home.path, num_leases) )
5041+        return lease_counts
5042+
5043     def copy_shares(self, uri):
5044         shares = {}
5045hunk ./src/allmydata/test/no_network.py 379
5046-        for (shnum, serverid, sharefile) in self.find_uri_shares(uri):
5047-            shares[sharefile] = open(sharefile, "rb").read()
5048+        for (shnum, serverid, sharefp) in self.find_uri_shares(uri):
5049+            shares[sharefp.path] = sharefp.getContent()
5050         return shares
5051 
5052hunk ./src/allmydata/test/no_network.py 383
5053+    def copy_share(self, from_share, uri, to_server):
5054+        si = uri.from_string(self.uri).get_storage_index()
5055+        (i_shnum, i_serverid, i_sharefp) = from_share
5056+        shares_dir = to_server.backend.get_shareset(si)._sharehomedir
5057+        i_sharefp.copyTo(shares_dir.child(str(i_shnum)))
5058+
5059     def restore_all_shares(self, shares):
5060hunk ./src/allmydata/test/no_network.py 390
5061-        for sharefile, data in shares.items():
5062-            open(sharefile, "wb").write(data)
5063+        for share, data in shares.items():
5064+            share.home.setContent(data)
5065 
5066hunk ./src/allmydata/test/no_network.py 393
5067-    def delete_share(self, (shnum, serverid, sharefile)):
5068-        os.unlink(sharefile)
5069+    def delete_share(self, (shnum, serverid, sharefp)):
5070+        sharefp.remove()
5071 
5072     def delete_shares_numbered(self, uri, shnums):
5073hunk ./src/allmydata/test/no_network.py 397
5074-        for (i_shnum, i_serverid, i_sharefile) in self.find_uri_shares(uri):
5075+        for (i_shnum, i_serverid, i_sharefp) in self.find_uri_shares(uri):
5076             if i_shnum in shnums:
5077hunk ./src/allmydata/test/no_network.py 399
5078-                os.unlink(i_sharefile)
5079+                i_sharefp.remove()
5080 
5081hunk ./src/allmydata/test/no_network.py 401
5082-    def corrupt_share(self, (shnum, serverid, sharefile), corruptor_function):
5083-        sharedata = open(sharefile, "rb").read()
5084-        corruptdata = corruptor_function(sharedata)
5085-        open(sharefile, "wb").write(corruptdata)
5086+    def corrupt_share(self, (shnum, serverid, sharefp), corruptor_function, debug=False):
5087+        sharedata = sharefp.getContent()
5088+        corruptdata = corruptor_function(sharedata, debug=debug)
5089+        sharefp.setContent(corruptdata)
5090 
5091     def corrupt_shares_numbered(self, uri, shnums, corruptor, debug=False):
5092hunk ./src/allmydata/test/no_network.py 407
5093-        for (i_shnum, i_serverid, i_sharefile) in self.find_uri_shares(uri):
5094+        for (i_shnum, i_serverid, i_sharefp) in self.find_uri_shares(uri):
5095             if i_shnum in shnums:
5096hunk ./src/allmydata/test/no_network.py 409
5097-                sharedata = open(i_sharefile, "rb").read()
5098-                corruptdata = corruptor(sharedata, debug=debug)
5099-                open(i_sharefile, "wb").write(corruptdata)
5100+                self.corrupt_share((i_shnum, i_serverid, i_sharefp), corruptor, debug=debug)
5101 
5102     def corrupt_all_shares(self, uri, corruptor, debug=False):
5103hunk ./src/allmydata/test/no_network.py 412
5104-        for (i_shnum, i_serverid, i_sharefile) in self.find_uri_shares(uri):
5105-            sharedata = open(i_sharefile, "rb").read()
5106-            corruptdata = corruptor(sharedata, debug=debug)
5107-            open(i_sharefile, "wb").write(corruptdata)
5108+        for (i_shnum, i_serverid, i_sharefp) in self.find_uri_shares(uri):
5109+            self.corrupt_share((i_shnum, i_serverid, i_sharefp), corruptor, debug=debug)
5110 
5111     def GET(self, urlpath, followRedirect=False, return_response=False,
5112             method="GET", clientnum=0, **kwargs):
5113hunk ./src/allmydata/test/test_download.py 6
5114 # a previous run. This asserts that the current code is capable of decoding
5115 # shares from a previous version.
5116 
5117-import os
5118 from twisted.trial import unittest
5119 from twisted.internet import defer, reactor
5120 from allmydata import uri
5121hunk ./src/allmydata/test/test_download.py 9
5122-from allmydata.storage.server import storage_index_to_dir
5123 from allmydata.util import base32, fileutil, spans, log, hashutil
5124 from allmydata.util.consumer import download_to_data, MemoryConsumer
5125 from allmydata.immutable import upload, layout
5126hunk ./src/allmydata/test/test_download.py 85
5127         u = upload.Data(plaintext, None)
5128         d = self.c0.upload(u)
5129         f = open("stored_shares.py", "w")
5130-        def _created_immutable(ur):
5131-            # write the generated shares and URI to a file, which can then be
5132-            # incorporated into this one next time.
5133-            f.write('immutable_uri = "%s"\n' % ur.uri)
5134-            f.write('immutable_shares = {\n')
5135-            si = uri.from_string(ur.uri).get_storage_index()
5136-            si_dir = storage_index_to_dir(si)
5137+
5138+        def _write_py(uri):
5139+            si = uri.from_string(uri).get_storage_index()
5140             for (i,ss,ssdir) in self.iterate_servers():
5141hunk ./src/allmydata/test/test_download.py 89
5142-                sharedir = os.path.join(ssdir, "shares", si_dir)
5143                 shares = {}
5144hunk ./src/allmydata/test/test_download.py 90
5145-                for fn in os.listdir(sharedir):
5146-                    shnum = int(fn)
5147-                    sharedata = open(os.path.join(sharedir, fn), "rb").read()
5148-                    shares[shnum] = sharedata
5149-                fileutil.rm_dir(sharedir)
5150+                shareset = ss.backend.get_shareset(si)
5151+                for share in shareset.get_shares():
5152+                    sharedata = share._home.getContent()
5153+                    shares[share.get_shnum()] = sharedata
5154+
5155+                fileutil.fp_remove(shareset._sharehomedir)
5156                 if shares:
5157                     f.write(' %d: { # client[%d]\n' % (i, i))
5158                     for shnum in sorted(shares.keys()):
5159hunk ./src/allmydata/test/test_download.py 103
5160                                 (shnum, base32.b2a(shares[shnum])))
5161                     f.write('    },\n')
5162             f.write('}\n')
5163-            f.write('\n')
5164 
5165hunk ./src/allmydata/test/test_download.py 104
5166+        def _created_immutable(ur):
5167+            # write the generated shares and URI to a file, which can then be
5168+            # incorporated into this one next time.
5169+            f.write('immutable_uri = "%s"\n' % ur.uri)
5170+            f.write('immutable_shares = {\n')
5171+            _write_py(ur.uri)
5172+            f.write('\n')
5173         d.addCallback(_created_immutable)
5174 
5175         d.addCallback(lambda ignored:
5176hunk ./src/allmydata/test/test_download.py 118
5177         def _created_mutable(n):
5178             f.write('mutable_uri = "%s"\n' % n.get_uri())
5179             f.write('mutable_shares = {\n')
5180-            si = uri.from_string(n.get_uri()).get_storage_index()
5181-            si_dir = storage_index_to_dir(si)
5182-            for (i,ss,ssdir) in self.iterate_servers():
5183-                sharedir = os.path.join(ssdir, "shares", si_dir)
5184-                shares = {}
5185-                for fn in os.listdir(sharedir):
5186-                    shnum = int(fn)
5187-                    sharedata = open(os.path.join(sharedir, fn), "rb").read()
5188-                    shares[shnum] = sharedata
5189-                fileutil.rm_dir(sharedir)
5190-                if shares:
5191-                    f.write(' %d: { # client[%d]\n' % (i, i))
5192-                    for shnum in sorted(shares.keys()):
5193-                        f.write('  %d: base32.a2b("%s"),\n' %
5194-                                (shnum, base32.b2a(shares[shnum])))
5195-                    f.write('    },\n')
5196-            f.write('}\n')
5197-
5198-            f.close()
5199+            _write_py(n.get_uri())
5200         d.addCallback(_created_mutable)
5201 
5202         def _done(ignored):
5203hunk ./src/allmydata/test/test_download.py 123
5204             f.close()
5205-        d.addCallback(_done)
5206+        d.addBoth(_done)
5207 
5208         return d
5209 
5210hunk ./src/allmydata/test/test_download.py 127
5211+    def _write_shares(self, uri, shares):
5212+        si = uri.from_string(uri).get_storage_index()
5213+        for i in shares:
5214+            shares_for_server = shares[i]
5215+            for shnum in shares_for_server:
5216+                share_dir = self.get_server(i).backend.get_shareset(si)._sharehomedir
5217+                fileutil.fp_make_dirs(share_dir)
5218+                share_dir.child(str(shnum)).setContent(shares[shnum])
5219+
5220     def load_shares(self, ignored=None):
5221         # this uses the data generated by create_shares() to populate the
5222         # storage servers with pre-generated shares
5223hunk ./src/allmydata/test/test_download.py 139
5224-        si = uri.from_string(immutable_uri).get_storage_index()
5225-        si_dir = storage_index_to_dir(si)
5226-        for i in immutable_shares:
5227-            shares = immutable_shares[i]
5228-            for shnum in shares:
5229-                dn = os.path.join(self.get_serverdir(i), "shares", si_dir)
5230-                fileutil.make_dirs(dn)
5231-                fn = os.path.join(dn, str(shnum))
5232-                f = open(fn, "wb")
5233-                f.write(shares[shnum])
5234-                f.close()
5235-
5236-        si = uri.from_string(mutable_uri).get_storage_index()
5237-        si_dir = storage_index_to_dir(si)
5238-        for i in mutable_shares:
5239-            shares = mutable_shares[i]
5240-            for shnum in shares:
5241-                dn = os.path.join(self.get_serverdir(i), "shares", si_dir)
5242-                fileutil.make_dirs(dn)
5243-                fn = os.path.join(dn, str(shnum))
5244-                f = open(fn, "wb")
5245-                f.write(shares[shnum])
5246-                f.close()
5247+        self._write_shares(immutable_uri, immutable_shares)
5248+        self._write_shares(mutable_uri, mutable_shares)
5249 
5250     def download_immutable(self, ignored=None):
5251         n = self.c0.create_node_from_uri(immutable_uri)
5252hunk ./src/allmydata/test/test_download.py 183
5253 
5254         self.load_shares()
5255         si = uri.from_string(immutable_uri).get_storage_index()
5256-        si_dir = storage_index_to_dir(si)
5257 
5258         n = self.c0.create_node_from_uri(immutable_uri)
5259         d = download_to_data(n)
5260hunk ./src/allmydata/test/test_download.py 198
5261                 for clientnum in immutable_shares:
5262                     for shnum in immutable_shares[clientnum]:
5263                         if s._shnum == shnum:
5264-                            fn = os.path.join(self.get_serverdir(clientnum),
5265-                                              "shares", si_dir, str(shnum))
5266-                            os.unlink(fn)
5267+                            share_dir = self.get_server(clientnum).backend.get_shareset(si)._sharehomedir
5268+                            share_dir.child(str(shnum)).remove()
5269         d.addCallback(_clobber_some_shares)
5270         d.addCallback(lambda ign: download_to_data(n))
5271         d.addCallback(_got_data)
5272hunk ./src/allmydata/test/test_download.py 212
5273                 for shnum in immutable_shares[clientnum]:
5274                     if shnum == save_me:
5275                         continue
5276-                    fn = os.path.join(self.get_serverdir(clientnum),
5277-                                      "shares", si_dir, str(shnum))
5278-                    if os.path.exists(fn):
5279-                        os.unlink(fn)
5280+                    share_dir = self.get_server(clientnum).backend.get_shareset(si)._sharehomedir
5281+                    fileutil.fp_remove(share_dir.child(str(shnum)))
5282             # now the download should fail with NotEnoughSharesError
5283             return self.shouldFail(NotEnoughSharesError, "1shares", None,
5284                                    download_to_data, n)
5285hunk ./src/allmydata/test/test_download.py 223
5286             # delete the last remaining share
5287             for clientnum in immutable_shares:
5288                 for shnum in immutable_shares[clientnum]:
5289-                    fn = os.path.join(self.get_serverdir(clientnum),
5290-                                      "shares", si_dir, str(shnum))
5291-                    if os.path.exists(fn):
5292-                        os.unlink(fn)
5293+                    share_dir = self.get_server(clientnum).backend.get_shareset(si)._sharehomedir
5294+                    share_dir.child(str(shnum)).remove()
5295             # now a new download should fail with NoSharesError. We want a
5296             # new ImmutableFileNode so it will forget about the old shares.
5297             # If we merely called create_node_from_uri() without first
5298hunk ./src/allmydata/test/test_download.py 801
5299         # will report two shares, and the ShareFinder will handle the
5300         # duplicate by attaching both to the same CommonShare instance.
5301         si = uri.from_string(immutable_uri).get_storage_index()
5302-        si_dir = storage_index_to_dir(si)
5303-        sh0_file = [sharefile
5304-                    for (shnum, serverid, sharefile)
5305-                    in self.find_uri_shares(immutable_uri)
5306-                    if shnum == 0][0]
5307-        sh0_data = open(sh0_file, "rb").read()
5308+        sh0_fp = [sharefp for (shnum, serverid, sharefp)
5309+                          in self.find_uri_shares(immutable_uri)
5310+                          if shnum == 0][0]
5311+        sh0_data = sh0_fp.getContent()
5312         for clientnum in immutable_shares:
5313             if 0 in immutable_shares[clientnum]:
5314                 continue
5315hunk ./src/allmydata/test/test_download.py 808
5316-            cdir = self.get_serverdir(clientnum)
5317-            target = os.path.join(cdir, "shares", si_dir, "0")
5318-            outf = open(target, "wb")
5319-            outf.write(sh0_data)
5320-            outf.close()
5321+            cdir = self.get_server(clientnum).backend.get_shareset(si)._sharehomedir
5322+            fileutil.fp_make_dirs(cdir)
5323+            cdir.child(str(shnum)).setContent(sh0_data)
5324 
5325         d = self.download_immutable()
5326         return d
5327hunk ./src/allmydata/test/test_encode.py 134
5328         d.addCallback(_try)
5329         return d
5330 
5331-    def get_share_hashes(self, at_least_these=()):
5332+    def get_share_hashes(self):
5333         d = self._start()
5334         def _try(unused=None):
5335             if self.mode == "bad sharehash":
5336hunk ./src/allmydata/test/test_hung_server.py 3
5337 # -*- coding: utf-8 -*-
5338 
5339-import os, shutil
5340 from twisted.trial import unittest
5341 from twisted.internet import defer
5342hunk ./src/allmydata/test/test_hung_server.py 5
5343-from allmydata import uri
5344+
5345 from allmydata.util.consumer import download_to_data
5346 from allmydata.immutable import upload
5347 from allmydata.mutable.common import UnrecoverableFileError
5348hunk ./src/allmydata/test/test_hung_server.py 10
5349 from allmydata.mutable.publish import MutableData
5350-from allmydata.storage.common import storage_index_to_dir
5351 from allmydata.test.no_network import GridTestMixin
5352 from allmydata.test.common import ShouldFailMixin
5353 from allmydata.util.pollmixin import PollMixin
5354hunk ./src/allmydata/test/test_hung_server.py 18
5355 immutable_plaintext = "data" * 10000
5356 mutable_plaintext = "muta" * 10000
5357 
5358+
5359 class HungServerDownloadTest(GridTestMixin, ShouldFailMixin, PollMixin,
5360                              unittest.TestCase):
5361     # Many of these tests take around 60 seconds on François's ARM buildslave:
5362hunk ./src/allmydata/test/test_hung_server.py 31
5363     timeout = 240
5364 
5365     def _break(self, servers):
5366-        for (id, ss) in servers:
5367-            self.g.break_server(id)
5368+        for ss in servers:
5369+            self.g.break_server(ss.get_serverid())
5370 
5371     def _hang(self, servers, **kwargs):
5372hunk ./src/allmydata/test/test_hung_server.py 35
5373-        for (id, ss) in servers:
5374-            self.g.hang_server(id, **kwargs)
5375+        for ss in servers:
5376+            self.g.hang_server(ss.get_serverid(), **kwargs)
5377 
5378     def _unhang(self, servers, **kwargs):
5379hunk ./src/allmydata/test/test_hung_server.py 39
5380-        for (id, ss) in servers:
5381-            self.g.unhang_server(id, **kwargs)
5382+        for ss in servers:
5383+            self.g.unhang_server(ss.get_serverid(), **kwargs)
5384 
5385     def _hang_shares(self, shnums, **kwargs):
5386         # hang all servers who are holding the given shares
5387hunk ./src/allmydata/test/test_hung_server.py 52
5388                     hung_serverids.add(i_serverid)
5389 
5390     def _delete_all_shares_from(self, servers):
5391-        serverids = [id for (id, ss) in servers]
5392-        for (i_shnum, i_serverid, i_sharefile) in self.shares:
5393+        serverids = [ss.get_serverid() for ss in servers]
5394+        for (i_shnum, i_serverid, i_sharefp) in self.shares:
5395             if i_serverid in serverids:
5396hunk ./src/allmydata/test/test_hung_server.py 55
5397-                os.unlink(i_sharefile)
5398+                i_sharefp.remove()
5399 
5400     def _corrupt_all_shares_in(self, servers, corruptor_func):
5401hunk ./src/allmydata/test/test_hung_server.py 58
5402-        serverids = [id for (id, ss) in servers]
5403-        for (i_shnum, i_serverid, i_sharefile) in self.shares:
5404+        serverids = [ss.get_serverid() for ss in servers]
5405+        for (i_shnum, i_serverid, i_sharefp) in self.shares:
5406             if i_serverid in serverids:
5407hunk ./src/allmydata/test/test_hung_server.py 61
5408-                self._corrupt_share((i_shnum, i_sharefile), corruptor_func)
5409+                self.corrupt_share((i_shnum, i_serverid, i_sharefp), corruptor_func)
5410 
5411     def _copy_all_shares_from(self, from_servers, to_server):
5412hunk ./src/allmydata/test/test_hung_server.py 64
5413-        serverids = [id for (id, ss) in from_servers]
5414-        for (i_shnum, i_serverid, i_sharefile) in self.shares:
5415+        serverids = [ss.get_serverid() for ss in from_servers]
5416+        for (i_shnum, i_serverid, i_sharefp) in self.shares:
5417             if i_serverid in serverids:
5418hunk ./src/allmydata/test/test_hung_server.py 67
5419-                self._copy_share((i_shnum, i_sharefile), to_server)
5420+                self.copy_share((i_shnum, i_serverid, i_sharefp), self.uri, to_server)
5421 
5422hunk ./src/allmydata/test/test_hung_server.py 69
5423-    def _copy_share(self, share, to_server):
5424-        (sharenum, sharefile) = share
5425-        (id, ss) = to_server
5426-        shares_dir = os.path.join(ss.original.storedir, "shares")
5427-        si = uri.from_string(self.uri).get_storage_index()
5428-        si_dir = os.path.join(shares_dir, storage_index_to_dir(si))
5429-        if not os.path.exists(si_dir):
5430-            os.makedirs(si_dir)
5431-        new_sharefile = os.path.join(si_dir, str(sharenum))
5432-        shutil.copy(sharefile, new_sharefile)
5433         self.shares = self.find_uri_shares(self.uri)
5434hunk ./src/allmydata/test/test_hung_server.py 70
5435-        # Make sure that the storage server has the share.
5436-        self.failUnless((sharenum, ss.original.my_nodeid, new_sharefile)
5437-                        in self.shares)
5438-
5439-    def _corrupt_share(self, share, corruptor_func):
5440-        (sharenum, sharefile) = share
5441-        data = open(sharefile, "rb").read()
5442-        newdata = corruptor_func(data)
5443-        os.unlink(sharefile)
5444-        wf = open(sharefile, "wb")
5445-        wf.write(newdata)
5446-        wf.close()
5447 
5448     def _set_up(self, mutable, testdir, num_clients=1, num_servers=10):
5449         self.mutable = mutable
5450hunk ./src/allmydata/test/test_hung_server.py 82
5451 
5452         self.c0 = self.g.clients[0]
5453         nm = self.c0.nodemaker
5454-        self.servers = sorted([(s.get_serverid(), s.get_rref())
5455-                               for s in nm.storage_broker.get_connected_servers()])
5456+        unsorted = [(s.get_serverid(), s.get_rref()) for s in nm.storage_broker.get_connected_servers()]
5457+        self.servers = [ss for (id, ss) in sorted(unsorted)]
5458         self.servers = self.servers[5:] + self.servers[:5]
5459 
5460         if mutable:
5461hunk ./src/allmydata/test/test_hung_server.py 244
5462             # stuck-but-not-overdue, and 4 live requests. All 4 live requests
5463             # will retire before the download is complete and the ShareFinder
5464             # is shut off. That will leave 4 OVERDUE and 1
5465-            # stuck-but-not-overdue, for a total of 5 requests in in
5466+            # stuck-but-not-overdue, for a total of 5 requests in
5467             # _sf.pending_requests
5468             for t in self._sf.overdue_timers.values()[:4]:
5469                 t.reset(-1.0)
5470hunk ./src/allmydata/test/test_mutable.py 21
5471 from foolscap.api import eventually, fireEventually
5472 from foolscap.logging import log
5473 from allmydata.storage_client import StorageFarmBroker
5474-from allmydata.storage.common import storage_index_to_dir
5475 from allmydata.scripts import debug
5476 
5477 from allmydata.mutable.filenode import MutableFileNode, BackoffAgent
5478hunk ./src/allmydata/test/test_mutable.py 3670
5479         # Now execute each assignment by writing the storage.
5480         for (share, servernum) in assignments:
5481             sharedata = base64.b64decode(self.sdmf_old_shares[share])
5482-            storedir = self.get_serverdir(servernum)
5483-            storage_path = os.path.join(storedir, "shares",
5484-                                        storage_index_to_dir(si))
5485-            fileutil.make_dirs(storage_path)
5486-            fileutil.write(os.path.join(storage_path, "%d" % share),
5487-                           sharedata)
5488+            storage_dir = self.get_server(servernum).backend.get_shareset(si).sharehomedir
5489+            fileutil.fp_make_dirs(storage_dir)
5490+            storage_dir.child("%d" % share).setContent(sharedata)
5491         # ...and verify that the shares are there.
5492         shares = self.find_uri_shares(self.sdmf_old_cap)
5493         assert len(shares) == 10
5494hunk ./src/allmydata/test/test_provisioning.py 13
5495 from nevow import inevow
5496 from zope.interface import implements
5497 
5498-class MyRequest:
5499+class MockRequest:
5500     implements(inevow.IRequest)
5501     pass
5502 
5503hunk ./src/allmydata/test/test_provisioning.py 26
5504     def test_load(self):
5505         pt = provisioning.ProvisioningTool()
5506         self.fields = {}
5507-        #r = MyRequest()
5508+        #r = MockRequest()
5509         #r.fields = self.fields
5510         #ctx = RequestContext()
5511         #unfilled = pt.renderSynchronously(ctx)
5512hunk ./src/allmydata/test/test_repairer.py 537
5513         # happiness setting.
5514         def _delete_some_servers(ignored):
5515             for i in xrange(7):
5516-                self.g.remove_server(self.g.servers_by_number[i].my_nodeid)
5517+                self.remove_server(i)
5518 
5519             assert len(self.g.servers_by_number) == 3
5520 
5521hunk ./src/allmydata/test/test_storage.py 14
5522 from allmydata import interfaces
5523 from allmydata.util import fileutil, hashutil, base32, pollmixin, time_format
5524 from allmydata.storage.server import StorageServer
5525-from allmydata.storage.mutable import MutableShareFile
5526-from allmydata.storage.immutable import BucketWriter, BucketReader
5527-from allmydata.storage.common import DataTooLargeError, storage_index_to_dir, \
5528+from allmydata.storage.backends.disk.mutable import MutableDiskShare
5529+from allmydata.storage.bucket import BucketWriter, BucketReader
5530+from allmydata.storage.common import DataTooLargeError, \
5531      UnknownMutableContainerVersionError, UnknownImmutableContainerVersionError
5532 from allmydata.storage.lease import LeaseInfo
5533 from allmydata.storage.crawler import BucketCountingCrawler
5534hunk ./src/allmydata/test/test_storage.py 474
5535         w[0].remote_write(0, "\xff"*10)
5536         w[0].remote_close()
5537 
5538-        fn = os.path.join(ss.sharedir, storage_index_to_dir("si1"), "0")
5539-        f = open(fn, "rb+")
5540+        fp = ss.backend.get_shareset("si1").sharehomedir.child("0")
5541+        f = fp.open("rb+")
5542         f.seek(0)
5543         f.write(struct.pack(">L", 0)) # this is invalid: minimum used is v1
5544         f.close()
5545hunk ./src/allmydata/test/test_storage.py 814
5546     def test_bad_magic(self):
5547         ss = self.create("test_bad_magic")
5548         self.allocate(ss, "si1", "we1", self._lease_secret.next(), set([0]), 10)
5549-        fn = os.path.join(ss.sharedir, storage_index_to_dir("si1"), "0")
5550-        f = open(fn, "rb+")
5551+        fp = ss.backend.get_shareset("si1").sharehomedir.child("0")
5552+        f = fp.open("rb+")
5553         f.seek(0)
5554         f.write("BAD MAGIC")
5555         f.close()
5556hunk ./src/allmydata/test/test_storage.py 842
5557 
5558         # Trying to make the container too large (by sending a write vector
5559         # whose offset is too high) will raise an exception.
5560-        TOOBIG = MutableShareFile.MAX_SIZE + 10
5561+        TOOBIG = MutableDiskShare.MAX_SIZE + 10
5562         self.failUnlessRaises(DataTooLargeError,
5563                               rstaraw, "si1", secrets,
5564                               {0: ([], [(TOOBIG,data)], None)},
5565hunk ./src/allmydata/test/test_storage.py 1229
5566 
5567         # create a random non-numeric file in the bucket directory, to
5568         # exercise the code that's supposed to ignore those.
5569-        bucket_dir = os.path.join(self.workdir("test_leases"),
5570-                                  "shares", storage_index_to_dir("si1"))
5571-        f = open(os.path.join(bucket_dir, "ignore_me.txt"), "w")
5572-        f.write("you ought to be ignoring me\n")
5573-        f.close()
5574+        bucket_dir = ss.backend.get_shareset("si1").sharehomedir
5575+        bucket_dir.child("ignore_me.txt").setContent("you ought to be ignoring me\n")
5576 
5577hunk ./src/allmydata/test/test_storage.py 1232
5578-        s0 = MutableShareFile(os.path.join(bucket_dir, "0"))
5579+        s0 = MutableDiskShare(os.path.join(bucket_dir, "0"))
5580         self.failUnlessEqual(len(list(s0.get_leases())), 1)
5581 
5582         # add-lease on a missing storage index is silently ignored
5583hunk ./src/allmydata/test/test_storage.py 3118
5584         [immutable_si_0, immutable_si_1, mutable_si_2, mutable_si_3] = self.sis
5585 
5586         # add a non-sharefile to exercise another code path
5587-        fn = os.path.join(ss.sharedir,
5588-                          storage_index_to_dir(immutable_si_0),
5589-                          "not-a-share")
5590-        f = open(fn, "wb")
5591-        f.write("I am not a share.\n")
5592-        f.close()
5593+        fp = ss.backend.get_shareset(immutable_si_0).sharehomedir.child("not-a-share")
5594+        fp.setContent("I am not a share.\n")
5595 
5596         # this is before the crawl has started, so we're not in a cycle yet
5597         initial_state = lc.get_state()
5598hunk ./src/allmydata/test/test_storage.py 3282
5599     def test_expire_age(self):
5600         basedir = "storage/LeaseCrawler/expire_age"
5601         fileutil.make_dirs(basedir)
5602-        # setting expiration_time to 2000 means that any lease which is more
5603-        # than 2000s old will be expired.
5604-        ss = InstrumentedStorageServer(basedir, "\x00" * 20,
5605-                                       expiration_enabled=True,
5606-                                       expiration_mode="age",
5607-                                       expiration_override_lease_duration=2000)
5608+        # setting 'override_lease_duration' to 2000 means that any lease that
5609+        # is more than 2000 seconds old will be expired.
5610+        expiration_policy = {
5611+            'enabled': True,
5612+            'mode': 'age',
5613+            'override_lease_duration': 2000,
5614+            'sharetypes': ('mutable', 'immutable'),
5615+        }
5616+        ss = InstrumentedStorageServer(basedir, "\x00" * 20, expiration_policy)
5617         # make it start sooner than usual.
5618         lc = ss.lease_checker
5619         lc.slow_start = 0
5620hunk ./src/allmydata/test/test_storage.py 3423
5621     def test_expire_cutoff_date(self):
5622         basedir = "storage/LeaseCrawler/expire_cutoff_date"
5623         fileutil.make_dirs(basedir)
5624-        # setting cutoff-date to 2000 seconds ago means that any lease which
5625-        # is more than 2000s old will be expired.
5626+        # setting 'cutoff_date' to 2000 seconds ago means that any lease that
5627+        # is more than 2000 seconds old will be expired.
5628         now = time.time()
5629         then = int(now - 2000)
5630hunk ./src/allmydata/test/test_storage.py 3427
5631-        ss = InstrumentedStorageServer(basedir, "\x00" * 20,
5632-                                       expiration_enabled=True,
5633-                                       expiration_mode="cutoff-date",
5634-                                       expiration_cutoff_date=then)
5635+        expiration_policy = {
5636+            'enabled': True,
5637+            'mode': 'cutoff-date',
5638+            'cutoff_date': then,
5639+            'sharetypes': ('mutable', 'immutable'),
5640+        }
5641+        ss = InstrumentedStorageServer(basedir, "\x00" * 20, expiration_policy)
5642         # make it start sooner than usual.
5643         lc = ss.lease_checker
5644         lc.slow_start = 0
5645hunk ./src/allmydata/test/test_storage.py 3575
5646     def test_only_immutable(self):
5647         basedir = "storage/LeaseCrawler/only_immutable"
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 3579
5654-        ss = StorageServer(basedir, "\x00" * 20,
5655-                           expiration_enabled=True,
5656-                           expiration_mode="cutoff-date",
5657-                           expiration_cutoff_date=then,
5658-                           expiration_sharetypes=("immutable",))
5659+        expiration_policy = {
5660+            'enabled': True,
5661+            'mode': 'cutoff-date',
5662+            'cutoff_date': then,
5663+            'sharetypes': ('immutable',),
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 3636
5670     def test_only_mutable(self):
5671         basedir = "storage/LeaseCrawler/only_mutable"
5672         fileutil.make_dirs(basedir)
5673+        # setting 'cutoff_date' to 2000 seconds ago means that any lease that
5674+        # is more than 2000 seconds old will be expired.
5675         now = time.time()
5676         then = int(now - 2000)
5677hunk ./src/allmydata/test/test_storage.py 3640
5678-        ss = StorageServer(basedir, "\x00" * 20,
5679-                           expiration_enabled=True,
5680-                           expiration_mode="cutoff-date",
5681-                           expiration_cutoff_date=then,
5682-                           expiration_sharetypes=("mutable",))
5683+        expiration_policy = {
5684+            'enabled': True,
5685+            'mode': 'cutoff-date',
5686+            'cutoff_date': then,
5687+            'sharetypes': ('mutable',),
5688+        }
5689+        ss = StorageServer(basedir, "\x00" * 20, expiration_policy)
5690         lc = ss.lease_checker
5691         lc.slow_start = 0
5692         webstatus = StorageStatus(ss)
5693hunk ./src/allmydata/test/test_storage.py 3819
5694     def test_no_st_blocks(self):
5695         basedir = "storage/LeaseCrawler/no_st_blocks"
5696         fileutil.make_dirs(basedir)
5697-        ss = No_ST_BLOCKS_StorageServer(basedir, "\x00" * 20,
5698-                                        expiration_mode="age",
5699-                                        expiration_override_lease_duration=-1000)
5700-        # a negative expiration_time= means the "configured-"
5701+        # A negative 'override_lease_duration' means that the "configured-"
5702         # space-recovered counts will be non-zero, since all shares will have
5703hunk ./src/allmydata/test/test_storage.py 3821
5704-        # expired by then
5705+        # expired by then.
5706+        expiration_policy = {
5707+            'enabled': True,
5708+            'mode': 'age',
5709+            'override_lease_duration': -1000,
5710+            'sharetypes': ('mutable', 'immutable'),
5711+        }
5712+        ss = No_ST_BLOCKS_StorageServer(basedir, "\x00" * 20, expiration_policy)
5713 
5714         # make it start sooner than usual.
5715         lc = ss.lease_checker
5716hunk ./src/allmydata/test/test_storage.py 3877
5717         [immutable_si_0, immutable_si_1, mutable_si_2, mutable_si_3] = self.sis
5718         first = min(self.sis)
5719         first_b32 = base32.b2a(first)
5720-        fn = os.path.join(ss.sharedir, storage_index_to_dir(first), "0")
5721-        f = open(fn, "rb+")
5722+        fp = ss.backend.get_shareset(first).sharehomedir.child("0")
5723+        f = fp.open("rb+")
5724         f.seek(0)
5725         f.write("BAD MAGIC")
5726         f.close()
5727hunk ./src/allmydata/test/test_storage.py 3890
5728 
5729         # also create an empty bucket
5730         empty_si = base32.b2a("\x04"*16)
5731-        empty_bucket_dir = os.path.join(ss.sharedir,
5732-                                        storage_index_to_dir(empty_si))
5733-        fileutil.make_dirs(empty_bucket_dir)
5734+        empty_bucket_dir = ss.backend.get_shareset(empty_si).sharehomedir
5735+        fileutil.fp_make_dirs(empty_bucket_dir)
5736 
5737         ss.setServiceParent(self.s)
5738 
5739hunk ./src/allmydata/test/test_system.py 10
5740 
5741 import allmydata
5742 from allmydata import uri
5743-from allmydata.storage.mutable import MutableShareFile
5744+from allmydata.storage.backends.disk.mutable import MutableDiskShare
5745 from allmydata.storage.server import si_a2b
5746 from allmydata.immutable import offloaded, upload
5747 from allmydata.immutable.literal import LiteralFileNode
5748hunk ./src/allmydata/test/test_system.py 421
5749         return shares
5750 
5751     def _corrupt_mutable_share(self, filename, which):
5752-        msf = MutableShareFile(filename)
5753+        msf = MutableDiskShare(filename)
5754         datav = msf.readv([ (0, 1000000) ])
5755         final_share = datav[0]
5756         assert len(final_share) < 1000000 # ought to be truncated
5757hunk ./src/allmydata/test/test_upload.py 22
5758 from allmydata.util.happinessutil import servers_of_happiness, \
5759                                          shares_by_server, merge_servers
5760 from allmydata.storage_client import StorageFarmBroker
5761-from allmydata.storage.server import storage_index_to_dir
5762 
5763 MiB = 1024*1024
5764 
5765hunk ./src/allmydata/test/test_upload.py 821
5766 
5767     def _copy_share_to_server(self, share_number, server_number):
5768         ss = self.g.servers_by_number[server_number]
5769-        # Copy share i from the directory associated with the first
5770-        # storage server to the directory associated with this one.
5771-        assert self.g, "I tried to find a grid at self.g, but failed"
5772-        assert self.shares, "I tried to find shares at self.shares, but failed"
5773-        old_share_location = self.shares[share_number][2]
5774-        new_share_location = os.path.join(ss.storedir, "shares")
5775-        si = uri.from_string(self.uri).get_storage_index()
5776-        new_share_location = os.path.join(new_share_location,
5777-                                          storage_index_to_dir(si))
5778-        if not os.path.exists(new_share_location):
5779-            os.makedirs(new_share_location)
5780-        new_share_location = os.path.join(new_share_location,
5781-                                          str(share_number))
5782-        if old_share_location != new_share_location:
5783-            shutil.copy(old_share_location, new_share_location)
5784-        shares = self.find_uri_shares(self.uri)
5785-        # Make sure that the storage server has the share.
5786-        self.failUnless((share_number, ss.my_nodeid, new_share_location)
5787-                        in shares)
5788+        self.copy_share(self.shares[share_number], ss)
5789 
5790     def _setup_grid(self):
5791         """
5792hunk ./src/allmydata/test/test_upload.py 1103
5793                 self._copy_share_to_server(i, 2)
5794         d.addCallback(_copy_shares)
5795         # Remove the first server, and add a placeholder with share 0
5796-        d.addCallback(lambda ign:
5797-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
5798+        d.addCallback(lambda ign: self.remove_server(0))
5799         d.addCallback(lambda ign:
5800             self._add_server_with_share(server_number=4, share_number=0))
5801         # Now try uploading.
5802hunk ./src/allmydata/test/test_upload.py 1134
5803         d.addCallback(lambda ign:
5804             self._add_server(server_number=4))
5805         d.addCallback(_copy_shares)
5806-        d.addCallback(lambda ign:
5807-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
5808+        d.addCallback(lambda ign: self.remove_server(0))
5809         d.addCallback(_reset_encoding_parameters)
5810         d.addCallback(lambda client:
5811             client.upload(upload.Data("data" * 10000, convergence="")))
5812hunk ./src/allmydata/test/test_upload.py 1196
5813                 self._copy_share_to_server(i, 2)
5814         d.addCallback(_copy_shares)
5815         # Remove server 0, and add another in its place
5816-        d.addCallback(lambda ign:
5817-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
5818+        d.addCallback(lambda ign: self.remove_server(0))
5819         d.addCallback(lambda ign:
5820             self._add_server_with_share(server_number=4, share_number=0,
5821                                         readonly=True))
5822hunk ./src/allmydata/test/test_upload.py 1237
5823             for i in xrange(1, 10):
5824                 self._copy_share_to_server(i, 2)
5825         d.addCallback(_copy_shares)
5826-        d.addCallback(lambda ign:
5827-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
5828+        d.addCallback(lambda ign: self.remove_server(0))
5829         def _reset_encoding_parameters(ign, happy=4):
5830             client = self.g.clients[0]
5831             client.DEFAULT_ENCODING_PARAMETERS['happy'] = happy
5832hunk ./src/allmydata/test/test_upload.py 1273
5833         # remove the original server
5834         # (necessary to ensure that the Tahoe2ServerSelector will distribute
5835         #  all the shares)
5836-        def _remove_server(ign):
5837-            server = self.g.servers_by_number[0]
5838-            self.g.remove_server(server.my_nodeid)
5839-        d.addCallback(_remove_server)
5840+        d.addCallback(lambda ign: self.remove_server(0))
5841         # This should succeed; we still have 4 servers, and the
5842         # happiness of the upload is 4.
5843         d.addCallback(lambda ign:
5844hunk ./src/allmydata/test/test_upload.py 1285
5845         d.addCallback(lambda ign:
5846             self._setup_and_upload())
5847         d.addCallback(_do_server_setup)
5848-        d.addCallback(_remove_server)
5849+        d.addCallback(lambda ign: self.remove_server(0))
5850         d.addCallback(lambda ign:
5851             self.shouldFail(UploadUnhappinessError,
5852                             "test_dropped_servers_in_encoder",
5853hunk ./src/allmydata/test/test_upload.py 1307
5854             self._add_server_with_share(4, 7, readonly=True)
5855             self._add_server_with_share(5, 8, readonly=True)
5856         d.addCallback(_do_server_setup_2)
5857-        d.addCallback(_remove_server)
5858+        d.addCallback(lambda ign: self.remove_server(0))
5859         d.addCallback(lambda ign:
5860             self._do_upload_with_broken_servers(1))
5861         d.addCallback(_set_basedir)
5862hunk ./src/allmydata/test/test_upload.py 1314
5863         d.addCallback(lambda ign:
5864             self._setup_and_upload())
5865         d.addCallback(_do_server_setup_2)
5866-        d.addCallback(_remove_server)
5867+        d.addCallback(lambda ign: self.remove_server(0))
5868         d.addCallback(lambda ign:
5869             self.shouldFail(UploadUnhappinessError,
5870                             "test_dropped_servers_in_encoder",
5871hunk ./src/allmydata/test/test_upload.py 1528
5872             for i in xrange(1, 10):
5873                 self._copy_share_to_server(i, 1)
5874         d.addCallback(_copy_shares)
5875-        d.addCallback(lambda ign:
5876-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
5877+        d.addCallback(lambda ign: self.remove_server(0))
5878         def _prepare_client(ign):
5879             client = self.g.clients[0]
5880             client.DEFAULT_ENCODING_PARAMETERS['happy'] = 4
5881hunk ./src/allmydata/test/test_upload.py 1550
5882         def _setup(ign):
5883             for i in xrange(1, 11):
5884                 self._add_server(server_number=i)
5885-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid)
5886+            self.remove_server(0)
5887             c = self.g.clients[0]
5888             # We set happy to an unsatisfiable value so that we can check the
5889             # counting in the exception message. The same progress message
5890hunk ./src/allmydata/test/test_upload.py 1577
5891                 self._add_server(server_number=i)
5892             self._add_server(server_number=11, readonly=True)
5893             self._add_server(server_number=12, readonly=True)
5894-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid)
5895+            self.remove_server(0)
5896             c = self.g.clients[0]
5897             c.DEFAULT_ENCODING_PARAMETERS['happy'] = 45
5898             return c
5899hunk ./src/allmydata/test/test_upload.py 1605
5900             # the first one that the selector sees.
5901             for i in xrange(10):
5902                 self._copy_share_to_server(i, 9)
5903-            # Remove server 0, and its contents
5904-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid)
5905+            self.remove_server(0)
5906             # Make happiness unsatisfiable
5907             c = self.g.clients[0]
5908             c.DEFAULT_ENCODING_PARAMETERS['happy'] = 45
5909hunk ./src/allmydata/test/test_upload.py 1625
5910         def _then(ign):
5911             for i in xrange(1, 11):
5912                 self._add_server(server_number=i, readonly=True)
5913-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid)
5914+            self.remove_server(0)
5915             c = self.g.clients[0]
5916             c.DEFAULT_ENCODING_PARAMETERS['k'] = 2
5917             c.DEFAULT_ENCODING_PARAMETERS['happy'] = 4
5918hunk ./src/allmydata/test/test_upload.py 1661
5919             self._add_server(server_number=4, readonly=True))
5920         d.addCallback(lambda ign:
5921             self._add_server(server_number=5, readonly=True))
5922-        d.addCallback(lambda ign:
5923-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
5924+        d.addCallback(lambda ign: self.remove_server(0))
5925         def _reset_encoding_parameters(ign, happy=4):
5926             client = self.g.clients[0]
5927             client.DEFAULT_ENCODING_PARAMETERS['happy'] = happy
5928hunk ./src/allmydata/test/test_upload.py 1696
5929         d.addCallback(lambda ign:
5930             self._add_server(server_number=2))
5931         def _break_server_2(ign):
5932-            serverid = self.g.servers_by_number[2].my_nodeid
5933+            serverid = self.get_server(2).get_serverid()
5934             self.g.break_server(serverid)
5935         d.addCallback(_break_server_2)
5936         d.addCallback(lambda ign:
5937hunk ./src/allmydata/test/test_upload.py 1705
5938             self._add_server(server_number=4, readonly=True))
5939         d.addCallback(lambda ign:
5940             self._add_server(server_number=5, readonly=True))
5941-        d.addCallback(lambda ign:
5942-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
5943+        d.addCallback(lambda ign: self.remove_server(0))
5944         d.addCallback(_reset_encoding_parameters)
5945         d.addCallback(lambda client:
5946             self.shouldFail(UploadUnhappinessError, "test_selection_exceptions",
5947hunk ./src/allmydata/test/test_upload.py 1816
5948             # Copy shares
5949             self._copy_share_to_server(1, 1)
5950             self._copy_share_to_server(2, 1)
5951-            # Remove server 0
5952-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid)
5953+            self.remove_server(0)
5954             client = self.g.clients[0]
5955             client.DEFAULT_ENCODING_PARAMETERS['happy'] = 3
5956             return client
5957hunk ./src/allmydata/test/test_upload.py 1930
5958                                         readonly=True)
5959             self._add_server_with_share(server_number=4, share_number=3,
5960                                         readonly=True)
5961-            # Remove server 0.
5962-            self.g.remove_server(self.g.servers_by_number[0].my_nodeid)
5963+            self.remove_server(0)
5964             # Set the client appropriately
5965             c = self.g.clients[0]
5966             c.DEFAULT_ENCODING_PARAMETERS['happy'] = 4
5967hunk ./src/allmydata/test/test_util.py 9
5968 from twisted.trial import unittest
5969 from twisted.internet import defer, reactor
5970 from twisted.python.failure import Failure
5971+from twisted.python.filepath import FilePath
5972 from twisted.python import log
5973 from pycryptopp.hash.sha256 import SHA256 as _hash
5974 
5975hunk ./src/allmydata/test/test_util.py 508
5976                 os.chdir(saved_cwd)
5977 
5978     def test_disk_stats(self):
5979-        avail = fileutil.get_available_space('.', 2**14)
5980+        avail = fileutil.get_available_space(FilePath('.'), 2**14)
5981         if avail == 0:
5982             raise unittest.SkipTest("This test will spuriously fail there is no disk space left.")
5983 
5984hunk ./src/allmydata/test/test_util.py 512
5985-        disk = fileutil.get_disk_stats('.', 2**13)
5986+        disk = fileutil.get_disk_stats(FilePath('.'), 2**13)
5987         self.failUnless(disk['total'] > 0, disk['total'])
5988         self.failUnless(disk['used'] > 0, disk['used'])
5989         self.failUnless(disk['free_for_root'] > 0, disk['free_for_root'])
5990hunk ./src/allmydata/test/test_util.py 521
5991 
5992     def test_disk_stats_avail_nonnegative(self):
5993         # This test will spuriously fail if you have more than 2^128
5994-        # bytes of available space on your filesystem.
5995-        disk = fileutil.get_disk_stats('.', 2**128)
5996+        # bytes of available space on your filesystem (lucky you).
5997+        disk = fileutil.get_disk_stats(FilePath('.'), 2**128)
5998         self.failUnlessEqual(disk['avail'], 0)
5999 
6000 class PollMixinTests(unittest.TestCase):
6001hunk ./src/allmydata/test/test_web.py 12
6002 from twisted.python import failure, log
6003 from nevow import rend
6004 from allmydata import interfaces, uri, webish, dirnode
6005-from allmydata.storage.shares import get_share_file
6006 from allmydata.storage_client import StorageFarmBroker
6007 from allmydata.immutable import upload
6008 from allmydata.immutable.downloader.status import DownloadStatus
6009hunk ./src/allmydata/test/test_web.py 4111
6010             good_shares = self.find_uri_shares(self.uris["good"])
6011             self.failUnlessReallyEqual(len(good_shares), 10)
6012             sick_shares = self.find_uri_shares(self.uris["sick"])
6013-            os.unlink(sick_shares[0][2])
6014+            sick_shares[0][2].remove()
6015             dead_shares = self.find_uri_shares(self.uris["dead"])
6016             for i in range(1, 10):
6017hunk ./src/allmydata/test/test_web.py 4114
6018-                os.unlink(dead_shares[i][2])
6019+                dead_shares[i][2].remove()
6020             c_shares = self.find_uri_shares(self.uris["corrupt"])
6021             cso = CorruptShareOptions()
6022             cso.stdout = StringIO()
6023hunk ./src/allmydata/test/test_web.py 4118
6024-            cso.parseOptions([c_shares[0][2]])
6025+            cso.parseOptions([c_shares[0][2].path])
6026             corrupt_share(cso)
6027         d.addCallback(_clobber_shares)
6028 
6029hunk ./src/allmydata/test/test_web.py 4253
6030             good_shares = self.find_uri_shares(self.uris["good"])
6031             self.failUnlessReallyEqual(len(good_shares), 10)
6032             sick_shares = self.find_uri_shares(self.uris["sick"])
6033-            os.unlink(sick_shares[0][2])
6034+            sick_shares[0][2].remove()
6035             dead_shares = self.find_uri_shares(self.uris["dead"])
6036             for i in range(1, 10):
6037hunk ./src/allmydata/test/test_web.py 4256
6038-                os.unlink(dead_shares[i][2])
6039+                dead_shares[i][2].remove()
6040             c_shares = self.find_uri_shares(self.uris["corrupt"])
6041             cso = CorruptShareOptions()
6042             cso.stdout = StringIO()
6043hunk ./src/allmydata/test/test_web.py 4260
6044-            cso.parseOptions([c_shares[0][2]])
6045+            cso.parseOptions([c_shares[0][2].path])
6046             corrupt_share(cso)
6047         d.addCallback(_clobber_shares)
6048 
6049hunk ./src/allmydata/test/test_web.py 4319
6050 
6051         def _clobber_shares(ignored):
6052             sick_shares = self.find_uri_shares(self.uris["sick"])
6053-            os.unlink(sick_shares[0][2])
6054+            sick_shares[0][2].remove()
6055         d.addCallback(_clobber_shares)
6056 
6057         d.addCallback(self.CHECK, "sick", "t=check&repair=true&output=json")
6058hunk ./src/allmydata/test/test_web.py 4811
6059             good_shares = self.find_uri_shares(self.uris["good"])
6060             self.failUnlessReallyEqual(len(good_shares), 10)
6061             sick_shares = self.find_uri_shares(self.uris["sick"])
6062-            os.unlink(sick_shares[0][2])
6063+            sick_shares[0][2].remove()
6064             #dead_shares = self.find_uri_shares(self.uris["dead"])
6065             #for i in range(1, 10):
6066hunk ./src/allmydata/test/test_web.py 4814
6067-            #    os.unlink(dead_shares[i][2])
6068+            #    dead_shares[i][2].remove()
6069 
6070             #c_shares = self.find_uri_shares(self.uris["corrupt"])
6071             #cso = CorruptShareOptions()
6072hunk ./src/allmydata/test/test_web.py 4819
6073             #cso.stdout = StringIO()
6074-            #cso.parseOptions([c_shares[0][2]])
6075+            #cso.parseOptions([c_shares[0][2].path])
6076             #corrupt_share(cso)
6077         d.addCallback(_clobber_shares)
6078 
6079hunk ./src/allmydata/test/test_web.py 4870
6080         d.addErrback(self.explain_web_error)
6081         return d
6082 
6083-    def _count_leases(self, ignored, which):
6084-        u = self.uris[which]
6085-        shares = self.find_uri_shares(u)
6086-        lease_counts = []
6087-        for shnum, serverid, fn in shares:
6088-            sf = get_share_file(fn)
6089-            num_leases = len(list(sf.get_leases()))
6090-            lease_counts.append( (fn, num_leases) )
6091-        return lease_counts
6092-
6093-    def _assert_leasecount(self, lease_counts, expected):
6094+    def _assert_leasecount(self, ignored, which, expected):
6095+        lease_counts = self.count_leases(self.uris[which])
6096         for (fn, num_leases) in lease_counts:
6097             if num_leases != expected:
6098                 self.fail("expected %d leases, have %d, on %s" %
6099hunk ./src/allmydata/test/test_web.py 4903
6100                 self.fileurls[which] = "uri/" + urllib.quote(self.uris[which])
6101         d.addCallback(_compute_fileurls)
6102 
6103-        d.addCallback(self._count_leases, "one")
6104-        d.addCallback(self._assert_leasecount, 1)
6105-        d.addCallback(self._count_leases, "two")
6106-        d.addCallback(self._assert_leasecount, 1)
6107-        d.addCallback(self._count_leases, "mutable")
6108-        d.addCallback(self._assert_leasecount, 1)
6109+        d.addCallback(self._assert_leasecount, "one", 1)
6110+        d.addCallback(self._assert_leasecount, "two", 1)
6111+        d.addCallback(self._assert_leasecount, "mutable", 1)
6112 
6113         d.addCallback(self.CHECK, "one", "t=check") # no add-lease
6114         def _got_html_good(res):
6115hunk ./src/allmydata/test/test_web.py 4913
6116             self.failIf("Not Healthy" in res, res)
6117         d.addCallback(_got_html_good)
6118 
6119-        d.addCallback(self._count_leases, "one")
6120-        d.addCallback(self._assert_leasecount, 1)
6121-        d.addCallback(self._count_leases, "two")
6122-        d.addCallback(self._assert_leasecount, 1)
6123-        d.addCallback(self._count_leases, "mutable")
6124-        d.addCallback(self._assert_leasecount, 1)
6125+        d.addCallback(self._assert_leasecount, "one", 1)
6126+        d.addCallback(self._assert_leasecount, "two", 1)
6127+        d.addCallback(self._assert_leasecount, "mutable", 1)
6128 
6129         # this CHECK uses the original client, which uses the same
6130         # lease-secrets, so it will just renew the original lease
6131hunk ./src/allmydata/test/test_web.py 4922
6132         d.addCallback(self.CHECK, "one", "t=check&add-lease=true")
6133         d.addCallback(_got_html_good)
6134 
6135-        d.addCallback(self._count_leases, "one")
6136-        d.addCallback(self._assert_leasecount, 1)
6137-        d.addCallback(self._count_leases, "two")
6138-        d.addCallback(self._assert_leasecount, 1)
6139-        d.addCallback(self._count_leases, "mutable")
6140-        d.addCallback(self._assert_leasecount, 1)
6141+        d.addCallback(self._assert_leasecount, "one", 1)
6142+        d.addCallback(self._assert_leasecount, "two", 1)
6143+        d.addCallback(self._assert_leasecount, "mutable", 1)
6144 
6145         # this CHECK uses an alternate client, which adds a second lease
6146         d.addCallback(self.CHECK, "one", "t=check&add-lease=true", clientnum=1)
6147hunk ./src/allmydata/test/test_web.py 4930
6148         d.addCallback(_got_html_good)
6149 
6150-        d.addCallback(self._count_leases, "one")
6151-        d.addCallback(self._assert_leasecount, 2)
6152-        d.addCallback(self._count_leases, "two")
6153-        d.addCallback(self._assert_leasecount, 1)
6154-        d.addCallback(self._count_leases, "mutable")
6155-        d.addCallback(self._assert_leasecount, 1)
6156+        d.addCallback(self._assert_leasecount, "one", 2)
6157+        d.addCallback(self._assert_leasecount, "two", 1)
6158+        d.addCallback(self._assert_leasecount, "mutable", 1)
6159 
6160         d.addCallback(self.CHECK, "mutable", "t=check&add-lease=true")
6161         d.addCallback(_got_html_good)
6162hunk ./src/allmydata/test/test_web.py 4937
6163 
6164-        d.addCallback(self._count_leases, "one")
6165-        d.addCallback(self._assert_leasecount, 2)
6166-        d.addCallback(self._count_leases, "two")
6167-        d.addCallback(self._assert_leasecount, 1)
6168-        d.addCallback(self._count_leases, "mutable")
6169-        d.addCallback(self._assert_leasecount, 1)
6170+        d.addCallback(self._assert_leasecount, "one", 2)
6171+        d.addCallback(self._assert_leasecount, "two", 1)
6172+        d.addCallback(self._assert_leasecount, "mutable", 1)
6173 
6174         d.addCallback(self.CHECK, "mutable", "t=check&add-lease=true",
6175                       clientnum=1)
6176hunk ./src/allmydata/test/test_web.py 4945
6177         d.addCallback(_got_html_good)
6178 
6179-        d.addCallback(self._count_leases, "one")
6180-        d.addCallback(self._assert_leasecount, 2)
6181-        d.addCallback(self._count_leases, "two")
6182-        d.addCallback(self._assert_leasecount, 1)
6183-        d.addCallback(self._count_leases, "mutable")
6184-        d.addCallback(self._assert_leasecount, 2)
6185+        d.addCallback(self._assert_leasecount, "one", 2)
6186+        d.addCallback(self._assert_leasecount, "two", 1)
6187+        d.addCallback(self._assert_leasecount, "mutable", 2)
6188 
6189         d.addErrback(self.explain_web_error)
6190         return d
6191hunk ./src/allmydata/test/test_web.py 4989
6192             self.failUnlessReallyEqual(len(units), 4+1)
6193         d.addCallback(_done)
6194 
6195-        d.addCallback(self._count_leases, "root")
6196-        d.addCallback(self._assert_leasecount, 1)
6197-        d.addCallback(self._count_leases, "one")
6198-        d.addCallback(self._assert_leasecount, 1)
6199-        d.addCallback(self._count_leases, "mutable")
6200-        d.addCallback(self._assert_leasecount, 1)
6201+        d.addCallback(self._assert_leasecount, "root", 1)
6202+        d.addCallback(self._assert_leasecount, "one", 1)
6203+        d.addCallback(self._assert_leasecount, "mutable", 1)
6204 
6205         d.addCallback(self.CHECK, "root", "t=stream-deep-check&add-lease=true")
6206         d.addCallback(_done)
6207hunk ./src/allmydata/test/test_web.py 4996
6208 
6209-        d.addCallback(self._count_leases, "root")
6210-        d.addCallback(self._assert_leasecount, 1)
6211-        d.addCallback(self._count_leases, "one")
6212-        d.addCallback(self._assert_leasecount, 1)
6213-        d.addCallback(self._count_leases, "mutable")
6214-        d.addCallback(self._assert_leasecount, 1)
6215+        d.addCallback(self._assert_leasecount, "root", 1)
6216+        d.addCallback(self._assert_leasecount, "one", 1)
6217+        d.addCallback(self._assert_leasecount, "mutable", 1)
6218 
6219         d.addCallback(self.CHECK, "root", "t=stream-deep-check&add-lease=true",
6220                       clientnum=1)
6221hunk ./src/allmydata/test/test_web.py 5004
6222         d.addCallback(_done)
6223 
6224-        d.addCallback(self._count_leases, "root")
6225-        d.addCallback(self._assert_leasecount, 2)
6226-        d.addCallback(self._count_leases, "one")
6227-        d.addCallback(self._assert_leasecount, 2)
6228-        d.addCallback(self._count_leases, "mutable")
6229-        d.addCallback(self._assert_leasecount, 2)
6230+        d.addCallback(self._assert_leasecount, "root", 2)
6231+        d.addCallback(self._assert_leasecount, "one", 2)
6232+        d.addCallback(self._assert_leasecount, "mutable", 2)
6233 
6234         d.addErrback(self.explain_web_error)
6235         return d
6236merger 0.0 (
6237hunk ./src/allmydata/uri.py 829
6238+    def is_readonly(self):
6239+        return True
6240+
6241+    def get_readonly(self):
6242+        return self
6243+
6244+
6245hunk ./src/allmydata/uri.py 829
6246+    def is_readonly(self):
6247+        return True
6248+
6249+    def get_readonly(self):
6250+        return self
6251+
6252+
6253)
6254merger 0.0 (
6255hunk ./src/allmydata/uri.py 848
6256+    def is_readonly(self):
6257+        return True
6258+
6259+    def get_readonly(self):
6260+        return self
6261+
6262hunk ./src/allmydata/uri.py 848
6263+    def is_readonly(self):
6264+        return True
6265+
6266+    def get_readonly(self):
6267+        return self
6268+
6269)
6270hunk ./src/allmydata/util/encodingutil.py 221
6271 def quote_path(path, quotemarks=True):
6272     return quote_output("/".join(map(to_str, path)), quotemarks=quotemarks)
6273 
6274+def quote_filepath(fp, quotemarks=True, encoding=None):
6275+    path = fp.path
6276+    if isinstance(path, str):
6277+        try:
6278+            path = path.decode(filesystem_encoding)
6279+        except UnicodeDecodeError:
6280+            return 'b"%s"' % (ESCAPABLE_8BIT.sub(_str_escape, path),)
6281+
6282+    return quote_output(path, quotemarks=quotemarks, encoding=encoding)
6283+
6284 
6285 def unicode_platform():
6286     """
6287hunk ./src/allmydata/util/fileutil.py 5
6288 Futz with files like a pro.
6289 """
6290 
6291-import sys, exceptions, os, stat, tempfile, time, binascii
6292+import errno, sys, exceptions, os, stat, tempfile, time, binascii
6293+
6294+from allmydata.util.assertutil import precondition
6295 
6296 from twisted.python import log
6297hunk ./src/allmydata/util/fileutil.py 10
6298+from twisted.python.filepath import FilePath, UnlistableError
6299 
6300 from pycryptopp.cipher.aes import AES
6301 
6302hunk ./src/allmydata/util/fileutil.py 189
6303             raise tx
6304         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...
6305 
6306-def rm_dir(dirname):
6307+def fp_make_dirs(dirfp):
6308+    """
6309+    An idempotent version of FilePath.makedirs().  If the dir already
6310+    exists, do nothing and return without raising an exception.  If this
6311+    call creates the dir, return without raising an exception.  If there is
6312+    an error that prevents creation or if the directory gets deleted after
6313+    fp_make_dirs() creates it and before fp_make_dirs() checks that it
6314+    exists, raise an exception.
6315+    """
6316+    log.msg( "xxx 0 %s" % (dirfp,))
6317+    tx = None
6318+    try:
6319+        dirfp.makedirs()
6320+    except OSError, x:
6321+        tx = x
6322+
6323+    if not dirfp.isdir():
6324+        if tx:
6325+            raise tx
6326+        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...
6327+
6328+def fp_rmdir_if_empty(dirfp):
6329+    """ Remove the directory if it is empty. """
6330+    try:
6331+        os.rmdir(dirfp.path)
6332+    except OSError, e:
6333+        if e.errno != errno.ENOTEMPTY:
6334+            raise
6335+    else:
6336+        dirfp.changed()
6337+
6338+def rmtree(dirname):
6339     """
6340     A threadsafe and idempotent version of shutil.rmtree().  If the dir is
6341     already gone, do nothing and return without raising an exception.  If this
6342hunk ./src/allmydata/util/fileutil.py 239
6343             else:
6344                 remove(fullname)
6345         os.rmdir(dirname)
6346-    except Exception, le:
6347-        # Ignore "No such file or directory"
6348-        if (not isinstance(le, OSError)) or le.args[0] != 2:
6349+    except EnvironmentError, le:
6350+        # Ignore "No such file or directory", collect any other exception.
6351+        if (le.args[0] != 2 and le.args[0] != 3) or (le.args[0] != errno.ENOENT):
6352             excs.append(le)
6353hunk ./src/allmydata/util/fileutil.py 243
6354+    except Exception, le:
6355+        excs.append(le)
6356 
6357     # Okay, now we've recursively removed everything, ignoring any "No
6358     # such file or directory" errors, and collecting any other errors.
6359hunk ./src/allmydata/util/fileutil.py 256
6360             raise OSError, "Failed to remove dir for unknown reason."
6361         raise OSError, excs
6362 
6363+def fp_remove(fp):
6364+    """
6365+    An idempotent version of shutil.rmtree().  If the file/dir is already
6366+    gone, do nothing and return without raising an exception.  If this call
6367+    removes the file/dir, return without raising an exception.  If there is
6368+    an error that prevents removal, or if a file or directory at the same
6369+    path gets created again by someone else after this deletes it and before
6370+    this checks that it is gone, raise an exception.
6371+    """
6372+    try:
6373+        fp.remove()
6374+    except UnlistableError, e:
6375+        if e.originalException.errno != errno.ENOENT:
6376+            raise
6377+    except OSError, e:
6378+        if e.errno != errno.ENOENT:
6379+            raise
6380+
6381+def rm_dir(dirname):
6382+    # Renamed to be like shutil.rmtree and unlike rmdir.
6383+    return rmtree(dirname)
6384 
6385 def remove_if_possible(f):
6386     try:
6387hunk ./src/allmydata/util/fileutil.py 387
6388         import traceback
6389         traceback.print_exc()
6390 
6391-def get_disk_stats(whichdir, reserved_space=0):
6392+def get_disk_stats(whichdirfp, reserved_space=0):
6393     """Return disk statistics for the storage disk, in the form of a dict
6394     with the following fields.
6395       total:            total bytes on disk
6396hunk ./src/allmydata/util/fileutil.py 408
6397     you can pass how many bytes you would like to leave unused on this
6398     filesystem as reserved_space.
6399     """
6400+    precondition(isinstance(whichdirfp, FilePath), whichdirfp)
6401 
6402     if have_GetDiskFreeSpaceExW:
6403         # If this is a Windows system and GetDiskFreeSpaceExW is available, use it.
6404hunk ./src/allmydata/util/fileutil.py 419
6405         n_free_for_nonroot = c_ulonglong(0)
6406         n_total            = c_ulonglong(0)
6407         n_free_for_root    = c_ulonglong(0)
6408-        retval = GetDiskFreeSpaceExW(whichdir, byref(n_free_for_nonroot),
6409-                                               byref(n_total),
6410-                                               byref(n_free_for_root))
6411+        retval = GetDiskFreeSpaceExW(whichdirfp.path, byref(n_free_for_nonroot),
6412+                                                      byref(n_total),
6413+                                                      byref(n_free_for_root))
6414         if retval == 0:
6415             raise OSError("Windows error %d attempting to get disk statistics for %r"
6416hunk ./src/allmydata/util/fileutil.py 424
6417-                          % (GetLastError(), whichdir))
6418+                          % (GetLastError(), whichdirfp.path))
6419         free_for_nonroot = n_free_for_nonroot.value
6420         total            = n_total.value
6421         free_for_root    = n_free_for_root.value
6422hunk ./src/allmydata/util/fileutil.py 433
6423         # <http://docs.python.org/library/os.html#os.statvfs>
6424         # <http://opengroup.org/onlinepubs/7990989799/xsh/fstatvfs.html>
6425         # <http://opengroup.org/onlinepubs/7990989799/xsh/sysstatvfs.h.html>
6426-        s = os.statvfs(whichdir)
6427+        s = os.statvfs(whichdirfp.path)
6428 
6429         # on my mac laptop:
6430         #  statvfs(2) is a wrapper around statfs(2).
6431hunk ./src/allmydata/util/fileutil.py 460
6432              'avail': avail,
6433            }
6434 
6435-def get_available_space(whichdir, reserved_space):
6436+def get_available_space(whichdirfp, reserved_space):
6437     """Returns available space for share storage in bytes, or None if no
6438     API to get this information is available.
6439 
6440hunk ./src/allmydata/util/fileutil.py 472
6441     you can pass how many bytes you would like to leave unused on this
6442     filesystem as reserved_space.
6443     """
6444+    precondition(isinstance(whichdirfp, FilePath), whichdirfp)
6445     try:
6446hunk ./src/allmydata/util/fileutil.py 474
6447-        return get_disk_stats(whichdir, reserved_space)['avail']
6448+        return get_disk_stats(whichdirfp, reserved_space)['avail']
6449     except AttributeError:
6450         return None
6451hunk ./src/allmydata/util/fileutil.py 477
6452-    except EnvironmentError:
6453-        log.msg("OS call to get disk statistics failed")
6454+
6455+
6456+def get_used_space(fp):
6457+    if fp is None:
6458         return 0
6459hunk ./src/allmydata/util/fileutil.py 482
6460+    try:
6461+        s = os.stat(fp.path)
6462+    except EnvironmentError:
6463+        if not fp.exists():
6464+            return 0
6465+        raise
6466+    else:
6467+        # POSIX defines st_blocks (originally a BSDism):
6468+        #   <http://pubs.opengroup.org/onlinepubs/009695399/basedefs/sys/stat.h.html>
6469+        # but does not require stat() to give it a "meaningful value"
6470+        #   <http://pubs.opengroup.org/onlinepubs/009695399/functions/stat.html>
6471+        # and says:
6472+        #   "The unit for the st_blocks member of the stat structure is not defined
6473+        #    within IEEE Std 1003.1-2001. In some implementations it is 512 bytes.
6474+        #    It may differ on a file system basis. There is no correlation between
6475+        #    values of the st_blocks and st_blksize, and the f_bsize (from <sys/statvfs.h>)
6476+        #    structure members."
6477+        #
6478+        # The Linux docs define it as "the number of blocks allocated to the file,
6479+        # [in] 512-byte units." It is also defined that way on MacOS X. Python does
6480+        # not set the attribute on Windows.
6481+        #
6482+        # We consider platforms that define st_blocks but give it a wrong value, or
6483+        # measure it in a unit other than 512 bytes, to be broken. See also
6484+        # <http://bugs.python.org/issue12350>.
6485+
6486+        if hasattr(s, 'st_blocks'):
6487+            return s.st_blocks * 512
6488+        else:
6489+            return s.st_size
6490}
6491[Work-in-progress, includes fix to bug involving BucketWriter. refs #999
6492david-sarah@jacaranda.org**20110920033803
6493 Ignore-this: 64e9e019421454e4d08141d10b6e4eed
6494] {
6495hunk ./src/allmydata/client.py 9
6496 from twisted.internet import reactor, defer
6497 from twisted.application import service
6498 from twisted.application.internet import TimerService
6499+from twisted.python.filepath import FilePath
6500 from foolscap.api import Referenceable
6501 from pycryptopp.publickey import rsa
6502 
6503hunk ./src/allmydata/client.py 15
6504 import allmydata
6505 from allmydata.storage.server import StorageServer
6506+from allmydata.storage.backends.disk.disk_backend import DiskBackend
6507 from allmydata import storage_client
6508 from allmydata.immutable.upload import Uploader
6509 from allmydata.immutable.offloaded import Helper
6510hunk ./src/allmydata/client.py 213
6511             return
6512         readonly = self.get_config("storage", "readonly", False, boolean=True)
6513 
6514-        storedir = os.path.join(self.basedir, self.STOREDIR)
6515+        storedir = FilePath(self.basedir).child(self.STOREDIR)
6516 
6517         data = self.get_config("storage", "reserved_space", None)
6518         reserved = None
6519hunk ./src/allmydata/client.py 255
6520             'cutoff_date': cutoff_date,
6521             'sharetypes': tuple(sharetypes),
6522         }
6523-        ss = StorageServer(storedir, self.nodeid,
6524-                           reserved_space=reserved,
6525-                           discard_storage=discard,
6526-                           readonly_storage=readonly,
6527+
6528+        backend = DiskBackend(storedir, readonly=readonly, reserved_space=reserved,
6529+                              discard_storage=discard)
6530+        ss = StorageServer(nodeid, backend, storedir,
6531                            stats_provider=self.stats_provider,
6532                            expiration_policy=expiration_policy)
6533         self.add_service(ss)
6534hunk ./src/allmydata/interfaces.py 348
6535 
6536     def get_shares():
6537         """
6538-        Generates the IStoredShare objects held in this shareset.
6539+        Generates IStoredShare objects for all completed shares in this shareset.
6540         """
6541 
6542     def has_incoming(shnum):
6543hunk ./src/allmydata/storage/backends/base.py 69
6544         # def _create_mutable_share(self, storageserver, shnum, write_enabler):
6545         #     """create a mutable share with the given shnum and write_enabler"""
6546 
6547-        # secrets might be a triple with cancel_secret in secrets[2], but if
6548-        # so we ignore the cancel_secret.
6549         write_enabler = secrets[0]
6550         renew_secret = secrets[1]
6551hunk ./src/allmydata/storage/backends/base.py 71
6552+        cancel_secret = '\x00'*32
6553+        if len(secrets) > 2:
6554+            cancel_secret = secrets[2]
6555 
6556         si_s = self.get_storage_index_string()
6557         shares = {}
6558hunk ./src/allmydata/storage/backends/base.py 110
6559             read_data[shnum] = share.readv(read_vector)
6560 
6561         ownerid = 1 # TODO
6562-        lease_info = LeaseInfo(ownerid, renew_secret,
6563+        lease_info = LeaseInfo(ownerid, renew_secret, cancel_secret,
6564                                expiration_time, storageserver.get_serverid())
6565 
6566         if testv_is_good:
6567hunk ./src/allmydata/storage/backends/disk/disk_backend.py 34
6568     return newfp.child(sia)
6569 
6570 
6571-def get_share(fp):
6572+def get_share(storageindex, shnum, fp):
6573     f = fp.open('rb')
6574     try:
6575         prefix = f.read(32)
6576hunk ./src/allmydata/storage/backends/disk/disk_backend.py 42
6577         f.close()
6578 
6579     if prefix == MutableDiskShare.MAGIC:
6580-        return MutableDiskShare(fp)
6581+        return MutableDiskShare(storageindex, shnum, fp)
6582     else:
6583         # assume it's immutable
6584hunk ./src/allmydata/storage/backends/disk/disk_backend.py 45
6585-        return ImmutableDiskShare(fp)
6586+        return ImmutableDiskShare(storageindex, shnum, fp)
6587 
6588 
6589 class DiskBackend(Backend):
6590hunk ./src/allmydata/storage/backends/disk/disk_backend.py 174
6591                 if not NUM_RE.match(shnumstr):
6592                     continue
6593                 sharehome = self._sharehomedir.child(shnumstr)
6594-                yield self.get_share(sharehome)
6595+                yield get_share(self.get_storage_index(), int(shnumstr), sharehome)
6596         except UnlistableError:
6597             # There is no shares directory at all.
6598             pass
6599hunk ./src/allmydata/storage/backends/disk/disk_backend.py 185
6600         return self._incominghomedir.child(str(shnum)).exists()
6601 
6602     def make_bucket_writer(self, storageserver, shnum, max_space_per_bucket, lease_info, canary):
6603-        sharehome = self._sharehomedir.child(str(shnum))
6604+        finalhome = self._sharehomedir.child(str(shnum))
6605         incominghome = self._incominghomedir.child(str(shnum))
6606hunk ./src/allmydata/storage/backends/disk/disk_backend.py 187
6607-        immsh = ImmutableDiskShare(self.get_storage_index(), shnum, sharehome, incominghome,
6608-                                   max_size=max_space_per_bucket, create=True)
6609+        immsh = ImmutableDiskShare(self.get_storage_index(), shnum, incominghome, finalhome,
6610+                                   max_size=max_space_per_bucket)
6611         bw = BucketWriter(storageserver, immsh, max_space_per_bucket, lease_info, canary)
6612         if self._discard_storage:
6613             bw.throw_out_all_data = True
6614hunk ./src/allmydata/storage/backends/disk/disk_backend.py 198
6615         fileutil.fp_make_dirs(self._sharehomedir)
6616         sharehome = self._sharehomedir.child(str(shnum))
6617         serverid = storageserver.get_serverid()
6618-        return create_mutable_disk_share(sharehome, serverid, write_enabler, storageserver)
6619+        return create_mutable_disk_share(self.get_storage_index(), shnum, sharehome, serverid, write_enabler, storageserver)
6620 
6621     def _clean_up_after_unlink(self):
6622         fileutil.fp_rmdir_if_empty(self._sharehomedir)
6623hunk ./src/allmydata/storage/backends/disk/immutable.py 48
6624     LEASE_SIZE = struct.calcsize(">L32s32sL")
6625 
6626 
6627-    def __init__(self, storageindex, shnum, finalhome=None, incominghome=None, max_size=None, create=False):
6628-        """ If max_size is not None then I won't allow more than
6629-        max_size to be written to me. If create=True then max_size
6630-        must not be None. """
6631-        precondition((max_size is not None) or (not create), max_size, create)
6632+    def __init__(self, storageindex, shnum, home, finalhome=None, max_size=None):
6633+        """
6634+        If max_size is not None then I won't allow more than max_size to be written to me.
6635+        If finalhome is not None (meaning that we are creating the share) then max_size
6636+        must not be None.
6637+        """
6638+        precondition((max_size is not None) or (finalhome is None), max_size, finalhome)
6639         self._storageindex = storageindex
6640         self._max_size = max_size
6641hunk ./src/allmydata/storage/backends/disk/immutable.py 57
6642-        self._incominghome = incominghome
6643-        self._home = finalhome
6644+
6645+        # If we are creating the share, _finalhome refers to the final path and
6646+        # _home to the incoming path. Otherwise, _finalhome is None.
6647+        self._finalhome = finalhome
6648+        self._home = home
6649         self._shnum = shnum
6650hunk ./src/allmydata/storage/backends/disk/immutable.py 63
6651-        if create:
6652-            # touch the file, so later callers will see that we're working on
6653+
6654+        if self._finalhome is not None:
6655+            # Touch the file, so later callers will see that we're working on
6656             # it. Also construct the metadata.
6657hunk ./src/allmydata/storage/backends/disk/immutable.py 67
6658-            assert not finalhome.exists()
6659-            fp_make_dirs(self._incominghome.parent())
6660+            assert not self._finalhome.exists()
6661+            fp_make_dirs(self._home.parent())
6662             # The second field -- the four-byte share data length -- is no
6663             # longer used as of Tahoe v1.3.0, but we continue to write it in
6664             # there in case someone downgrades a storage server from >=
6665hunk ./src/allmydata/storage/backends/disk/immutable.py 78
6666             # the largest length that can fit into the field. That way, even
6667             # if this does happen, the old < v1.3.0 server will still allow
6668             # clients to read the first part of the share.
6669-            self._incominghome.setContent(struct.pack(">LLL", 1, min(2**32-1, max_size), 0) )
6670+            self._home.setContent(struct.pack(">LLL", 1, min(2**32-1, max_size), 0) )
6671             self._lease_offset = max_size + 0x0c
6672             self._num_leases = 0
6673         else:
6674hunk ./src/allmydata/storage/backends/disk/immutable.py 101
6675                 % (si_b2a(self._storageindex), self._shnum, quote_filepath(self._home)))
6676 
6677     def close(self):
6678-        fileutil.fp_make_dirs(self._home.parent())
6679-        self._incominghome.moveTo(self._home)
6680-        try:
6681-            # self._incominghome is like storage/shares/incoming/ab/abcde/4 .
6682-            # We try to delete the parent (.../ab/abcde) to avoid leaving
6683-            # these directories lying around forever, but the delete might
6684-            # fail if we're working on another share for the same storage
6685-            # index (like ab/abcde/5). The alternative approach would be to
6686-            # use a hierarchy of objects (PrefixHolder, BucketHolder,
6687-            # ShareWriter), each of which is responsible for a single
6688-            # directory on disk, and have them use reference counting of
6689-            # their children to know when they should do the rmdir. This
6690-            # approach is simpler, but relies on os.rmdir refusing to delete
6691-            # a non-empty directory. Do *not* use fileutil.fp_remove() here!
6692-            fileutil.fp_rmdir_if_empty(self._incominghome.parent())
6693-            # we also delete the grandparent (prefix) directory, .../ab ,
6694-            # again to avoid leaving directories lying around. This might
6695-            # fail if there is another bucket open that shares a prefix (like
6696-            # ab/abfff).
6697-            fileutil.fp_rmdir_if_empty(self._incominghome.parent().parent())
6698-            # we leave the great-grandparent (incoming/) directory in place.
6699-        except EnvironmentError:
6700-            # ignore the "can't rmdir because the directory is not empty"
6701-            # exceptions, those are normal consequences of the
6702-            # above-mentioned conditions.
6703-            pass
6704-        pass
6705+        fileutil.fp_make_dirs(self._finalhome.parent())
6706+        self._home.moveTo(self._finalhome)
6707+
6708+        # self._home is like storage/shares/incoming/ab/abcde/4 .
6709+        # We try to delete the parent (.../ab/abcde) to avoid leaving
6710+        # these directories lying around forever, but the delete might
6711+        # fail if we're working on another share for the same storage
6712+        # index (like ab/abcde/5). The alternative approach would be to
6713+        # use a hierarchy of objects (PrefixHolder, BucketHolder,
6714+        # ShareWriter), each of which is responsible for a single
6715+        # directory on disk, and have them use reference counting of
6716+        # their children to know when they should do the rmdir. This
6717+        # approach is simpler, but relies on os.rmdir (used by
6718+        # fp_rmdir_if_empty) refusing to delete a non-empty directory.
6719+        # Do *not* use fileutil.fp_remove() here!
6720+        parent = self._home.parent()
6721+        fileutil.fp_rmdir_if_empty(parent)
6722+
6723+        # we also delete the grandparent (prefix) directory, .../ab ,
6724+        # again to avoid leaving directories lying around. This might
6725+        # fail if there is another bucket open that shares a prefix (like
6726+        # ab/abfff).
6727+        fileutil.fp_rmdir_if_empty(parent.parent())
6728+
6729+        # we leave the great-grandparent (incoming/) directory in place.
6730+
6731+        # allow lease changes after closing.
6732+        self._home = self._finalhome
6733+        self._finalhome = None
6734 
6735     def get_used_space(self):
6736hunk ./src/allmydata/storage/backends/disk/immutable.py 132
6737-        return (fileutil.get_used_space(self._home) +
6738-                fileutil.get_used_space(self._incominghome))
6739+        return (fileutil.get_used_space(self._finalhome) +
6740+                fileutil.get_used_space(self._home))
6741 
6742     def get_storage_index(self):
6743         return self._storageindex
6744hunk ./src/allmydata/storage/backends/disk/immutable.py 175
6745         precondition(offset >= 0, offset)
6746         if self._max_size is not None and offset+length > self._max_size:
6747             raise DataTooLargeError(self._max_size, offset, length)
6748-        f = self._incominghome.open(mode='rb+')
6749+        f = self._home.open(mode='rb+')
6750         try:
6751             real_offset = self._data_offset+offset
6752             f.seek(real_offset)
6753hunk ./src/allmydata/storage/backends/disk/immutable.py 205
6754 
6755     # These lease operations are intended for use by disk_backend.py.
6756     # Other clients should not depend on the fact that the disk backend
6757-    # stores leases in share files.
6758+    # stores leases in share files. XXX bucket.py also relies on this.
6759 
6760     def get_leases(self):
6761         """Yields a LeaseInfo instance for all leases."""
6762hunk ./src/allmydata/storage/backends/disk/immutable.py 221
6763             f.close()
6764 
6765     def add_lease(self, lease_info):
6766-        f = self._incominghome.open(mode='rb')
6767+        f = self._home.open(mode='rb+')
6768         try:
6769             num_leases = self._read_num_leases(f)
6770hunk ./src/allmydata/storage/backends/disk/immutable.py 224
6771-        finally:
6772-            f.close()
6773-        f = self._home.open(mode='wb+')
6774-        try:
6775             self._write_lease_record(f, num_leases, lease_info)
6776             self._write_num_leases(f, num_leases+1)
6777         finally:
6778hunk ./src/allmydata/storage/backends/disk/mutable.py 440
6779         pass
6780 
6781 
6782-def create_mutable_disk_share(fp, serverid, write_enabler, parent):
6783-    ms = MutableDiskShare(fp, parent)
6784+def create_mutable_disk_share(storageindex, shnum, fp, serverid, write_enabler, parent):
6785+    ms = MutableDiskShare(storageindex, shnum, fp, parent)
6786     ms.create(serverid, write_enabler)
6787     del ms
6788hunk ./src/allmydata/storage/backends/disk/mutable.py 444
6789-    return MutableDiskShare(fp, parent)
6790+    return MutableDiskShare(storageindex, shnum, fp, parent)
6791hunk ./src/allmydata/storage/bucket.py 44
6792         start = time.time()
6793 
6794         self._share.close()
6795-        filelen = self._share.stat()
6796+        # XXX should this be self._share.get_used_space() ?
6797+        consumed_size = self._share.get_size()
6798         self._share = None
6799 
6800         self.closed = True
6801hunk ./src/allmydata/storage/bucket.py 51
6802         self._canary.dontNotifyOnDisconnect(self._disconnect_marker)
6803 
6804-        self.ss.bucket_writer_closed(self, filelen)
6805+        self.ss.bucket_writer_closed(self, consumed_size)
6806         self.ss.add_latency("close", time.time() - start)
6807         self.ss.count("close")
6808 
6809hunk ./src/allmydata/storage/server.py 182
6810                                 renew_secret, cancel_secret,
6811                                 sharenums, allocated_size,
6812                                 canary, owner_num=0):
6813-        # cancel_secret is no longer used.
6814         # owner_num is not for clients to set, but rather it should be
6815         # curried into a StorageServer instance dedicated to a particular
6816         # owner.
6817hunk ./src/allmydata/storage/server.py 195
6818         # Note that the lease should not be added until the BucketWriter
6819         # has been closed.
6820         expire_time = time.time() + 31*24*60*60
6821-        lease_info = LeaseInfo(owner_num, renew_secret,
6822+        lease_info = LeaseInfo(owner_num, renew_secret, cancel_secret,
6823                                expire_time, self._serverid)
6824 
6825         max_space_per_bucket = allocated_size
6826hunk ./src/allmydata/test/no_network.py 349
6827         return self.g.servers_by_number[i]
6828 
6829     def get_serverdir(self, i):
6830-        return self.g.servers_by_number[i].backend.storedir
6831+        return self.g.servers_by_number[i].backend._storedir
6832 
6833     def remove_server(self, i):
6834         self.g.remove_server(self.g.servers_by_number[i].get_serverid())
6835hunk ./src/allmydata/test/no_network.py 357
6836     def iterate_servers(self):
6837         for i in sorted(self.g.servers_by_number.keys()):
6838             ss = self.g.servers_by_number[i]
6839-            yield (i, ss, ss.backend.storedir)
6840+            yield (i, ss, ss.backend._storedir)
6841 
6842     def find_uri_shares(self, uri):
6843         si = tahoe_uri.from_string(uri).get_storage_index()
6844hunk ./src/allmydata/test/no_network.py 384
6845         return shares
6846 
6847     def copy_share(self, from_share, uri, to_server):
6848-        si = uri.from_string(self.uri).get_storage_index()
6849+        si = tahoe_uri.from_string(uri).get_storage_index()
6850         (i_shnum, i_serverid, i_sharefp) = from_share
6851         shares_dir = to_server.backend.get_shareset(si)._sharehomedir
6852         i_sharefp.copyTo(shares_dir.child(str(i_shnum)))
6853hunk ./src/allmydata/test/test_download.py 127
6854 
6855         return d
6856 
6857-    def _write_shares(self, uri, shares):
6858-        si = uri.from_string(uri).get_storage_index()
6859+    def _write_shares(self, fileuri, shares):
6860+        si = uri.from_string(fileuri).get_storage_index()
6861         for i in shares:
6862             shares_for_server = shares[i]
6863             for shnum in shares_for_server:
6864hunk ./src/allmydata/test/test_hung_server.py 36
6865 
6866     def _hang(self, servers, **kwargs):
6867         for ss in servers:
6868-            self.g.hang_server(ss.get_serverid(), **kwargs)
6869+            self.g.hang_server(ss.original.get_serverid(), **kwargs)
6870 
6871     def _unhang(self, servers, **kwargs):
6872         for ss in servers:
6873hunk ./src/allmydata/test/test_hung_server.py 40
6874-            self.g.unhang_server(ss.get_serverid(), **kwargs)
6875+            self.g.unhang_server(ss.original.get_serverid(), **kwargs)
6876 
6877     def _hang_shares(self, shnums, **kwargs):
6878         # hang all servers who are holding the given shares
6879hunk ./src/allmydata/test/test_hung_server.py 52
6880                     hung_serverids.add(i_serverid)
6881 
6882     def _delete_all_shares_from(self, servers):
6883-        serverids = [ss.get_serverid() for ss in servers]
6884+        serverids = [ss.original.get_serverid() for ss in servers]
6885         for (i_shnum, i_serverid, i_sharefp) in self.shares:
6886             if i_serverid in serverids:
6887                 i_sharefp.remove()
6888hunk ./src/allmydata/test/test_hung_server.py 58
6889 
6890     def _corrupt_all_shares_in(self, servers, corruptor_func):
6891-        serverids = [ss.get_serverid() for ss in servers]
6892+        serverids = [ss.original.get_serverid() for ss in servers]
6893         for (i_shnum, i_serverid, i_sharefp) in self.shares:
6894             if i_serverid in serverids:
6895                 self.corrupt_share((i_shnum, i_serverid, i_sharefp), corruptor_func)
6896hunk ./src/allmydata/test/test_hung_server.py 64
6897 
6898     def _copy_all_shares_from(self, from_servers, to_server):
6899-        serverids = [ss.get_serverid() for ss in from_servers]
6900+        serverids = [ss.original.get_serverid() for ss in from_servers]
6901         for (i_shnum, i_serverid, i_sharefp) in self.shares:
6902             if i_serverid in serverids:
6903                 self.copy_share((i_shnum, i_serverid, i_sharefp), self.uri, to_server)
6904hunk ./src/allmydata/test/test_mutable.py 2991
6905             fso = debug.FindSharesOptions()
6906             storage_index = base32.b2a(n.get_storage_index())
6907             fso.si_s = storage_index
6908-            fso.nodedirs = [unicode(os.path.dirname(os.path.abspath(storedir)))
6909+            fso.nodedirs = [unicode(storedir.parent().path)
6910                             for (i,ss,storedir)
6911                             in self.iterate_servers()]
6912             fso.stdout = StringIO()
6913hunk ./src/allmydata/test/test_upload.py 818
6914         if share_number is not None:
6915             self._copy_share_to_server(share_number, server_number)
6916 
6917-
6918     def _copy_share_to_server(self, share_number, server_number):
6919         ss = self.g.servers_by_number[server_number]
6920hunk ./src/allmydata/test/test_upload.py 820
6921-        self.copy_share(self.shares[share_number], ss)
6922+        self.copy_share(self.shares[share_number], self.uri, ss)
6923 
6924     def _setup_grid(self):
6925         """
6926}
6927[docs/backends: document the configuration options for the pluggable backends scheme. refs #999
6928david-sarah@jacaranda.org**20110920171737
6929 Ignore-this: 5947e864682a43cb04e557334cda7c19
6930] {
6931adddir ./docs/backends
6932addfile ./docs/backends/S3.rst
6933hunk ./docs/backends/S3.rst 1
6934+====================================================
6935+Storing Shares in Amazon Simple Storage Service (S3)
6936+====================================================
6937+
6938+S3 is a commercial storage service provided by Amazon, described at
6939+`<https://aws.amazon.com/s3/>`_.
6940+
6941+The Tahoe-LAFS storage server can be configured to store its shares in
6942+an S3 bucket, rather than on local filesystem. To enable this, add the
6943+following keys to the server's ``tahoe.cfg`` file:
6944+
6945+``[storage]``
6946+
6947+``backend = s3``
6948+
6949+    This turns off the local filesystem backend and enables use of S3.
6950+
6951+``s3.access_key_id = (string, required)``
6952+``s3.secret_access_key = (string, required)``
6953+
6954+    These two give the storage server permission to access your Amazon
6955+    Web Services account, allowing them to upload and download shares
6956+    from S3.
6957+
6958+``s3.bucket = (string, required)``
6959+
6960+    This controls which bucket will be used to hold shares. The Tahoe-LAFS
6961+    storage server will only modify and access objects in the configured S3
6962+    bucket.
6963+
6964+``s3.url = (URL string, optional)``
6965+
6966+    This URL tells the storage server how to access the S3 service. It
6967+    defaults to ``http://s3.amazonaws.com``, but by setting it to something
6968+    else, you may be able to use some other S3-like service if it is
6969+    sufficiently compatible.
6970+
6971+``s3.max_space = (str, optional)``
6972+
6973+    This tells the server to limit how much space can be used in the S3
6974+    bucket. Before each share is uploaded, the server will ask S3 for the
6975+    current bucket usage, and will only accept the share if it does not cause
6976+    the usage to grow above this limit.
6977+
6978+    The string contains a number, with an optional case-insensitive scale
6979+    suffix like "K" or "M" or "G", and an optional "B" or "iB" suffix. So
6980+    "100MB", "100M", "100000000B", "100000000", and "100000kb" all mean the
6981+    same thing. Likewise, "1MiB", "1024KiB", and "1048576B" all mean the same
6982+    thing.
6983+
6984+    If ``s3.max_space`` is omitted, the default behavior is to allow
6985+    unlimited usage.
6986+
6987+
6988+Once configured, the WUI "storage server" page will provide information about
6989+how much space is being used and how many shares are being stored.
6990+
6991+
6992+Issues
6993+------
6994+
6995+Objects in an S3 bucket cannot be read for free. As a result, when Tahoe-LAFS
6996+is configured to store shares in S3 rather than on local disk, some common
6997+operations may behave differently:
6998+
6999+* Lease crawling/expiration is not yet implemented. As a result, shares will
7000+  be retained forever, and the Storage Server status web page will not show
7001+  information about the number of mutable/immutable shares present.
7002+
7003+* Enabling ``s3.max_space`` causes an extra S3 usage query to be sent for
7004+  each share upload, causing the upload process to run slightly slower and
7005+  incur more S3 request charges.
7006addfile ./docs/backends/disk.rst
7007hunk ./docs/backends/disk.rst 1
7008+====================================
7009+Storing Shares on a Local Filesystem
7010+====================================
7011+
7012+The "disk" backend stores shares on the local filesystem. Versions of
7013+Tahoe-LAFS <= 1.9.0 always stored shares in this way.
7014+
7015+``[storage]``
7016+
7017+``backend = disk``
7018+
7019+    This enables use of the disk backend, and is the default.
7020+
7021+``reserved_space = (str, optional)``
7022+
7023+    If provided, this value defines how much disk space is reserved: the
7024+    storage server will not accept any share that causes the amount of free
7025+    disk space to drop below this value. (The free space is measured by a
7026+    call to statvfs(2) on Unix, or GetDiskFreeSpaceEx on Windows, and is the
7027+    space available to the user account under which the storage server runs.)
7028+
7029+    This string contains a number, with an optional case-insensitive scale
7030+    suffix like "K" or "M" or "G", and an optional "B" or "iB" suffix. So
7031+    "100MB", "100M", "100000000B", "100000000", and "100000kb" all mean the
7032+    same thing. Likewise, "1MiB", "1024KiB", and "1048576B" all mean the same
7033+    thing.
7034+
7035+    "``tahoe create-node``" generates a tahoe.cfg with
7036+    "``reserved_space=1G``", but you may wish to raise, lower, or remove the
7037+    reservation to suit your needs.
7038+
7039+``expire.enabled =``
7040+
7041+``expire.mode =``
7042+
7043+``expire.override_lease_duration =``
7044+
7045+``expire.cutoff_date =``
7046+
7047+``expire.immutable =``
7048+
7049+``expire.mutable =``
7050+
7051+    These settings control garbage collection, causing the server to
7052+    delete shares that no longer have an up-to-date lease on them. Please
7053+    see `<garbage-collection.rst>`_ for full details.
7054hunk ./docs/configuration.rst 412
7055     <http://tahoe-lafs.org/trac/tahoe-lafs/ticket/390>`_ for the current
7056     status of this bug. The default value is ``False``.
7057 
7058-``reserved_space = (str, optional)``
7059+``backend = (string, optional)``
7060 
7061hunk ./docs/configuration.rst 414
7062-    If provided, this value defines how much disk space is reserved: the
7063-    storage server will not accept any share that causes the amount of free
7064-    disk space to drop below this value. (The free space is measured by a
7065-    call to statvfs(2) on Unix, or GetDiskFreeSpaceEx on Windows, and is the
7066-    space available to the user account under which the storage server runs.)
7067+    Storage servers can store the data into different "backends". Clients
7068+    need not be aware of which backend is used by a server. The default
7069+    value is ``disk``.
7070 
7071hunk ./docs/configuration.rst 418
7072-    This string contains a number, with an optional case-insensitive scale
7073-    suffix like "K" or "M" or "G", and an optional "B" or "iB" suffix. So
7074-    "100MB", "100M", "100000000B", "100000000", and "100000kb" all mean the
7075-    same thing. Likewise, "1MiB", "1024KiB", and "1048576B" all mean the same
7076-    thing.
7077+``backend = disk``
7078 
7079hunk ./docs/configuration.rst 420
7080-    "``tahoe create-node``" generates a tahoe.cfg with
7081-    "``reserved_space=1G``", but you may wish to raise, lower, or remove the
7082-    reservation to suit your needs.
7083+    The default is to store shares on the local filesystem (in
7084+    BASEDIR/storage/shares/). For configuration details (including how to
7085+    reserve a minimum amount of free space), see `<backends/disk.rst>`_.
7086 
7087hunk ./docs/configuration.rst 424
7088-``expire.enabled =``
7089+``backend = S3``
7090 
7091hunk ./docs/configuration.rst 426
7092-``expire.mode =``
7093-
7094-``expire.override_lease_duration =``
7095-
7096-``expire.cutoff_date =``
7097-
7098-``expire.immutable =``
7099-
7100-``expire.mutable =``
7101-
7102-    These settings control garbage collection, in which the server will
7103-    delete shares that no longer have an up-to-date lease on them. Please see
7104-    `<garbage-collection.rst>`_ for full details.
7105+    The storage server can store all shares to an Amazon Simple Storage
7106+    Service (S3) bucket. For configuration details, see `<backends/S3.rst>`_.
7107 
7108 
7109 Running A Helper
7110}
7111[Fix some incorrect attribute accesses. refs #999
7112david-sarah@jacaranda.org**20110921031207
7113 Ignore-this: f1ea4c3ea191f6d4b719afaebd2b2bcd
7114] {
7115hunk ./src/allmydata/client.py 258
7116 
7117         backend = DiskBackend(storedir, readonly=readonly, reserved_space=reserved,
7118                               discard_storage=discard)
7119-        ss = StorageServer(nodeid, backend, storedir,
7120+        ss = StorageServer(self.nodeid, backend, storedir,
7121                            stats_provider=self.stats_provider,
7122                            expiration_policy=expiration_policy)
7123         self.add_service(ss)
7124hunk ./src/allmydata/interfaces.py 449
7125         Returns the storage index.
7126         """
7127 
7128+    def get_storage_index_string():
7129+        """
7130+        Returns the base32-encoded storage index.
7131+        """
7132+
7133     def get_shnum():
7134         """
7135         Returns the share number.
7136hunk ./src/allmydata/storage/backends/disk/immutable.py 138
7137     def get_storage_index(self):
7138         return self._storageindex
7139 
7140+    def get_storage_index_string(self):
7141+        return si_b2a(self._storageindex)
7142+
7143     def get_shnum(self):
7144         return self._shnum
7145 
7146hunk ./src/allmydata/storage/backends/disk/mutable.py 119
7147     def get_storage_index(self):
7148         return self._storageindex
7149 
7150+    def get_storage_index_string(self):
7151+        return si_b2a(self._storageindex)
7152+
7153     def get_shnum(self):
7154         return self._shnum
7155 
7156hunk ./src/allmydata/storage/bucket.py 86
7157     def __init__(self, ss, share):
7158         self.ss = ss
7159         self._share = share
7160-        self.storageindex = share.storageindex
7161-        self.shnum = share.shnum
7162+        self.storageindex = share.get_storage_index()
7163+        self.shnum = share.get_shnum()
7164 
7165     def __repr__(self):
7166         return "<%s %s %s>" % (self.__class__.__name__,
7167hunk ./src/allmydata/storage/expirer.py 6
7168 from twisted.python import log as twlog
7169 
7170 from allmydata.storage.crawler import ShareCrawler
7171-from allmydata.storage.common import si_b2a, UnknownMutableContainerVersionError, \
7172+from allmydata.storage.common import UnknownMutableContainerVersionError, \
7173      UnknownImmutableContainerVersionError
7174 
7175 
7176hunk ./src/allmydata/storage/expirer.py 124
7177                     struct.error):
7178                 twlog.msg("lease-checker error processing %r" % (share,))
7179                 twlog.err()
7180-                which = (si_b2a(share.storageindex), share.get_shnum())
7181+                which = (share.get_storage_index_string(), share.get_shnum())
7182                 self.state["cycle-to-date"]["corrupt-shares"].append(which)
7183                 wks = (1, 1, 1, "unknown")
7184             would_keep_shares.append(wks)
7185hunk ./src/allmydata/storage/server.py 221
7186         alreadygot = set()
7187         for share in shareset.get_shares():
7188             share.add_or_renew_lease(lease_info)
7189-            alreadygot.add(share.shnum)
7190+            alreadygot.add(share.get_shnum())
7191 
7192         for shnum in sharenums - alreadygot:
7193             if shareset.has_incoming(shnum):
7194hunk ./src/allmydata/storage/server.py 324
7195 
7196         try:
7197             shareset = self.backend.get_shareset(storageindex)
7198-            return shareset.readv(self, shares, readv)
7199+            return shareset.readv(shares, readv)
7200         finally:
7201             self.add_latency("readv", time.time() - start)
7202 
7203hunk ./src/allmydata/storage/shares.py 1
7204-#! /usr/bin/python
7205-
7206-from allmydata.storage.mutable import MutableShareFile
7207-from allmydata.storage.immutable import ShareFile
7208-
7209-def get_share_file(filename):
7210-    f = open(filename, "rb")
7211-    prefix = f.read(32)
7212-    f.close()
7213-    if prefix == MutableShareFile.MAGIC:
7214-        return MutableShareFile(filename)
7215-    # otherwise assume it's immutable
7216-    return ShareFile(filename)
7217-
7218rmfile ./src/allmydata/storage/shares.py
7219hunk ./src/allmydata/test/no_network.py 387
7220         si = tahoe_uri.from_string(uri).get_storage_index()
7221         (i_shnum, i_serverid, i_sharefp) = from_share
7222         shares_dir = to_server.backend.get_shareset(si)._sharehomedir
7223+        fileutil.fp_make_dirs(shares_dir)
7224         i_sharefp.copyTo(shares_dir.child(str(i_shnum)))
7225 
7226     def restore_all_shares(self, shares):
7227hunk ./src/allmydata/test/no_network.py 391
7228-        for share, data in shares.items():
7229-            share.home.setContent(data)
7230+        for sharepath, data in shares.items():
7231+            FilePath(sharepath).setContent(data)
7232 
7233     def delete_share(self, (shnum, serverid, sharefp)):
7234         sharefp.remove()
7235hunk ./src/allmydata/test/test_upload.py 744
7236         servertoshnums = {} # k: server, v: set(shnum)
7237 
7238         for i, c in self.g.servers_by_number.iteritems():
7239-            for (dirp, dirns, fns) in os.walk(c.sharedir):
7240+            for (dirp, dirns, fns) in os.walk(c.backend._sharedir.path):
7241                 for fn in fns:
7242                     try:
7243                         sharenum = int(fn)
7244}
7245[docs/backends/S3.rst: remove Issues section. refs #999
7246david-sarah@jacaranda.org**20110921031625
7247 Ignore-this: c83d8f52b790bc32488869e6ee1df8c2
7248] hunk ./docs/backends/S3.rst 57
7249 
7250 Once configured, the WUI "storage server" page will provide information about
7251 how much space is being used and how many shares are being stored.
7252-
7253-
7254-Issues
7255-------
7256-
7257-Objects in an S3 bucket cannot be read for free. As a result, when Tahoe-LAFS
7258-is configured to store shares in S3 rather than on local disk, some common
7259-operations may behave differently:
7260-
7261-* Lease crawling/expiration is not yet implemented. As a result, shares will
7262-  be retained forever, and the Storage Server status web page will not show
7263-  information about the number of mutable/immutable shares present.
7264-
7265-* Enabling ``s3.max_space`` causes an extra S3 usage query to be sent for
7266-  each share upload, causing the upload process to run slightly slower and
7267-  incur more S3 request charges.
7268[docs/backends/S3.rst, disk.rst: describe type of space settings as 'quantity of space', not 'str'. refs #999
7269david-sarah@jacaranda.org**20110921031705
7270 Ignore-this: a74ed8e01b0a1ab5f07a1487d7bf138
7271] {
7272hunk ./docs/backends/S3.rst 38
7273     else, you may be able to use some other S3-like service if it is
7274     sufficiently compatible.
7275 
7276-``s3.max_space = (str, optional)``
7277+``s3.max_space = (quantity of space, optional)``
7278 
7279     This tells the server to limit how much space can be used in the S3
7280     bucket. Before each share is uploaded, the server will ask S3 for the
7281hunk ./docs/backends/disk.rst 14
7282 
7283     This enables use of the disk backend, and is the default.
7284 
7285-``reserved_space = (str, optional)``
7286+``reserved_space = (quantity of space, optional)``
7287 
7288     If provided, this value defines how much disk space is reserved: the
7289     storage server will not accept any share that causes the amount of free
7290}
7291[More fixes to tests needed for pluggable backends. refs #999
7292david-sarah@jacaranda.org**20110921184649
7293 Ignore-this: 9be0d3a98e350fd4e17a07d2c00bb4ca
7294] {
7295hunk ./src/allmydata/scripts/debug.py 8
7296 from twisted.python import usage, failure
7297 from twisted.internet import defer
7298 from twisted.scripts import trial as twisted_trial
7299+from twisted.python.filepath import FilePath
7300 
7301 
7302 class DumpOptions(usage.Options):
7303hunk ./src/allmydata/scripts/debug.py 38
7304         self['filename'] = argv_to_abspath(filename)
7305 
7306 def dump_share(options):
7307-    from allmydata.storage.mutable import MutableShareFile
7308+    from allmydata.storage.backends.disk.disk_backend import get_share
7309     from allmydata.util.encodingutil import quote_output
7310 
7311     out = options.stdout
7312hunk ./src/allmydata/scripts/debug.py 46
7313     # check the version, to see if we have a mutable or immutable share
7314     print >>out, "share filename: %s" % quote_output(options['filename'])
7315 
7316-    f = open(options['filename'], "rb")
7317-    prefix = f.read(32)
7318-    f.close()
7319-    if prefix == MutableShareFile.MAGIC:
7320-        return dump_mutable_share(options)
7321-    # otherwise assume it's immutable
7322-    return dump_immutable_share(options)
7323-
7324-def dump_immutable_share(options):
7325-    from allmydata.storage.immutable import ShareFile
7326+    share = get_share("", 0, fp)
7327+    if share.sharetype == "mutable":
7328+        return dump_mutable_share(options, share)
7329+    else:
7330+        assert share.sharetype == "immutable", share.sharetype
7331+        return dump_immutable_share(options)
7332 
7333hunk ./src/allmydata/scripts/debug.py 53
7334+def dump_immutable_share(options, share):
7335     out = options.stdout
7336hunk ./src/allmydata/scripts/debug.py 55
7337-    f = ShareFile(options['filename'])
7338     if not options["leases-only"]:
7339hunk ./src/allmydata/scripts/debug.py 56
7340-        dump_immutable_chk_share(f, out, options)
7341-    dump_immutable_lease_info(f, out)
7342+        dump_immutable_chk_share(share, out, options)
7343+    dump_immutable_lease_info(share, out)
7344     print >>out
7345     return 0
7346 
7347hunk ./src/allmydata/scripts/debug.py 166
7348     return when
7349 
7350 
7351-def dump_mutable_share(options):
7352-    from allmydata.storage.mutable import MutableShareFile
7353+def dump_mutable_share(options, m):
7354     from allmydata.util import base32, idlib
7355     out = options.stdout
7356hunk ./src/allmydata/scripts/debug.py 169
7357-    m = MutableShareFile(options['filename'])
7358     f = open(options['filename'], "rb")
7359     WE, nodeid = m._read_write_enabler_and_nodeid(f)
7360     num_extra_leases = m._read_num_extra_leases(f)
7361hunk ./src/allmydata/scripts/debug.py 641
7362     /home/warner/testnet/node-1/storage/shares/44k/44kai1tui348689nrw8fjegc8c/9
7363     /home/warner/testnet/node-2/storage/shares/44k/44kai1tui348689nrw8fjegc8c/2
7364     """
7365-    from allmydata.storage.server import si_a2b, storage_index_to_dir
7366-    from allmydata.util.encodingutil import listdir_unicode
7367+    from allmydata.storage.server import si_a2b
7368+    from allmydata.storage.backends.disk_backend import si_si2dir
7369+    from allmydata.util.encodingutil import quote_filepath
7370 
7371     out = options.stdout
7372hunk ./src/allmydata/scripts/debug.py 646
7373-    sharedir = storage_index_to_dir(si_a2b(options.si_s))
7374-    for d in options.nodedirs:
7375-        d = os.path.join(d, "storage/shares", sharedir)
7376-        if os.path.exists(d):
7377-            for shnum in listdir_unicode(d):
7378-                print >>out, os.path.join(d, shnum)
7379+    si = si_a2b(options.si_s)
7380+    for nodedir in options.nodedirs:
7381+        sharedir = si_si2dir(nodedir.child("storage").child("shares"), si)
7382+        if sharedir.exists():
7383+            for sharefp in sharedir.children():
7384+                print >>out, quote_filepath(sharefp, quotemarks=False)
7385 
7386     return 0
7387 
7388hunk ./src/allmydata/scripts/debug.py 878
7389         print >>err, "Error processing %s" % quote_output(si_dir)
7390         failure.Failure().printTraceback(err)
7391 
7392+
7393 class CorruptShareOptions(usage.Options):
7394     def getSynopsis(self):
7395         return "Usage: tahoe debug corrupt-share SHARE_FILENAME"
7396hunk ./src/allmydata/scripts/debug.py 902
7397 Obviously, this command should not be used in normal operation.
7398 """
7399         return t
7400+
7401     def parseArgs(self, filename):
7402         self['filename'] = filename
7403 
7404hunk ./src/allmydata/scripts/debug.py 907
7405 def corrupt_share(options):
7406+    do_corrupt_share(options.stdout, FilePath(options['filename']), options['offset'])
7407+
7408+def do_corrupt_share(out, fp, offset="block-random"):
7409     import random
7410hunk ./src/allmydata/scripts/debug.py 911
7411-    from allmydata.storage.mutable import MutableShareFile
7412-    from allmydata.storage.immutable import ShareFile
7413+    from allmydata.storage.backends.disk.mutable import MutableDiskShare
7414+    from allmydata.storage.backends.disk.immutable import ImmutableDiskShare
7415     from allmydata.mutable.layout import unpack_header
7416     from allmydata.immutable.layout import ReadBucketProxy
7417hunk ./src/allmydata/scripts/debug.py 915
7418-    out = options.stdout
7419-    fn = options['filename']
7420-    assert options["offset"] == "block-random", "other offsets not implemented"
7421+
7422+    assert offset == "block-random", "other offsets not implemented"
7423+
7424     # first, what kind of share is it?
7425 
7426     def flip_bit(start, end):
7427hunk ./src/allmydata/scripts/debug.py 924
7428         offset = random.randrange(start, end)
7429         bit = random.randrange(0, 8)
7430         print >>out, "[%d..%d):  %d.b%d" % (start, end, offset, bit)
7431-        f = open(fn, "rb+")
7432-        f.seek(offset)
7433-        d = f.read(1)
7434-        d = chr(ord(d) ^ 0x01)
7435-        f.seek(offset)
7436-        f.write(d)
7437-        f.close()
7438+        f = fp.open("rb+")
7439+        try:
7440+            f.seek(offset)
7441+            d = f.read(1)
7442+            d = chr(ord(d) ^ 0x01)
7443+            f.seek(offset)
7444+            f.write(d)
7445+        finally:
7446+            f.close()
7447 
7448hunk ./src/allmydata/scripts/debug.py 934
7449-    f = open(fn, "rb")
7450-    prefix = f.read(32)
7451-    f.close()
7452-    if prefix == MutableShareFile.MAGIC:
7453-        # mutable
7454-        m = MutableShareFile(fn)
7455-        f = open(fn, "rb")
7456-        f.seek(m.DATA_OFFSET)
7457-        data = f.read(2000)
7458-        # make sure this slot contains an SMDF share
7459-        assert data[0] == "\x00", "non-SDMF mutable shares not supported"
7460+    f = fp.open("rb")
7461+    try:
7462+        prefix = f.read(32)
7463+    finally:
7464         f.close()
7465hunk ./src/allmydata/scripts/debug.py 939
7466+    if prefix == MutableDiskShare.MAGIC:
7467+        # mutable
7468+        m = MutableDiskShare("", 0, fp)
7469+        f = fp.open("rb")
7470+        try:
7471+            f.seek(m.DATA_OFFSET)
7472+            data = f.read(2000)
7473+            # make sure this slot contains an SMDF share
7474+            assert data[0] == "\x00", "non-SDMF mutable shares not supported"
7475+        finally:
7476+            f.close()
7477 
7478         (version, ig_seqnum, ig_roothash, ig_IV, ig_k, ig_N, ig_segsize,
7479          ig_datalen, offsets) = unpack_header(data)
7480hunk ./src/allmydata/scripts/debug.py 960
7481         flip_bit(start, end)
7482     else:
7483         # otherwise assume it's immutable
7484-        f = ShareFile(fn)
7485+        f = ImmutableDiskShare("", 0, fp)
7486         bp = ReadBucketProxy(None, None, '')
7487         offsets = bp._parse_offsets(f.read_share_data(0, 0x24))
7488         start = f._data_offset + offsets["data"]
7489hunk ./src/allmydata/storage/backends/base.py 92
7490             (testv, datav, new_length) = test_and_write_vectors[sharenum]
7491             if sharenum in shares:
7492                 if not shares[sharenum].check_testv(testv):
7493-                    self.log("testv failed: [%d]: %r" % (sharenum, testv))
7494+                    storageserver.log("testv failed: [%d]: %r" % (sharenum, testv))
7495                     testv_is_good = False
7496                     break
7497             else:
7498hunk ./src/allmydata/storage/backends/base.py 99
7499                 # compare the vectors against an empty share, in which all
7500                 # reads return empty strings
7501                 if not EmptyShare().check_testv(testv):
7502-                    self.log("testv failed (empty): [%d] %r" % (sharenum,
7503-                                                                testv))
7504+                    storageserver.log("testv failed (empty): [%d] %r" % (sharenum, testv))
7505                     testv_is_good = False
7506                     break
7507 
7508hunk ./src/allmydata/test/test_cli.py 2892
7509             # delete one, corrupt a second
7510             shares = self.find_uri_shares(self.uri)
7511             self.failUnlessReallyEqual(len(shares), 10)
7512-            os.unlink(shares[0][2])
7513-            cso = debug.CorruptShareOptions()
7514-            cso.stdout = StringIO()
7515-            cso.parseOptions([shares[1][2]])
7516+            shares[0][2].remove()
7517+            stdout = StringIO()
7518+            sharefile = shares[1][2]
7519             storage_index = uri.from_string(self.uri).get_storage_index()
7520             self._corrupt_share_line = "  server %s, SI %s, shnum %d" % \
7521                                        (base32.b2a(shares[1][1]),
7522hunk ./src/allmydata/test/test_cli.py 2900
7523                                         base32.b2a(storage_index),
7524                                         shares[1][0])
7525-            debug.corrupt_share(cso)
7526+            debug.do_corrupt_share(stdout, sharefile)
7527         d.addCallback(_clobber_shares)
7528 
7529         d.addCallback(lambda ign: self.do_cli("check", "--verify", self.uri))
7530hunk ./src/allmydata/test/test_cli.py 3017
7531         def _clobber_shares(ignored):
7532             shares = self.find_uri_shares(self.uris[u"g\u00F6\u00F6d"])
7533             self.failUnlessReallyEqual(len(shares), 10)
7534-            os.unlink(shares[0][2])
7535+            shares[0][2].remove()
7536 
7537             shares = self.find_uri_shares(self.uris["mutable"])
7538hunk ./src/allmydata/test/test_cli.py 3020
7539-            cso = debug.CorruptShareOptions()
7540-            cso.stdout = StringIO()
7541-            cso.parseOptions([shares[1][2]])
7542+            stdout = StringIO()
7543+            sharefile = shares[1][2]
7544             storage_index = uri.from_string(self.uris["mutable"]).get_storage_index()
7545             self._corrupt_share_line = " corrupt: server %s, SI %s, shnum %d" % \
7546                                        (base32.b2a(shares[1][1]),
7547hunk ./src/allmydata/test/test_cli.py 3027
7548                                         base32.b2a(storage_index),
7549                                         shares[1][0])
7550-            debug.corrupt_share(cso)
7551+            debug.do_corrupt_share(stdout, sharefile)
7552         d.addCallback(_clobber_shares)
7553 
7554         # root
7555hunk ./src/allmydata/test/test_client.py 90
7556                            "enabled = true\n" + \
7557                            "reserved_space = 1000\n")
7558         c = client.Client(basedir)
7559-        self.failUnlessEqual(c.getServiceNamed("storage").reserved_space, 1000)
7560+        self.failUnlessEqual(c.getServiceNamed("storage").backend._reserved_space, 1000)
7561 
7562     def test_reserved_2(self):
7563         basedir = "client.Basic.test_reserved_2"
7564hunk ./src/allmydata/test/test_client.py 101
7565                            "enabled = true\n" + \
7566                            "reserved_space = 10K\n")
7567         c = client.Client(basedir)
7568-        self.failUnlessEqual(c.getServiceNamed("storage").reserved_space, 10*1000)
7569+        self.failUnlessEqual(c.getServiceNamed("storage").backend._reserved_space, 10*1000)
7570 
7571     def test_reserved_3(self):
7572         basedir = "client.Basic.test_reserved_3"
7573hunk ./src/allmydata/test/test_client.py 112
7574                            "enabled = true\n" + \
7575                            "reserved_space = 5mB\n")
7576         c = client.Client(basedir)
7577-        self.failUnlessEqual(c.getServiceNamed("storage").reserved_space,
7578+        self.failUnlessEqual(c.getServiceNamed("storage").backend._reserved_space,
7579                              5*1000*1000)
7580 
7581     def test_reserved_4(self):
7582hunk ./src/allmydata/test/test_client.py 124
7583                            "enabled = true\n" + \
7584                            "reserved_space = 78Gb\n")
7585         c = client.Client(basedir)
7586-        self.failUnlessEqual(c.getServiceNamed("storage").reserved_space,
7587+        self.failUnlessEqual(c.getServiceNamed("storage").backend._reserved_space,
7588                              78*1000*1000*1000)
7589 
7590     def test_reserved_bad(self):
7591hunk ./src/allmydata/test/test_client.py 136
7592                            "enabled = true\n" + \
7593                            "reserved_space = bogus\n")
7594         c = client.Client(basedir)
7595-        self.failUnlessEqual(c.getServiceNamed("storage").reserved_space, 0)
7596+        self.failUnlessEqual(c.getServiceNamed("storage").backend._reserved_space, 0)
7597 
7598     def _permute(self, sb, key):
7599         return [ s.get_serverid() for s in sb.get_servers_for_psi(key) ]
7600hunk ./src/allmydata/test/test_crawler.py 7
7601 from twisted.trial import unittest
7602 from twisted.application import service
7603 from twisted.internet import defer
7604+from twisted.python.filepath import FilePath
7605 from foolscap.api import eventually, fireEventually
7606 
7607 from allmydata.util import fileutil, hashutil, pollmixin
7608hunk ./src/allmydata/test/test_crawler.py 13
7609 from allmydata.storage.server import StorageServer, si_b2a
7610 from allmydata.storage.crawler import ShareCrawler, TimeSliceExceeded
7611+from allmydata.storage.backends.disk.disk_backend import DiskBackend
7612 
7613 from allmydata.test.test_storage import FakeCanary
7614 from allmydata.test.common_util import StallMixin
7615hunk ./src/allmydata/test/test_crawler.py 115
7616 
7617     def test_immediate(self):
7618         self.basedir = "crawler/Basic/immediate"
7619-        fileutil.make_dirs(self.basedir)
7620         serverid = "\x00" * 20
7621hunk ./src/allmydata/test/test_crawler.py 116
7622-        ss = StorageServer(self.basedir, serverid)
7623+        fp = FilePath(self.basedir)
7624+        backend = DiskBackend(fp)
7625+        ss = StorageServer(serverid, backend, fp)
7626         ss.setServiceParent(self.s)
7627 
7628         sis = [self.write(i, ss, serverid) for i in range(10)]
7629hunk ./src/allmydata/test/test_crawler.py 122
7630-        statefile = os.path.join(self.basedir, "statefile")
7631+        statefp = fp.child("statefile")
7632 
7633hunk ./src/allmydata/test/test_crawler.py 124
7634-        c = BucketEnumeratingCrawler(ss, statefile, allowed_cpu_percentage=.1)
7635+        c = BucketEnumeratingCrawler(backend, statefp, allowed_cpu_percentage=.1)
7636         c.load_state()
7637 
7638         c.start_current_prefix(time.time())
7639hunk ./src/allmydata/test/test_crawler.py 137
7640         self.failUnlessEqual(sorted(sis), sorted(c.all_buckets))
7641 
7642         # check that a new crawler picks up on the state file properly
7643-        c2 = BucketEnumeratingCrawler(ss, statefile)
7644+        c2 = BucketEnumeratingCrawler(backend, statefp)
7645         c2.load_state()
7646 
7647         c2.start_current_prefix(time.time())
7648hunk ./src/allmydata/test/test_crawler.py 145
7649 
7650     def test_service(self):
7651         self.basedir = "crawler/Basic/service"
7652-        fileutil.make_dirs(self.basedir)
7653         serverid = "\x00" * 20
7654hunk ./src/allmydata/test/test_crawler.py 146
7655-        ss = StorageServer(self.basedir, serverid)
7656+        fp = FilePath(self.basedir)
7657+        backend = DiskBackend(fp)
7658+        ss = StorageServer(serverid, backend, fp)
7659         ss.setServiceParent(self.s)
7660 
7661         sis = [self.write(i, ss, serverid) for i in range(10)]
7662hunk ./src/allmydata/test/test_crawler.py 153
7663 
7664-        statefile = os.path.join(self.basedir, "statefile")
7665-        c = BucketEnumeratingCrawler(ss, statefile)
7666+        statefp = fp.child("statefile")
7667+        c = BucketEnumeratingCrawler(backend, statefp)
7668         c.setServiceParent(self.s)
7669 
7670         # it should be legal to call get_state() and get_progress() right
7671hunk ./src/allmydata/test/test_crawler.py 174
7672 
7673     def test_paced(self):
7674         self.basedir = "crawler/Basic/paced"
7675-        fileutil.make_dirs(self.basedir)
7676         serverid = "\x00" * 20
7677hunk ./src/allmydata/test/test_crawler.py 175
7678-        ss = StorageServer(self.basedir, serverid)
7679+        fp = FilePath(self.basedir)
7680+        backend = DiskBackend(fp)
7681+        ss = StorageServer(serverid, backend, fp)
7682         ss.setServiceParent(self.s)
7683 
7684         # put four buckets in each prefixdir
7685hunk ./src/allmydata/test/test_crawler.py 186
7686             for tail in range(4):
7687                 sis.append(self.write(i, ss, serverid, tail))
7688 
7689-        statefile = os.path.join(self.basedir, "statefile")
7690+        statefp = fp.child("statefile")
7691 
7692hunk ./src/allmydata/test/test_crawler.py 188
7693-        c = PacedCrawler(ss, statefile)
7694+        c = PacedCrawler(backend, statefp)
7695         c.load_state()
7696         try:
7697             c.start_current_prefix(time.time())
7698hunk ./src/allmydata/test/test_crawler.py 213
7699         del c
7700 
7701         # start a new crawler, it should start from the beginning
7702-        c = PacedCrawler(ss, statefile)
7703+        c = PacedCrawler(backend, statefp)
7704         c.load_state()
7705         try:
7706             c.start_current_prefix(time.time())
7707hunk ./src/allmydata/test/test_crawler.py 226
7708         c.cpu_slice = PacedCrawler.cpu_slice
7709 
7710         # a third crawler should pick up from where it left off
7711-        c2 = PacedCrawler(ss, statefile)
7712+        c2 = PacedCrawler(backend, statefp)
7713         c2.all_buckets = c.all_buckets[:]
7714         c2.load_state()
7715         c2.countdown = -1
7716hunk ./src/allmydata/test/test_crawler.py 237
7717 
7718         # now stop it at the end of a bucket (countdown=4), to exercise a
7719         # different place that checks the time
7720-        c = PacedCrawler(ss, statefile)
7721+        c = PacedCrawler(backend, statefp)
7722         c.load_state()
7723         c.countdown = 4
7724         try:
7725hunk ./src/allmydata/test/test_crawler.py 256
7726 
7727         # stop it again at the end of the bucket, check that a new checker
7728         # picks up correctly
7729-        c = PacedCrawler(ss, statefile)
7730+        c = PacedCrawler(backend, statefp)
7731         c.load_state()
7732         c.countdown = 4
7733         try:
7734hunk ./src/allmydata/test/test_crawler.py 266
7735         # that should stop at the end of one of the buckets.
7736         c.save_state()
7737 
7738-        c2 = PacedCrawler(ss, statefile)
7739+        c2 = PacedCrawler(backend, statefp)
7740         c2.all_buckets = c.all_buckets[:]
7741         c2.load_state()
7742         c2.countdown = -1
7743hunk ./src/allmydata/test/test_crawler.py 277
7744 
7745     def test_paced_service(self):
7746         self.basedir = "crawler/Basic/paced_service"
7747-        fileutil.make_dirs(self.basedir)
7748         serverid = "\x00" * 20
7749hunk ./src/allmydata/test/test_crawler.py 278
7750-        ss = StorageServer(self.basedir, serverid)
7751+        fp = FilePath(self.basedir)
7752+        backend = DiskBackend(fp)
7753+        ss = StorageServer(serverid, backend, fp)
7754         ss.setServiceParent(self.s)
7755 
7756         sis = [self.write(i, ss, serverid) for i in range(10)]
7757hunk ./src/allmydata/test/test_crawler.py 285
7758 
7759-        statefile = os.path.join(self.basedir, "statefile")
7760-        c = PacedCrawler(ss, statefile)
7761+        statefp = fp.child("statefile")
7762+        c = PacedCrawler(backend, statefp)
7763 
7764         did_check_progress = [False]
7765         def check_progress():
7766hunk ./src/allmydata/test/test_crawler.py 345
7767         # and read the stdout when it runs.
7768 
7769         self.basedir = "crawler/Basic/cpu_usage"
7770-        fileutil.make_dirs(self.basedir)
7771         serverid = "\x00" * 20
7772hunk ./src/allmydata/test/test_crawler.py 346
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 354
7781             self.write(i, ss, serverid)
7782 
7783-        statefile = os.path.join(self.basedir, "statefile")
7784-        c = ConsumingCrawler(ss, statefile)
7785+        statefp = fp.child("statefile")
7786+        c = ConsumingCrawler(backend, statefp)
7787         c.setServiceParent(self.s)
7788 
7789         # this will run as fast as it can, consuming about 50ms per call to
7790hunk ./src/allmydata/test/test_crawler.py 391
7791 
7792     def test_empty_subclass(self):
7793         self.basedir = "crawler/Basic/empty_subclass"
7794-        fileutil.make_dirs(self.basedir)
7795         serverid = "\x00" * 20
7796hunk ./src/allmydata/test/test_crawler.py 392
7797-        ss = StorageServer(self.basedir, serverid)
7798+        fp = FilePath(self.basedir)
7799+        backend = DiskBackend(fp)
7800+        ss = StorageServer(serverid, backend, fp)
7801         ss.setServiceParent(self.s)
7802 
7803         for i in range(10):
7804hunk ./src/allmydata/test/test_crawler.py 400
7805             self.write(i, ss, serverid)
7806 
7807-        statefile = os.path.join(self.basedir, "statefile")
7808-        c = ShareCrawler(ss, statefile)
7809+        statefp = fp.child("statefile")
7810+        c = ShareCrawler(backend, statefp)
7811         c.slow_start = 0
7812         c.setServiceParent(self.s)
7813 
7814hunk ./src/allmydata/test/test_crawler.py 417
7815         d.addCallback(_done)
7816         return d
7817 
7818-
7819     def test_oneshot(self):
7820         self.basedir = "crawler/Basic/oneshot"
7821hunk ./src/allmydata/test/test_crawler.py 419
7822-        fileutil.make_dirs(self.basedir)
7823         serverid = "\x00" * 20
7824hunk ./src/allmydata/test/test_crawler.py 420
7825-        ss = StorageServer(self.basedir, serverid)
7826+        fp = FilePath(self.basedir)
7827+        backend = DiskBackend(fp)
7828+        ss = StorageServer(serverid, backend, fp)
7829         ss.setServiceParent(self.s)
7830 
7831         for i in range(30):
7832hunk ./src/allmydata/test/test_crawler.py 428
7833             self.write(i, ss, serverid)
7834 
7835-        statefile = os.path.join(self.basedir, "statefile")
7836-        c = OneShotCrawler(ss, statefile)
7837+        statefp = fp.child("statefile")
7838+        c = OneShotCrawler(backend, statefp)
7839         c.setServiceParent(self.s)
7840 
7841         d = c.finished_d
7842hunk ./src/allmydata/test/test_crawler.py 447
7843             self.failUnlessEqual(s["current-cycle"], None)
7844         d.addCallback(_check)
7845         return d
7846-
7847hunk ./src/allmydata/test/test_deepcheck.py 23
7848      ShouldFailMixin
7849 from allmydata.test.common_util import StallMixin
7850 from allmydata.test.no_network import GridTestMixin
7851+from allmydata.scripts import debug
7852+
7853 
7854 timeout = 2400 # One of these took 1046.091s on Zandr's ARM box.
7855 
7856hunk ./src/allmydata/test/test_deepcheck.py 905
7857         d.addErrback(self.explain_error)
7858         return d
7859 
7860-
7861-
7862     def set_up_damaged_tree(self):
7863         # 6.4s
7864 
7865hunk ./src/allmydata/test/test_deepcheck.py 989
7866 
7867         return d
7868 
7869-    def _run_cli(self, argv):
7870-        stdout, stderr = StringIO(), StringIO()
7871-        # this can only do synchronous operations
7872-        assert argv[0] == "debug"
7873-        runner.runner(argv, run_by_human=False, stdout=stdout, stderr=stderr)
7874-        return stdout.getvalue()
7875-
7876     def _delete_some_shares(self, node):
7877         self.delete_shares_numbered(node.get_uri(), [0,1])
7878 
7879hunk ./src/allmydata/test/test_deepcheck.py 995
7880     def _corrupt_some_shares(self, node):
7881         for (shnum, serverid, sharefile) in self.find_uri_shares(node.get_uri()):
7882             if shnum in (0,1):
7883-                self._run_cli(["debug", "corrupt-share", sharefile])
7884+                debug.do_corrupt_share(StringIO(), sharefile)
7885 
7886     def _delete_most_shares(self, node):
7887         self.delete_shares_numbered(node.get_uri(), range(1,10))
7888hunk ./src/allmydata/test/test_deepcheck.py 1000
7889 
7890-
7891     def check_is_healthy(self, cr, where):
7892         try:
7893             self.failUnless(ICheckResults.providedBy(cr), (cr, type(cr), where))
7894hunk ./src/allmydata/test/test_download.py 134
7895             for shnum in shares_for_server:
7896                 share_dir = self.get_server(i).backend.get_shareset(si)._sharehomedir
7897                 fileutil.fp_make_dirs(share_dir)
7898-                share_dir.child(str(shnum)).setContent(shares[shnum])
7899+                share_dir.child(str(shnum)).setContent(shares_for_server[shnum])
7900 
7901     def load_shares(self, ignored=None):
7902         # this uses the data generated by create_shares() to populate the
7903hunk ./src/allmydata/test/test_hung_server.py 32
7904 
7905     def _break(self, servers):
7906         for ss in servers:
7907-            self.g.break_server(ss.get_serverid())
7908+            self.g.break_server(ss.original.get_serverid())
7909 
7910     def _hang(self, servers, **kwargs):
7911         for ss in servers:
7912hunk ./src/allmydata/test/test_hung_server.py 67
7913         serverids = [ss.original.get_serverid() for ss in from_servers]
7914         for (i_shnum, i_serverid, i_sharefp) in self.shares:
7915             if i_serverid in serverids:
7916-                self.copy_share((i_shnum, i_serverid, i_sharefp), self.uri, to_server)
7917+                self.copy_share((i_shnum, i_serverid, i_sharefp), self.uri, to_server.original)
7918 
7919         self.shares = self.find_uri_shares(self.uri)
7920 
7921hunk ./src/allmydata/test/test_mutable.py 3670
7922         # Now execute each assignment by writing the storage.
7923         for (share, servernum) in assignments:
7924             sharedata = base64.b64decode(self.sdmf_old_shares[share])
7925-            storage_dir = self.get_server(servernum).backend.get_shareset(si).sharehomedir
7926+            storage_dir = self.get_server(servernum).backend.get_shareset(si)._sharehomedir
7927             fileutil.fp_make_dirs(storage_dir)
7928             storage_dir.child("%d" % share).setContent(sharedata)
7929         # ...and verify that the shares are there.
7930hunk ./src/allmydata/test/test_no_network.py 10
7931 from allmydata.immutable.upload import Data
7932 from allmydata.util.consumer import download_to_data
7933 
7934+
7935 class Harness(unittest.TestCase):
7936     def setUp(self):
7937         self.s = service.MultiService()
7938hunk ./src/allmydata/test/test_storage.py 1
7939-import time, os.path, platform, stat, re, simplejson, struct, shutil
7940+import time, os.path, platform, stat, re, simplejson, struct, shutil, itertools
7941 
7942 import mock
7943 
7944hunk ./src/allmydata/test/test_storage.py 6
7945 from twisted.trial import unittest
7946-
7947 from twisted.internet import defer
7948 from twisted.application import service
7949hunk ./src/allmydata/test/test_storage.py 8
7950+from twisted.python.filepath import FilePath
7951 from foolscap.api import fireEventually
7952hunk ./src/allmydata/test/test_storage.py 10
7953-import itertools
7954+
7955 from allmydata import interfaces
7956 from allmydata.util import fileutil, hashutil, base32, pollmixin, time_format
7957 from allmydata.storage.server import StorageServer
7958hunk ./src/allmydata/test/test_storage.py 14
7959+from allmydata.storage.backends.disk.disk_backend import DiskBackend
7960 from allmydata.storage.backends.disk.mutable import MutableDiskShare
7961 from allmydata.storage.bucket import BucketWriter, BucketReader
7962 from allmydata.storage.common import DataTooLargeError, \
7963hunk ./src/allmydata/test/test_storage.py 310
7964         return self.sparent.stopService()
7965 
7966     def workdir(self, name):
7967-        basedir = os.path.join("storage", "Server", name)
7968-        return basedir
7969+        return FilePath("storage").child("Server").child(name)
7970 
7971     def create(self, name, reserved_space=0, klass=StorageServer):
7972         workdir = self.workdir(name)
7973hunk ./src/allmydata/test/test_storage.py 314
7974-        ss = klass(workdir, "\x00" * 20, reserved_space=reserved_space,
7975+        backend = DiskBackend(workdir, readonly=False, reserved_space=reserved_space)
7976+        ss = klass("\x00" * 20, backend, workdir,
7977                    stats_provider=FakeStatsProvider())
7978         ss.setServiceParent(self.sparent)
7979         return ss
7980hunk ./src/allmydata/test/test_storage.py 1386
7981 
7982     def tearDown(self):
7983         self.sparent.stopService()
7984-        shutil.rmtree(self.workdir("MDMFProxies storage test server"))
7985+        fileutil.fp_remove(self.workdir("MDMFProxies storage test server"))
7986 
7987 
7988     def write_enabler(self, we_tag):
7989hunk ./src/allmydata/test/test_storage.py 2781
7990         return self.sparent.stopService()
7991 
7992     def workdir(self, name):
7993-        basedir = os.path.join("storage", "Server", name)
7994-        return basedir
7995+        return FilePath("storage").child("Server").child(name)
7996 
7997     def create(self, name):
7998         workdir = self.workdir(name)
7999hunk ./src/allmydata/test/test_storage.py 2785
8000-        ss = StorageServer(workdir, "\x00" * 20)
8001+        backend = DiskBackend(workdir)
8002+        ss = StorageServer("\x00" * 20, backend, workdir)
8003         ss.setServiceParent(self.sparent)
8004         return ss
8005 
8006hunk ./src/allmydata/test/test_storage.py 4061
8007         }
8008 
8009         basedir = "storage/WebStatus/status_right_disk_stats"
8010-        fileutil.make_dirs(basedir)
8011-        ss = StorageServer(basedir, "\x00" * 20, reserved_space=reserved_space)
8012-        expecteddir = ss.sharedir
8013+        fp = FilePath(basedir)
8014+        backend = DiskBackend(fp, readonly=False, reserved_space=reserved_space)
8015+        ss = StorageServer("\x00" * 20, backend, fp)
8016+        expecteddir = backend._sharedir
8017         ss.setServiceParent(self.s)
8018         w = StorageStatus(ss)
8019         html = w.renderSynchronously()
8020hunk ./src/allmydata/test/test_storage.py 4084
8021 
8022     def test_readonly(self):
8023         basedir = "storage/WebStatus/readonly"
8024-        fileutil.make_dirs(basedir)
8025-        ss = StorageServer(basedir, "\x00" * 20, readonly_storage=True)
8026+        fp = FilePath(basedir)
8027+        backend = DiskBackend(fp, readonly=True)
8028+        ss = StorageServer("\x00" * 20, backend, fp)
8029         ss.setServiceParent(self.s)
8030         w = StorageStatus(ss)
8031         html = w.renderSynchronously()
8032hunk ./src/allmydata/test/test_storage.py 4096
8033 
8034     def test_reserved(self):
8035         basedir = "storage/WebStatus/reserved"
8036-        fileutil.make_dirs(basedir)
8037-        ss = StorageServer(basedir, "\x00" * 20, reserved_space=10e6)
8038-        ss.setServiceParent(self.s)
8039-        w = StorageStatus(ss)
8040-        html = w.renderSynchronously()
8041-        self.failUnlessIn("<h1>Storage Server Status</h1>", html)
8042-        s = remove_tags(html)
8043-        self.failUnlessIn("Reserved space: - 10.00 MB (10000000)", s)
8044-
8045-    def test_huge_reserved(self):
8046-        basedir = "storage/WebStatus/reserved"
8047-        fileutil.make_dirs(basedir)
8048-        ss = StorageServer(basedir, "\x00" * 20, reserved_space=10e6)
8049+        fp = FilePath(basedir)
8050+        backend = DiskBackend(fp, readonly=False, reserved_space=10e6)
8051+        ss = StorageServer("\x00" * 20, backend, fp)
8052         ss.setServiceParent(self.s)
8053         w = StorageStatus(ss)
8054         html = w.renderSynchronously()
8055hunk ./src/allmydata/test/test_upload.py 3
8056 # -*- coding: utf-8 -*-
8057 
8058-import os, shutil
8059+import os
8060 from cStringIO import StringIO
8061 from twisted.trial import unittest
8062 from twisted.python.failure import Failure
8063hunk ./src/allmydata/test/test_upload.py 14
8064 from allmydata import uri, monitor, client
8065 from allmydata.immutable import upload, encode
8066 from allmydata.interfaces import FileTooLargeError, UploadUnhappinessError
8067-from allmydata.util import log
8068+from allmydata.util import log, fileutil
8069 from allmydata.util.assertutil import precondition
8070 from allmydata.util.deferredutil import DeferredListShouldSucceed
8071 from allmydata.test.no_network import GridTestMixin
8072hunk ./src/allmydata/test/test_upload.py 972
8073                                         readonly=True))
8074         # Remove the first share from server 0.
8075         def _remove_share_0_from_server_0():
8076-            share_location = self.shares[0][2]
8077-            os.remove(share_location)
8078+            self.shares[0][2].remove()
8079         d.addCallback(lambda ign:
8080             _remove_share_0_from_server_0())
8081         # Set happy = 4 in the client.
8082hunk ./src/allmydata/test/test_upload.py 1847
8083             self._copy_share_to_server(3, 1)
8084             storedir = self.get_serverdir(0)
8085             # remove the storedir, wiping out any existing shares
8086-            shutil.rmtree(storedir)
8087+            fileutil.fp_remove(storedir)
8088             # create an empty storedir to replace the one we just removed
8089hunk ./src/allmydata/test/test_upload.py 1849
8090-            os.mkdir(storedir)
8091+            storedir.mkdir()
8092             client = self.g.clients[0]
8093             client.DEFAULT_ENCODING_PARAMETERS['happy'] = 4
8094             return client
8095hunk ./src/allmydata/test/test_upload.py 1888
8096             self._copy_share_to_server(3, 1)
8097             storedir = self.get_serverdir(0)
8098             # remove the storedir, wiping out any existing shares
8099-            shutil.rmtree(storedir)
8100+            fileutil.fp_remove(storedir)
8101             # create an empty storedir to replace the one we just removed
8102hunk ./src/allmydata/test/test_upload.py 1890
8103-            os.mkdir(storedir)
8104+            storedir.mkdir()
8105             client = self.g.clients[0]
8106             client.DEFAULT_ENCODING_PARAMETERS['happy'] = 4
8107             return client
8108hunk ./src/allmydata/test/test_web.py 4870
8109         d.addErrback(self.explain_web_error)
8110         return d
8111 
8112-    def _assert_leasecount(self, ignored, which, expected):
8113+    def _assert_leasecount(self, which, expected):
8114         lease_counts = self.count_leases(self.uris[which])
8115         for (fn, num_leases) in lease_counts:
8116             if num_leases != expected:
8117hunk ./src/allmydata/test/test_web.py 4903
8118                 self.fileurls[which] = "uri/" + urllib.quote(self.uris[which])
8119         d.addCallback(_compute_fileurls)
8120 
8121-        d.addCallback(self._assert_leasecount, "one", 1)
8122-        d.addCallback(self._assert_leasecount, "two", 1)
8123-        d.addCallback(self._assert_leasecount, "mutable", 1)
8124+        d.addCallback(lambda ign: self._assert_leasecount("one", 1))
8125+        d.addCallback(lambda ign: self._assert_leasecount("two", 1))
8126+        d.addCallback(lambda ign: self._assert_leasecount("mutable", 1))
8127 
8128         d.addCallback(self.CHECK, "one", "t=check") # no add-lease
8129         def _got_html_good(res):
8130hunk ./src/allmydata/test/test_web.py 4913
8131             self.failIf("Not Healthy" in res, res)
8132         d.addCallback(_got_html_good)
8133 
8134-        d.addCallback(self._assert_leasecount, "one", 1)
8135-        d.addCallback(self._assert_leasecount, "two", 1)
8136-        d.addCallback(self._assert_leasecount, "mutable", 1)
8137+        d.addCallback(lambda ign: self._assert_leasecount("one", 1))
8138+        d.addCallback(lambda ign: self._assert_leasecount("two", 1))
8139+        d.addCallback(lambda ign: self._assert_leasecount("mutable", 1))
8140 
8141         # this CHECK uses the original client, which uses the same
8142         # lease-secrets, so it will just renew the original lease
8143hunk ./src/allmydata/test/test_web.py 4922
8144         d.addCallback(self.CHECK, "one", "t=check&add-lease=true")
8145         d.addCallback(_got_html_good)
8146 
8147-        d.addCallback(self._assert_leasecount, "one", 1)
8148-        d.addCallback(self._assert_leasecount, "two", 1)
8149-        d.addCallback(self._assert_leasecount, "mutable", 1)
8150+        d.addCallback(lambda ign: self._assert_leasecount("one", 1))
8151+        d.addCallback(lambda ign: self._assert_leasecount("two", 1))
8152+        d.addCallback(lambda ign: self._assert_leasecount("mutable", 1))
8153 
8154         # this CHECK uses an alternate client, which adds a second lease
8155         d.addCallback(self.CHECK, "one", "t=check&add-lease=true", clientnum=1)
8156hunk ./src/allmydata/test/test_web.py 4930
8157         d.addCallback(_got_html_good)
8158 
8159-        d.addCallback(self._assert_leasecount, "one", 2)
8160-        d.addCallback(self._assert_leasecount, "two", 1)
8161-        d.addCallback(self._assert_leasecount, "mutable", 1)
8162+        d.addCallback(lambda ign: self._assert_leasecount("one", 2))
8163+        d.addCallback(lambda ign: self._assert_leasecount("two", 1))
8164+        d.addCallback(lambda ign: self._assert_leasecount("mutable", 1))
8165 
8166         d.addCallback(self.CHECK, "mutable", "t=check&add-lease=true")
8167         d.addCallback(_got_html_good)
8168hunk ./src/allmydata/test/test_web.py 4937
8169 
8170-        d.addCallback(self._assert_leasecount, "one", 2)
8171-        d.addCallback(self._assert_leasecount, "two", 1)
8172-        d.addCallback(self._assert_leasecount, "mutable", 1)
8173+        d.addCallback(lambda ign: self._assert_leasecount("one", 2))
8174+        d.addCallback(lambda ign: self._assert_leasecount("two", 1))
8175+        d.addCallback(lambda ign: self._assert_leasecount("mutable", 1))
8176 
8177         d.addCallback(self.CHECK, "mutable", "t=check&add-lease=true",
8178                       clientnum=1)
8179hunk ./src/allmydata/test/test_web.py 4945
8180         d.addCallback(_got_html_good)
8181 
8182-        d.addCallback(self._assert_leasecount, "one", 2)
8183-        d.addCallback(self._assert_leasecount, "two", 1)
8184-        d.addCallback(self._assert_leasecount, "mutable", 2)
8185+        d.addCallback(lambda ign: self._assert_leasecount("one", 2))
8186+        d.addCallback(lambda ign: self._assert_leasecount("two", 1))
8187+        d.addCallback(lambda ign: self._assert_leasecount("mutable", 2))
8188 
8189         d.addErrback(self.explain_web_error)
8190         return d
8191hunk ./src/allmydata/test/test_web.py 4989
8192             self.failUnlessReallyEqual(len(units), 4+1)
8193         d.addCallback(_done)
8194 
8195-        d.addCallback(self._assert_leasecount, "root", 1)
8196-        d.addCallback(self._assert_leasecount, "one", 1)
8197-        d.addCallback(self._assert_leasecount, "mutable", 1)
8198+        d.addCallback(lambda ign: self._assert_leasecount("root", 1))
8199+        d.addCallback(lambda ign: self._assert_leasecount("one", 1))
8200+        d.addCallback(lambda ign: self._assert_leasecount("mutable", 1))
8201 
8202         d.addCallback(self.CHECK, "root", "t=stream-deep-check&add-lease=true")
8203         d.addCallback(_done)
8204hunk ./src/allmydata/test/test_web.py 4996
8205 
8206-        d.addCallback(self._assert_leasecount, "root", 1)
8207-        d.addCallback(self._assert_leasecount, "one", 1)
8208-        d.addCallback(self._assert_leasecount, "mutable", 1)
8209+        d.addCallback(lambda ign: self._assert_leasecount("root", 1))
8210+        d.addCallback(lambda ign: self._assert_leasecount("one", 1))
8211+        d.addCallback(lambda ign: self._assert_leasecount("mutable", 1))
8212 
8213         d.addCallback(self.CHECK, "root", "t=stream-deep-check&add-lease=true",
8214                       clientnum=1)
8215hunk ./src/allmydata/test/test_web.py 5004
8216         d.addCallback(_done)
8217 
8218-        d.addCallback(self._assert_leasecount, "root", 2)
8219-        d.addCallback(self._assert_leasecount, "one", 2)
8220-        d.addCallback(self._assert_leasecount, "mutable", 2)
8221+        d.addCallback(lambda ign: self._assert_leasecount("root", 2))
8222+        d.addCallback(lambda ign: self._assert_leasecount("one", 2))
8223+        d.addCallback(lambda ign: self._assert_leasecount("mutable", 2))
8224 
8225         d.addErrback(self.explain_web_error)
8226         return d
8227}
8228[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
8229david-sarah@jacaranda.org**20110921221421
8230 Ignore-this: 600e3ccef8533aa43442fa576c7d88cf
8231] {
8232hunk ./src/allmydata/scripts/debug.py 642
8233     /home/warner/testnet/node-2/storage/shares/44k/44kai1tui348689nrw8fjegc8c/2
8234     """
8235     from allmydata.storage.server import si_a2b
8236-    from allmydata.storage.backends.disk_backend import si_si2dir
8237+    from allmydata.storage.backends.disk.disk_backend import si_si2dir
8238     from allmydata.util.encodingutil import quote_filepath
8239 
8240     out = options.stdout
8241hunk ./src/allmydata/scripts/debug.py 648
8242     si = si_a2b(options.si_s)
8243     for nodedir in options.nodedirs:
8244-        sharedir = si_si2dir(nodedir.child("storage").child("shares"), si)
8245+        sharedir = si_si2dir(FilePath(nodedir).child("storage").child("shares"), si)
8246         if sharedir.exists():
8247             for sharefp in sharedir.children():
8248                 print >>out, quote_filepath(sharefp, quotemarks=False)
8249hunk ./src/allmydata/storage/backends/disk/disk_backend.py 189
8250         incominghome = self._incominghomedir.child(str(shnum))
8251         immsh = ImmutableDiskShare(self.get_storage_index(), shnum, incominghome, finalhome,
8252                                    max_size=max_space_per_bucket)
8253-        bw = BucketWriter(storageserver, immsh, max_space_per_bucket, lease_info, canary)
8254+        bw = BucketWriter(storageserver, immsh, lease_info, canary)
8255         if self._discard_storage:
8256             bw.throw_out_all_data = True
8257         return bw
8258hunk ./src/allmydata/storage/backends/disk/immutable.py 147
8259     def unlink(self):
8260         self._home.remove()
8261 
8262+    def get_allocated_size(self):
8263+        return self._max_size
8264+
8265     def get_size(self):
8266         return self._home.getsize()
8267 
8268hunk ./src/allmydata/storage/bucket.py 15
8269 class BucketWriter(Referenceable):
8270     implements(RIBucketWriter)
8271 
8272-    def __init__(self, ss, immutableshare, max_size, lease_info, canary):
8273+    def __init__(self, ss, immutableshare, lease_info, canary):
8274         self.ss = ss
8275hunk ./src/allmydata/storage/bucket.py 17
8276-        self._max_size = max_size # don't allow the client to write more than this
8277         self._canary = canary
8278         self._disconnect_marker = canary.notifyOnDisconnect(self._disconnected)
8279         self.closed = False
8280hunk ./src/allmydata/storage/bucket.py 27
8281         self._share.add_lease(lease_info)
8282 
8283     def allocated_size(self):
8284-        return self._max_size
8285+        return self._share.get_allocated_size()
8286 
8287     def remote_write(self, offset, data):
8288         start = time.time()
8289hunk ./src/allmydata/storage/crawler.py 480
8290             self.state["bucket-counts"][cycle] = {}
8291         self.state["bucket-counts"][cycle][prefix] = len(sharesets)
8292         if prefix in self.prefixes[:self.num_sample_prefixes]:
8293-            self.state["storage-index-samples"][prefix] = (cycle, sharesets)
8294+            si_strings = [shareset.get_storage_index_string() for shareset in sharesets]
8295+            self.state["storage-index-samples"][prefix] = (cycle, si_strings)
8296 
8297     def finished_cycle(self, cycle):
8298         last_counts = self.state["bucket-counts"].get(cycle, [])
8299hunk ./src/allmydata/storage/expirer.py 281
8300         # copy() needs to become a deepcopy
8301         h["space-recovered"] = s["space-recovered"].copy()
8302 
8303-        history = pickle.load(self.historyfp.getContent())
8304+        history = pickle.loads(self.historyfp.getContent())
8305         history[cycle] = h
8306         while len(history) > 10:
8307             oldcycles = sorted(history.keys())
8308hunk ./src/allmydata/storage/expirer.py 355
8309         progress = self.get_progress()
8310 
8311         state = ShareCrawler.get_state(self) # does a shallow copy
8312-        history = pickle.load(self.historyfp.getContent())
8313+        history = pickle.loads(self.historyfp.getContent())
8314         state["history"] = history
8315 
8316         if not progress["cycle-in-progress"]:
8317hunk ./src/allmydata/test/test_download.py 199
8318                     for shnum in immutable_shares[clientnum]:
8319                         if s._shnum == shnum:
8320                             share_dir = self.get_server(clientnum).backend.get_shareset(si)._sharehomedir
8321-                            share_dir.child(str(shnum)).remove()
8322+                            fileutil.fp_remove(share_dir.child(str(shnum)))
8323         d.addCallback(_clobber_some_shares)
8324         d.addCallback(lambda ign: download_to_data(n))
8325         d.addCallback(_got_data)
8326hunk ./src/allmydata/test/test_download.py 224
8327             for clientnum in immutable_shares:
8328                 for shnum in immutable_shares[clientnum]:
8329                     share_dir = self.get_server(clientnum).backend.get_shareset(si)._sharehomedir
8330-                    share_dir.child(str(shnum)).remove()
8331+                    fileutil.fp_remove(share_dir.child(str(shnum)))
8332             # now a new download should fail with NoSharesError. We want a
8333             # new ImmutableFileNode so it will forget about the old shares.
8334             # If we merely called create_node_from_uri() without first
8335hunk ./src/allmydata/test/test_repairer.py 415
8336         def _test_corrupt(ignored):
8337             olddata = {}
8338             shares = self.find_uri_shares(self.uri)
8339-            for (shnum, serverid, sharefile) in shares:
8340-                olddata[ (shnum, serverid) ] = open(sharefile, "rb").read()
8341+            for (shnum, serverid, sharefp) in shares:
8342+                olddata[ (shnum, serverid) ] = sharefp.getContent()
8343             for sh in shares:
8344                 self.corrupt_share(sh, common._corrupt_uri_extension)
8345hunk ./src/allmydata/test/test_repairer.py 419
8346-            for (shnum, serverid, sharefile) in shares:
8347-                newdata = open(sharefile, "rb").read()
8348+            for (shnum, serverid, sharefp) in shares:
8349+                newdata = sharefp.getContent()
8350                 self.failIfEqual(olddata[ (shnum, serverid) ], newdata)
8351         d.addCallback(_test_corrupt)
8352 
8353hunk ./src/allmydata/test/test_storage.py 63
8354 
8355 class Bucket(unittest.TestCase):
8356     def make_workdir(self, name):
8357-        basedir = os.path.join("storage", "Bucket", name)
8358-        incoming = os.path.join(basedir, "tmp", "bucket")
8359-        final = os.path.join(basedir, "bucket")
8360-        fileutil.make_dirs(basedir)
8361-        fileutil.make_dirs(os.path.join(basedir, "tmp"))
8362+        basedir = FilePath("storage").child("Bucket").child(name)
8363+        tmpdir = basedir.child("tmp")
8364+        tmpdir.makedirs()
8365+        incoming = tmpdir.child("bucket")
8366+        final = basedir.child("bucket")
8367         return incoming, final
8368 
8369     def bucket_writer_closed(self, bw, consumed):
8370hunk ./src/allmydata/test/test_storage.py 87
8371 
8372     def test_create(self):
8373         incoming, final = self.make_workdir("test_create")
8374-        bw = BucketWriter(self, incoming, final, 200, self.make_lease(),
8375-                          FakeCanary())
8376+        share = ImmutableDiskShare("", 0, incoming, final, 200)
8377+        bw = BucketWriter(self, share, self.make_lease(), FakeCanary())
8378         bw.remote_write(0, "a"*25)
8379         bw.remote_write(25, "b"*25)
8380         bw.remote_write(50, "c"*25)
8381hunk ./src/allmydata/test/test_storage.py 97
8382 
8383     def test_readwrite(self):
8384         incoming, final = self.make_workdir("test_readwrite")
8385-        bw = BucketWriter(self, incoming, final, 200, self.make_lease(),
8386-                          FakeCanary())
8387+        share = ImmutableDiskShare("", 0, incoming, 200)
8388+        bw = BucketWriter(self, share, self.make_lease(), FakeCanary())
8389         bw.remote_write(0, "a"*25)
8390         bw.remote_write(25, "b"*25)
8391         bw.remote_write(50, "c"*7) # last block may be short
8392hunk ./src/allmydata/test/test_storage.py 140
8393 
8394         incoming, final = self.make_workdir("test_read_past_end_of_share_data")
8395 
8396-        fileutil.write(final, share_file_data)
8397+        final.setContent(share_file_data)
8398 
8399         mockstorageserver = mock.Mock()
8400 
8401hunk ./src/allmydata/test/test_storage.py 179
8402 
8403 class BucketProxy(unittest.TestCase):
8404     def make_bucket(self, name, size):
8405-        basedir = os.path.join("storage", "BucketProxy", name)
8406-        incoming = os.path.join(basedir, "tmp", "bucket")
8407-        final = os.path.join(basedir, "bucket")
8408-        fileutil.make_dirs(basedir)
8409-        fileutil.make_dirs(os.path.join(basedir, "tmp"))
8410-        bw = BucketWriter(self, incoming, final, size, self.make_lease(),
8411-                          FakeCanary())
8412+        basedir = FilePath("storage").child("BucketProxy").child(name)
8413+        tmpdir = basedir.child("tmp")
8414+        tmpdir.makedirs()
8415+        incoming = tmpdir.child("bucket")
8416+        final = basedir.child("bucket")
8417+        share = ImmutableDiskShare("", 0, incoming, final, size)
8418+        bw = BucketWriter(self, share, self.make_lease(), FakeCanary())
8419         rb = RemoteBucket()
8420         rb.target = bw
8421         return bw, rb, final
8422hunk ./src/allmydata/test/test_storage.py 206
8423         pass
8424 
8425     def test_create(self):
8426-        bw, rb, sharefname = self.make_bucket("test_create", 500)
8427+        bw, rb, sharefp = self.make_bucket("test_create", 500)
8428         bp = WriteBucketProxy(rb, None,
8429                               data_size=300,
8430                               block_size=10,
8431hunk ./src/allmydata/test/test_storage.py 237
8432                         for i in (1,9,13)]
8433         uri_extension = "s" + "E"*498 + "e"
8434 
8435-        bw, rb, sharefname = self.make_bucket(name, sharesize)
8436+        bw, rb, sharefp = self.make_bucket(name, sharesize)
8437         bp = wbp_class(rb, None,
8438                        data_size=95,
8439                        block_size=25,
8440hunk ./src/allmydata/test/test_storage.py 258
8441 
8442         # now read everything back
8443         def _start_reading(res):
8444-            br = BucketReader(self, sharefname)
8445+            br = BucketReader(self, sharefp)
8446             rb = RemoteBucket()
8447             rb.target = br
8448             server = NoNetworkServer("abc", None)
8449hunk ./src/allmydata/test/test_storage.py 373
8450         for i, wb in writers.items():
8451             wb.remote_write(0, "%10d" % i)
8452             wb.remote_close()
8453-        storedir = os.path.join(self.workdir("test_dont_overfill_dirs"),
8454-                                "shares")
8455-        children_of_storedir = set(os.listdir(storedir))
8456+        storedir = self.workdir("test_dont_overfill_dirs").child("shares")
8457+        children_of_storedir = sorted([child.basename() for child in storedir.children()])
8458 
8459         # Now store another one under another storageindex that has leading
8460         # chars the same as the first storageindex.
8461hunk ./src/allmydata/test/test_storage.py 382
8462         for i, wb in writers.items():
8463             wb.remote_write(0, "%10d" % i)
8464             wb.remote_close()
8465-        storedir = os.path.join(self.workdir("test_dont_overfill_dirs"),
8466-                                "shares")
8467-        new_children_of_storedir = set(os.listdir(storedir))
8468+        storedir = self.workdir("test_dont_overfill_dirs").child("shares")
8469+        new_children_of_storedir = sorted([child.basename() for child in storedir.children()])
8470         self.failUnlessEqual(children_of_storedir, new_children_of_storedir)
8471 
8472     def test_remove_incoming(self):
8473hunk ./src/allmydata/test/test_storage.py 390
8474         ss = self.create("test_remove_incoming")
8475         already, writers = self.allocate(ss, "vid", range(3), 10)
8476         for i,wb in writers.items():
8477+            incoming_share_home = wb._share._home
8478             wb.remote_write(0, "%10d" % i)
8479             wb.remote_close()
8480hunk ./src/allmydata/test/test_storage.py 393
8481-        incoming_share_dir = wb.incominghome
8482-        incoming_bucket_dir = os.path.dirname(incoming_share_dir)
8483-        incoming_prefix_dir = os.path.dirname(incoming_bucket_dir)
8484-        incoming_dir = os.path.dirname(incoming_prefix_dir)
8485-        self.failIf(os.path.exists(incoming_bucket_dir), incoming_bucket_dir)
8486-        self.failIf(os.path.exists(incoming_prefix_dir), incoming_prefix_dir)
8487-        self.failUnless(os.path.exists(incoming_dir), incoming_dir)
8488+        incoming_bucket_dir = incoming_share_home.parent()
8489+        incoming_prefix_dir = incoming_bucket_dir.parent()
8490+        incoming_dir = incoming_prefix_dir.parent()
8491+        self.failIf(incoming_bucket_dir.exists(), incoming_bucket_dir)
8492+        self.failIf(incoming_prefix_dir.exists(), incoming_prefix_dir)
8493+        self.failUnless(incoming_dir.exists(), incoming_dir)
8494 
8495     def test_abort(self):
8496         # remote_abort, when called on a writer, should make sure that
8497hunk ./src/allmydata/test/test_upload.py 1849
8498             # remove the storedir, wiping out any existing shares
8499             fileutil.fp_remove(storedir)
8500             # create an empty storedir to replace the one we just removed
8501-            storedir.mkdir()
8502+            storedir.makedirs()
8503             client = self.g.clients[0]
8504             client.DEFAULT_ENCODING_PARAMETERS['happy'] = 4
8505             return client
8506hunk ./src/allmydata/test/test_upload.py 1890
8507             # remove the storedir, wiping out any existing shares
8508             fileutil.fp_remove(storedir)
8509             # create an empty storedir to replace the one we just removed
8510-            storedir.mkdir()
8511+            storedir.makedirs()
8512             client = self.g.clients[0]
8513             client.DEFAULT_ENCODING_PARAMETERS['happy'] = 4
8514             return client
8515}
8516[uri.py: resolve a conflict between trunk and the pluggable-backends patches. refs #999
8517david-sarah@jacaranda.org**20110921222038
8518 Ignore-this: ffeeab60d8e71a6a29a002d024d76fcf
8519] {
8520hunk ./src/allmydata/uri.py 829
8521     def is_mutable(self):
8522         return False
8523 
8524+    def is_readonly(self):
8525+        return True
8526+
8527+    def get_readonly(self):
8528+        return self
8529+
8530+
8531 class DirectoryURIVerifier(_DirectoryBaseURI):
8532     implements(IVerifierURI)
8533 
8534hunk ./src/allmydata/uri.py 855
8535     def is_mutable(self):
8536         return False
8537 
8538+    def is_readonly(self):
8539+        return True
8540+
8541+    def get_readonly(self):
8542+        return self
8543+
8544 
8545 class ImmutableDirectoryURIVerifier(DirectoryURIVerifier):
8546     implements(IVerifierURI)
8547}
8548[Fix some more test failures. refs #999
8549david-sarah@jacaranda.org**20110922045451
8550 Ignore-this: b726193cbd03a7c3d343f6e4a0f33ee7
8551] {
8552hunk ./src/allmydata/scripts/debug.py 42
8553     from allmydata.util.encodingutil import quote_output
8554 
8555     out = options.stdout
8556+    filename = options['filename']
8557 
8558     # check the version, to see if we have a mutable or immutable share
8559hunk ./src/allmydata/scripts/debug.py 45
8560-    print >>out, "share filename: %s" % quote_output(options['filename'])
8561+    print >>out, "share filename: %s" % quote_output(filename)
8562 
8563hunk ./src/allmydata/scripts/debug.py 47
8564-    share = get_share("", 0, fp)
8565+    share = get_share("", 0, FilePath(filename))
8566     if share.sharetype == "mutable":
8567         return dump_mutable_share(options, share)
8568     else:
8569hunk ./src/allmydata/storage/backends/disk/mutable.py 85
8570         self.parent = parent # for logging
8571 
8572     def log(self, *args, **kwargs):
8573-        return self.parent.log(*args, **kwargs)
8574+        if self.parent:
8575+            return self.parent.log(*args, **kwargs)
8576 
8577     def create(self, serverid, write_enabler):
8578         assert not self._home.exists()
8579hunk ./src/allmydata/storage/common.py 6
8580 class DataTooLargeError(Exception):
8581     pass
8582 
8583-class UnknownMutableContainerVersionError(Exception):
8584+class UnknownContainerVersionError(Exception):
8585     pass
8586 
8587hunk ./src/allmydata/storage/common.py 9
8588-class UnknownImmutableContainerVersionError(Exception):
8589+class UnknownMutableContainerVersionError(UnknownContainerVersionError):
8590+    pass
8591+
8592+class UnknownImmutableContainerVersionError(UnknownContainerVersionError):
8593     pass
8594 
8595 
8596hunk ./src/allmydata/storage/crawler.py 208
8597         try:
8598             state = pickle.loads(self.statefp.getContent())
8599         except EnvironmentError:
8600+            if self.statefp.exists():
8601+                raise
8602             state = {"version": 1,
8603                      "last-cycle-finished": None,
8604                      "current-cycle": None,
8605hunk ./src/allmydata/storage/server.py 24
8606 
8607     name = 'storage'
8608     LeaseCheckerClass = LeaseCheckingCrawler
8609+    BucketCounterClass = BucketCountingCrawler
8610     DEFAULT_EXPIRATION_POLICY = {
8611         'enabled': False,
8612         'mode': 'age',
8613hunk ./src/allmydata/storage/server.py 70
8614 
8615     def _setup_bucket_counter(self):
8616         statefp = self._statedir.child("bucket_counter.state")
8617-        self.bucket_counter = BucketCountingCrawler(self.backend, statefp)
8618+        self.bucket_counter = self.BucketCounterClass(self.backend, statefp)
8619         self.bucket_counter.setServiceParent(self)
8620 
8621     def _setup_lease_checker(self, expiration_policy):
8622hunk ./src/allmydata/storage/server.py 224
8623             share.add_or_renew_lease(lease_info)
8624             alreadygot.add(share.get_shnum())
8625 
8626-        for shnum in sharenums - alreadygot:
8627+        for shnum in set(sharenums) - alreadygot:
8628             if shareset.has_incoming(shnum):
8629                 # Note that we don't create BucketWriters for shnums that
8630                 # have a partial share (in incoming/), so if a second upload
8631hunk ./src/allmydata/storage/server.py 247
8632 
8633     def remote_add_lease(self, storageindex, renew_secret, cancel_secret,
8634                          owner_num=1):
8635-        # cancel_secret is no longer used.
8636         start = time.time()
8637         self.count("add-lease")
8638         new_expire_time = time.time() + 31*24*60*60
8639hunk ./src/allmydata/storage/server.py 250
8640-        lease_info = LeaseInfo(owner_num, renew_secret,
8641+        lease_info = LeaseInfo(owner_num, renew_secret, cancel_secret,
8642                                new_expire_time, self._serverid)
8643 
8644         try:
8645hunk ./src/allmydata/storage/server.py 254
8646-            self.backend.add_or_renew_lease(lease_info)
8647+            shareset = self.backend.get_shareset(storageindex)
8648+            shareset.add_or_renew_lease(lease_info)
8649         finally:
8650             self.add_latency("add-lease", time.time() - start)
8651 
8652hunk ./src/allmydata/test/test_crawler.py 3
8653 
8654 import time
8655-import os.path
8656+
8657 from twisted.trial import unittest
8658 from twisted.application import service
8659 from twisted.internet import defer
8660hunk ./src/allmydata/test/test_crawler.py 10
8661 from twisted.python.filepath import FilePath
8662 from foolscap.api import eventually, fireEventually
8663 
8664-from allmydata.util import fileutil, hashutil, pollmixin
8665+from allmydata.util import hashutil, pollmixin
8666 from allmydata.storage.server import StorageServer, si_b2a
8667 from allmydata.storage.crawler import ShareCrawler, TimeSliceExceeded
8668 from allmydata.storage.backends.disk.disk_backend import DiskBackend
8669hunk ./src/allmydata/test/test_mutable.py 3025
8670             cso.stderr = StringIO()
8671             debug.catalog_shares(cso)
8672             shares = cso.stdout.getvalue().splitlines()
8673+            self.failIf(len(shares) < 1, shares)
8674             oneshare = shares[0] # all shares should be MDMF
8675             self.failIf(oneshare.startswith("UNKNOWN"), oneshare)
8676             self.failUnless(oneshare.startswith("MDMF"), oneshare)
8677hunk ./src/allmydata/test/test_storage.py 1
8678-import time, os.path, platform, stat, re, simplejson, struct, shutil, itertools
8679+import time, os.path, platform, re, simplejson, struct, itertools
8680 
8681 import mock
8682 
8683hunk ./src/allmydata/test/test_storage.py 15
8684 from allmydata.util import fileutil, hashutil, base32, pollmixin, time_format
8685 from allmydata.storage.server import StorageServer
8686 from allmydata.storage.backends.disk.disk_backend import DiskBackend
8687+from allmydata.storage.backends.disk.immutable import ImmutableDiskShare
8688 from allmydata.storage.backends.disk.mutable import MutableDiskShare
8689 from allmydata.storage.bucket import BucketWriter, BucketReader
8690hunk ./src/allmydata/test/test_storage.py 18
8691-from allmydata.storage.common import DataTooLargeError, \
8692+from allmydata.storage.common import DataTooLargeError, UnknownContainerVersionError, \
8693      UnknownMutableContainerVersionError, UnknownImmutableContainerVersionError
8694 from allmydata.storage.lease import LeaseInfo
8695 from allmydata.storage.crawler import BucketCountingCrawler
8696hunk ./src/allmydata/test/test_storage.py 88
8697 
8698     def test_create(self):
8699         incoming, final = self.make_workdir("test_create")
8700-        share = ImmutableDiskShare("", 0, incoming, final, 200)
8701+        share = ImmutableDiskShare("", 0, incoming, final, max_size=200)
8702         bw = BucketWriter(self, share, self.make_lease(), FakeCanary())
8703         bw.remote_write(0, "a"*25)
8704         bw.remote_write(25, "b"*25)
8705hunk ./src/allmydata/test/test_storage.py 98
8706 
8707     def test_readwrite(self):
8708         incoming, final = self.make_workdir("test_readwrite")
8709-        share = ImmutableDiskShare("", 0, incoming, 200)
8710+        share = ImmutableDiskShare("", 0, incoming, final, max_size=200)
8711         bw = BucketWriter(self, share, self.make_lease(), FakeCanary())
8712         bw.remote_write(0, "a"*25)
8713         bw.remote_write(25, "b"*25)
8714hunk ./src/allmydata/test/test_storage.py 106
8715         bw.remote_close()
8716 
8717         # now read from it
8718-        br = BucketReader(self, bw.finalhome)
8719+        br = BucketReader(self, share)
8720         self.failUnlessEqual(br.remote_read(0, 25), "a"*25)
8721         self.failUnlessEqual(br.remote_read(25, 25), "b"*25)
8722         self.failUnlessEqual(br.remote_read(50, 7), "c"*7)
8723hunk ./src/allmydata/test/test_storage.py 131
8724         ownernumber = struct.pack('>L', 0)
8725         renewsecret  = 'THIS LETS ME RENEW YOUR FILE....'
8726         assert len(renewsecret) == 32
8727-        cancelsecret = 'THIS LETS ME KILL YOUR FILE HAHA'
8728+        cancelsecret = 'THIS USED TO LET ME KILL YR FILE'
8729         assert len(cancelsecret) == 32
8730         expirationtime = struct.pack('>L', 60*60*24*31) # 31 days in seconds
8731 
8732hunk ./src/allmydata/test/test_storage.py 142
8733         incoming, final = self.make_workdir("test_read_past_end_of_share_data")
8734 
8735         final.setContent(share_file_data)
8736+        share = ImmutableDiskShare("", 0, final)
8737 
8738         mockstorageserver = mock.Mock()
8739 
8740hunk ./src/allmydata/test/test_storage.py 147
8741         # Now read from it.
8742-        br = BucketReader(mockstorageserver, final)
8743+        br = BucketReader(mockstorageserver, share)
8744 
8745         self.failUnlessEqual(br.remote_read(0, len(share_data)), share_data)
8746 
8747hunk ./src/allmydata/test/test_storage.py 260
8748 
8749         # now read everything back
8750         def _start_reading(res):
8751-            br = BucketReader(self, sharefp)
8752+            share = ImmutableDiskShare("", 0, sharefp)
8753+            br = BucketReader(self, share)
8754             rb = RemoteBucket()
8755             rb.target = br
8756             server = NoNetworkServer("abc", None)
8757hunk ./src/allmydata/test/test_storage.py 346
8758         if 'cygwin' in syslow or 'windows' in syslow or 'darwin' in syslow:
8759             raise unittest.SkipTest("If your filesystem doesn't support efficient sparse files then it is very expensive (Mac OS X and Windows don't support efficient sparse files).")
8760 
8761-        avail = fileutil.get_available_space('.', 512*2**20)
8762+        avail = fileutil.get_available_space(FilePath('.'), 512*2**20)
8763         if avail <= 4*2**30:
8764             raise unittest.SkipTest("This test will spuriously fail if you have less than 4 GiB free on your filesystem.")
8765 
8766hunk ./src/allmydata/test/test_storage.py 476
8767         w[0].remote_write(0, "\xff"*10)
8768         w[0].remote_close()
8769 
8770-        fp = ss.backend.get_shareset("si1").sharehomedir.child("0")
8771+        fp = ss.backend.get_shareset("si1")._sharehomedir.child("0")
8772         f = fp.open("rb+")
8773hunk ./src/allmydata/test/test_storage.py 478
8774-        f.seek(0)
8775-        f.write(struct.pack(">L", 0)) # this is invalid: minimum used is v1
8776-        f.close()
8777+        try:
8778+            f.seek(0)
8779+            f.write(struct.pack(">L", 0)) # this is invalid: minimum used is v1
8780+        finally:
8781+            f.close()
8782 
8783         ss.remote_get_buckets("allocate")
8784 
8785hunk ./src/allmydata/test/test_storage.py 575
8786 
8787     def test_seek(self):
8788         basedir = self.workdir("test_seek_behavior")
8789-        fileutil.make_dirs(basedir)
8790-        filename = os.path.join(basedir, "testfile")
8791-        f = open(filename, "wb")
8792-        f.write("start")
8793-        f.close()
8794+        basedir.makedirs()
8795+        fp = basedir.child("testfile")
8796+        fp.setContent("start")
8797+
8798         # mode="w" allows seeking-to-create-holes, but truncates pre-existing
8799         # files. mode="a" preserves previous contents but does not allow
8800         # seeking-to-create-holes. mode="r+" allows both.
8801hunk ./src/allmydata/test/test_storage.py 582
8802-        f = open(filename, "rb+")
8803-        f.seek(100)
8804-        f.write("100")
8805-        f.close()
8806-        filelen = os.stat(filename)[stat.ST_SIZE]
8807+        f = fp.open("rb+")
8808+        try:
8809+            f.seek(100)
8810+            f.write("100")
8811+        finally:
8812+            f.close()
8813+        fp.restat()
8814+        filelen = fp.getsize()
8815         self.failUnlessEqual(filelen, 100+3)
8816hunk ./src/allmydata/test/test_storage.py 591
8817-        f2 = open(filename, "rb")
8818-        self.failUnlessEqual(f2.read(5), "start")
8819-
8820+        f2 = fp.open("rb")
8821+        try:
8822+            self.failUnlessEqual(f2.read(5), "start")
8823+        finally:
8824+            f2.close()
8825 
8826     def test_leases(self):
8827         ss = self.create("test_leases")
8828hunk ./src/allmydata/test/test_storage.py 693
8829 
8830     def test_readonly(self):
8831         workdir = self.workdir("test_readonly")
8832-        ss = StorageServer(workdir, "\x00" * 20, readonly_storage=True)
8833+        backend = DiskBackend(workdir, readonly=True)
8834+        ss = StorageServer("\x00" * 20, backend, workdir)
8835         ss.setServiceParent(self.sparent)
8836 
8837         already,writers = self.allocate(ss, "vid", [0,1,2], 75)
8838hunk ./src/allmydata/test/test_storage.py 710
8839 
8840     def test_discard(self):
8841         # discard is really only used for other tests, but we test it anyways
8842+        # XXX replace this with a null backend test
8843         workdir = self.workdir("test_discard")
8844hunk ./src/allmydata/test/test_storage.py 712
8845-        ss = StorageServer(workdir, "\x00" * 20, discard_storage=True)
8846+        backend = DiskBackend(workdir, readonly=False, discard_storage=True)
8847+        ss = StorageServer("\x00" * 20, backend, workdir)
8848         ss.setServiceParent(self.sparent)
8849 
8850         already,writers = self.allocate(ss, "vid", [0,1,2], 75)
8851hunk ./src/allmydata/test/test_storage.py 731
8852 
8853     def test_advise_corruption(self):
8854         workdir = self.workdir("test_advise_corruption")
8855-        ss = StorageServer(workdir, "\x00" * 20, discard_storage=True)
8856+        backend = DiskBackend(workdir, readonly=False, discard_storage=True)
8857+        ss = StorageServer("\x00" * 20, backend, workdir)
8858         ss.setServiceParent(self.sparent)
8859 
8860         si0_s = base32.b2a("si0")
8861hunk ./src/allmydata/test/test_storage.py 738
8862         ss.remote_advise_corrupt_share("immutable", "si0", 0,
8863                                        "This share smells funny.\n")
8864-        reportdir = os.path.join(workdir, "corruption-advisories")
8865-        reports = os.listdir(reportdir)
8866+        reportdir = workdir.child("corruption-advisories")
8867+        reports = [child.basename() for child in reportdir.children()]
8868         self.failUnlessEqual(len(reports), 1)
8869         report_si0 = reports[0]
8870hunk ./src/allmydata/test/test_storage.py 742
8871-        self.failUnlessIn(si0_s, report_si0)
8872-        f = open(os.path.join(reportdir, report_si0), "r")
8873-        report = f.read()
8874-        f.close()
8875+        self.failUnlessIn(si0_s, str(report_si0))
8876+        report = reportdir.child(report_si0).getContent()
8877+
8878         self.failUnlessIn("type: immutable", report)
8879         self.failUnlessIn("storage_index: %s" % si0_s, report)
8880         self.failUnlessIn("share_number: 0", report)
8881hunk ./src/allmydata/test/test_storage.py 762
8882         self.failUnlessEqual(set(b.keys()), set([1]))
8883         b[1].remote_advise_corrupt_share("This share tastes like dust.\n")
8884 
8885-        reports = os.listdir(reportdir)
8886+        reports = [child.basename() for child in reportdir.children()]
8887         self.failUnlessEqual(len(reports), 2)
8888hunk ./src/allmydata/test/test_storage.py 764
8889-        report_si1 = [r for r in reports if si1_s in r][0]
8890-        f = open(os.path.join(reportdir, report_si1), "r")
8891-        report = f.read()
8892-        f.close()
8893+        report_si1 = [r for r in reports if si1_s in str(r)][0]
8894+        report = reportdir.child(report_si1).getContent()
8895+
8896         self.failUnlessIn("type: immutable", report)
8897         self.failUnlessIn("storage_index: %s" % si1_s, report)
8898         self.failUnlessIn("share_number: 1", report)
8899hunk ./src/allmydata/test/test_storage.py 783
8900         return self.sparent.stopService()
8901 
8902     def workdir(self, name):
8903-        basedir = os.path.join("storage", "MutableServer", name)
8904-        return basedir
8905+        return FilePath("storage").child("MutableServer").child(name)
8906 
8907     def create(self, name):
8908         workdir = self.workdir(name)
8909hunk ./src/allmydata/test/test_storage.py 787
8910-        ss = StorageServer(workdir, "\x00" * 20)
8911+        backend = DiskBackend(workdir)
8912+        ss = StorageServer("\x00" * 20, backend, workdir)
8913         ss.setServiceParent(self.sparent)
8914         return ss
8915 
8916hunk ./src/allmydata/test/test_storage.py 810
8917         cancel_secret = self.cancel_secret(lease_tag)
8918         rstaraw = ss.remote_slot_testv_and_readv_and_writev
8919         testandwritev = dict( [ (shnum, ([], [], None) )
8920-                         for shnum in sharenums ] )
8921+                                for shnum in sharenums ] )
8922         readv = []
8923         rc = rstaraw(storage_index,
8924                      (write_enabler, renew_secret, cancel_secret),
8925hunk ./src/allmydata/test/test_storage.py 824
8926     def test_bad_magic(self):
8927         ss = self.create("test_bad_magic")
8928         self.allocate(ss, "si1", "we1", self._lease_secret.next(), set([0]), 10)
8929-        fp = ss.backend.get_shareset("si1").sharehomedir.child("0")
8930+        fp = ss.backend.get_shareset("si1")._sharehomedir.child("0")
8931         f = fp.open("rb+")
8932hunk ./src/allmydata/test/test_storage.py 826
8933-        f.seek(0)
8934-        f.write("BAD MAGIC")
8935-        f.close()
8936+        try:
8937+            f.seek(0)
8938+            f.write("BAD MAGIC")
8939+        finally:
8940+            f.close()
8941         read = ss.remote_slot_readv
8942hunk ./src/allmydata/test/test_storage.py 832
8943-        e = self.failUnlessRaises(UnknownMutableContainerVersionError,
8944+
8945+        # This used to test for UnknownMutableContainerVersionError,
8946+        # but the current code raises UnknownImmutableContainerVersionError.
8947+        # (It changed because remote_slot_readv now works with either
8948+        # mutable or immutable shares.) Since the share file doesn't have
8949+        # the mutable magic, it's not clear that this is wrong.
8950+        # For now, accept either exception.
8951+        e = self.failUnlessRaises(UnknownContainerVersionError,
8952                                   read, "si1", [0], [(0,10)])
8953hunk ./src/allmydata/test/test_storage.py 841
8954-        self.failUnlessIn(" had magic ", str(e))
8955+        self.failUnlessIn(" had ", str(e))
8956         self.failUnlessIn(" but we wanted ", str(e))
8957 
8958     def test_container_size(self):
8959hunk ./src/allmydata/test/test_storage.py 1248
8960 
8961         # create a random non-numeric file in the bucket directory, to
8962         # exercise the code that's supposed to ignore those.
8963-        bucket_dir = ss.backend.get_shareset("si1").sharehomedir
8964+        bucket_dir = ss.backend.get_shareset("si1")._sharehomedir
8965         bucket_dir.child("ignore_me.txt").setContent("you ought to be ignoring me\n")
8966 
8967hunk ./src/allmydata/test/test_storage.py 1251
8968-        s0 = MutableDiskShare(os.path.join(bucket_dir, "0"))
8969+        s0 = MutableDiskShare("", 0, bucket_dir.child("0"))
8970         self.failUnlessEqual(len(list(s0.get_leases())), 1)
8971 
8972         # add-lease on a missing storage index is silently ignored
8973hunk ./src/allmydata/test/test_storage.py 1365
8974         # note: this is a detail of the storage server implementation, and
8975         # may change in the future
8976         prefix = si[:2]
8977-        prefixdir = os.path.join(self.workdir("test_remove"), "shares", prefix)
8978-        bucketdir = os.path.join(prefixdir, si)
8979-        self.failUnless(os.path.exists(prefixdir), prefixdir)
8980-        self.failIf(os.path.exists(bucketdir), bucketdir)
8981+        prefixdir = self.workdir("test_remove").child("shares").child(prefix)
8982+        bucketdir = prefixdir.child(si)
8983+        self.failUnless(prefixdir.exists(), prefixdir)
8984+        self.failIf(bucketdir.exists(), bucketdir)
8985 
8986 
8987 class MDMFProxies(unittest.TestCase, ShouldFailMixin):
8988hunk ./src/allmydata/test/test_storage.py 1420
8989 
8990 
8991     def workdir(self, name):
8992-        basedir = os.path.join("storage", "MutableServer", name)
8993-        return basedir
8994-
8995+        return FilePath("storage").child("MDMFProxies").child(name)
8996 
8997     def create(self, name):
8998         workdir = self.workdir(name)
8999hunk ./src/allmydata/test/test_storage.py 1424
9000-        ss = StorageServer(workdir, "\x00" * 20)
9001+        backend = DiskBackend(workdir)
9002+        ss = StorageServer("\x00" * 20, backend, workdir)
9003         ss.setServiceParent(self.sparent)
9004         return ss
9005 
9006hunk ./src/allmydata/test/test_storage.py 2798
9007         return self.sparent.stopService()
9008 
9009     def workdir(self, name):
9010-        return FilePath("storage").child("Server").child(name)
9011+        return FilePath("storage").child("Stats").child(name)
9012 
9013     def create(self, name):
9014         workdir = self.workdir(name)
9015hunk ./src/allmydata/test/test_storage.py 2886
9016             d.callback(None)
9017 
9018 class MyStorageServer(StorageServer):
9019-    def add_bucket_counter(self):
9020-        statefile = os.path.join(self.storedir, "bucket_counter.state")
9021-        self.bucket_counter = MyBucketCountingCrawler(self, statefile)
9022-        self.bucket_counter.setServiceParent(self)
9023+    BucketCounterClass = MyBucketCountingCrawler
9024+
9025 
9026 class BucketCounter(unittest.TestCase, pollmixin.PollMixin):
9027 
9028hunk ./src/allmydata/test/test_storage.py 2899
9029 
9030     def test_bucket_counter(self):
9031         basedir = "storage/BucketCounter/bucket_counter"
9032-        fileutil.make_dirs(basedir)
9033-        ss = StorageServer(basedir, "\x00" * 20)
9034+        fp = FilePath(basedir)
9035+        backend = DiskBackend(fp)
9036+        ss = StorageServer("\x00" * 20, backend, fp)
9037+
9038         # to make sure we capture the bucket-counting-crawler in the middle
9039         # of a cycle, we reach in and reduce its maximum slice time to 0. We
9040         # also make it start sooner than usual.
9041hunk ./src/allmydata/test/test_storage.py 2958
9042 
9043     def test_bucket_counter_cleanup(self):
9044         basedir = "storage/BucketCounter/bucket_counter_cleanup"
9045-        fileutil.make_dirs(basedir)
9046-        ss = StorageServer(basedir, "\x00" * 20)
9047+        fp = FilePath(basedir)
9048+        backend = DiskBackend(fp)
9049+        ss = StorageServer("\x00" * 20, backend, fp)
9050+
9051         # to make sure we capture the bucket-counting-crawler in the middle
9052         # of a cycle, we reach in and reduce its maximum slice time to 0.
9053         ss.bucket_counter.slow_start = 0
9054hunk ./src/allmydata/test/test_storage.py 3002
9055 
9056     def test_bucket_counter_eta(self):
9057         basedir = "storage/BucketCounter/bucket_counter_eta"
9058-        fileutil.make_dirs(basedir)
9059-        ss = MyStorageServer(basedir, "\x00" * 20)
9060+        fp = FilePath(basedir)
9061+        backend = DiskBackend(fp)
9062+        ss = MyStorageServer("\x00" * 20, backend, fp)
9063         ss.bucket_counter.slow_start = 0
9064         # these will be fired inside finished_prefix()
9065         hooks = ss.bucket_counter.hook_ds = [defer.Deferred() for i in range(3)]
9066hunk ./src/allmydata/test/test_storage.py 3125
9067 
9068     def test_basic(self):
9069         basedir = "storage/LeaseCrawler/basic"
9070-        fileutil.make_dirs(basedir)
9071-        ss = InstrumentedStorageServer(basedir, "\x00" * 20)
9072+        fp = FilePath(basedir)
9073+        backend = DiskBackend(fp)
9074+        ss = InstrumentedStorageServer("\x00" * 20, backend, fp)
9075+
9076         # make it start sooner than usual.
9077         lc = ss.lease_checker
9078         lc.slow_start = 0
9079hunk ./src/allmydata/test/test_storage.py 3141
9080         [immutable_si_0, immutable_si_1, mutable_si_2, mutable_si_3] = self.sis
9081 
9082         # add a non-sharefile to exercise another code path
9083-        fp = ss.backend.get_shareset(immutable_si_0).sharehomedir.child("not-a-share")
9084+        fp = ss.backend.get_shareset(immutable_si_0)._sharehomedir.child("not-a-share")
9085         fp.setContent("I am not a share.\n")
9086 
9087         # this is before the crawl has started, so we're not in a cycle yet
9088hunk ./src/allmydata/test/test_storage.py 3264
9089             self.failUnlessEqual(rec["configured-sharebytes"], 0)
9090 
9091             def _get_sharefile(si):
9092-                return list(ss._iter_share_files(si))[0]
9093+                return list(ss.backend.get_shareset(si).get_shares())[0]
9094             def count_leases(si):
9095                 return len(list(_get_sharefile(si).get_leases()))
9096             self.failUnlessEqual(count_leases(immutable_si_0), 1)
9097hunk ./src/allmydata/test/test_storage.py 3296
9098         for i,lease in enumerate(sf.get_leases()):
9099             if lease.renew_secret == renew_secret:
9100                 lease.expiration_time = new_expire_time
9101-                f = open(sf.home, 'rb+')
9102-                sf._write_lease_record(f, i, lease)
9103-                f.close()
9104+                f = sf._home.open('rb+')
9105+                try:
9106+                    sf._write_lease_record(f, i, lease)
9107+                finally:
9108+                    f.close()
9109                 return
9110         raise IndexError("unable to renew non-existent lease")
9111 
9112hunk ./src/allmydata/test/test_storage.py 3306
9113     def test_expire_age(self):
9114         basedir = "storage/LeaseCrawler/expire_age"
9115-        fileutil.make_dirs(basedir)
9116+        fp = FilePath(basedir)
9117+        backend = DiskBackend(fp)
9118+
9119         # setting 'override_lease_duration' to 2000 means that any lease that
9120         # is more than 2000 seconds old will be expired.
9121         expiration_policy = {
9122hunk ./src/allmydata/test/test_storage.py 3317
9123             'override_lease_duration': 2000,
9124             'sharetypes': ('mutable', 'immutable'),
9125         }
9126-        ss = InstrumentedStorageServer(basedir, "\x00" * 20, expiration_policy)
9127+        ss = InstrumentedStorageServer("\x00" * 20, backend, fp, expiration_policy=expiration_policy)
9128+
9129         # make it start sooner than usual.
9130         lc = ss.lease_checker
9131         lc.slow_start = 0
9132hunk ./src/allmydata/test/test_storage.py 3330
9133         [immutable_si_0, immutable_si_1, mutable_si_2, mutable_si_3] = self.sis
9134 
9135         def count_shares(si):
9136-            return len(list(ss._iter_share_files(si)))
9137+            return len(list(ss.backend.get_shareset(si).get_shares()))
9138         def _get_sharefile(si):
9139hunk ./src/allmydata/test/test_storage.py 3332
9140-            return list(ss._iter_share_files(si))[0]
9141+            return list(ss.backend.get_shareset(si).get_shares())[0]
9142         def count_leases(si):
9143             return len(list(_get_sharefile(si).get_leases()))
9144 
9145hunk ./src/allmydata/test/test_storage.py 3355
9146 
9147         sf0 = _get_sharefile(immutable_si_0)
9148         self.backdate_lease(sf0, self.renew_secrets[0], now - 1000)
9149-        sf0_size = os.stat(sf0.home).st_size
9150+        sf0_size = sf0.get_size()
9151 
9152         # immutable_si_1 gets an extra lease
9153         sf1 = _get_sharefile(immutable_si_1)
9154hunk ./src/allmydata/test/test_storage.py 3363
9155 
9156         sf2 = _get_sharefile(mutable_si_2)
9157         self.backdate_lease(sf2, self.renew_secrets[3], now - 1000)
9158-        sf2_size = os.stat(sf2.home).st_size
9159+        sf2_size = sf2.get_size()
9160 
9161         # mutable_si_3 gets an extra lease
9162         sf3 = _get_sharefile(mutable_si_3)
9163hunk ./src/allmydata/test/test_storage.py 3450
9164 
9165     def test_expire_cutoff_date(self):
9166         basedir = "storage/LeaseCrawler/expire_cutoff_date"
9167-        fileutil.make_dirs(basedir)
9168+        fp = FilePath(basedir)
9169+        backend = DiskBackend(fp)
9170+
9171         # setting 'cutoff_date' to 2000 seconds ago means that any lease that
9172         # is more than 2000 seconds old will be expired.
9173         now = time.time()
9174hunk ./src/allmydata/test/test_storage.py 3463
9175             'cutoff_date': then,
9176             'sharetypes': ('mutable', 'immutable'),
9177         }
9178-        ss = InstrumentedStorageServer(basedir, "\x00" * 20, expiration_policy)
9179+        ss = InstrumentedStorageServer("\x00" * 20, backend, fp, expiration_policy=expiration_policy)
9180+
9181         # make it start sooner than usual.
9182         lc = ss.lease_checker
9183         lc.slow_start = 0
9184hunk ./src/allmydata/test/test_storage.py 3476
9185         [immutable_si_0, immutable_si_1, mutable_si_2, mutable_si_3] = self.sis
9186 
9187         def count_shares(si):
9188-            return len(list(ss._iter_share_files(si)))
9189+            return len(list(ss.backend.get_shareset(si).get_shares()))
9190         def _get_sharefile(si):
9191hunk ./src/allmydata/test/test_storage.py 3478
9192-            return list(ss._iter_share_files(si))[0]
9193+            return list(ss.backend.get_shareset(si).get_shares())[0]
9194         def count_leases(si):
9195             return len(list(_get_sharefile(si).get_leases()))
9196 
9197hunk ./src/allmydata/test/test_storage.py 3505
9198 
9199         sf0 = _get_sharefile(immutable_si_0)
9200         self.backdate_lease(sf0, self.renew_secrets[0], new_expiration_time)
9201-        sf0_size = os.stat(sf0.home).st_size
9202+        sf0_size = sf0.get_size()
9203 
9204         # immutable_si_1 gets an extra lease
9205         sf1 = _get_sharefile(immutable_si_1)
9206hunk ./src/allmydata/test/test_storage.py 3513
9207 
9208         sf2 = _get_sharefile(mutable_si_2)
9209         self.backdate_lease(sf2, self.renew_secrets[3], new_expiration_time)
9210-        sf2_size = os.stat(sf2.home).st_size
9211+        sf2_size = sf2.get_size()
9212 
9213         # mutable_si_3 gets an extra lease
9214         sf3 = _get_sharefile(mutable_si_3)
9215hunk ./src/allmydata/test/test_storage.py 3605
9216 
9217     def test_only_immutable(self):
9218         basedir = "storage/LeaseCrawler/only_immutable"
9219-        fileutil.make_dirs(basedir)
9220+        fp = FilePath(basedir)
9221+        backend = DiskBackend(fp)
9222+
9223         # setting 'cutoff_date' to 2000 seconds ago means that any lease that
9224         # is more than 2000 seconds old will be expired.
9225         now = time.time()
9226hunk ./src/allmydata/test/test_storage.py 3618
9227             'cutoff_date': then,
9228             'sharetypes': ('immutable',),
9229         }
9230-        ss = StorageServer(basedir, "\x00" * 20, expiration_policy)
9231+        ss = StorageServer("\x00" * 20, backend, fp, expiration_policy=expiration_policy)
9232         lc = ss.lease_checker
9233         lc.slow_start = 0
9234         webstatus = StorageStatus(ss)
9235hunk ./src/allmydata/test/test_storage.py 3629
9236         new_expiration_time = now - 3000 + 31*24*60*60
9237 
9238         def count_shares(si):
9239-            return len(list(ss._iter_share_files(si)))
9240+            return len(list(ss.backend.get_shareset(si).get_shares()))
9241         def _get_sharefile(si):
9242hunk ./src/allmydata/test/test_storage.py 3631
9243-            return list(ss._iter_share_files(si))[0]
9244+            return list(ss.backend.get_shareset(si).get_shares())[0]
9245         def count_leases(si):
9246             return len(list(_get_sharefile(si).get_leases()))
9247 
9248hunk ./src/allmydata/test/test_storage.py 3668
9249 
9250     def test_only_mutable(self):
9251         basedir = "storage/LeaseCrawler/only_mutable"
9252-        fileutil.make_dirs(basedir)
9253+        fp = FilePath(basedir)
9254+        backend = DiskBackend(fp)
9255+
9256         # setting 'cutoff_date' to 2000 seconds ago means that any lease that
9257         # is more than 2000 seconds old will be expired.
9258         now = time.time()
9259hunk ./src/allmydata/test/test_storage.py 3681
9260             'cutoff_date': then,
9261             'sharetypes': ('mutable',),
9262         }
9263-        ss = StorageServer(basedir, "\x00" * 20, expiration_policy)
9264+        ss = StorageServer("\x00" * 20, backend, fp, expiration_policy=expiration_policy)
9265         lc = ss.lease_checker
9266         lc.slow_start = 0
9267         webstatus = StorageStatus(ss)
9268hunk ./src/allmydata/test/test_storage.py 3692
9269         new_expiration_time = now - 3000 + 31*24*60*60
9270 
9271         def count_shares(si):
9272-            return len(list(ss._iter_share_files(si)))
9273+            return len(list(ss.backend.get_shareset(si).get_shares()))
9274         def _get_sharefile(si):
9275hunk ./src/allmydata/test/test_storage.py 3694
9276-            return list(ss._iter_share_files(si))[0]
9277+            return list(ss.backend.get_shareset(si).get_shares())[0]
9278         def count_leases(si):
9279             return len(list(_get_sharefile(si).get_leases()))
9280 
9281hunk ./src/allmydata/test/test_storage.py 3731
9282 
9283     def test_bad_mode(self):
9284         basedir = "storage/LeaseCrawler/bad_mode"
9285-        fileutil.make_dirs(basedir)
9286+        fp = FilePath(basedir)
9287+        backend = DiskBackend(fp)
9288+
9289+        expiration_policy = {
9290+            'enabled': True,
9291+            'mode': 'bogus',
9292+            'override_lease_duration': None,
9293+            'cutoff_date': None,
9294+            'sharetypes': ('mutable', 'immutable'),
9295+        }
9296         e = self.failUnlessRaises(ValueError,
9297hunk ./src/allmydata/test/test_storage.py 3742
9298-                                  StorageServer, basedir, "\x00" * 20,
9299-                                  expiration_mode="bogus")
9300+                                  StorageServer, "\x00" * 20, backend, fp,
9301+                                  expiration_policy=expiration_policy)
9302         self.failUnlessIn("GC mode 'bogus' must be 'age' or 'cutoff-date'", str(e))
9303 
9304     def test_parse_duration(self):
9305hunk ./src/allmydata/test/test_storage.py 3767
9306 
9307     def test_limited_history(self):
9308         basedir = "storage/LeaseCrawler/limited_history"
9309-        fileutil.make_dirs(basedir)
9310-        ss = StorageServer(basedir, "\x00" * 20)
9311+        fp = FilePath(basedir)
9312+        backend = DiskBackend(fp)
9313+        ss = StorageServer("\x00" * 20, backend, fp)
9314+
9315         # make it start sooner than usual.
9316         lc = ss.lease_checker
9317         lc.slow_start = 0
9318hunk ./src/allmydata/test/test_storage.py 3801
9319 
9320     def test_unpredictable_future(self):
9321         basedir = "storage/LeaseCrawler/unpredictable_future"
9322-        fileutil.make_dirs(basedir)
9323-        ss = StorageServer(basedir, "\x00" * 20)
9324+        fp = FilePath(basedir)
9325+        backend = DiskBackend(fp)
9326+        ss = StorageServer("\x00" * 20, backend, fp)
9327+
9328         # make it start sooner than usual.
9329         lc = ss.lease_checker
9330         lc.slow_start = 0
9331hunk ./src/allmydata/test/test_storage.py 3866
9332 
9333     def test_no_st_blocks(self):
9334         basedir = "storage/LeaseCrawler/no_st_blocks"
9335-        fileutil.make_dirs(basedir)
9336+        fp = FilePath(basedir)
9337+        backend = DiskBackend(fp)
9338+
9339         # A negative 'override_lease_duration' means that the "configured-"
9340         # space-recovered counts will be non-zero, since all shares will have
9341         # expired by then.
9342hunk ./src/allmydata/test/test_storage.py 3878
9343             'override_lease_duration': -1000,
9344             'sharetypes': ('mutable', 'immutable'),
9345         }
9346-        ss = No_ST_BLOCKS_StorageServer(basedir, "\x00" * 20, expiration_policy)
9347+        ss = No_ST_BLOCKS_StorageServer("\x00" * 20, backend, fp, expiration_policy=expiration_policy)
9348 
9349         # make it start sooner than usual.
9350         lc = ss.lease_checker
9351hunk ./src/allmydata/test/test_storage.py 3911
9352             UnknownImmutableContainerVersionError,
9353             ]
9354         basedir = "storage/LeaseCrawler/share_corruption"
9355-        fileutil.make_dirs(basedir)
9356-        ss = InstrumentedStorageServer(basedir, "\x00" * 20)
9357+        fp = FilePath(basedir)
9358+        backend = DiskBackend(fp)
9359+        ss = InstrumentedStorageServer("\x00" * 20, backend, fp)
9360         w = StorageStatus(ss)
9361         # make it start sooner than usual.
9362         lc = ss.lease_checker
9363hunk ./src/allmydata/test/test_storage.py 3928
9364         [immutable_si_0, immutable_si_1, mutable_si_2, mutable_si_3] = self.sis
9365         first = min(self.sis)
9366         first_b32 = base32.b2a(first)
9367-        fp = ss.backend.get_shareset(first).sharehomedir.child("0")
9368+        fp = ss.backend.get_shareset(first)._sharehomedir.child("0")
9369         f = fp.open("rb+")
9370hunk ./src/allmydata/test/test_storage.py 3930
9371-        f.seek(0)
9372-        f.write("BAD MAGIC")
9373-        f.close()
9374+        try:
9375+            f.seek(0)
9376+            f.write("BAD MAGIC")
9377+        finally:
9378+            f.close()
9379         # if get_share_file() doesn't see the correct mutable magic, it
9380         # assumes the file is an immutable share, and then
9381         # immutable.ShareFile sees a bad version. So regardless of which kind
9382hunk ./src/allmydata/test/test_storage.py 3943
9383 
9384         # also create an empty bucket
9385         empty_si = base32.b2a("\x04"*16)
9386-        empty_bucket_dir = ss.backend.get_shareset(empty_si).sharehomedir
9387+        empty_bucket_dir = ss.backend.get_shareset(empty_si)._sharehomedir
9388         fileutil.fp_make_dirs(empty_bucket_dir)
9389 
9390         ss.setServiceParent(self.s)
9391hunk ./src/allmydata/test/test_storage.py 4031
9392 
9393     def test_status(self):
9394         basedir = "storage/WebStatus/status"
9395-        fileutil.make_dirs(basedir)
9396-        ss = StorageServer(basedir, "\x00" * 20)
9397+        fp = FilePath(basedir)
9398+        backend = DiskBackend(fp)
9399+        ss = StorageServer("\x00" * 20, backend, fp)
9400         ss.setServiceParent(self.s)
9401         w = StorageStatus(ss)
9402         d = self.render1(w)
9403hunk ./src/allmydata/test/test_storage.py 4065
9404         # Some platforms may have no disk stats API. Make sure the code can handle that
9405         # (test runs on all platforms).
9406         basedir = "storage/WebStatus/status_no_disk_stats"
9407-        fileutil.make_dirs(basedir)
9408-        ss = StorageServer(basedir, "\x00" * 20)
9409+        fp = FilePath(basedir)
9410+        backend = DiskBackend(fp)
9411+        ss = StorageServer("\x00" * 20, backend, fp)
9412         ss.setServiceParent(self.s)
9413         w = StorageStatus(ss)
9414         html = w.renderSynchronously()
9415hunk ./src/allmydata/test/test_storage.py 4085
9416         # If the API to get disk stats exists but a call to it fails, then the status should
9417         # show that no shares will be accepted, and get_available_space() should be 0.
9418         basedir = "storage/WebStatus/status_bad_disk_stats"
9419-        fileutil.make_dirs(basedir)
9420-        ss = StorageServer(basedir, "\x00" * 20)
9421+        fp = FilePath(basedir)
9422+        backend = DiskBackend(fp)
9423+        ss = StorageServer("\x00" * 20, backend, fp)
9424         ss.setServiceParent(self.s)
9425         w = StorageStatus(ss)
9426         html = w.renderSynchronously()
9427}
9428[Fix most of the crawler tests. refs #999
9429david-sarah@jacaranda.org**20110922183008
9430 Ignore-this: 116c0848008f3989ba78d87c07ec783c
9431] {
9432hunk ./src/allmydata/storage/backends/disk/disk_backend.py 160
9433         self._discard_storage = discard_storage
9434 
9435     def get_overhead(self):
9436-        return (fileutil.get_disk_usage(self._sharehomedir) +
9437-                fileutil.get_disk_usage(self._incominghomedir))
9438+        return (fileutil.get_used_space(self._sharehomedir) +
9439+                fileutil.get_used_space(self._incominghomedir))
9440 
9441     def get_shares(self):
9442         """
9443hunk ./src/allmydata/storage/crawler.py 2
9444 
9445-import time, struct
9446-import cPickle as pickle
9447+import time, pickle, struct
9448 from twisted.internet import reactor
9449 from twisted.application import service
9450 
9451hunk ./src/allmydata/storage/crawler.py 205
9452         #                            shareset to be processed, or None if we
9453         #                            are sleeping between cycles
9454         try:
9455-            state = pickle.loads(self.statefp.getContent())
9456+            pickled = self.statefp.getContent()
9457         except EnvironmentError:
9458             if self.statefp.exists():
9459                 raise
9460hunk ./src/allmydata/storage/crawler.py 215
9461                      "last-complete-prefix": None,
9462                      "last-complete-bucket": None,
9463                      }
9464+        else:
9465+            state = pickle.loads(pickled)
9466+
9467         state.setdefault("current-cycle-start-time", time.time()) # approximate
9468         self.state = state
9469         lcp = state["last-complete-prefix"]
9470hunk ./src/allmydata/storage/crawler.py 246
9471         else:
9472             last_complete_prefix = self.prefixes[lcpi]
9473         self.state["last-complete-prefix"] = last_complete_prefix
9474-        self.statefp.setContent(pickle.dumps(self.state))
9475+        pickled = pickle.dumps(self.state)
9476+        self.statefp.setContent(pickled)
9477 
9478     def startService(self):
9479         # arrange things to look like we were just sleeping, so
9480hunk ./src/allmydata/storage/expirer.py 86
9481         # initialize history
9482         if not self.historyfp.exists():
9483             history = {} # cyclenum -> dict
9484-            self.historyfp.setContent(pickle.dumps(history))
9485+            pickled = pickle.dumps(history)
9486+            self.historyfp.setContent(pickled)
9487 
9488     def create_empty_cycle_dict(self):
9489         recovered = self.create_empty_recovered_dict()
9490hunk ./src/allmydata/storage/expirer.py 111
9491     def started_cycle(self, cycle):
9492         self.state["cycle-to-date"] = self.create_empty_cycle_dict()
9493 
9494-    def process_storage_index(self, cycle, prefix, container):
9495+    def process_shareset(self, cycle, prefix, shareset):
9496         would_keep_shares = []
9497         wks = None
9498hunk ./src/allmydata/storage/expirer.py 114
9499-        sharetype = None
9500 
9501hunk ./src/allmydata/storage/expirer.py 115
9502-        for share in container.get_shares():
9503-            sharetype = share.sharetype
9504+        for share in shareset.get_shares():
9505             try:
9506                 wks = self.process_share(share)
9507             except (UnknownMutableContainerVersionError,
9508hunk ./src/allmydata/storage/expirer.py 128
9509                 wks = (1, 1, 1, "unknown")
9510             would_keep_shares.append(wks)
9511 
9512-        container_type = None
9513+        shareset_type = None
9514         if wks:
9515hunk ./src/allmydata/storage/expirer.py 130
9516-            # use the last share's sharetype as the container type
9517-            container_type = wks[3]
9518+            # use the last share's type as the shareset type
9519+            shareset_type = wks[3]
9520         rec = self.state["cycle-to-date"]["space-recovered"]
9521         self.increment(rec, "examined-buckets", 1)
9522hunk ./src/allmydata/storage/expirer.py 134
9523-        if sharetype:
9524-            self.increment(rec, "examined-buckets-"+container_type, 1)
9525+        if shareset_type:
9526+            self.increment(rec, "examined-buckets-"+shareset_type, 1)
9527 
9528hunk ./src/allmydata/storage/expirer.py 137
9529-        container_diskbytes = container.get_overhead()
9530+        shareset_diskbytes = shareset.get_overhead()
9531 
9532         if sum([wks[0] for wks in would_keep_shares]) == 0:
9533hunk ./src/allmydata/storage/expirer.py 140
9534-            self.increment_container_space("original", container_diskbytes, sharetype)
9535+            self.increment_shareset_space("original", shareset_diskbytes, shareset_type)
9536         if sum([wks[1] for wks in would_keep_shares]) == 0:
9537hunk ./src/allmydata/storage/expirer.py 142
9538-            self.increment_container_space("configured", container_diskbytes, sharetype)
9539+            self.increment_shareset_space("configured", shareset_diskbytes, shareset_type)
9540         if sum([wks[2] for wks in would_keep_shares]) == 0:
9541hunk ./src/allmydata/storage/expirer.py 144
9542-            self.increment_container_space("actual", container_diskbytes, sharetype)
9543+            self.increment_shareset_space("actual", shareset_diskbytes, shareset_type)
9544 
9545     def process_share(self, share):
9546         sharetype = share.sharetype
9547hunk ./src/allmydata/storage/expirer.py 189
9548 
9549         so_far = self.state["cycle-to-date"]
9550         self.increment(so_far["leases-per-share-histogram"], num_leases, 1)
9551-        self.increment_space("examined", diskbytes, sharetype)
9552+        self.increment_space("examined", sharebytes, diskbytes, sharetype)
9553 
9554         would_keep_share = [1, 1, 1, sharetype]
9555 
9556hunk ./src/allmydata/storage/expirer.py 220
9557             self.increment(so_far_sr, a+"-sharebytes-"+sharetype, sharebytes)
9558             self.increment(so_far_sr, a+"-diskbytes-"+sharetype, diskbytes)
9559 
9560-    def increment_container_space(self, a, container_diskbytes, container_type):
9561+    def increment_shareset_space(self, a, shareset_diskbytes, shareset_type):
9562         rec = self.state["cycle-to-date"]["space-recovered"]
9563hunk ./src/allmydata/storage/expirer.py 222
9564-        self.increment(rec, a+"-diskbytes", container_diskbytes)
9565+        self.increment(rec, a+"-diskbytes", shareset_diskbytes)
9566         self.increment(rec, a+"-buckets", 1)
9567hunk ./src/allmydata/storage/expirer.py 224
9568-        if container_type:
9569-            self.increment(rec, a+"-diskbytes-"+container_type, container_diskbytes)
9570-            self.increment(rec, a+"-buckets-"+container_type, 1)
9571+        if shareset_type:
9572+            self.increment(rec, a+"-diskbytes-"+shareset_type, shareset_diskbytes)
9573+            self.increment(rec, a+"-buckets-"+shareset_type, 1)
9574 
9575     def increment(self, d, k, delta=1):
9576         if k not in d:
9577hunk ./src/allmydata/storage/expirer.py 280
9578         # copy() needs to become a deepcopy
9579         h["space-recovered"] = s["space-recovered"].copy()
9580 
9581-        history = pickle.loads(self.historyfp.getContent())
9582+        pickled = self.historyfp.getContent()
9583+        history = pickle.loads(pickled)
9584         history[cycle] = h
9585         while len(history) > 10:
9586             oldcycles = sorted(history.keys())
9587hunk ./src/allmydata/storage/expirer.py 286
9588             del history[oldcycles[0]]
9589-        self.historyfp.setContent(pickle.dumps(history))
9590+        repickled = pickle.dumps(history)
9591+        self.historyfp.setContent(repickled)
9592 
9593     def get_state(self):
9594         """In addition to the crawler state described in
9595hunk ./src/allmydata/storage/expirer.py 356
9596         progress = self.get_progress()
9597 
9598         state = ShareCrawler.get_state(self) # does a shallow copy
9599-        history = pickle.loads(self.historyfp.getContent())
9600+        pickled = self.historyfp.getContent()
9601+        history = pickle.loads(pickled)
9602         state["history"] = history
9603 
9604         if not progress["cycle-in-progress"]:
9605hunk ./src/allmydata/test/test_crawler.py 25
9606         ShareCrawler.__init__(self, *args, **kwargs)
9607         self.all_buckets = []
9608         self.finished_d = defer.Deferred()
9609-    def process_bucket(self, cycle, prefix, prefixdir, storage_index_b32):
9610-        self.all_buckets.append(storage_index_b32)
9611+
9612+    def process_shareset(self, cycle, prefix, shareset):
9613+        self.all_buckets.append(shareset.get_storage_index_string())
9614+
9615     def finished_cycle(self, cycle):
9616         eventually(self.finished_d.callback, None)
9617 
9618hunk ./src/allmydata/test/test_crawler.py 41
9619         self.all_buckets = []
9620         self.finished_d = defer.Deferred()
9621         self.yield_cb = None
9622-    def process_bucket(self, cycle, prefix, prefixdir, storage_index_b32):
9623-        self.all_buckets.append(storage_index_b32)
9624+
9625+    def process_shareset(self, cycle, prefix, shareset):
9626+        self.all_buckets.append(shareset.get_storage_index_string())
9627         self.countdown -= 1
9628         if self.countdown == 0:
9629             # force a timeout. We restore it in yielding()
9630hunk ./src/allmydata/test/test_crawler.py 66
9631         self.accumulated = 0.0
9632         self.cycles = 0
9633         self.last_yield = 0.0
9634-    def process_bucket(self, cycle, prefix, prefixdir, storage_index_b32):
9635+
9636+    def process_shareset(self, cycle, prefix, shareset):
9637         start = time.time()
9638         time.sleep(0.05)
9639         elapsed = time.time() - start
9640hunk ./src/allmydata/test/test_crawler.py 85
9641         ShareCrawler.__init__(self, *args, **kwargs)
9642         self.counter = 0
9643         self.finished_d = defer.Deferred()
9644-    def process_bucket(self, cycle, prefix, prefixdir, storage_index_b32):
9645+
9646+    def process_shareset(self, cycle, prefix, shareset):
9647         self.counter += 1
9648     def finished_cycle(self, cycle):
9649         self.finished_d.callback(None)
9650hunk ./src/allmydata/test/test_storage.py 3041
9651 
9652 class InstrumentedLeaseCheckingCrawler(LeaseCheckingCrawler):
9653     stop_after_first_bucket = False
9654-    def process_bucket(self, *args, **kwargs):
9655-        LeaseCheckingCrawler.process_bucket(self, *args, **kwargs)
9656+
9657+    def process_shareset(self, cycle, prefix, shareset):
9658+        LeaseCheckingCrawler.process_shareset(self, cycle, prefix, shareset)
9659         if self.stop_after_first_bucket:
9660             self.stop_after_first_bucket = False
9661             self.cpu_slice = -1.0
9662hunk ./src/allmydata/test/test_storage.py 3051
9663         if not self.stop_after_first_bucket:
9664             self.cpu_slice = 500
9665 
9666+class InstrumentedStorageServer(StorageServer):
9667+    LeaseCheckerClass = InstrumentedLeaseCheckingCrawler
9668+
9669+
9670 class BrokenStatResults:
9671     pass
9672 class No_ST_BLOCKS_LeaseCheckingCrawler(LeaseCheckingCrawler):
9673hunk ./src/allmydata/test/test_storage.py 3069
9674             setattr(bsr, attrname, getattr(s, attrname))
9675         return bsr
9676 
9677-class InstrumentedStorageServer(StorageServer):
9678-    LeaseCheckerClass = InstrumentedLeaseCheckingCrawler
9679 class No_ST_BLOCKS_StorageServer(StorageServer):
9680     LeaseCheckerClass = No_ST_BLOCKS_LeaseCheckingCrawler
9681 
9682}
9683[Reinstate the cancel_lease methods of ImmutableDiskShare and MutableDiskShare, since they are needed for lease expiry. refs #999
9684david-sarah@jacaranda.org**20110922183323
9685 Ignore-this: a11fb0dd0078ff627cb727fc769ec848
9686] {
9687hunk ./src/allmydata/storage/backends/disk/immutable.py 260
9688         except IndexError:
9689             self.add_lease(lease_info)
9690 
9691+    def cancel_lease(self, cancel_secret):
9692+        """Remove a lease with the given cancel_secret. If the last lease is
9693+        cancelled, the file will be removed. Return the number of bytes that
9694+        were freed (by truncating the list of leases, and possibly by
9695+        deleting the file). Raise IndexError if there was no lease with the
9696+        given cancel_secret.
9697+        """
9698+
9699+        leases = list(self.get_leases())
9700+        num_leases_removed = 0
9701+        for i, lease in enumerate(leases):
9702+            if constant_time_compare(lease.cancel_secret, cancel_secret):
9703+                leases[i] = None
9704+                num_leases_removed += 1
9705+        if not num_leases_removed:
9706+            raise IndexError("unable to find matching lease to cancel")
9707+
9708+        space_freed = 0
9709+        if num_leases_removed:
9710+            # pack and write out the remaining leases. We write these out in
9711+            # the same order as they were added, so that if we crash while
9712+            # doing this, we won't lose any non-cancelled leases.
9713+            leases = [l for l in leases if l] # remove the cancelled leases
9714+            if len(leases) > 0:
9715+                f = self._home.open('rb+')
9716+                try:
9717+                    for i, lease in enumerate(leases):
9718+                        self._write_lease_record(f, i, lease)
9719+                    self._write_num_leases(f, len(leases))
9720+                    self._truncate_leases(f, len(leases))
9721+                finally:
9722+                    f.close()
9723+                space_freed = self.LEASE_SIZE * num_leases_removed
9724+            else:
9725+                space_freed = fileutil.get_used_space(self._home)
9726+                self.unlink()
9727+        return space_freed
9728+
9729hunk ./src/allmydata/storage/backends/disk/mutable.py 361
9730         except IndexError:
9731             self.add_lease(lease_info)
9732 
9733+    def cancel_lease(self, cancel_secret):
9734+        """Remove any leases with the given cancel_secret. If the last lease
9735+        is cancelled, the file will be removed. Return the number of bytes
9736+        that were freed (by truncating the list of leases, and possibly by
9737+        deleting the file). Raise IndexError if there was no lease with the
9738+        given cancel_secret."""
9739+
9740+        # XXX can this be more like ImmutableDiskShare.cancel_lease?
9741+
9742+        accepting_nodeids = set()
9743+        modified = 0
9744+        remaining = 0
9745+        blank_lease = LeaseInfo(owner_num=0,
9746+                                renew_secret="\x00"*32,
9747+                                cancel_secret="\x00"*32,
9748+                                expiration_time=0,
9749+                                nodeid="\x00"*20)
9750+        f = self._home.open('rb+')
9751+        try:
9752+            for (leasenum, lease) in self._enumerate_leases(f):
9753+                accepting_nodeids.add(lease.nodeid)
9754+                if constant_time_compare(lease.cancel_secret, cancel_secret):
9755+                    self._write_lease_record(f, leasenum, blank_lease)
9756+                    modified += 1
9757+                else:
9758+                    remaining += 1
9759+            if modified:
9760+                freed_space = self._pack_leases(f)
9761+        finally:
9762+            f.close()
9763+
9764+        if modified > 0:
9765+            if remaining == 0:
9766+                freed_space = fileutil.get_used_space(self._home)
9767+                self.unlink()
9768+            return freed_space
9769+
9770+        msg = ("Unable to cancel non-existent lease. I have leases "
9771+               "accepted by nodeids: ")
9772+        msg += ",".join([("'%s'" % idlib.nodeid_b2a(anid))
9773+                         for anid in accepting_nodeids])
9774+        msg += " ."
9775+        raise IndexError(msg)
9776+
9777+    def _pack_leases(self, f):
9778+        # TODO: reclaim space from cancelled leases
9779+        return 0
9780+
9781     def _read_write_enabler_and_nodeid(self, f):
9782         f.seek(0)
9783         data = f.read(self.HEADER_SIZE)
9784}
9785[Blank line cleanups.
9786david-sarah@jacaranda.org**20110923012044
9787 Ignore-this: 8e1c4ecb5b0c65673af35872876a8591
9788] {
9789hunk ./src/allmydata/interfaces.py 33
9790 LeaseRenewSecret = Hash # used to protect lease renewal requests
9791 LeaseCancelSecret = Hash # used to protect lease cancellation requests
9792 
9793+
9794 class RIStubClient(RemoteInterface):
9795     """Each client publishes a service announcement for a dummy object called
9796     the StubClient. This object doesn't actually offer any services, but the
9797hunk ./src/allmydata/interfaces.py 42
9798     the grid and the client versions in use). This is the (empty)
9799     RemoteInterface for the StubClient."""
9800 
9801+
9802 class RIBucketWriter(RemoteInterface):
9803     """ Objects of this kind live on the server side. """
9804     def write(offset=Offset, data=ShareData):
9805hunk ./src/allmydata/interfaces.py 61
9806         """
9807         return None
9808 
9809+
9810 class RIBucketReader(RemoteInterface):
9811     def read(offset=Offset, length=ReadSize):
9812         return ShareData
9813hunk ./src/allmydata/interfaces.py 78
9814         documentation.
9815         """
9816 
9817+
9818 TestVector = ListOf(TupleOf(Offset, ReadSize, str, str))
9819 # elements are (offset, length, operator, specimen)
9820 # operator is one of "lt, le, eq, ne, ge, gt"
9821hunk ./src/allmydata/interfaces.py 95
9822 ReadData = ListOf(ShareData)
9823 # returns data[offset:offset+length] for each element of TestVector
9824 
9825+
9826 class RIStorageServer(RemoteInterface):
9827     __remote_name__ = "RIStorageServer.tahoe.allmydata.com"
9828 
9829hunk ./src/allmydata/interfaces.py 2255
9830 
9831     def get_storage_index():
9832         """Return a string with the (binary) storage index."""
9833+
9834     def get_storage_index_string():
9835         """Return a string with the (printable) abbreviated storage index."""
9836hunk ./src/allmydata/interfaces.py 2258
9837+
9838     def get_uri():
9839         """Return the (string) URI of the object that was checked."""
9840 
9841hunk ./src/allmydata/interfaces.py 2353
9842     def get_report():
9843         """Return a list of strings with more detailed results."""
9844 
9845+
9846 class ICheckAndRepairResults(Interface):
9847     """I contain the detailed results of a check/verify/repair operation.
9848 
9849hunk ./src/allmydata/interfaces.py 2363
9850 
9851     def get_storage_index():
9852         """Return a string with the (binary) storage index."""
9853+
9854     def get_storage_index_string():
9855         """Return a string with the (printable) abbreviated storage index."""
9856hunk ./src/allmydata/interfaces.py 2366
9857+
9858     def get_repair_attempted():
9859         """Return a boolean, True if a repair was attempted. We might not
9860         attempt to repair the file because it was healthy, or healthy enough
9861hunk ./src/allmydata/interfaces.py 2372
9862         (i.e. some shares were missing but not enough to exceed some
9863         threshold), or because we don't know how to repair this object."""
9864+
9865     def get_repair_successful():
9866         """Return a boolean, True if repair was attempted and the file/dir
9867         was fully healthy afterwards. False if no repair was attempted or if
9868hunk ./src/allmydata/interfaces.py 2377
9869         a repair attempt failed."""
9870+
9871     def get_pre_repair_results():
9872         """Return an ICheckResults instance that describes the state of the
9873         file/dir before any repair was attempted."""
9874hunk ./src/allmydata/interfaces.py 2381
9875+
9876     def get_post_repair_results():
9877         """Return an ICheckResults instance that describes the state of the
9878         file/dir after any repair was attempted. If no repair was attempted,
9879hunk ./src/allmydata/interfaces.py 2615
9880         (childnode, metadata_dict) tuples), the directory will be populated
9881         with those children, otherwise it will be empty."""
9882 
9883+
9884 class IClientStatus(Interface):
9885     def list_all_uploads():
9886         """Return a list of uploader objects, one for each upload that
9887hunk ./src/allmydata/interfaces.py 2621
9888         currently has an object available (tracked with weakrefs). This is
9889         intended for debugging purposes."""
9890+
9891     def list_active_uploads():
9892         """Return a list of active IUploadStatus objects."""
9893hunk ./src/allmydata/interfaces.py 2624
9894+
9895     def list_recent_uploads():
9896         """Return a list of IUploadStatus objects for the most recently
9897         started uploads."""
9898hunk ./src/allmydata/interfaces.py 2633
9899         """Return a list of downloader objects, one for each download that
9900         currently has an object available (tracked with weakrefs). This is
9901         intended for debugging purposes."""
9902+
9903     def list_active_downloads():
9904         """Return a list of active IDownloadStatus objects."""
9905hunk ./src/allmydata/interfaces.py 2636
9906+
9907     def list_recent_downloads():
9908         """Return a list of IDownloadStatus objects for the most recently
9909         started downloads."""
9910hunk ./src/allmydata/interfaces.py 2641
9911 
9912+
9913 class IUploadStatus(Interface):
9914     def get_started():
9915         """Return a timestamp (float with seconds since epoch) indicating
9916hunk ./src/allmydata/interfaces.py 2646
9917         when the operation was started."""
9918+
9919     def get_storage_index():
9920         """Return a string with the (binary) storage index in use on this
9921         upload. Returns None if the storage index has not yet been
9922hunk ./src/allmydata/interfaces.py 2651
9923         calculated."""
9924+
9925     def get_size():
9926         """Return an integer with the number of bytes that will eventually
9927         be uploaded for this file. Returns None if the size is not yet known.
9928hunk ./src/allmydata/interfaces.py 2656
9929         """
9930+
9931     def using_helper():
9932         """Return True if this upload is using a Helper, False if not."""
9933hunk ./src/allmydata/interfaces.py 2659
9934+
9935     def get_status():
9936         """Return a string describing the current state of the upload
9937         process."""
9938hunk ./src/allmydata/interfaces.py 2663
9939+
9940     def get_progress():
9941         """Returns a tuple of floats, (chk, ciphertext, encode_and_push),
9942         each from 0.0 to 1.0 . 'chk' describes how much progress has been
9943hunk ./src/allmydata/interfaces.py 2675
9944         process has finished: for helper uploads this is dependent upon the
9945         helper providing progress reports. It might be reasonable to add all
9946         three numbers and report the sum to the user."""
9947+
9948     def get_active():
9949         """Return True if the upload is currently active, False if not."""
9950hunk ./src/allmydata/interfaces.py 2678
9951+
9952     def get_results():
9953         """Return an instance of UploadResults (which contains timing and
9954         sharemap information). Might return None if the upload is not yet
9955hunk ./src/allmydata/interfaces.py 2683
9956         finished."""
9957+
9958     def get_counter():
9959         """Each upload status gets a unique number: this method returns that
9960         number. This provides a handle to this particular upload, so a web
9961hunk ./src/allmydata/interfaces.py 2689
9962         page can generate a suitable hyperlink."""
9963 
9964+
9965 class IDownloadStatus(Interface):
9966     def get_started():
9967         """Return a timestamp (float with seconds since epoch) indicating
9968hunk ./src/allmydata/interfaces.py 2694
9969         when the operation was started."""
9970+
9971     def get_storage_index():
9972         """Return a string with the (binary) storage index in use on this
9973         download. This may be None if there is no storage index (i.e. LIT
9974hunk ./src/allmydata/interfaces.py 2699
9975         files)."""
9976+
9977     def get_size():
9978         """Return an integer with the number of bytes that will eventually be
9979         retrieved for this file. Returns None if the size is not yet known.
9980hunk ./src/allmydata/interfaces.py 2704
9981         """
9982+
9983     def using_helper():
9984         """Return True if this download is using a Helper, False if not."""
9985hunk ./src/allmydata/interfaces.py 2707
9986+
9987     def get_status():
9988         """Return a string describing the current state of the download
9989         process."""
9990hunk ./src/allmydata/interfaces.py 2711
9991+
9992     def get_progress():
9993         """Returns a float (from 0.0 to 1.0) describing the amount of the
9994         download that has completed. This value will remain at 0.0 until the
9995hunk ./src/allmydata/interfaces.py 2716
9996         first byte of plaintext is pushed to the download target."""
9997+
9998     def get_active():
9999         """Return True if the download is currently active, False if not."""
10000hunk ./src/allmydata/interfaces.py 2719
10001+
10002     def get_counter():
10003         """Each download status gets a unique number: this method returns
10004         that number. This provides a handle to this particular download, so a
10005hunk ./src/allmydata/interfaces.py 2725
10006         web page can generate a suitable hyperlink."""
10007 
10008+
10009 class IServermapUpdaterStatus(Interface):
10010     pass
10011hunk ./src/allmydata/interfaces.py 2728
10012+
10013+
10014 class IPublishStatus(Interface):
10015     pass
10016hunk ./src/allmydata/interfaces.py 2732
10017+
10018+
10019 class IRetrieveStatus(Interface):
10020     pass
10021 
10022hunk ./src/allmydata/interfaces.py 2737
10023+
10024 class NotCapableError(Exception):
10025     """You have tried to write to a read-only node."""
10026 
10027hunk ./src/allmydata/interfaces.py 2741
10028+
10029 class BadWriteEnablerError(Exception):
10030     pass
10031 
10032hunk ./src/allmydata/interfaces.py 2745
10033-class RIControlClient(RemoteInterface):
10034 
10035hunk ./src/allmydata/interfaces.py 2746
10036+class RIControlClient(RemoteInterface):
10037     def wait_for_client_connections(num_clients=int):
10038         """Do not return until we have connections to at least NUM_CLIENTS
10039         storage servers.
10040hunk ./src/allmydata/interfaces.py 2801
10041 
10042         return DictOf(str, float)
10043 
10044+
10045 UploadResults = Any() #DictOf(str, str)
10046 
10047hunk ./src/allmydata/interfaces.py 2804
10048+
10049 class RIEncryptedUploadable(RemoteInterface):
10050     __remote_name__ = "RIEncryptedUploadable.tahoe.allmydata.com"
10051 
10052hunk ./src/allmydata/interfaces.py 2877
10053         """
10054         return DictOf(str, DictOf(str, ChoiceOf(float, int, long, None)))
10055 
10056+
10057 class RIStatsGatherer(RemoteInterface):
10058     __remote_name__ = "RIStatsGatherer.tahoe.allmydata.com"
10059     """
10060hunk ./src/allmydata/interfaces.py 2917
10061 class FileTooLargeError(Exception):
10062     pass
10063 
10064+
10065 class IValidatedThingProxy(Interface):
10066     def start():
10067         """ Acquire a thing and validate it. Return a deferred that is
10068hunk ./src/allmydata/interfaces.py 2924
10069         eventually fired with self if the thing is valid or errbacked if it
10070         can't be acquired or validated."""
10071 
10072+
10073 class InsufficientVersionError(Exception):
10074     def __init__(self, needed, got):
10075         self.needed = needed
10076hunk ./src/allmydata/interfaces.py 2933
10077         return "InsufficientVersionError(need '%s', got %s)" % (self.needed,
10078                                                                 self.got)
10079 
10080+
10081 class EmptyPathnameComponentError(Exception):
10082     """The webapi disallows empty pathname components."""
10083hunk ./src/allmydata/test/test_crawler.py 21
10084 class BucketEnumeratingCrawler(ShareCrawler):
10085     cpu_slice = 500 # make sure it can complete in a single slice
10086     slow_start = 0
10087+
10088     def __init__(self, *args, **kwargs):
10089         ShareCrawler.__init__(self, *args, **kwargs)
10090         self.all_buckets = []
10091hunk ./src/allmydata/test/test_crawler.py 33
10092     def finished_cycle(self, cycle):
10093         eventually(self.finished_d.callback, None)
10094 
10095+
10096 class PacedCrawler(ShareCrawler):
10097     cpu_slice = 500 # make sure it can complete in a single slice
10098     slow_start = 0
10099hunk ./src/allmydata/test/test_crawler.py 37
10100+
10101     def __init__(self, *args, **kwargs):
10102         ShareCrawler.__init__(self, *args, **kwargs)
10103         self.countdown = 6
10104hunk ./src/allmydata/test/test_crawler.py 51
10105         if self.countdown == 0:
10106             # force a timeout. We restore it in yielding()
10107             self.cpu_slice = -1.0
10108+
10109     def yielding(self, sleep_time):
10110         self.cpu_slice = 500
10111         if self.yield_cb:
10112hunk ./src/allmydata/test/test_crawler.py 56
10113             self.yield_cb()
10114+
10115     def finished_cycle(self, cycle):
10116         eventually(self.finished_d.callback, None)
10117 
10118hunk ./src/allmydata/test/test_crawler.py 60
10119+
10120 class ConsumingCrawler(ShareCrawler):
10121     cpu_slice = 0.5
10122     allowed_cpu_percentage = 0.5
10123hunk ./src/allmydata/test/test_crawler.py 79
10124         elapsed = time.time() - start
10125         self.accumulated += elapsed
10126         self.last_yield += elapsed
10127+
10128     def finished_cycle(self, cycle):
10129         self.cycles += 1
10130hunk ./src/allmydata/test/test_crawler.py 82
10131+
10132     def yielding(self, sleep_time):
10133         self.last_yield = 0.0
10134 
10135hunk ./src/allmydata/test/test_crawler.py 86
10136+
10137 class OneShotCrawler(ShareCrawler):
10138     cpu_slice = 500 # make sure it can complete in a single slice
10139     slow_start = 0
10140hunk ./src/allmydata/test/test_crawler.py 90
10141+
10142     def __init__(self, *args, **kwargs):
10143         ShareCrawler.__init__(self, *args, **kwargs)
10144         self.counter = 0
10145hunk ./src/allmydata/test/test_crawler.py 98
10146 
10147     def process_shareset(self, cycle, prefix, shareset):
10148         self.counter += 1
10149+
10150     def finished_cycle(self, cycle):
10151         self.finished_d.callback(None)
10152         self.disownServiceParent()
10153hunk ./src/allmydata/test/test_crawler.py 103
10154 
10155+
10156 class Basic(unittest.TestCase, StallMixin, pollmixin.PollMixin):
10157     def setUp(self):
10158         self.s = service.MultiService()
10159hunk ./src/allmydata/test/test_crawler.py 114
10160 
10161     def si(self, i):
10162         return hashutil.storage_index_hash(str(i))
10163+
10164     def rs(self, i, serverid):
10165         return hashutil.bucket_renewal_secret_hash(str(i), serverid)
10166hunk ./src/allmydata/test/test_crawler.py 117
10167+
10168     def cs(self, i, serverid):
10169         return hashutil.bucket_cancel_secret_hash(str(i), serverid)
10170 
10171hunk ./src/allmydata/test/test_storage.py 39
10172 from allmydata.test.no_network import NoNetworkServer
10173 from allmydata.web.storage import StorageStatus, remove_prefix
10174 
10175+
10176 class Marker:
10177     pass
10178hunk ./src/allmydata/test/test_storage.py 42
10179+
10180+
10181 class FakeCanary:
10182     def __init__(self, ignore_disconnectors=False):
10183         self.ignore = ignore_disconnectors
10184hunk ./src/allmydata/test/test_storage.py 59
10185             return
10186         del self.disconnectors[marker]
10187 
10188+
10189 class FakeStatsProvider:
10190     def count(self, name, delta=1):
10191         pass
10192hunk ./src/allmydata/test/test_storage.py 66
10193     def register_producer(self, producer):
10194         pass
10195 
10196+
10197 class Bucket(unittest.TestCase):
10198     def make_workdir(self, name):
10199         basedir = FilePath("storage").child("Bucket").child(name)
10200hunk ./src/allmydata/test/test_storage.py 165
10201         result_of_read = br.remote_read(0, len(share_data)+1)
10202         self.failUnlessEqual(result_of_read, share_data)
10203 
10204+
10205 class RemoteBucket:
10206 
10207     def __init__(self):
10208hunk ./src/allmydata/test/test_storage.py 309
10209         return self._do_test_readwrite("test_readwrite_v2",
10210                                        0x44, WriteBucketProxy_v2, ReadBucketProxy)
10211 
10212+
10213 class Server(unittest.TestCase):
10214 
10215     def setUp(self):
10216hunk ./src/allmydata/test/test_storage.py 780
10217         self.failUnlessIn("This share tastes like dust.", report)
10218 
10219 
10220-
10221 class MutableServer(unittest.TestCase):
10222 
10223     def setUp(self):
10224hunk ./src/allmydata/test/test_storage.py 1407
10225         # header.
10226         self.salt_hash_tree_s = self.serialize_blockhashes(self.salt_hash_tree[1:])
10227 
10228-
10229     def tearDown(self):
10230         self.sparent.stopService()
10231         fileutil.fp_remove(self.workdir("MDMFProxies storage test server"))
10232hunk ./src/allmydata/test/test_storage.py 1411
10233 
10234-
10235     def write_enabler(self, we_tag):
10236         return hashutil.tagged_hash("we_blah", we_tag)
10237 
10238hunk ./src/allmydata/test/test_storage.py 1414
10239-
10240     def renew_secret(self, tag):
10241         return hashutil.tagged_hash("renew_blah", str(tag))
10242 
10243hunk ./src/allmydata/test/test_storage.py 1417
10244-
10245     def cancel_secret(self, tag):
10246         return hashutil.tagged_hash("cancel_blah", str(tag))
10247 
10248hunk ./src/allmydata/test/test_storage.py 1420
10249-
10250     def workdir(self, name):
10251         return FilePath("storage").child("MDMFProxies").child(name)
10252 
10253hunk ./src/allmydata/test/test_storage.py 1430
10254         ss.setServiceParent(self.sparent)
10255         return ss
10256 
10257-
10258     def build_test_mdmf_share(self, tail_segment=False, empty=False):
10259         # Start with the checkstring
10260         data = struct.pack(">BQ32s",
10261hunk ./src/allmydata/test/test_storage.py 1527
10262         data += self.block_hash_tree_s
10263         return data
10264 
10265-
10266     def write_test_share_to_server(self,
10267                                    storage_index,
10268                                    tail_segment=False,
10269hunk ./src/allmydata/test/test_storage.py 1548
10270         results = write(storage_index, self.secrets, tws, readv)
10271         self.failUnless(results[0])
10272 
10273-
10274     def build_test_sdmf_share(self, empty=False):
10275         if empty:
10276             sharedata = ""
10277hunk ./src/allmydata/test/test_storage.py 1598
10278         self.offsets['EOF'] = eof_offset
10279         return final_share
10280 
10281-
10282     def write_sdmf_share_to_server(self,
10283                                    storage_index,
10284                                    empty=False):
10285hunk ./src/allmydata/test/test_storage.py 1613
10286         results = write(storage_index, self.secrets, tws, readv)
10287         self.failUnless(results[0])
10288 
10289-
10290     def test_read(self):
10291         self.write_test_share_to_server("si1")
10292         mr = MDMFSlotReadProxy(self.rref, "si1", 0)
10293hunk ./src/allmydata/test/test_storage.py 1682
10294             self.failUnlessEqual(checkstring, checkstring))
10295         return d
10296 
10297-
10298     def test_read_with_different_tail_segment_size(self):
10299         self.write_test_share_to_server("si1", tail_segment=True)
10300         mr = MDMFSlotReadProxy(self.rref, "si1", 0)
10301hunk ./src/allmydata/test/test_storage.py 1693
10302         d.addCallback(_check_tail_segment)
10303         return d
10304 
10305-
10306     def test_get_block_with_invalid_segnum(self):
10307         self.write_test_share_to_server("si1")
10308         mr = MDMFSlotReadProxy(self.rref, "si1", 0)
10309hunk ./src/allmydata/test/test_storage.py 1703
10310                             mr.get_block_and_salt, 7))
10311         return d
10312 
10313-
10314     def test_get_encoding_parameters_first(self):
10315         self.write_test_share_to_server("si1")
10316         mr = MDMFSlotReadProxy(self.rref, "si1", 0)
10317hunk ./src/allmydata/test/test_storage.py 1715
10318         d.addCallback(_check_encoding_parameters)
10319         return d
10320 
10321-
10322     def test_get_seqnum_first(self):
10323         self.write_test_share_to_server("si1")
10324         mr = MDMFSlotReadProxy(self.rref, "si1", 0)
10325hunk ./src/allmydata/test/test_storage.py 1723
10326             self.failUnlessEqual(seqnum, 0))
10327         return d
10328 
10329-
10330     def test_get_root_hash_first(self):
10331         self.write_test_share_to_server("si1")
10332         mr = MDMFSlotReadProxy(self.rref, "si1", 0)
10333hunk ./src/allmydata/test/test_storage.py 1731
10334             self.failUnlessEqual(root_hash, self.root_hash))
10335         return d
10336 
10337-
10338     def test_get_checkstring_first(self):
10339         self.write_test_share_to_server("si1")
10340         mr = MDMFSlotReadProxy(self.rref, "si1", 0)
10341hunk ./src/allmydata/test/test_storage.py 1739
10342             self.failUnlessEqual(checkstring, self.checkstring))
10343         return d
10344 
10345-
10346     def test_write_read_vectors(self):
10347         # When writing for us, the storage server will return to us a
10348         # read vector, along with its result. If a write fails because
10349hunk ./src/allmydata/test/test_storage.py 1777
10350         # The checkstring remains the same for the rest of the process.
10351         return d
10352 
10353-
10354     def test_private_key_after_share_hash_chain(self):
10355         mw = self._make_new_mw("si1", 0)
10356         d = defer.succeed(None)
10357hunk ./src/allmydata/test/test_storage.py 1795
10358                             mw.put_encprivkey, self.encprivkey))
10359         return d
10360 
10361-
10362     def test_signature_after_verification_key(self):
10363         mw = self._make_new_mw("si1", 0)
10364         d = defer.succeed(None)
10365hunk ./src/allmydata/test/test_storage.py 1821
10366                             mw.put_signature, self.signature))
10367         return d
10368 
10369-
10370     def test_uncoordinated_write(self):
10371         # Make two mutable writers, both pointing to the same storage
10372         # server, both at the same storage index, and try writing to the
10373hunk ./src/allmydata/test/test_storage.py 1853
10374         d.addCallback(_check_failure)
10375         return d
10376 
10377-
10378     def test_invalid_salt_size(self):
10379         # Salts need to be 16 bytes in size. Writes that attempt to
10380         # write more or less than this should be rejected.
10381hunk ./src/allmydata/test/test_storage.py 1871
10382                             another_invalid_salt))
10383         return d
10384 
10385-
10386     def test_write_test_vectors(self):
10387         # If we give the write proxy a bogus test vector at
10388         # any point during the process, it should fail to write when we
10389hunk ./src/allmydata/test/test_storage.py 1904
10390         d.addCallback(_check_success)
10391         return d
10392 
10393-
10394     def serialize_blockhashes(self, blockhashes):
10395         return "".join(blockhashes)
10396 
10397hunk ./src/allmydata/test/test_storage.py 1907
10398-
10399     def serialize_sharehashes(self, sharehashes):
10400         ret = "".join([struct.pack(">H32s", i, sharehashes[i])
10401                         for i in sorted(sharehashes.keys())])
10402hunk ./src/allmydata/test/test_storage.py 1912
10403         return ret
10404 
10405-
10406     def test_write(self):
10407         # This translates to a file with 6 6-byte segments, and with 2-byte
10408         # blocks.
10409hunk ./src/allmydata/test/test_storage.py 2043
10410                                 6, datalength)
10411         return mw
10412 
10413-
10414     def test_write_rejected_with_too_many_blocks(self):
10415         mw = self._make_new_mw("si0", 0)
10416 
10417hunk ./src/allmydata/test/test_storage.py 2059
10418                             mw.put_block, self.block, 7, self.salt))
10419         return d
10420 
10421-
10422     def test_write_rejected_with_invalid_salt(self):
10423         # Try writing an invalid salt. Salts are 16 bytes -- any more or
10424         # less should cause an error.
10425hunk ./src/allmydata/test/test_storage.py 2070
10426                             None, mw.put_block, self.block, 7, bad_salt))
10427         return d
10428 
10429-
10430     def test_write_rejected_with_invalid_root_hash(self):
10431         # Try writing an invalid root hash. This should be SHA256d, and
10432         # 32 bytes long as a result.
10433hunk ./src/allmydata/test/test_storage.py 2095
10434                             None, mw.put_root_hash, invalid_root_hash))
10435         return d
10436 
10437-
10438     def test_write_rejected_with_invalid_blocksize(self):
10439         # The blocksize implied by the writer that we get from
10440         # _make_new_mw is 2bytes -- any more or any less than this
10441hunk ./src/allmydata/test/test_storage.py 2128
10442             mw.put_block(valid_block, 5, self.salt))
10443         return d
10444 
10445-
10446     def test_write_enforces_order_constraints(self):
10447         # We require that the MDMFSlotWriteProxy be interacted with in a
10448         # specific way.
10449hunk ./src/allmydata/test/test_storage.py 2213
10450             mw0.put_verification_key(self.verification_key))
10451         return d
10452 
10453-
10454     def test_end_to_end(self):
10455         mw = self._make_new_mw("si1", 0)
10456         # Write a share using the mutable writer, and make sure that the
10457hunk ./src/allmydata/test/test_storage.py 2378
10458             self.failUnlessEqual(root_hash, self.root_hash, root_hash))
10459         return d
10460 
10461-
10462     def test_only_reads_one_segment_sdmf(self):
10463         # SDMF shares have only one segment, so it doesn't make sense to
10464         # read more segments than that. The reader should know this and
10465hunk ./src/allmydata/test/test_storage.py 2395
10466                             mr.get_block_and_salt, 1))
10467         return d
10468 
10469-
10470     def test_read_with_prefetched_mdmf_data(self):
10471         # The MDMFSlotReadProxy will prefill certain fields if you pass
10472         # it data that you have already fetched. This is useful for
10473hunk ./src/allmydata/test/test_storage.py 2459
10474         d.addCallback(_check_block_and_salt)
10475         return d
10476 
10477-
10478     def test_read_with_prefetched_sdmf_data(self):
10479         sdmf_data = self.build_test_sdmf_share()
10480         self.write_sdmf_share_to_server("si1")
10481hunk ./src/allmydata/test/test_storage.py 2522
10482         d.addCallback(_check_block_and_salt)
10483         return d
10484 
10485-
10486     def test_read_with_empty_mdmf_file(self):
10487         # Some tests upload a file with no contents to test things
10488         # unrelated to the actual handling of the content of the file.
10489hunk ./src/allmydata/test/test_storage.py 2550
10490                             mr.get_block_and_salt, 0))
10491         return d
10492 
10493-
10494     def test_read_with_empty_sdmf_file(self):
10495         self.write_sdmf_share_to_server("si1", empty=True)
10496         mr = MDMFSlotReadProxy(self.rref, "si1", 0)
10497hunk ./src/allmydata/test/test_storage.py 2575
10498                             mr.get_block_and_salt, 0))
10499         return d
10500 
10501-
10502     def test_verinfo_with_sdmf_file(self):
10503         self.write_sdmf_share_to_server("si1")
10504         mr = MDMFSlotReadProxy(self.rref, "si1", 0)
10505hunk ./src/allmydata/test/test_storage.py 2615
10506         d.addCallback(_check_verinfo)
10507         return d
10508 
10509-
10510     def test_verinfo_with_mdmf_file(self):
10511         self.write_test_share_to_server("si1")
10512         mr = MDMFSlotReadProxy(self.rref, "si1", 0)
10513hunk ./src/allmydata/test/test_storage.py 2653
10514         d.addCallback(_check_verinfo)
10515         return d
10516 
10517-
10518     def test_sdmf_writer(self):
10519         # Go through the motions of writing an SDMF share to the storage
10520         # server. Then read the storage server to see that the share got
10521hunk ./src/allmydata/test/test_storage.py 2696
10522         d.addCallback(_then)
10523         return d
10524 
10525-
10526     def test_sdmf_writer_preexisting_share(self):
10527         data = self.build_test_sdmf_share()
10528         self.write_sdmf_share_to_server("si1")
10529hunk ./src/allmydata/test/test_storage.py 2839
10530         self.failUnless(output["get"]["99_0_percentile"] is None, output)
10531         self.failUnless(output["get"]["99_9_percentile"] is None, output)
10532 
10533+
10534 def remove_tags(s):
10535     s = re.sub(r'<[^>]*>', ' ', s)
10536     s = re.sub(r'\s+', ' ', s)
10537hunk ./src/allmydata/test/test_storage.py 2845
10538     return s
10539 
10540+
10541 class MyBucketCountingCrawler(BucketCountingCrawler):
10542     def finished_prefix(self, cycle, prefix):
10543         BucketCountingCrawler.finished_prefix(self, cycle, prefix)
10544hunk ./src/allmydata/test/test_storage.py 2974
10545         backend = DiskBackend(fp)
10546         ss = MyStorageServer("\x00" * 20, backend, fp)
10547         ss.bucket_counter.slow_start = 0
10548+
10549         # these will be fired inside finished_prefix()
10550         hooks = ss.bucket_counter.hook_ds = [defer.Deferred() for i in range(3)]
10551         w = StorageStatus(ss)
10552hunk ./src/allmydata/test/test_storage.py 3008
10553         ss.setServiceParent(self.s)
10554         return d
10555 
10556+
10557 class InstrumentedLeaseCheckingCrawler(LeaseCheckingCrawler):
10558     stop_after_first_bucket = False
10559 
10560hunk ./src/allmydata/test/test_storage.py 3017
10561         if self.stop_after_first_bucket:
10562             self.stop_after_first_bucket = False
10563             self.cpu_slice = -1.0
10564+
10565     def yielding(self, sleep_time):
10566         if not self.stop_after_first_bucket:
10567             self.cpu_slice = 500
10568hunk ./src/allmydata/test/test_storage.py 3028
10569 
10570 class BrokenStatResults:
10571     pass
10572+
10573 class No_ST_BLOCKS_LeaseCheckingCrawler(LeaseCheckingCrawler):
10574     def stat(self, fn):
10575         s = os.stat(fn)
10576hunk ./src/allmydata/test/test_storage.py 3044
10577 class No_ST_BLOCKS_StorageServer(StorageServer):
10578     LeaseCheckerClass = No_ST_BLOCKS_LeaseCheckingCrawler
10579 
10580+
10581 class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin, WebRenderingMixin):
10582 
10583     def setUp(self):
10584hunk ./src/allmydata/test/test_storage.py 3891
10585         backend = DiskBackend(fp)
10586         ss = InstrumentedStorageServer("\x00" * 20, backend, fp)
10587         w = StorageStatus(ss)
10588+
10589         # make it start sooner than usual.
10590         lc = ss.lease_checker
10591         lc.stop_after_first_bucket = True
10592hunk ./src/allmydata/util/fileutil.py 460
10593              'avail': avail,
10594            }
10595 
10596+
10597 def get_available_space(whichdirfp, reserved_space):
10598     """Returns available space for share storage in bytes, or None if no
10599     API to get this information is available.
10600}
10601[mutable/publish.py: elements should not be removed from a dictionary while it is being iterated over. refs #393
10602david-sarah@jacaranda.org**20110923040825
10603 Ignore-this: 135da94bd344db6ccd59a576b54901c1
10604] {
10605hunk ./src/allmydata/mutable/publish.py 6
10606 import os, time
10607 from StringIO import StringIO
10608 from itertools import count
10609+from copy import copy
10610 from zope.interface import implements
10611 from twisted.internet import defer
10612 from twisted.python import failure
10613hunk ./src/allmydata/mutable/publish.py 865
10614         ds = []
10615         verification_key = self._pubkey.serialize()
10616 
10617-
10618-        # TODO: Bad, since we remove from this same dict. We need to
10619-        # make a copy, or just use a non-iterated value.
10620-        for (shnum, writer) in self.writers.iteritems():
10621+        for (shnum, writer) in copy(self.writers).iteritems():
10622             writer.put_verification_key(verification_key)
10623             d = writer.finish_publishing()
10624             d.addErrback(self._connection_problem, writer)
10625}
10626[A few comment cleanups. refs #999
10627david-sarah@jacaranda.org**20110923041003
10628 Ignore-this: f574b4a3954b6946016646011ad15edf
10629] {
10630hunk ./src/allmydata/storage/backends/disk/disk_backend.py 17
10631 
10632 # storage/
10633 # storage/shares/incoming
10634-#   incoming/ holds temp dirs named $START/$STORAGEINDEX/$SHARENUM which will
10635-#   be moved to storage/shares/$START/$STORAGEINDEX/$SHARENUM upon success
10636-# storage/shares/$START/$STORAGEINDEX
10637-# storage/shares/$START/$STORAGEINDEX/$SHARENUM
10638+#   incoming/ holds temp dirs named $PREFIX/$STORAGEINDEX/$SHNUM which will
10639+#   be moved to storage/shares/$PREFIX/$STORAGEINDEX/$SHNUM upon success
10640+# storage/shares/$PREFIX/$STORAGEINDEX
10641+# storage/shares/$PREFIX/$STORAGEINDEX/$SHNUM
10642 
10643hunk ./src/allmydata/storage/backends/disk/disk_backend.py 22
10644-# Where "$START" denotes the first 10 bits worth of $STORAGEINDEX (that's 2
10645+# Where "$PREFIX" denotes the first 10 bits worth of $STORAGEINDEX (that's 2
10646 # base-32 chars).
10647 # $SHARENUM matches this regex:
10648 NUM_RE=re.compile("^[0-9]+$")
10649hunk ./src/allmydata/storage/backends/disk/immutable.py 16
10650 from allmydata.storage.lease import LeaseInfo
10651 
10652 
10653-# each share file (in storage/shares/$SI/$SHNUM) contains lease information
10654-# and share data. The share data is accessed by RIBucketWriter.write and
10655-# RIBucketReader.read . The lease information is not accessible through these
10656-# interfaces.
10657+# Each share file (in storage/shares/$PREFIX/$STORAGEINDEX/$SHNUM) contains
10658+# lease information and share data. The share data is accessed by
10659+# RIBucketWriter.write and RIBucketReader.read . The lease information is not
10660+# accessible through these remote interfaces.
10661 
10662 # The share file has the following layout:
10663 #  0x00: share file version number, four bytes, current version is 1
10664hunk ./src/allmydata/storage/backends/disk/immutable.py 211
10665 
10666     # These lease operations are intended for use by disk_backend.py.
10667     # Other clients should not depend on the fact that the disk backend
10668-    # stores leases in share files. XXX bucket.py also relies on this.
10669+    # stores leases in share files.
10670+    # XXX BucketWriter in bucket.py also relies on add_lease.
10671 
10672     def get_leases(self):
10673         """Yields a LeaseInfo instance for all leases."""
10674}
10675[Move advise_corrupt_share to allmydata/storage/backends/base.py, since it will be common to the disk and S3 backends. refs #999
10676david-sarah@jacaranda.org**20110923041115
10677 Ignore-this: 782b49f243bd98fcb6c249f8e40fd9f
10678] {
10679hunk ./src/allmydata/storage/backends/base.py 4
10680 
10681 from twisted.application import service
10682 
10683+from allmydata.util import fileutil, log, time_format
10684 from allmydata.storage.common import si_b2a
10685 from allmydata.storage.lease import LeaseInfo
10686 from allmydata.storage.bucket import BucketReader
10687hunk ./src/allmydata/storage/backends/base.py 13
10688 class Backend(service.MultiService):
10689     def __init__(self):
10690         service.MultiService.__init__(self)
10691+        self._corruption_advisory_dir = None
10692+
10693+    def advise_corrupt_share(self, sharetype, storageindex, shnum, reason):
10694+        if self._corruption_advisory_dir is not None:
10695+            fileutil.fp_make_dirs(self._corruption_advisory_dir)
10696+            now = time_format.iso_utc(sep="T")
10697+            si_s = si_b2a(storageindex)
10698+
10699+            # Windows can't handle colons in the filename.
10700+            name = ("%s--%s-%d" % (now, si_s, shnum)).replace(":", "")
10701+            f = self._corruption_advisory_dir.child(name).open("w")
10702+            try:
10703+                f.write("report: Share Corruption\n")
10704+                f.write("type: %s\n" % sharetype)
10705+                f.write("storage_index: %s\n" % si_s)
10706+                f.write("share_number: %d\n" % shnum)
10707+                f.write("\n")
10708+                f.write(reason)
10709+                f.write("\n")
10710+            finally:
10711+                f.close()
10712+
10713+        log.msg(format=("client claims corruption in (%(share_type)s) " +
10714+                        "%(si)s-%(shnum)d: %(reason)s"),
10715+                share_type=sharetype, si=si_s, shnum=shnum, reason=reason,
10716+                level=log.SCARY, umid="2fASGx")
10717 
10718 
10719 class ShareSet(object):
10720hunk ./src/allmydata/storage/backends/disk/disk_backend.py 8
10721 
10722 from zope.interface import implements
10723 from allmydata.interfaces import IStorageBackend, IShareSet
10724-from allmydata.util import fileutil, log, time_format
10725+from allmydata.util import fileutil, log
10726 from allmydata.storage.common import si_b2a, si_a2b
10727 from allmydata.storage.bucket import BucketWriter
10728 from allmydata.storage.backends.base import Backend, ShareSet
10729hunk ./src/allmydata/storage/backends/disk/disk_backend.py 125
10730             return 0
10731         return fileutil.get_available_space(self._sharedir, self._reserved_space)
10732 
10733-    def advise_corrupt_share(self, sharetype, storageindex, shnum, reason):
10734-        fileutil.fp_make_dirs(self._corruption_advisory_dir)
10735-        now = time_format.iso_utc(sep="T")
10736-        si_s = si_b2a(storageindex)
10737-
10738-        # Windows can't handle colons in the filename.
10739-        name = ("%s--%s-%d" % (now, si_s, shnum)).replace(":", "")
10740-        f = self._corruption_advisory_dir.child(name).open("w")
10741-        try:
10742-            f.write("report: Share Corruption\n")
10743-            f.write("type: %s\n" % sharetype)
10744-            f.write("storage_index: %s\n" % si_s)
10745-            f.write("share_number: %d\n" % shnum)
10746-            f.write("\n")
10747-            f.write(reason)
10748-            f.write("\n")
10749-        finally:
10750-            f.close()
10751-
10752-        log.msg(format=("client claims corruption in (%(share_type)s) " +
10753-                        "%(si)s-%(shnum)d: %(reason)s"),
10754-                share_type=sharetype, si=si_s, shnum=shnum, reason=reason,
10755-                level=log.SCARY, umid="SGx2fA")
10756-
10757 
10758 class DiskShareSet(ShareSet):
10759     implements(IShareSet)
10760}
10761[Add incomplete S3 backend. refs #999
10762david-sarah@jacaranda.org**20110923041314
10763 Ignore-this: b48df65699e3926dcbb87b5f755cdbf1
10764] {
10765adddir ./src/allmydata/storage/backends/s3
10766addfile ./src/allmydata/storage/backends/s3/__init__.py
10767addfile ./src/allmydata/storage/backends/s3/immutable.py
10768hunk ./src/allmydata/storage/backends/s3/immutable.py 1
10769+
10770+import struct
10771+
10772+from zope.interface import implements
10773+
10774+from allmydata.interfaces import IStoredShare
10775+from allmydata.util.assertutil import precondition
10776+from allmydata.storage.common import si_b2a, UnknownImmutableContainerVersionError, DataTooLargeError
10777+
10778+
10779+# Each share file (in storage/shares/$PREFIX/$STORAGEINDEX/$SHNUM) contains
10780+# lease information [currently inaccessible] and share data. The share data is
10781+# accessed by RIBucketWriter.write and RIBucketReader.read .
10782+
10783+# The share file has the following layout:
10784+#  0x00: share file version number, four bytes, current version is 1
10785+#  0x04: always zero (was share data length prior to Tahoe-LAFS v1.3.0)
10786+#  0x08: number of leases, four bytes big-endian
10787+#  0x0c: beginning of share data (see immutable.layout.WriteBucketProxy)
10788+#  data_length+0x0c: first lease. Each lease record is 72 bytes.
10789+
10790+
10791+class ImmutableS3Share(object):
10792+    implements(IStoredShare)
10793+
10794+    sharetype = "immutable"
10795+    LEASE_SIZE = struct.calcsize(">L32s32sL")  # for compatibility
10796+
10797+
10798+    def __init__(self, storageindex, shnum, s3bucket, create=False, max_size=None):
10799+        """
10800+        If max_size is not None then I won't allow more than max_size to be written to me.
10801+        """
10802+        precondition((max_size is not None) or not create, max_size, create)
10803+        self._storageindex = storageindex
10804+        self._max_size = max_size
10805+
10806+        self._s3bucket = s3bucket
10807+        si_s = si_b2a(storageindex)
10808+        self._key = "storage/shares/%s/%s/%d" % (si_s[:2], si_s, shnum)
10809+        self._shnum = shnum
10810+
10811+        if create:
10812+            # The second field, which was the four-byte share data length in
10813+            # Tahoe-LAFS versions prior to 1.3.0, is not used; we always write 0.
10814+            # We also write 0 for the number of leases.
10815+            self._home.setContent(struct.pack(">LLL", 1, 0, 0) )
10816+            self._end_offset = max_size + 0x0c
10817+
10818+            # TODO: start write to S3.
10819+        else:
10820+            # TODO: get header
10821+            header = "\x00"*12
10822+            (version, unused, num_leases) = struct.unpack(">LLL", header)
10823+
10824+            if version != 1:
10825+                msg = "sharefile %s had version %d but we wanted 1" % \
10826+                      (self._home, version)
10827+                raise UnknownImmutableContainerVersionError(msg)
10828+
10829+            # We cannot write leases in share files, but allow them to be present
10830+            # in case a share file is copied from a disk backend, or in case we
10831+            # need them in future.
10832+            # TODO: filesize = size of S3 object
10833+            self._end_offset = filesize - (num_leases * self.LEASE_SIZE)
10834+        self._data_offset = 0xc
10835+
10836+    def __repr__(self):
10837+        return ("<ImmutableS3Share %s:%r at %r>"
10838+                % (si_b2a(self._storageindex), self._shnum, self._key))
10839+
10840+    def close(self):
10841+        # TODO: finalize write to S3.
10842+        pass
10843+
10844+    def get_used_space(self):
10845+        return self._size
10846+
10847+    def get_storage_index(self):
10848+        return self._storageindex
10849+
10850+    def get_storage_index_string(self):
10851+        return si_b2a(self._storageindex)
10852+
10853+    def get_shnum(self):
10854+        return self._shnum
10855+
10856+    def unlink(self):
10857+        # TODO: remove the S3 object.
10858+        pass
10859+
10860+    def get_allocated_size(self):
10861+        return self._max_size
10862+
10863+    def get_size(self):
10864+        return self._size
10865+
10866+    def get_data_length(self):
10867+        return self._end_offset - self._data_offset
10868+
10869+    def read_share_data(self, offset, length):
10870+        precondition(offset >= 0)
10871+
10872+        # Reads beyond the end of the data are truncated. Reads that start
10873+        # beyond the end of the data return an empty string.
10874+        seekpos = self._data_offset+offset
10875+        actuallength = max(0, min(length, self._end_offset-seekpos))
10876+        if actuallength == 0:
10877+            return ""
10878+
10879+        # TODO: perform an S3 GET request, possibly with a Content-Range header.
10880+        return "\x00"*actuallength
10881+
10882+    def write_share_data(self, offset, data):
10883+        assert offset >= self._size, "offset = %r, size = %r" % (offset, self._size)
10884+
10885+        # TODO: write data to S3. If offset > self._size, fill the space
10886+        # between with zeroes.
10887+
10888+        self._size = offset + len(data)
10889+
10890+    def add_lease(self, lease_info):
10891+        pass
10892addfile ./src/allmydata/storage/backends/s3/mutable.py
10893hunk ./src/allmydata/storage/backends/s3/mutable.py 1
10894+
10895+import struct
10896+
10897+from zope.interface import implements
10898+
10899+from allmydata.interfaces import IStoredMutableShare, BadWriteEnablerError
10900+from allmydata.util import fileutil, idlib, log
10901+from allmydata.util.assertutil import precondition
10902+from allmydata.util.hashutil import constant_time_compare
10903+from allmydata.util.encodingutil import quote_filepath
10904+from allmydata.storage.common import si_b2a, UnknownMutableContainerVersionError, \
10905+     DataTooLargeError
10906+from allmydata.storage.lease import LeaseInfo
10907+from allmydata.storage.backends.base import testv_compare
10908+
10909+
10910+# The MutableDiskShare is like the ImmutableDiskShare, but used for mutable data.
10911+# It has a different layout. See docs/mutable.rst for more details.
10912+
10913+# #   offset    size    name
10914+# 1   0         32      magic verstr "tahoe mutable container v1" plus binary
10915+# 2   32        20      write enabler's nodeid
10916+# 3   52        32      write enabler
10917+# 4   84        8       data size (actual share data present) (a)
10918+# 5   92        8       offset of (8) count of extra leases (after data)
10919+# 6   100       368     four leases, 92 bytes each
10920+#                        0    4   ownerid (0 means "no lease here")
10921+#                        4    4   expiration timestamp
10922+#                        8   32   renewal token
10923+#                        40  32   cancel token
10924+#                        72  20   nodeid that accepted the tokens
10925+# 7   468       (a)     data
10926+# 8   ??        4       count of extra leases
10927+# 9   ??        n*92    extra leases
10928+
10929+
10930+# The struct module doc says that L's are 4 bytes in size, and that Q's are
10931+# 8 bytes in size. Since compatibility depends upon this, double-check it.
10932+assert struct.calcsize(">L") == 4, struct.calcsize(">L")
10933+assert struct.calcsize(">Q") == 8, struct.calcsize(">Q")
10934+
10935+
10936+class MutableDiskShare(object):
10937+    implements(IStoredMutableShare)
10938+
10939+    sharetype = "mutable"
10940+    DATA_LENGTH_OFFSET = struct.calcsize(">32s20s32s")
10941+    EXTRA_LEASE_OFFSET = DATA_LENGTH_OFFSET + 8
10942+    HEADER_SIZE = struct.calcsize(">32s20s32sQQ") # doesn't include leases
10943+    LEASE_SIZE = struct.calcsize(">LL32s32s20s")
10944+    assert LEASE_SIZE == 92
10945+    DATA_OFFSET = HEADER_SIZE + 4*LEASE_SIZE
10946+    assert DATA_OFFSET == 468, DATA_OFFSET
10947+
10948+    # our sharefiles share with a recognizable string, plus some random
10949+    # binary data to reduce the chance that a regular text file will look
10950+    # like a sharefile.
10951+    MAGIC = "Tahoe mutable container v1\n" + "\x75\x09\x44\x03\x8e"
10952+    assert len(MAGIC) == 32
10953+    MAX_SIZE = 2*1000*1000*1000 # 2GB, kind of arbitrary
10954+    # TODO: decide upon a policy for max share size
10955+
10956+    def __init__(self, storageindex, shnum, home, parent=None):
10957+        self._storageindex = storageindex
10958+        self._shnum = shnum
10959+        self._home = home
10960+        if self._home.exists():
10961+            # we don't cache anything, just check the magic
10962+            f = self._home.open('rb')
10963+            try:
10964+                data = f.read(self.HEADER_SIZE)
10965+                (magic,
10966+                 write_enabler_nodeid, write_enabler,
10967+                 data_length, extra_least_offset) = \
10968+                 struct.unpack(">32s20s32sQQ", data)
10969+                if magic != self.MAGIC:
10970+                    msg = "sharefile %s had magic '%r' but we wanted '%r'" % \
10971+                          (quote_filepath(self._home), magic, self.MAGIC)
10972+                    raise UnknownMutableContainerVersionError(msg)
10973+            finally:
10974+                f.close()
10975+        self.parent = parent # for logging
10976+
10977+    def log(self, *args, **kwargs):
10978+        if self.parent:
10979+            return self.parent.log(*args, **kwargs)
10980+
10981+    def create(self, serverid, write_enabler):
10982+        assert not self._home.exists()
10983+        data_length = 0
10984+        extra_lease_offset = (self.HEADER_SIZE
10985+                              + 4 * self.LEASE_SIZE
10986+                              + data_length)
10987+        assert extra_lease_offset == self.DATA_OFFSET # true at creation
10988+        num_extra_leases = 0
10989+        f = self._home.open('wb')
10990+        try:
10991+            header = struct.pack(">32s20s32sQQ",
10992+                                 self.MAGIC, serverid, write_enabler,
10993+                                 data_length, extra_lease_offset,
10994+                                 )
10995+            leases = ("\x00"*self.LEASE_SIZE) * 4
10996+            f.write(header + leases)
10997+            # data goes here, empty after creation
10998+            f.write(struct.pack(">L", num_extra_leases))
10999+            # extra leases go here, none at creation
11000+        finally:
11001+            f.close()
11002+
11003+    def __repr__(self):
11004+        return ("<MutableDiskShare %s:%r at %s>"
11005+                % (si_b2a(self._storageindex), self._shnum, quote_filepath(self._home)))
11006+
11007+    def get_used_space(self):
11008+        return fileutil.get_used_space(self._home)
11009+
11010+    def get_storage_index(self):
11011+        return self._storageindex
11012+
11013+    def get_storage_index_string(self):
11014+        return si_b2a(self._storageindex)
11015+
11016+    def get_shnum(self):
11017+        return self._shnum
11018+
11019+    def unlink(self):
11020+        self._home.remove()
11021+
11022+    def _read_data_length(self, f):
11023+        f.seek(self.DATA_LENGTH_OFFSET)
11024+        (data_length,) = struct.unpack(">Q", f.read(8))
11025+        return data_length
11026+
11027+    def _write_data_length(self, f, data_length):
11028+        f.seek(self.DATA_LENGTH_OFFSET)
11029+        f.write(struct.pack(">Q", data_length))
11030+
11031+    def _read_share_data(self, f, offset, length):
11032+        precondition(offset >= 0)
11033+        data_length = self._read_data_length(f)
11034+        if offset+length > data_length:
11035+            # reads beyond the end of the data are truncated. Reads that
11036+            # start beyond the end of the data return an empty string.
11037+            length = max(0, data_length-offset)
11038+        if length == 0:
11039+            return ""
11040+        precondition(offset+length <= data_length)
11041+        f.seek(self.DATA_OFFSET+offset)
11042+        data = f.read(length)
11043+        return data
11044+
11045+    def _read_extra_lease_offset(self, f):
11046+        f.seek(self.EXTRA_LEASE_OFFSET)
11047+        (extra_lease_offset,) = struct.unpack(">Q", f.read(8))
11048+        return extra_lease_offset
11049+
11050+    def _write_extra_lease_offset(self, f, offset):
11051+        f.seek(self.EXTRA_LEASE_OFFSET)
11052+        f.write(struct.pack(">Q", offset))
11053+
11054+    def _read_num_extra_leases(self, f):
11055+        offset = self._read_extra_lease_offset(f)
11056+        f.seek(offset)
11057+        (num_extra_leases,) = struct.unpack(">L", f.read(4))
11058+        return num_extra_leases
11059+
11060+    def _write_num_extra_leases(self, f, num_leases):
11061+        extra_lease_offset = self._read_extra_lease_offset(f)
11062+        f.seek(extra_lease_offset)
11063+        f.write(struct.pack(">L", num_leases))
11064+
11065+    def _change_container_size(self, f, new_container_size):
11066+        if new_container_size > self.MAX_SIZE:
11067+            raise DataTooLargeError()
11068+        old_extra_lease_offset = self._read_extra_lease_offset(f)
11069+        new_extra_lease_offset = self.DATA_OFFSET + new_container_size
11070+        if new_extra_lease_offset < old_extra_lease_offset:
11071+            # TODO: allow containers to shrink. For now they remain large.
11072+            return
11073+        num_extra_leases = self._read_num_extra_leases(f)
11074+        f.seek(old_extra_lease_offset)
11075+        leases_size = 4 + num_extra_leases * self.LEASE_SIZE
11076+        extra_lease_data = f.read(leases_size)
11077+
11078+        # Zero out the old lease info (in order to minimize the chance that
11079+        # it could accidentally be exposed to a reader later, re #1528).
11080+        f.seek(old_extra_lease_offset)
11081+        f.write('\x00' * leases_size)
11082+        f.flush()
11083+
11084+        # An interrupt here will corrupt the leases.
11085+
11086+        f.seek(new_extra_lease_offset)
11087+        f.write(extra_lease_data)
11088+        self._write_extra_lease_offset(f, new_extra_lease_offset)
11089+
11090+    def _write_share_data(self, f, offset, data):
11091+        length = len(data)
11092+        precondition(offset >= 0)
11093+        data_length = self._read_data_length(f)
11094+        extra_lease_offset = self._read_extra_lease_offset(f)
11095+
11096+        if offset+length >= data_length:
11097+            # They are expanding their data size.
11098+
11099+            if self.DATA_OFFSET+offset+length > extra_lease_offset:
11100+                # TODO: allow containers to shrink. For now, they remain
11101+                # large.
11102+
11103+                # Their new data won't fit in the current container, so we
11104+                # have to move the leases. With luck, they're expanding it
11105+                # more than the size of the extra lease block, which will
11106+                # minimize the corrupt-the-share window
11107+                self._change_container_size(f, offset+length)
11108+                extra_lease_offset = self._read_extra_lease_offset(f)
11109+
11110+                # an interrupt here is ok.. the container has been enlarged
11111+                # but the data remains untouched
11112+
11113+            assert self.DATA_OFFSET+offset+length <= extra_lease_offset
11114+            # Their data now fits in the current container. We must write
11115+            # their new data and modify the recorded data size.
11116+
11117+            # Fill any newly exposed empty space with 0's.
11118+            if offset > data_length:
11119+                f.seek(self.DATA_OFFSET+data_length)
11120+                f.write('\x00'*(offset - data_length))
11121+                f.flush()
11122+
11123+            new_data_length = offset+length
11124+            self._write_data_length(f, new_data_length)
11125+            # an interrupt here will result in a corrupted share
11126+
11127+        # now all that's left to do is write out their data
11128+        f.seek(self.DATA_OFFSET+offset)
11129+        f.write(data)
11130+        return
11131+
11132+    def _write_lease_record(self, f, lease_number, lease_info):
11133+        extra_lease_offset = self._read_extra_lease_offset(f)
11134+        num_extra_leases = self._read_num_extra_leases(f)
11135+        if lease_number < 4:
11136+            offset = self.HEADER_SIZE + lease_number * self.LEASE_SIZE
11137+        elif (lease_number-4) < num_extra_leases:
11138+            offset = (extra_lease_offset
11139+                      + 4
11140+                      + (lease_number-4)*self.LEASE_SIZE)
11141+        else:
11142+            # must add an extra lease record
11143+            self._write_num_extra_leases(f, num_extra_leases+1)
11144+            offset = (extra_lease_offset
11145+                      + 4
11146+                      + (lease_number-4)*self.LEASE_SIZE)
11147+        f.seek(offset)
11148+        assert f.tell() == offset
11149+        f.write(lease_info.to_mutable_data())
11150+
11151+    def _read_lease_record(self, f, lease_number):
11152+        # returns a LeaseInfo instance, or None
11153+        extra_lease_offset = self._read_extra_lease_offset(f)
11154+        num_extra_leases = self._read_num_extra_leases(f)
11155+        if lease_number < 4:
11156+            offset = self.HEADER_SIZE + lease_number * self.LEASE_SIZE
11157+        elif (lease_number-4) < num_extra_leases:
11158+            offset = (extra_lease_offset
11159+                      + 4
11160+                      + (lease_number-4)*self.LEASE_SIZE)
11161+        else:
11162+            raise IndexError("No such lease number %d" % lease_number)
11163+        f.seek(offset)
11164+        assert f.tell() == offset
11165+        data = f.read(self.LEASE_SIZE)
11166+        lease_info = LeaseInfo().from_mutable_data(data)
11167+        if lease_info.owner_num == 0:
11168+            return None
11169+        return lease_info
11170+
11171+    def _get_num_lease_slots(self, f):
11172+        # how many places do we have allocated for leases? Not all of them
11173+        # are filled.
11174+        num_extra_leases = self._read_num_extra_leases(f)
11175+        return 4+num_extra_leases
11176+
11177+    def _get_first_empty_lease_slot(self, f):
11178+        # return an int with the index of an empty slot, or None if we do not
11179+        # currently have an empty slot
11180+
11181+        for i in range(self._get_num_lease_slots(f)):
11182+            if self._read_lease_record(f, i) is None:
11183+                return i
11184+        return None
11185+
11186+    def get_leases(self):
11187+        """Yields a LeaseInfo instance for all leases."""
11188+        f = self._home.open('rb')
11189+        try:
11190+            for i, lease in self._enumerate_leases(f):
11191+                yield lease
11192+        finally:
11193+            f.close()
11194+
11195+    def _enumerate_leases(self, f):
11196+        for i in range(self._get_num_lease_slots(f)):
11197+            try:
11198+                data = self._read_lease_record(f, i)
11199+                if data is not None:
11200+                    yield i, data
11201+            except IndexError:
11202+                return
11203+
11204+    # These lease operations are intended for use by disk_backend.py.
11205+    # Other non-test clients should not depend on the fact that the disk
11206+    # backend stores leases in share files.
11207+
11208+    def add_lease(self, lease_info):
11209+        precondition(lease_info.owner_num != 0) # 0 means "no lease here"
11210+        f = self._home.open('rb+')
11211+        try:
11212+            num_lease_slots = self._get_num_lease_slots(f)
11213+            empty_slot = self._get_first_empty_lease_slot(f)
11214+            if empty_slot is not None:
11215+                self._write_lease_record(f, empty_slot, lease_info)
11216+            else:
11217+                self._write_lease_record(f, num_lease_slots, lease_info)
11218+        finally:
11219+            f.close()
11220+
11221+    def renew_lease(self, renew_secret, new_expire_time):
11222+        accepting_nodeids = set()
11223+        f = self._home.open('rb+')
11224+        try:
11225+            for (leasenum, lease) in self._enumerate_leases(f):
11226+                if constant_time_compare(lease.renew_secret, renew_secret):
11227+                    # yup. See if we need to update the owner time.
11228+                    if new_expire_time > lease.expiration_time:
11229+                        # yes
11230+                        lease.expiration_time = new_expire_time
11231+                        self._write_lease_record(f, leasenum, lease)
11232+                    return
11233+                accepting_nodeids.add(lease.nodeid)
11234+        finally:
11235+            f.close()
11236+        # Return the accepting_nodeids set, to give the client a chance to
11237+        # update the leases on a share that has been migrated from its
11238+        # original server to a new one.
11239+        msg = ("Unable to renew non-existent lease. I have leases accepted by"
11240+               " nodeids: ")
11241+        msg += ",".join([("'%s'" % idlib.nodeid_b2a(anid))
11242+                         for anid in accepting_nodeids])
11243+        msg += " ."
11244+        raise IndexError(msg)
11245+
11246+    def add_or_renew_lease(self, lease_info):
11247+        precondition(lease_info.owner_num != 0) # 0 means "no lease here"
11248+        try:
11249+            self.renew_lease(lease_info.renew_secret,
11250+                             lease_info.expiration_time)
11251+        except IndexError:
11252+            self.add_lease(lease_info)
11253+
11254+    def cancel_lease(self, cancel_secret):
11255+        """Remove any leases with the given cancel_secret. If the last lease
11256+        is cancelled, the file will be removed. Return the number of bytes
11257+        that were freed (by truncating the list of leases, and possibly by
11258+        deleting the file). Raise IndexError if there was no lease with the
11259+        given cancel_secret."""
11260+
11261+        # XXX can this be more like ImmutableDiskShare.cancel_lease?
11262+
11263+        accepting_nodeids = set()
11264+        modified = 0
11265+        remaining = 0
11266+        blank_lease = LeaseInfo(owner_num=0,
11267+                                renew_secret="\x00"*32,
11268+                                cancel_secret="\x00"*32,
11269+                                expiration_time=0,
11270+                                nodeid="\x00"*20)
11271+        f = self._home.open('rb+')
11272+        try:
11273+            for (leasenum, lease) in self._enumerate_leases(f):
11274+                accepting_nodeids.add(lease.nodeid)
11275+                if constant_time_compare(lease.cancel_secret, cancel_secret):
11276+                    self._write_lease_record(f, leasenum, blank_lease)
11277+                    modified += 1
11278+                else:
11279+                    remaining += 1
11280+            if modified:
11281+                freed_space = self._pack_leases(f)
11282+        finally:
11283+            f.close()
11284+
11285+        if modified > 0:
11286+            if remaining == 0:
11287+                freed_space = fileutil.get_used_space(self._home)
11288+                self.unlink()
11289+            return freed_space
11290+
11291+        msg = ("Unable to cancel non-existent lease. I have leases "
11292+               "accepted by nodeids: ")
11293+        msg += ",".join([("'%s'" % idlib.nodeid_b2a(anid))
11294+                         for anid in accepting_nodeids])
11295+        msg += " ."
11296+        raise IndexError(msg)
11297+
11298+    def _pack_leases(self, f):
11299+        # TODO: reclaim space from cancelled leases
11300+        return 0
11301+
11302+    def _read_write_enabler_and_nodeid(self, f):
11303+        f.seek(0)
11304+        data = f.read(self.HEADER_SIZE)
11305+        (magic,
11306+         write_enabler_nodeid, write_enabler,
11307+         data_length, extra_least_offset) = \
11308+         struct.unpack(">32s20s32sQQ", data)
11309+        assert magic == self.MAGIC
11310+        return (write_enabler, write_enabler_nodeid)
11311+
11312+    def readv(self, readv):
11313+        datav = []
11314+        f = self._home.open('rb')
11315+        try:
11316+            for (offset, length) in readv:
11317+                datav.append(self._read_share_data(f, offset, length))
11318+        finally:
11319+            f.close()
11320+        return datav
11321+
11322+    def get_size(self):
11323+        return self._home.getsize()
11324+
11325+    def get_data_length(self):
11326+        f = self._home.open('rb')
11327+        try:
11328+            data_length = self._read_data_length(f)
11329+        finally:
11330+            f.close()
11331+        return data_length
11332+
11333+    def check_write_enabler(self, write_enabler, si_s):
11334+        f = self._home.open('rb+')
11335+        try:
11336+            (real_write_enabler, write_enabler_nodeid) = self._read_write_enabler_and_nodeid(f)
11337+        finally:
11338+            f.close()
11339+        # avoid a timing attack
11340+        #if write_enabler != real_write_enabler:
11341+        if not constant_time_compare(write_enabler, real_write_enabler):
11342+            # accomodate share migration by reporting the nodeid used for the
11343+            # old write enabler.
11344+            self.log(format="bad write enabler on SI %(si)s,"
11345+                     " recorded by nodeid %(nodeid)s",
11346+                     facility="tahoe.storage",
11347+                     level=log.WEIRD, umid="cE1eBQ",
11348+                     si=si_s, nodeid=idlib.nodeid_b2a(write_enabler_nodeid))
11349+            msg = "The write enabler was recorded by nodeid '%s'." % \
11350+                  (idlib.nodeid_b2a(write_enabler_nodeid),)
11351+            raise BadWriteEnablerError(msg)
11352+
11353+    def check_testv(self, testv):
11354+        test_good = True
11355+        f = self._home.open('rb+')
11356+        try:
11357+            for (offset, length, operator, specimen) in testv:
11358+                data = self._read_share_data(f, offset, length)
11359+                if not testv_compare(data, operator, specimen):
11360+                    test_good = False
11361+                    break
11362+        finally:
11363+            f.close()
11364+        return test_good
11365+
11366+    def writev(self, datav, new_length):
11367+        f = self._home.open('rb+')
11368+        try:
11369+            for (offset, data) in datav:
11370+                self._write_share_data(f, offset, data)
11371+            if new_length is not None:
11372+                cur_length = self._read_data_length(f)
11373+                if new_length < cur_length:
11374+                    self._write_data_length(f, new_length)
11375+                    # TODO: if we're going to shrink the share file when the
11376+                    # share data has shrunk, then call
11377+                    # self._change_container_size() here.
11378+        finally:
11379+            f.close()
11380+
11381+    def close(self):
11382+        pass
11383+
11384+
11385+def create_mutable_disk_share(storageindex, shnum, fp, serverid, write_enabler, parent):
11386+    ms = MutableDiskShare(storageindex, shnum, fp, parent)
11387+    ms.create(serverid, write_enabler)
11388+    del ms
11389+    return MutableDiskShare(storageindex, shnum, fp, parent)
11390addfile ./src/allmydata/storage/backends/s3/s3_backend.py
11391hunk ./src/allmydata/storage/backends/s3/s3_backend.py 1
11392+
11393+from zope.interface import implements
11394+from allmydata.interfaces import IStorageBackend, IShareSet
11395+from allmydata.storage.common import si_b2a, si_a2b
11396+from allmydata.storage.bucket import BucketWriter
11397+from allmydata.storage.backends.base import Backend, ShareSet
11398+from allmydata.storage.backends.s3.immutable import ImmutableS3Share
11399+from allmydata.storage.backends.s3.mutable import MutableS3Share
11400+
11401+# The S3 bucket has keys of the form shares/$STORAGEINDEX/$SHARENUM
11402+
11403+
11404+class S3Backend(Backend):
11405+    implements(IStorageBackend)
11406+
11407+    def __init__(self, s3bucket, readonly=False, max_space=None, corruption_advisory_dir=None):
11408+        Backend.__init__(self)
11409+        self._s3bucket = s3bucket
11410+        self._readonly = readonly
11411+        if max_space is None:
11412+            self._max_space = 2**64
11413+        else:
11414+            self._max_space = int(max_space)
11415+
11416+        # TODO: any set-up for S3?
11417+
11418+        # we don't actually create the corruption-advisory dir until necessary
11419+        self._corruption_advisory_dir = corruption_advisory_dir
11420+
11421+    def get_sharesets_for_prefix(self, prefix):
11422+        # TODO: query S3 for keys matching prefix
11423+        return []
11424+
11425+    def get_shareset(self, storageindex):
11426+        return S3ShareSet(storageindex, self._s3bucket)
11427+
11428+    def fill_in_space_stats(self, stats):
11429+        stats['storage_server.max_space'] = self._max_space
11430+
11431+        # TODO: query space usage of S3 bucket
11432+        stats['storage_server.accepting_immutable_shares'] = int(not self._readonly)
11433+
11434+    def get_available_space(self):
11435+        if self._readonly:
11436+            return 0
11437+        # TODO: query space usage of S3 bucket
11438+        return self._max_space
11439+
11440+
11441+class S3ShareSet(ShareSet):
11442+    implements(IShareSet)
11443+
11444+    def __init__(self, storageindex, s3bucket):
11445+        ShareSet.__init__(self, storageindex)
11446+        self._s3bucket = s3bucket
11447+
11448+    def get_overhead(self):
11449+        return 0
11450+
11451+    def get_shares(self):
11452+        """
11453+        Generate IStorageBackendShare objects for shares we have for this storage index.
11454+        ("Shares we have" means completed ones, excluding incoming ones.)
11455+        """
11456+        pass
11457+
11458+    def has_incoming(self, shnum):
11459+        # TODO: this might need to be more like the disk backend; review callers
11460+        return False
11461+
11462+    def make_bucket_writer(self, storageserver, shnum, max_space_per_bucket, lease_info, canary):
11463+        immsh = ImmutableS3Share(self.get_storage_index(), shnum, self._s3bucket,
11464+                                 max_size=max_space_per_bucket)
11465+        bw = BucketWriter(storageserver, immsh, lease_info, canary)
11466+        return bw
11467+
11468+    def _create_mutable_share(self, storageserver, shnum, write_enabler):
11469+        # TODO
11470+        serverid = storageserver.get_serverid()
11471+        return MutableS3Share(self.get_storage_index(), shnum, self._s3bucket, serverid, write_enabler, storageserver)
11472+
11473+    def _clean_up_after_unlink(self):
11474+        pass
11475+
11476}
11477
11478Context:
11479
11480[test_mutable.py: skip test_publish_surprise_mdmf, which is causing an error. refs #1534, #393
11481david-sarah@jacaranda.org**20110920183319
11482 Ignore-this: 6fb020e09e8de437cbcc2c9f57835b31
11483]
11484[test/test_mutable: write publish surprise test for MDMF, rename existing test_publish_surprise to clarify that it is for SDMF
11485kevan@isnotajoke.com**20110918003657
11486 Ignore-this: 722c507e8f5b537ff920e0555951059a
11487]
11488[test/test_mutable: refactor publish surprise test into common test fixture, rewrite test_publish_surprise to use test fixture
11489kevan@isnotajoke.com**20110918003533
11490 Ignore-this: 6f135888d400a99a09b5f9a4be443b6e
11491]
11492[mutable/publish: add errback immediately after write, don't consume errors from other parts of the publisher
11493kevan@isnotajoke.com**20110917234708
11494 Ignore-this: 12bf6b0918a5dc5ffc30ece669fad51d
11495]
11496[.darcs-boringfile: minor cleanups.
11497david-sarah@jacaranda.org**20110920154918
11498 Ignore-this: cab78e30d293da7e2832207dbee2ffeb
11499]
11500[uri.py: fix two interface violations in verifier URI classes. refs #1474
11501david-sarah@jacaranda.org**20110920030156
11502 Ignore-this: 454ddd1419556cb1d7576d914cb19598
11503]
11504[Make platform-detection code tolerate linux-3.0, patch by zooko.
11505Brian Warner <warner@lothar.com>**20110915202620
11506 Ignore-this: af63cf9177ae531984dea7a1cad03762
11507 
11508 Otherwise address-autodetection can't find ifconfig. refs #1536
11509]
11510[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.
11511david-sarah@jacaranda.org**20110915185126
11512 Ignore-this: d96632bc48d770b9b577cda1bbd8ff94
11513]
11514[docs: insert a newline at the beginning of known_issues.rst to see if this makes it render more nicely in trac
11515zooko@zooko.com**20110914064728
11516 Ignore-this: aca15190fa22083c5d4114d3965f5d65
11517]
11518[docs: remove the coding: utf-8 declaration at the to of known_issues.rst, since the trac rendering doesn't hide it
11519zooko@zooko.com**20110914055713
11520 Ignore-this: 941ed32f83ead377171aa7a6bd198fcf
11521]
11522[docs: more cleanup of known_issues.rst -- now it passes "rst2html --verbose" without comment
11523zooko@zooko.com**20110914055419
11524 Ignore-this: 5505b3d76934bd97d0312cc59ed53879
11525]
11526[docs: more formatting improvements to known_issues.rst
11527zooko@zooko.com**20110914051639
11528 Ignore-this: 9ae9230ec9a38a312cbacaf370826691
11529]
11530[docs: reformatting of known_issues.rst
11531zooko@zooko.com**20110914050240
11532 Ignore-this: b8be0375079fb478be9d07500f9aaa87
11533]
11534[docs: fix formatting error in docs/known_issues.rst
11535zooko@zooko.com**20110914045909
11536 Ignore-this: f73fe74ad2b9e655aa0c6075acced15a
11537]
11538[merge Tahoe-LAFS v1.8.3 release announcement with trunk
11539zooko@zooko.com**20110913210544
11540 Ignore-this: 163f2c3ddacca387d7308e4b9332516e
11541]
11542[docs: release notes for Tahoe-LAFS v1.8.3
11543zooko@zooko.com**20110913165826
11544 Ignore-this: 84223604985b14733a956d2fbaeb4e9f
11545]
11546[tests: bump up the timeout in this test that fails on FreeStorm's CentOS in order to see if it is just very slow
11547zooko@zooko.com**20110913024255
11548 Ignore-this: 6a86d691e878cec583722faad06fb8e4
11549]
11550[interfaces: document that the 'fills-holes-with-zero-bytes' key should be used to detect whether a storage server has that behavior. refs #1528
11551david-sarah@jacaranda.org**20110913002843
11552 Ignore-this: 1a00a6029d40f6792af48c5578c1fd69
11553]
11554[CREDITS: more CREDITS for Kevan and David-Sarah
11555zooko@zooko.com**20110912223357
11556 Ignore-this: 4ea8f0d6f2918171d2f5359c25ad1ada
11557]
11558[merge NEWS about the mutable file bounds fixes with NEWS about work-in-progress
11559zooko@zooko.com**20110913205521
11560 Ignore-this: 4289a4225f848d6ae6860dd39bc92fa8
11561]
11562[doc: add NEWS item about fixes to potential palimpsest issues in mutable files
11563zooko@zooko.com**20110912223329
11564 Ignore-this: 9d63c95ddf95c7d5453c94a1ba4d406a
11565 ref. #1528
11566]
11567[merge the NEWS about the security fix (#1528) with the work-in-progress NEWS
11568zooko@zooko.com**20110913205153
11569 Ignore-this: 88e88a2ad140238c62010cf7c66953fc
11570]
11571[doc: add NEWS entry about the issue which allows unauthorized deletion of shares
11572zooko@zooko.com**20110912223246
11573 Ignore-this: 77e06d09103d2ef6bb51ea3e5d6e80b0
11574 ref. #1528
11575]
11576[doc: add entry in known_issues.rst about the issue which allows unauthorized deletion of shares
11577zooko@zooko.com**20110912223135
11578 Ignore-this: b26c6ea96b6c8740b93da1f602b5a4cd
11579 ref. #1528
11580]
11581[storage: more paranoid handling of bounds and palimpsests in mutable share files
11582zooko@zooko.com**20110912222655
11583 Ignore-this: a20782fa423779ee851ea086901e1507
11584 * storage server ignores requests to extend shares by sending a new_length
11585 * 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
11586 * storage server zeroes out lease info at the old location when moving it to a new location
11587 ref. #1528
11588]
11589[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
11590zooko@zooko.com**20110912222554
11591 Ignore-this: 61ebd7b11250963efdf5b1734a35271
11592 ref. #1528
11593]
11594[immutable: prevent clients from reading past the end of share data, which would allow them to learn the cancellation secret
11595zooko@zooko.com**20110912222458
11596 Ignore-this: da1ebd31433ea052087b75b2e3480c25
11597 Declare explicitly that we prevent this problem in the server's version dict.
11598 fixes #1528 (there are two patches that are each a sufficient fix to #1528 and this is one of them)
11599]
11600[storage: remove the storage server's "remote_cancel_lease" function
11601zooko@zooko.com**20110912222331
11602 Ignore-this: 1c32dee50e0981408576daffad648c50
11603 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.
11604 fixes #1528 (there are two patches that are each a sufficient fix to #1528 and this is one of them)
11605]
11606[storage: test that the storage server does *not* have a "remote_cancel_lease" function
11607zooko@zooko.com**20110912222324
11608 Ignore-this: 21c652009704652d35f34651f98dd403
11609 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.
11610 ref. #1528
11611]
11612[immutable: test whether the server allows clients to read past the end of share data, which would allow them to learn the cancellation secret
11613zooko@zooko.com**20110912221201
11614 Ignore-this: 376e47b346c713d37096531491176349
11615 Also test whether the server explicitly declares that it prevents this problem.
11616 ref #1528
11617]
11618[Retrieve._activate_enough_peers: rewrite Verify logic
11619Brian Warner <warner@lothar.com>**20110909181150
11620 Ignore-this: 9367c11e1eacbf025f75ce034030d717
11621]
11622[Retrieve: implement/test stopProducing
11623Brian Warner <warner@lothar.com>**20110909181150
11624 Ignore-this: 47b2c3df7dc69835e0a066ca12e3c178
11625]
11626[move DownloadStopped from download.common to interfaces
11627Brian Warner <warner@lothar.com>**20110909181150
11628 Ignore-this: 8572acd3bb16e50341dbed8eb1d90a50
11629]
11630[retrieve.py: remove vestigal self._validated_readers
11631Brian Warner <warner@lothar.com>**20110909181150
11632 Ignore-this: faab2ec14e314a53a2ffb714de626e2d
11633]
11634[Retrieve: rewrite flow-control: use a top-level loop() to catch all errors
11635Brian Warner <warner@lothar.com>**20110909181150
11636 Ignore-this: e162d2cd53b3d3144fc6bc757e2c7714
11637 
11638 This ought to close the potential for dropped errors and hanging downloads.
11639 Verify needs to be examined, I may have broken it, although all tests pass.
11640]
11641[Retrieve: merge _validate_active_prefixes into _add_active_peers
11642Brian Warner <warner@lothar.com>**20110909181150
11643 Ignore-this: d3ead31e17e69394ae7058eeb5beaf4c
11644]
11645[Retrieve: remove the initial prefix-is-still-good check
11646Brian Warner <warner@lothar.com>**20110909181150
11647 Ignore-this: da66ee51c894eaa4e862e2dffb458acc
11648 
11649 This check needs to be done with each fetch from the storage server, to
11650 detect when someone has changed the share (i.e. our servermap goes stale).
11651 Doing it just once at the beginning of retrieve isn't enough: a write might
11652 occur after the first segment but before the second, etc.
11653 
11654 _try_to_validate_prefix() was not removed: it will be used by the future
11655 check-with-each-fetch code.
11656 
11657 test_mutable.Roundtrip.test_corrupt_all_seqnum_late was disabled, since it
11658 fails until this check is brought back. (the corruption it applies only
11659 touches the prefix, not the block data, so the check-less retrieve actually
11660 tolerates it). Don't forget to re-enable it once the check is brought back.
11661]
11662[MDMFSlotReadProxy: remove the queue
11663Brian Warner <warner@lothar.com>**20110909181150
11664 Ignore-this: 96673cb8dda7a87a423de2f4897d66d2
11665 
11666 This is a neat trick to reduce Foolscap overhead, but the need for an
11667 explicit flush() complicates the Retrieve path and makes it prone to
11668 lost-progress bugs.
11669 
11670 Also change test_mutable.FakeStorageServer to tolerate multiple reads of the
11671 same share in a row, a limitation exposed by turning off the queue.
11672]
11673[rearrange Retrieve: first step, shouldn't change order of execution
11674Brian Warner <warner@lothar.com>**20110909181149
11675 Ignore-this: e3006368bfd2802b82ea45c52409e8d6
11676]
11677[CLI: test_cli.py -- remove an unnecessary call in test_mkdir_mutable_type. refs #1527
11678david-sarah@jacaranda.org**20110906183730
11679 Ignore-this: 122e2ffbee84861c32eda766a57759cf
11680]
11681[CLI: improve test for 'tahoe mkdir --mutable-type='. refs #1527
11682david-sarah@jacaranda.org**20110906183020
11683 Ignore-this: f1d4598e6c536f0a2b15050b3bc0ef9d
11684]
11685[CLI: make the --mutable-type option value for 'tahoe put' and 'tahoe mkdir' case-insensitive, and change --help for these commands accordingly. fixes #1527
11686david-sarah@jacaranda.org**20110905020922
11687 Ignore-this: 75a6df0a2df9c467d8c010579e9a024e
11688]
11689[cli: make --mutable-type imply --mutable in 'tahoe put'
11690Kevan Carstensen <kevan@isnotajoke.com>**20110903190920
11691 Ignore-this: 23336d3c43b2a9554e40c2a11c675e93
11692]
11693[SFTP: add a comment about a subtle interaction between OverwriteableFileConsumer and GeneralSFTPFile, and test the case it is commenting on.
11694david-sarah@jacaranda.org**20110903222304
11695 Ignore-this: 980c61d4dd0119337f1463a69aeebaf0
11696]
11697[improve the storage/mutable.py asserts even more
11698warner@lothar.com**20110901160543
11699 Ignore-this: 5b2b13c49bc4034f96e6e3aaaa9a9946
11700]
11701[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
11702wilcoxjg@gmail.com**20110901084144
11703 Ignore-this: 28ace2b2678642e4d7269ddab8c67f30
11704]
11705[docs/write_coordination.rst: fix formatting and add more specific warning about access via sshfs.
11706david-sarah@jacaranda.org**20110831232148
11707 Ignore-this: cd9c851d3eb4e0a1e088f337c291586c
11708]
11709[test_mutable.Version: consolidate some tests, reduce runtime from 19s to 15s
11710warner@lothar.com**20110831050451
11711 Ignore-this: 64815284d9e536f8f3798b5f44cf580c
11712]
11713[mutable/retrieve: handle the case where self._read_length is 0.
11714Kevan Carstensen <kevan@isnotajoke.com>**20110830210141
11715 Ignore-this: fceafbe485851ca53f2774e5a4fd8d30
11716 
11717 Note that the downloader will still fetch a segment for a zero-length
11718 read, which is wasteful. Fixing that isn't specifically required to fix
11719 #1512, but it should probably be fixed before 1.9.
11720]
11721[NEWS: added summary of all changes since 1.8.2. Needs editing.
11722Brian Warner <warner@lothar.com>**20110830163205
11723 Ignore-this: 273899b37a899fc6919b74572454b8b2
11724]
11725[test_mutable.Update: only upload the files needed for each test. refs #1500
11726Brian Warner <warner@lothar.com>**20110829072717
11727 Ignore-this: 4d2ab4c7523af9054af7ecca9c3d9dc7
11728 
11729 This first step shaves 15% off the runtime: from 139s to 119s on my laptop.
11730 It also fixes a couple of places where a Deferred was being dropped, which
11731 would cause two tests to run in parallel and also confuse error reporting.
11732]
11733[Let Uploader retain History instead of passing it into upload(). Fixes #1079.
11734Brian Warner <warner@lothar.com>**20110829063246
11735 Ignore-this: 3902c58ec12bd4b2d876806248e19f17
11736 
11737 This consistently records all immutable uploads in the Recent Uploads And
11738 Downloads page, regardless of code path. Previously, certain webapi upload
11739 operations (like PUT /uri/$DIRCAP/newchildname) failed to pass the History
11740 object and were left out.
11741]
11742[Fix mutable publish/retrieve timing status displays. Fixes #1505.
11743Brian Warner <warner@lothar.com>**20110828232221
11744 Ignore-this: 4080ce065cf481b2180fd711c9772dd6
11745 
11746 publish:
11747 * encrypt and encode times are cumulative, not just current-segment
11748 
11749 retrieve:
11750 * same for decrypt and decode times
11751 * update "current status" to include segment number
11752 * set status to Finished/Failed when download is complete
11753 * set progress to 1.0 when complete
11754 
11755 More improvements to consider:
11756 * progress is currently 0% or 100%: should calculate how many segments are
11757   involved (remembering retrieve can be less than the whole file) and set it
11758   to a fraction
11759 * "fetch" time is fuzzy: what we want is to know how much of the delay is not
11760   our own fault, but since we do decode/decrypt work while waiting for more
11761   shares, it's not straightforward
11762]
11763[Teach 'tahoe debug catalog-shares about MDMF. Closes #1507.
11764Brian Warner <warner@lothar.com>**20110828080931
11765 Ignore-this: 56ef2951db1a648353d7daac6a04c7d1
11766]
11767[debug.py: remove some dead comments
11768Brian Warner <warner@lothar.com>**20110828074556
11769 Ignore-this: 40e74040dd4d14fd2f4e4baaae506b31
11770]
11771[hush pyflakes
11772Brian Warner <warner@lothar.com>**20110828074254
11773 Ignore-this: bef9d537a969fa82fe4decc4ba2acb09
11774]
11775[MutableFileNode.set_downloader_hints: never depend upon order of dict.values()
11776Brian Warner <warner@lothar.com>**20110828074103
11777 Ignore-this: caaf1aa518dbdde4d797b7f335230faa
11778 
11779 The old code was calculating the "extension parameters" (a list) from the
11780 downloader hints (a dictionary) with hints.values(), which is not stable, and
11781 would result in corrupted filecaps (with the 'k' and 'segsize' hints
11782 occasionally swapped). The new code always uses [k,segsize].
11783]
11784[layout.py: fix MDMF share layout documentation
11785Brian Warner <warner@lothar.com>**20110828073921
11786 Ignore-this: 3f13366fed75b5e31b51ae895450a225
11787]
11788[teach 'tahoe debug dump-share' about MDMF and offsets. refs #1507
11789Brian Warner <warner@lothar.com>**20110828073834
11790 Ignore-this: 3a9d2ef9c47a72bf1506ba41199a1dea
11791]
11792[test_mutable.Version.test_debug: use splitlines() to fix buildslaves
11793Brian Warner <warner@lothar.com>**20110828064728
11794 Ignore-this: c7f6245426fc80b9d1ae901d5218246a
11795 
11796 Any slave running in a directory with spaces in the name was miscounting
11797 shares, causing the test to fail.
11798]
11799[test_mutable.Version: exercise 'tahoe debug find-shares' on MDMF. refs #1507
11800Brian Warner <warner@lothar.com>**20110828005542
11801 Ignore-this: cb20bea1c28bfa50a72317d70e109672
11802 
11803 Also changes NoNetworkGrid to put shares in storage/shares/ .
11804]
11805[test_mutable.py: oops, missed a .todo
11806Brian Warner <warner@lothar.com>**20110828002118
11807 Ignore-this: fda09ae86481352b7a627c278d2a3940
11808]
11809[test_mutable: merge davidsarah's patch with my Version refactorings
11810warner@lothar.com**20110827235707
11811 Ignore-this: b5aaf481c90d99e33827273b5d118fd0
11812]
11813[Make the immutable/read-only constraint checking for MDMF URIs identical to that for SSK URIs. refs #393
11814david-sarah@jacaranda.org**20110823012720
11815 Ignore-this: e1f59d7ff2007c81dbef2aeb14abd721
11816]
11817[Additional tests for MDMF URIs and for zero-length files. refs #393
11818david-sarah@jacaranda.org**20110823011532
11819 Ignore-this: a7cc0c09d1d2d72413f9cd227c47a9d5
11820]
11821[Additional tests for zero-length partial reads and updates to mutable versions. refs #393
11822david-sarah@jacaranda.org**20110822014111
11823 Ignore-this: 5fc6f4d06e11910124e4a277ec8a43ea
11824]
11825[test_mutable.Version: factor out some expensive uploads, save 25% runtime
11826Brian Warner <warner@lothar.com>**20110827232737
11827 Ignore-this: ea37383eb85ea0894b254fe4dfb45544
11828]
11829[SDMF: update filenode with correct k/N after Retrieve. Fixes #1510.
11830Brian Warner <warner@lothar.com>**20110827225031
11831 Ignore-this: b50ae6e1045818c400079f118b4ef48
11832 
11833 Without this, we get a regression when modifying a mutable file that was
11834 created with more shares (larger N) than our current tahoe.cfg . The
11835 modification attempt creates new versions of the (0,1,..,newN-1) shares, but
11836 leaves the old versions of the (newN,..,oldN-1) shares alone (and throws a
11837 assertion error in SDMFSlotWriteProxy.finish_publishing in the process).
11838 
11839 The mixed versions that result (some shares with e.g. N=10, some with N=20,
11840 such that both versions are recoverable) cause problems for the Publish code,
11841 even before MDMF landed. Might be related to refs #1390 and refs #1042.
11842]
11843[layout.py: annotate assertion to figure out 'tahoe backup' failure
11844Brian Warner <warner@lothar.com>**20110827195253
11845 Ignore-this: 9b92b954e3ed0d0f80154fff1ff674e5
11846]
11847[Add 'tahoe debug dump-cap' support for MDMF, DIR2-CHK, DIR2-MDMF. refs #1507.
11848Brian Warner <warner@lothar.com>**20110827195048
11849 Ignore-this: 61c6af5e33fc88e0251e697a50addb2c
11850 
11851 This also adds tests for all those cases, and fixes an omission in uri.py
11852 that broke parsing of DIR2-MDMF-Verifier and DIR2-CHK-Verifier.
11853]
11854[MDMF: more writable/writeable consistentifications
11855warner@lothar.com**20110827190602
11856 Ignore-this: 22492a9e20c1819ddb12091062888b55
11857]
11858[MDMF: s/Writable/Writeable/g, for consistency with existing SDMF code
11859warner@lothar.com**20110827183357
11860 Ignore-this: 9dd312acedbdb2fc2f7bef0d0fb17c0b
11861]
11862[setup.cfg: remove no-longer-supported test_mac_diskimage alias. refs #1479
11863david-sarah@jacaranda.org**20110826230345
11864 Ignore-this: 40e908b8937322a290fb8012bfcad02a
11865]
11866[test_mutable.Update: increase timeout from 120s to 400s, slaves are failing
11867Brian Warner <warner@lothar.com>**20110825230140
11868 Ignore-this: 101b1924a30cdbda9b2e419e95ca15ec
11869]
11870[tests: fix check_memory test
11871zooko@zooko.com**20110825201116
11872 Ignore-this: 4d66299fa8cb61d2ca04b3f45344d835
11873 fixes #1503
11874]
11875[TAG allmydata-tahoe-1.9.0a1
11876warner@lothar.com**20110825161122
11877 Ignore-this: 3cbf49f00dbda58189f893c427f65605
11878]
11879Patch bundle hash:
118801262c933b9e985e8ab4b3a3e9b31bba609561caf