Ticket #1465: 20110829passespyflakes.darcs.patch

File 20110829passespyflakes.darcs.patch, 134.2 KB (added by zancas, at 2011-08-29T21:50:49Z)

pyflaked patch

Line 
1Tue Aug  9 13:39:10 MDT 2011  wilcoxjg@gmail.com
2  * storage: add tests of the new feature of having the storage backend in a separate object from the server
3
4Tue Aug  9 14:09:29 MDT 2011  wilcoxjg@gmail.com
5  * Added directories and new modules for the null backend
6
7Tue Aug  9 14:12:49 MDT 2011  wilcoxjg@gmail.com
8  * changes to null/core.py and storage/common.py necessary for test with null backend to pass
9
10Tue Aug  9 14:16:47 MDT 2011  wilcoxjg@gmail.com
11  * change storage/server.py to new "backend pluggable" version
12
13Tue Aug  9 14:18:22 MDT 2011  wilcoxjg@gmail.com
14  * modify null/core.py such that the correct interfaces are implemented
15
16Tue Aug  9 14:22:32 MDT 2011  wilcoxjg@gmail.com
17  * make changes to storage/immutable.py most changes are part of movement to DAS specific backend.
18
19Tue Aug  9 14:26:20 MDT 2011  wilcoxjg@gmail.com
20  * creates backends/das/core.py
21
22Tue Aug  9 14:31:23 MDT 2011  wilcoxjg@gmail.com
23  * change backends/das/core.py to correct which interfaces are implemented
24
25Tue Aug  9 14:33:21 MDT 2011  wilcoxjg@gmail.com
26  * util/fileutil.py now expects and manipulates twisted.python.filepath.FilePath objects
27
28Tue Aug  9 14:35:19 MDT 2011  wilcoxjg@gmail.com
29  * add expirer.py
30
31Tue Aug  9 14:38:11 MDT 2011  wilcoxjg@gmail.com
32  * Changes I have made that aren't necessary for the test_backends.py suite to pass.
33
34Tue Aug  9 21:37:51 MDT 2011  wilcoxjg@gmail.com
35  * add __init__.py to backend and core and null
36
37Wed Aug 10 11:08:47 MDT 2011  wilcoxjg@gmail.com
38  * whitespace-cleanup
39
40Wed Aug 10 11:38:49 MDT 2011  wilcoxjg@gmail.com
41  * das/__init__.py
42
43Wed Aug 10 14:10:41 MDT 2011  wilcoxjg@gmail.com
44  * test_backends.py: cleaned whitespace and removed unused variables
45
46Mon Aug 29 12:48:34 MDT 2011  wilcoxjg@gmail.com
47  * test_backends.py, backends/das -> backends/disk: renaming backend das to disk
48
49Mon Aug 29 15:36:31 MDT 2011  wilcoxjg@gmail.com
50  * disk/core.py: slips past pyflakes without causing errors
51
52Mon Aug 29 15:48:16 MDT 2011  wilcoxjg@gmail.com
53  * null/core.py, storage/common.py, storage/immutable.py: pyflaked clean
54
55New patches:
56
57[storage: add tests of the new feature of having the storage backend in a separate object from the server
58wilcoxjg@gmail.com**20110809193910
59 Ignore-this: 72b64dab1a9ce668607a4ece4429e29a
60] {
61addfile ./src/allmydata/test/test_backends.py
62hunk ./src/allmydata/test/test_backends.py 1
63+import os, stat
64+from twisted.trial import unittest
65+from allmydata.util.log import msg
66+from allmydata.test.common_util import ReallyEqualMixin
67+import mock
68+# This is the code that we're going to be testing.
69+from allmydata.storage.server import StorageServer
70+from allmydata.storage.backends.das.core import DASCore
71+from allmydata.storage.backends.null.core import NullCore
72+from allmydata.storage.common import si_si2dir
73+# The following share file content was generated with
74+# storage.immutable.ShareFile from Tahoe-LAFS v1.8.2
75+# with share data == 'a'. The total size of this input
76+# is 85 bytes.
77+shareversionnumber = '\x00\x00\x00\x01'
78+sharedatalength = '\x00\x00\x00\x01'
79+numberofleases = '\x00\x00\x00\x01'
80+shareinputdata = 'a'
81+ownernumber = '\x00\x00\x00\x00'
82+renewsecret  = 'x'*32
83+cancelsecret = 'y'*32
84+expirationtime = '\x00(\xde\x80'
85+nextlease = ''
86+containerdata = shareversionnumber + sharedatalength + numberofleases
87+client_data = shareinputdata + ownernumber + renewsecret + \
88+    cancelsecret + expirationtime + nextlease
89+share_data = containerdata + client_data
90+testnodeid = 'testnodeidxxxxxxxxxx'
91+expiration_policy = {'enabled' : False,
92+                     'mode' : 'age',
93+                     'override_lease_duration' : None,
94+                     'cutoff_date' : None,
95+                     'sharetypes' : None}
96+
97+
98+class MockFileSystem(unittest.TestCase):
99+    """ I simulate a filesystem that the code under test can use. I simulate
100+    just the parts of the filesystem that the current implementation of DAS
101+    backend needs. """
102+    def setUp(self):
103+        # Make patcher, patch, and make effects for fs using functions.
104+        msg( "%s.setUp()" % (self,))
105+        self.mockedfilepaths = {}
106+        #keys are pathnames, values are MockFilePath objects. This is necessary because
107+        #MockFilePath behavior sometimes depends on the filesystem. Where it does,
108+        #self.mockedfilepaths has the relevent info.
109+        self.storedir = MockFilePath('teststoredir', self.mockedfilepaths)
110+        self.basedir = self.storedir.child('shares')
111+        self.baseincdir = self.basedir.child('incoming')
112+        self.sharedirfinalname = self.basedir.child('or').child('orsxg5dtorxxeylhmvpws3temv4a')
113+        self.sharedirincomingname = self.baseincdir.child('or').child('orsxg5dtorxxeylhmvpws3temv4a')
114+        self.shareincomingname = self.sharedirincomingname.child('0')
115+        self.sharefinalname = self.sharedirfinalname.child('0')
116+
117+        self.FilePathFake = mock.patch('allmydata.storage.backends.das.core.FilePath', new = MockFilePath )
118+        FakePath = self.FilePathFake.__enter__()
119+
120+        self.BCountingCrawler = mock.patch('allmydata.storage.backends.das.core.BucketCountingCrawler')
121+        FakeBCC = self.BCountingCrawler.__enter__()
122+        FakeBCC.side_effect = self.call_FakeBCC
123+
124+        self.LeaseCheckingCrawler = mock.patch('allmydata.storage.backends.das.core.LeaseCheckingCrawler')
125+        FakeLCC = self.LeaseCheckingCrawler.__enter__()
126+        FakeLCC.side_effect = self.call_FakeLCC
127+
128+        self.get_available_space = mock.patch('allmydata.util.fileutil.get_available_space')
129+        GetSpace = self.get_available_space.__enter__()
130+        GetSpace.side_effect = self.call_get_available_space
131+
132+        self.statforsize = mock.patch('allmydata.storage.backends.das.core.filepath.stat')
133+        getsize = self.statforsize.__enter__()
134+        getsize.side_effect = self.call_statforsize
135+
136+    def call_FakeBCC(self, StateFile):
137+        return MockBCC()
138+
139+    def call_FakeLCC(self, StateFile, HistoryFile, ExpirationPolicy):
140+        return MockLCC()
141+
142+    def call_get_available_space(self, storedir, reservedspace):
143+        # The input vector has an input size of 85.
144+        return 85 - reservedspace
145+
146+    def call_statforsize(self, fakefpname):
147+        return self.mockedfilepaths[fakefpname].fileobject.size()
148+
149+    def tearDown(self):
150+        msg( "%s.tearDown()" % (self,))
151+        FakePath = self.FilePathFake.__exit__()       
152+        self.mockedfilepaths = {}
153+
154+
155+class MockFilePath:
156+    def __init__(self, pathstring, ffpathsenvironment, existance=False):
157+        #  I can't jsut make the values MockFileObjects because they may be directories.
158+        self.mockedfilepaths = ffpathsenvironment
159+        self.path = pathstring
160+        self.existance = existance
161+        if not self.mockedfilepaths.has_key(self.path):
162+            #  The first MockFilePath object is special
163+            self.mockedfilepaths[self.path] = self
164+            self.fileobject = None
165+        else:
166+            self.fileobject = self.mockedfilepaths[self.path].fileobject
167+        self.spawn = {}
168+        self.antecedent = os.path.dirname(self.path)
169+
170+    def setContent(self, contentstring):
171+        # This method rewrites the data in the file that corresponds to its path
172+        # name whether it preexisted or not.
173+        self.fileobject = MockFileObject(contentstring)
174+        self.existance = True
175+        self.mockedfilepaths[self.path].fileobject = self.fileobject
176+        self.mockedfilepaths[self.path].existance = self.existance
177+        self.setparents()
178+       
179+    def create(self):
180+        # This method chokes if there's a pre-existing file!
181+        if self.mockedfilepaths[self.path].fileobject:
182+            raise OSError
183+        else:
184+            self.fileobject = MockFileObject(contentstring)
185+            self.existance = True
186+            self.mockedfilepaths[self.path].fileobject = self.fileobject
187+            self.mockedfilepaths[self.path].existance = self.existance
188+            self.setparents()       
189+
190+    def open(self, mode='r'):
191+        # XXX Makes no use of mode.
192+        if not self.mockedfilepaths[self.path].fileobject:
193+            # If there's no fileobject there already then make one and put it there.
194+            self.fileobject = MockFileObject()
195+            self.existance = True
196+            self.mockedfilepaths[self.path].fileobject = self.fileobject
197+            self.mockedfilepaths[self.path].existance = self.existance
198+        else:
199+            # Otherwise get a ref to it.
200+            self.fileobject = self.mockedfilepaths[self.path].fileobject
201+            self.existance = self.mockedfilepaths[self.path].existance
202+        return self.fileobject.open(mode)
203+
204+    def child(self, childstring):
205+        arg2child = os.path.join(self.path, childstring)
206+        child = MockFilePath(arg2child, self.mockedfilepaths)
207+        return child
208+
209+    def children(self):
210+        childrenfromffs = [ffp for ffp in self.mockedfilepaths.values() if ffp.path.startswith(self.path)]
211+        childrenfromffs = [ffp for ffp in childrenfromffs if not ffp.path.endswith(self.path)]
212+        childrenfromffs = [ffp for ffp in childrenfromffs if ffp.exists()]
213+        self.spawn = frozenset(childrenfromffs)
214+        return self.spawn 
215+
216+    def parent(self):
217+        if self.mockedfilepaths.has_key(self.antecedent):
218+            parent = self.mockedfilepaths[self.antecedent]
219+        else:
220+            parent = MockFilePath(self.antecedent, self.mockedfilepaths)
221+        return parent
222+
223+    def parents(self):
224+        antecedents = []
225+        def f(fps, antecedents):
226+            newfps = os.path.split(fps)[0]
227+            if newfps:
228+                antecedents.append(newfps)
229+                f(newfps, antecedents)
230+        f(self.path, antecedents)
231+        return antecedents
232+
233+    def setparents(self):
234+        for fps in self.parents():
235+            if not self.mockedfilepaths.has_key(fps):
236+                self.mockedfilepaths[fps] = MockFilePath(fps, self.mockedfilepaths, exists=True)
237+
238+    def basename(self):
239+        return os.path.split(self.path)[1]
240+
241+    def moveTo(self, newffp):
242+        #  XXX Makes no distinction between file and directory arguments, this is deviation from filepath.moveTo
243+        if self.mockedfilepaths[newffp.path].exists():
244+            raise OSError
245+        else:
246+            self.mockedfilepaths[newffp.path] = self
247+            self.path = newffp.path
248+
249+    def getsize(self):
250+        return self.fileobject.getsize()
251+
252+    def exists(self):
253+        return self.existance
254+
255+    def isdir(self):
256+        return True
257+
258+    def makedirs(self):
259+        # XXX These methods assume that fp_<FOO> functions in fileutil will be tested elsewhere!
260+        pass
261+
262+    def remove(self):
263+        pass
264+
265+
266+class MockFileObject:
267+    def __init__(self, contentstring=''):
268+        self.buffer = contentstring
269+        self.pos = 0
270+    def open(self, mode='r'):
271+        return self
272+    def write(self, instring):
273+        begin = self.pos
274+        padlen = begin - len(self.buffer)
275+        if padlen > 0:
276+            self.buffer += '\x00' * padlen
277+        end = self.pos + len(instring)
278+        self.buffer = self.buffer[:begin]+instring+self.buffer[end:]
279+        self.pos = end
280+    def close(self):
281+        self.pos = 0
282+    def seek(self, pos):
283+        self.pos = pos
284+    def read(self, numberbytes):
285+        return self.buffer[self.pos:self.pos+numberbytes]
286+    def tell(self):
287+        return self.pos
288+    def size(self):
289+        # XXX This method A: Is not to be found in a real file B: Is part of a wild-mung-up of filepath.stat!
290+        # XXX Finally we shall hopefully use a getsize method soon, must consult first though.
291+        # Hmmm...  perhaps we need to sometimes stat the address when there's not a mockfileobject present?
292+        return {stat.ST_SIZE:len(self.buffer)}
293+    def getsize(self):
294+        return len(self.buffer)
295+
296+class MockBCC:
297+    def setServiceParent(self, Parent):
298+        pass
299+
300+
301+class MockLCC:
302+    def setServiceParent(self, Parent):
303+        pass
304+
305+
306+class TestServerWithNullBackend(unittest.TestCase, ReallyEqualMixin):
307+    """ NullBackend is just for testing and executable documentation, so
308+    this test is actually a test of StorageServer in which we're using
309+    NullBackend as helper code for the test, rather than a test of
310+    NullBackend. """
311+    def setUp(self):
312+        self.ss = StorageServer(testnodeid, backend=NullCore())
313+
314+    @mock.patch('os.mkdir')
315+    @mock.patch('__builtin__.open')
316+    @mock.patch('os.listdir')
317+    @mock.patch('os.path.isdir')
318+    def test_write_share(self, mockisdir, mocklistdir, mockopen, mockmkdir):
319+        """ Write a new share. """
320+
321+        alreadygot, bs = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
322+        bs[0].remote_write(0, 'a')
323+        self.failIf(mockisdir.called)
324+        self.failIf(mocklistdir.called)
325+        self.failIf(mockopen.called)
326+        self.failIf(mockmkdir.called)
327+
328+
329+class TestServerConstruction(MockFileSystem, ReallyEqualMixin):
330+    def test_create_server_fs_backend(self):
331+        """ This tests whether a server instance can be constructed with a
332+        filesystem backend. To pass the test, it mustn't use the filesystem
333+        outside of its configured storedir. """
334+        StorageServer(testnodeid, backend=DASCore(self.storedir, expiration_policy))
335+
336+
337+class TestServerAndFSBackend(MockFileSystem, ReallyEqualMixin):
338+    """ This tests both the StorageServer and the DAS backend together. """   
339+    def setUp(self):
340+        MockFileSystem.setUp(self)
341+        try:
342+            self.backend = DASCore(self.storedir, expiration_policy)
343+            self.ss = StorageServer(testnodeid, self.backend)
344+
345+            self.backendwithreserve = DASCore(self.storedir, expiration_policy, reserved_space = 1)
346+            self.sswithreserve = StorageServer(testnodeid, self.backendwithreserve)
347+        except:
348+            MockFileSystem.tearDown(self)
349+            raise
350+
351+    @mock.patch('time.time')
352+    @mock.patch('allmydata.util.fileutil.get_available_space')
353+    def test_out_of_space(self, mockget_available_space, mocktime):
354+        mocktime.return_value = 0
355+       
356+        def call_get_available_space(dir, reserve):
357+            return 0
358+
359+        mockget_available_space.side_effect = call_get_available_space
360+        alreadygotc, bsc = self.sswithreserve.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
361+        self.failUnlessReallyEqual(bsc, {})
362+
363+    @mock.patch('time.time')
364+    def test_write_and_read_share(self, mocktime):
365+        """
366+        Write a new share, read it, and test the server's (and FS backend's)
367+        handling of simultaneous and successive attempts to write the same
368+        share.
369+        """
370+        mocktime.return_value = 0
371+        # Inspect incoming and fail unless it's empty.
372+        incomingset = self.ss.backend.get_incoming_shnums('teststorage_index')
373+       
374+        self.failUnlessReallyEqual(incomingset, frozenset())
375+       
376+        # Populate incoming with the sharenum: 0.
377+        alreadygot, bs = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, frozenset((0,)), 1, mock.Mock())
378+
379+        # This is a transparent-box test: Inspect incoming and fail unless the sharenum: 0 is listed there.
380+        self.failUnlessReallyEqual(self.ss.backend.get_incoming_shnums('teststorage_index'), frozenset((0,)))
381+
382+
383+
384+        # Attempt to create a second share writer with the same sharenum.
385+        alreadygota, bsa = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, frozenset((0,)), 1, mock.Mock())
386+
387+        # Show that no sharewriter results from a remote_allocate_buckets
388+        # with the same si and sharenum, until BucketWriter.remote_close()
389+        # has been called.
390+        self.failIf(bsa)
391+
392+        # Test allocated size.
393+        spaceint = self.ss.allocated_size()
394+        self.failUnlessReallyEqual(spaceint, 1)
395+
396+        # Write 'a' to shnum 0. Only tested together with close and read.
397+        bs[0].remote_write(0, 'a')
398+       
399+        # Preclose: Inspect final, failUnless nothing there.
400+        self.failUnlessReallyEqual(len(list(self.backend.get_shares('teststorage_index'))), 0)
401+        bs[0].remote_close()
402+
403+        # Postclose: (Omnibus) failUnless written data is in final.
404+        sharesinfinal = list(self.backend.get_shares('teststorage_index'))
405+        self.failUnlessReallyEqual(len(sharesinfinal), 1)
406+        contents = sharesinfinal[0].read_share_data(0, 73)
407+        self.failUnlessReallyEqual(contents, client_data)
408+
409+        # Exercise the case that the share we're asking to allocate is
410+        # already (completely) uploaded.
411+        self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
412+       
413+
414+    def test_read_old_share(self):
415+        """ This tests whether the code correctly finds and reads
416+        shares written out by old (Tahoe-LAFS <= v1.8.2)
417+        servers. There is a similar test in test_download, but that one
418+        is from the perspective of the client and exercises a deeper
419+        stack of code. This one is for exercising just the
420+        StorageServer object. """
421+        # Contruct a file with the appropriate contents in the mockfilesystem.
422+        datalen = len(share_data)
423+        finalhome = si_si2dir(self.basedir, 'teststorage_index').child(str(0))
424+        finalhome.setContent(share_data)
425+
426+        # Now begin the test.
427+        bs = self.ss.remote_get_buckets('teststorage_index')
428+
429+        self.failUnlessEqual(len(bs), 1)
430+        b = bs['0']
431+        # These should match by definition, the next two cases cover cases without (completely) unambiguous behaviors.
432+        self.failUnlessReallyEqual(b.remote_read(0, datalen), client_data)
433+        # If you try to read past the end you get the as much data as is there.
434+        self.failUnlessReallyEqual(b.remote_read(0, datalen+20), client_data)
435+        # If you start reading past the end of the file you get the empty string.
436+        self.failUnlessReallyEqual(b.remote_read(datalen+1, 3), '')
437}
438[Added directories and new modules for the null backend
439wilcoxjg@gmail.com**20110809200929
440 Ignore-this: f5dfa418afced5141eb9247a9908109e
441] {
442hunk ./src/allmydata/interfaces.py 274
443         store that on disk.
444         """
445 
446+class IStorageBackend(Interface):
447+    """
448+    Objects of this kind live on the server side and are used by the
449+    storage server object.
450+    """
451+    def get_available_space(self, reserved_space):
452+        """ Returns available space for share storage in bytes, or
453+        None if this information is not available or if the available
454+        space is unlimited.
455+
456+        If the backend is configured for read-only mode then this will
457+        return 0.
458+
459+        reserved_space is how many bytes to subtract from the answer, so
460+        you can pass how many bytes you would like to leave unused on this
461+        filesystem as reserved_space. """
462+
463+    def get_bucket_shares(self):
464+        """XXX"""
465+
466+    def get_share(self):
467+        """XXX"""
468+
469+    def make_bucket_writer(self):
470+        """XXX"""
471+
472+class IStorageBackendShare(Interface):
473+    """
474+    This object contains as much as all of the share data.  It is intended
475+    for lazy evaluation such that in many use cases substantially less than
476+    all of the share data will be accessed.
477+    """
478+    def is_complete(self):
479+        """
480+        Returns the share state, or None if the share does not exist.
481+        """
482+
483 class IStorageBucketWriter(Interface):
484     """
485     Objects of this kind live on the client side.
486adddir ./src/allmydata/storage/backends
487addfile ./src/allmydata/storage/backends/base.py
488hunk ./src/allmydata/storage/backends/base.py 1
489+from twisted.application import service
490+
491+class Backend(service.MultiService):
492+    def __init__(self):
493+        service.MultiService.__init__(self)
494adddir ./src/allmydata/storage/backends/null
495addfile ./src/allmydata/storage/backends/null/core.py
496hunk ./src/allmydata/storage/backends/null/core.py 1
497+from allmydata.storage.backends.base import Backend
498+from allmydata.storage.immutable import BucketWriter, BucketReader
499+
500+class NullCore(Backend):
501+    def __init__(self):
502+        Backend.__init__(self)
503+
504+    def get_available_space(self):
505+        return None
506+
507+    def get_shares(self, storage_index):
508+        return set()
509+
510+    def get_share(self, storage_index, sharenum):
511+        return None
512+
513+    def make_bucket_writer(self, storageindex, shnum, max_space_per_bucket, lease_info, canary):
514+        immutableshare = ImmutableShare()
515+        return BucketWriter(self.ss, immutableshare, max_space_per_bucket, lease_info, canary)
516+
517+    def set_storage_server(self, ss):
518+        self.ss = ss
519+
520+    def get_incoming_shnums(self, storageindex):
521+        return frozenset()
522+
523+class ImmutableShare:
524+    sharetype = "immutable"
525+
526+    def __init__(self):
527+        """ If max_size is not None then I won't allow more than
528+        max_size to be written to me. If create=True then max_size
529+        must not be None. """
530+        pass
531+
532+    def get_shnum(self):
533+        return self.shnum
534+
535+    def unlink(self):
536+        os.unlink(self.fname)
537+
538+    def read_share_data(self, offset, length):
539+        precondition(offset >= 0)
540+        # Reads beyond the end of the data are truncated. Reads that start
541+        # beyond the end of the data return an empty string.
542+        seekpos = self._data_offset+offset
543+        fsize = os.path.getsize(self.fname)
544+        actuallength = max(0, min(length, fsize-seekpos))
545+        if actuallength == 0:
546+            return ""
547+        f = open(self.fname, 'rb')
548+        f.seek(seekpos)
549+        return f.read(actuallength)
550+
551+    def write_share_data(self, offset, data):
552+        pass
553+
554+    def _write_lease_record(self, f, lease_number, lease_info):
555+        offset = self._lease_offset + lease_number * self.LEASE_SIZE
556+        f.seek(offset)
557+        assert f.tell() == offset
558+        f.write(lease_info.to_immutable_data())
559+
560+    def _read_num_leases(self, f):
561+        f.seek(0x08)
562+        (num_leases,) = struct.unpack(">L", f.read(4))
563+        return num_leases
564+
565+    def _write_num_leases(self, f, num_leases):
566+        f.seek(0x08)
567+        f.write(struct.pack(">L", num_leases))
568+
569+    def _truncate_leases(self, f, num_leases):
570+        f.truncate(self._lease_offset + num_leases * self.LEASE_SIZE)
571+
572+    def get_leases(self):
573+        """Yields a LeaseInfo instance for all leases."""
574+        f = open(self.fname, 'rb')
575+        (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
576+        f.seek(self._lease_offset)
577+        for i in range(num_leases):
578+            data = f.read(self.LEASE_SIZE)
579+            if data:
580+                yield LeaseInfo().from_immutable_data(data)
581+
582+    def add_lease(self, lease):
583+        pass
584+
585+    def renew_lease(self, renew_secret, new_expire_time):
586+        for i,lease in enumerate(self.get_leases()):
587+            if constant_time_compare(lease.renew_secret, renew_secret):
588+                # yup. See if we need to update the owner time.
589+                if new_expire_time > lease.expiration_time:
590+                    # yes
591+                    lease.expiration_time = new_expire_time
592+                    f = open(self.fname, 'rb+')
593+                    self._write_lease_record(f, i, lease)
594+                    f.close()
595+                return
596+        raise IndexError("unable to renew non-existent lease")
597+
598+    def add_or_renew_lease(self, lease_info):
599+        try:
600+            self.renew_lease(lease_info.renew_secret,
601+                             lease_info.expiration_time)
602+        except IndexError:
603+            self.add_lease(lease_info)
604+
605+
606+    def cancel_lease(self, cancel_secret):
607+        """Remove a lease with the given cancel_secret. If the last lease is
608+        cancelled, the file will be removed. Return the number of bytes that
609+        were freed (by truncating the list of leases, and possibly by
610+        deleting the file. Raise IndexError if there was no lease with the
611+        given cancel_secret.
612+        """
613+
614+        leases = list(self.get_leases())
615+        num_leases_removed = 0
616+        for i,lease in enumerate(leases):
617+            if constant_time_compare(lease.cancel_secret, cancel_secret):
618+                leases[i] = None
619+                num_leases_removed += 1
620+        if not num_leases_removed:
621+            raise IndexError("unable to find matching lease to cancel")
622+        if num_leases_removed:
623+            # pack and write out the remaining leases. We write these out in
624+            # the same order as they were added, so that if we crash while
625+            # doing this, we won't lose any non-cancelled leases.
626+            leases = [l for l in leases if l] # remove the cancelled leases
627+            f = open(self.fname, 'rb+')
628+            for i,lease in enumerate(leases):
629+                self._write_lease_record(f, i, lease)
630+            self._write_num_leases(f, len(leases))
631+            self._truncate_leases(f, len(leases))
632+            f.close()
633+        space_freed = self.LEASE_SIZE * num_leases_removed
634+        if not len(leases):
635+            space_freed += os.stat(self.fname)[stat.ST_SIZE]
636+            self.unlink()
637+        return space_freed
638}
639[changes to null/core.py and storage/common.py necessary for test with null backend to pass
640wilcoxjg@gmail.com**20110809201249
641 Ignore-this: 9ddcd79f9962550ed20518ae85b6b6b2
642] {
643hunk ./src/allmydata/storage/backends/null/core.py 3
644 from allmydata.storage.backends.base import Backend
645 from allmydata.storage.immutable import BucketWriter, BucketReader
646+from zope.interface import implements
647 
648 class NullCore(Backend):
649hunk ./src/allmydata/storage/backends/null/core.py 6
650+    implements(IStorageBackend)
651     def __init__(self):
652         Backend.__init__(self)
653 
654hunk ./src/allmydata/storage/backends/null/core.py 30
655         return frozenset()
656 
657 class ImmutableShare:
658+    implements(IStorageBackendShare)
659     sharetype = "immutable"
660 
661     def __init__(self):
662hunk ./src/allmydata/storage/common.py 19
663 def si_a2b(ascii_storageindex):
664     return base32.a2b(ascii_storageindex)
665 
666-def storage_index_to_dir(storageindex):
667+def si_si2dir(startfp, storageindex):
668     sia = si_b2a(storageindex)
669hunk ./src/allmydata/storage/common.py 21
670-    return os.path.join(sia[:2], sia)
671+    newfp = startfp.child(sia[:2])
672+    return newfp.child(sia)
673}
674[change storage/server.py to new "backend pluggable" version
675wilcoxjg@gmail.com**20110809201647
676 Ignore-this: 1b0c5f9e831641287992bf45af55246e
677] {
678hunk ./src/allmydata/storage/server.py 1
679-import os, re, weakref, struct, time
680+import os, weakref, struct, time
681 
682 from foolscap.api import Referenceable
683 from twisted.application import service
684hunk ./src/allmydata/storage/server.py 11
685 from allmydata.util import fileutil, idlib, log, time_format
686 import allmydata # for __full_version__
687 
688-from allmydata.storage.common import si_b2a, si_a2b, storage_index_to_dir
689-_pyflakes_hush = [si_b2a, si_a2b, storage_index_to_dir] # re-exported
690+from allmydata.storage.common import si_b2a, si_a2b, si_si2dir
691+_pyflakes_hush = [si_b2a, si_a2b, si_si2dir] # re-exported
692 from allmydata.storage.lease import LeaseInfo
693 from allmydata.storage.mutable import MutableShareFile, EmptyShare, \
694      create_mutable_sharefile
695hunk ./src/allmydata/storage/server.py 16
696-from allmydata.storage.immutable import ShareFile, BucketWriter, BucketReader
697-from allmydata.storage.crawler import BucketCountingCrawler
698-from allmydata.storage.expirer import LeaseCheckingCrawler
699-
700-# storage/
701-# storage/shares/incoming
702-#   incoming/ holds temp dirs named $START/$STORAGEINDEX/$SHARENUM which will
703-#   be moved to storage/shares/$START/$STORAGEINDEX/$SHARENUM upon success
704-# storage/shares/$START/$STORAGEINDEX
705-# storage/shares/$START/$STORAGEINDEX/$SHARENUM
706-
707-# Where "$START" denotes the first 10 bits worth of $STORAGEINDEX (that's 2
708-# base-32 chars).
709-
710-# $SHARENUM matches this regex:
711-NUM_RE=re.compile("^[0-9]+$")
712-
713-
714 
715 class StorageServer(service.MultiService, Referenceable):
716     implements(RIStorageServer, IStatsProducer)
717hunk ./src/allmydata/storage/server.py 20
718     name = 'storage'
719-    LeaseCheckerClass = LeaseCheckingCrawler
720 
721hunk ./src/allmydata/storage/server.py 21
722-    def __init__(self, storedir, nodeid, reserved_space=0,
723-                 discard_storage=False, readonly_storage=False,
724-                 stats_provider=None,
725-                 expiration_enabled=False,
726-                 expiration_mode="age",
727-                 expiration_override_lease_duration=None,
728-                 expiration_cutoff_date=None,
729-                 expiration_sharetypes=("mutable", "immutable")):
730+    def __init__(self, nodeid, backend, reserved_space=0,
731+                 readonly_storage=False,
732+                 stats_provider=None ):
733         service.MultiService.__init__(self)
734         assert isinstance(nodeid, str)
735         assert len(nodeid) == 20
736hunk ./src/allmydata/storage/server.py 28
737         self.my_nodeid = nodeid
738-        self.storedir = storedir
739-        sharedir = os.path.join(storedir, "shares")
740-        fileutil.make_dirs(sharedir)
741-        self.sharedir = sharedir
742-        # we don't actually create the corruption-advisory dir until necessary
743-        self.corruption_advisory_dir = os.path.join(storedir,
744-                                                    "corruption-advisories")
745-        self.reserved_space = int(reserved_space)
746-        self.no_storage = discard_storage
747-        self.readonly_storage = readonly_storage
748         self.stats_provider = stats_provider
749         if self.stats_provider:
750             self.stats_provider.register_producer(self)
751hunk ./src/allmydata/storage/server.py 31
752-        self.incomingdir = os.path.join(sharedir, 'incoming')
753-        self._clean_incomplete()
754-        fileutil.make_dirs(self.incomingdir)
755         self._active_writers = weakref.WeakKeyDictionary()
756hunk ./src/allmydata/storage/server.py 32
757+        self.backend = backend
758+        self.backend.setServiceParent(self)
759+        self.backend.set_storage_server(self)
760         log.msg("StorageServer created", facility="tahoe.storage")
761 
762hunk ./src/allmydata/storage/server.py 37
763-        if reserved_space:
764-            if self.get_available_space() is None:
765-                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",
766-                        umin="0wZ27w", level=log.UNUSUAL)
767-
768         self.latencies = {"allocate": [], # immutable
769                           "write": [],
770                           "close": [],
771hunk ./src/allmydata/storage/server.py 48
772                           "renew": [],
773                           "cancel": [],
774                           }
775-        self.add_bucket_counter()
776-
777-        statefile = os.path.join(self.storedir, "lease_checker.state")
778-        historyfile = os.path.join(self.storedir, "lease_checker.history")
779-        klass = self.LeaseCheckerClass
780-        self.lease_checker = klass(self, statefile, historyfile,
781-                                   expiration_enabled, expiration_mode,
782-                                   expiration_override_lease_duration,
783-                                   expiration_cutoff_date,
784-                                   expiration_sharetypes)
785-        self.lease_checker.setServiceParent(self)
786 
787     def __repr__(self):
788         return "<StorageServer %s>" % (idlib.shortnodeid_b2a(self.my_nodeid),)
789hunk ./src/allmydata/storage/server.py 52
790 
791-    def add_bucket_counter(self):
792-        statefile = os.path.join(self.storedir, "bucket_counter.state")
793-        self.bucket_counter = BucketCountingCrawler(self, statefile)
794-        self.bucket_counter.setServiceParent(self)
795-
796     def count(self, name, delta=1):
797         if self.stats_provider:
798             self.stats_provider.count("storage_server." + name, delta)
799hunk ./src/allmydata/storage/server.py 66
800         """Return a dict, indexed by category, that contains a dict of
801         latency numbers for each category. If there are sufficient samples
802         for unambiguous interpretation, each dict will contain the
803-        following keys: mean, 01_0_percentile, 10_0_percentile,
804+        following keys: samplesize, mean, 01_0_percentile, 10_0_percentile,
805         50_0_percentile (median), 90_0_percentile, 95_0_percentile,
806         99_0_percentile, 99_9_percentile.  If there are insufficient
807         samples for a given percentile to be interpreted unambiguously
808hunk ./src/allmydata/storage/server.py 88
809             else:
810                 stats["mean"] = None
811 
812-            orderstatlist = [(0.01, "01_0_percentile", 100), (0.1, "10_0_percentile", 10),\
813-                             (0.50, "50_0_percentile", 10), (0.90, "90_0_percentile", 10),\
814-                             (0.95, "95_0_percentile", 20), (0.99, "99_0_percentile", 100),\
815+            orderstatlist = [(0.1, "10_0_percentile", 10), (0.5, "50_0_percentile", 10), \
816+                             (0.9, "90_0_percentile", 10), (0.95, "95_0_percentile", 20), \
817+                             (0.01, "01_0_percentile", 100),  (0.99, "99_0_percentile", 100),\
818                              (0.999, "99_9_percentile", 1000)]
819 
820             for percentile, percentilestring, minnumtoobserve in orderstatlist:
821hunk ./src/allmydata/storage/server.py 107
822             kwargs["facility"] = "tahoe.storage"
823         return log.msg(*args, **kwargs)
824 
825-    def _clean_incomplete(self):
826-        fileutil.rm_dir(self.incomingdir)
827-
828     def get_stats(self):
829         # remember: RIStatsProvider requires that our return dict
830hunk ./src/allmydata/storage/server.py 109
831-        # contains numeric values.
832+        # contains numeric, or None values.
833         stats = { 'storage_server.allocated': self.allocated_size(), }
834         stats['storage_server.reserved_space'] = self.reserved_space
835         for category,ld in self.get_latencies().items():
836merger 0.0 (
837hunk ./src/allmydata/storage/server.py 149
838-        return fileutil.get_available_space(self.storedir, self.reserved_space)
839+        return fileutil.get_available_space(self.sharedir, self.reserved_space)
840hunk ./src/allmydata/storage/server.py 143
841-    def get_available_space(self):
842-        """Returns available space for share storage in bytes, or None if no
843-        API to get this information is available."""
844-
845-        if self.readonly_storage:
846-            return 0
847-        return fileutil.get_available_space(self.storedir, self.reserved_space)
848-
849)
850hunk ./src/allmydata/storage/server.py 158
851         return space
852 
853     def remote_get_version(self):
854-        remaining_space = self.get_available_space()
855+        remaining_space = self.backend.get_available_space()
856         if remaining_space is None:
857             # We're on a platform that has no API to get disk stats.
858             remaining_space = 2**64
859hunk ./src/allmydata/storage/server.py 172
860                     }
861         return version
862 
863-    def remote_allocate_buckets(self, storage_index,
864+    def remote_allocate_buckets(self, storageindex,
865                                 renew_secret, cancel_secret,
866                                 sharenums, allocated_size,
867                                 canary, owner_num=0):
868hunk ./src/allmydata/storage/server.py 181
869         # to a particular owner.
870         start = time.time()
871         self.count("allocate")
872-        alreadygot = set()
873+        incoming = set()
874         bucketwriters = {} # k: shnum, v: BucketWriter
875hunk ./src/allmydata/storage/server.py 183
876-        si_dir = storage_index_to_dir(storage_index)
877-        si_s = si_b2a(storage_index)
878 
879hunk ./src/allmydata/storage/server.py 184
880+        si_s = si_b2a(storageindex)
881         log.msg("storage: allocate_buckets %s" % si_s)
882 
883         # in this implementation, the lease information (including secrets)
884hunk ./src/allmydata/storage/server.py 198
885 
886         max_space_per_bucket = allocated_size
887 
888-        remaining_space = self.get_available_space()
889+        remaining_space = self.backend.get_available_space()
890         limited = remaining_space is not None
891         if limited:
892             # this is a bit conservative, since some of this allocated_size()
893hunk ./src/allmydata/storage/server.py 207
894             remaining_space -= self.allocated_size()
895         # self.readonly_storage causes remaining_space <= 0
896 
897-        # fill alreadygot with all shares that we have, not just the ones
898+        # Fill alreadygot with all shares that we have, not just the ones
899         # they asked about: this will save them a lot of work. Add or update
900         # leases for all of them: if they want us to hold shares for this
901hunk ./src/allmydata/storage/server.py 210
902-        # file, they'll want us to hold leases for this file.
903-        for (shnum, fn) in self._get_bucket_shares(storage_index):
904-            alreadygot.add(shnum)
905-            sf = ShareFile(fn)
906-            sf.add_or_renew_lease(lease_info)
907+        # file, they'll want us to hold leases for all the shares of it.
908+        alreadygot = set()
909+        for share in self.backend.get_shares(storageindex):
910+            share.add_or_renew_lease(lease_info)
911+            alreadygot.add(share.shnum)
912 
913hunk ./src/allmydata/storage/server.py 216
914-        for shnum in sharenums:
915-            incominghome = os.path.join(self.incomingdir, si_dir, "%d" % shnum)
916-            finalhome = os.path.join(self.sharedir, si_dir, "%d" % shnum)
917-            if os.path.exists(finalhome):
918-                # great! we already have it. easy.
919-                pass
920-            elif os.path.exists(incominghome):
921-                # Note that we don't create BucketWriters for shnums that
922-                # have a partial share (in incoming/), so if a second upload
923-                # occurs while the first is still in progress, the second
924-                # uploader will use different storage servers.
925-                pass
926-            elif (not limited) or (remaining_space >= max_space_per_bucket):
927-                # ok! we need to create the new share file.
928-                bw = BucketWriter(self, incominghome, finalhome,
929-                                  max_space_per_bucket, lease_info, canary)
930-                if self.no_storage:
931-                    bw.throw_out_all_data = True
932+        # all share numbers that are incoming
933+        incoming = self.backend.get_incoming_shnums(storageindex)
934+
935+        for shnum in ((sharenums - alreadygot) - incoming):
936+            if (not limited) or (remaining_space >= max_space_per_bucket):
937+                bw = self.backend.make_bucket_writer(storageindex, shnum, max_space_per_bucket, lease_info, canary)
938                 bucketwriters[shnum] = bw
939                 self._active_writers[bw] = 1
940                 if limited:
941hunk ./src/allmydata/storage/server.py 227
942                     remaining_space -= max_space_per_bucket
943             else:
944-                # bummer! not enough space to accept this bucket
945+                # Bummer not enough space to accept this share.
946                 pass
947 
948hunk ./src/allmydata/storage/server.py 230
949-        if bucketwriters:
950-            fileutil.make_dirs(os.path.join(self.sharedir, si_dir))
951-
952         self.add_latency("allocate", time.time() - start)
953         return alreadygot, bucketwriters
954 
955hunk ./src/allmydata/storage/server.py 233
956-    def _iter_share_files(self, storage_index):
957-        for shnum, filename in self._get_bucket_shares(storage_index):
958+    def _iter_share_files(self, storageindex):
959+        for shnum, filename in self._get_shares(storageindex):
960             f = open(filename, 'rb')
961             header = f.read(32)
962             f.close()
963hunk ./src/allmydata/storage/server.py 239
964             if header[:32] == MutableShareFile.MAGIC:
965+                # XXX  Can I exploit this code?
966                 sf = MutableShareFile(filename, self)
967                 # note: if the share has been migrated, the renew_lease()
968                 # call will throw an exception, with information to help the
969hunk ./src/allmydata/storage/server.py 245
970                 # client update the lease.
971             elif header[:4] == struct.pack(">L", 1):
972+                # Check if version number is "1".
973+                # XXX WHAT ABOUT OTHER VERSIONS!!!!!!!?
974                 sf = ShareFile(filename)
975             else:
976                 continue # non-sharefile
977hunk ./src/allmydata/storage/server.py 252
978             yield sf
979 
980-    def remote_add_lease(self, storage_index, renew_secret, cancel_secret,
981+    def remote_add_lease(self, storageindex, renew_secret, cancel_secret,
982                          owner_num=1):
983         start = time.time()
984         self.count("add-lease")
985hunk ./src/allmydata/storage/server.py 260
986         lease_info = LeaseInfo(owner_num,
987                                renew_secret, cancel_secret,
988                                new_expire_time, self.my_nodeid)
989-        for sf in self._iter_share_files(storage_index):
990+        for sf in self._iter_share_files(storageindex):
991             sf.add_or_renew_lease(lease_info)
992         self.add_latency("add-lease", time.time() - start)
993         return None
994hunk ./src/allmydata/storage/server.py 265
995 
996-    def remote_renew_lease(self, storage_index, renew_secret):
997+    def remote_renew_lease(self, storageindex, renew_secret):
998         start = time.time()
999         self.count("renew")
1000         new_expire_time = time.time() + 31*24*60*60
1001hunk ./src/allmydata/storage/server.py 270
1002         found_buckets = False
1003-        for sf in self._iter_share_files(storage_index):
1004+        for sf in self._iter_share_files(storageindex):
1005             found_buckets = True
1006             sf.renew_lease(renew_secret, new_expire_time)
1007         self.add_latency("renew", time.time() - start)
1008hunk ./src/allmydata/storage/server.py 277
1009         if not found_buckets:
1010             raise IndexError("no such lease to renew")
1011 
1012-    def remote_cancel_lease(self, storage_index, cancel_secret):
1013+    def remote_cancel_lease(self, storageindex, cancel_secret):
1014         start = time.time()
1015         self.count("cancel")
1016 
1017hunk ./src/allmydata/storage/server.py 283
1018         total_space_freed = 0
1019         found_buckets = False
1020-        for sf in self._iter_share_files(storage_index):
1021+        for sf in self._iter_share_files(storageindex):
1022             # note: if we can't find a lease on one share, we won't bother
1023             # looking in the others. Unless something broke internally
1024             # (perhaps we ran out of disk space while adding a lease), the
1025hunk ./src/allmydata/storage/server.py 293
1026             total_space_freed += sf.cancel_lease(cancel_secret)
1027 
1028         if found_buckets:
1029-            storagedir = os.path.join(self.sharedir,
1030-                                      storage_index_to_dir(storage_index))
1031-            if not os.listdir(storagedir):
1032-                os.rmdir(storagedir)
1033+            # XXX  Yikes looks like code that shouldn't be in the server!
1034+            storagedir = si_si2dir(self.sharedir, storageindex)
1035+            fp_rmdir_if_empty(storagedir)
1036 
1037         if self.stats_provider:
1038             self.stats_provider.count('storage_server.bytes_freed',
1039hunk ./src/allmydata/storage/server.py 309
1040             self.stats_provider.count('storage_server.bytes_added', consumed_size)
1041         del self._active_writers[bw]
1042 
1043-    def _get_bucket_shares(self, storage_index):
1044-        """Return a list of (shnum, pathname) tuples for files that hold
1045-        shares for this storage_index. In each tuple, 'shnum' will always be
1046-        the integer form of the last component of 'pathname'."""
1047-        storagedir = os.path.join(self.sharedir, storage_index_to_dir(storage_index))
1048-        try:
1049-            for f in os.listdir(storagedir):
1050-                if NUM_RE.match(f):
1051-                    filename = os.path.join(storagedir, f)
1052-                    yield (int(f), filename)
1053-        except OSError:
1054-            # Commonly caused by there being no buckets at all.
1055-            pass
1056-
1057-    def remote_get_buckets(self, storage_index):
1058+    def remote_get_buckets(self, storageindex):
1059         start = time.time()
1060         self.count("get")
1061hunk ./src/allmydata/storage/server.py 312
1062-        si_s = si_b2a(storage_index)
1063+        si_s = si_b2a(storageindex)
1064         log.msg("storage: get_buckets %s" % si_s)
1065         bucketreaders = {} # k: sharenum, v: BucketReader
1066hunk ./src/allmydata/storage/server.py 315
1067-        for shnum, filename in self._get_bucket_shares(storage_index):
1068-            bucketreaders[shnum] = BucketReader(self, filename,
1069-                                                storage_index, shnum)
1070+        self.backend.set_storage_server(self)
1071+        for share in self.backend.get_shares(storageindex):
1072+            bucketreaders[share.get_shnum()] = self.backend.make_bucket_reader(share)
1073         self.add_latency("get", time.time() - start)
1074         return bucketreaders
1075 
1076hunk ./src/allmydata/storage/server.py 321
1077-    def get_leases(self, storage_index):
1078+    def get_leases(self, storageindex):
1079         """Provide an iterator that yields all of the leases attached to this
1080         bucket. Each lease is returned as a LeaseInfo instance.
1081 
1082hunk ./src/allmydata/storage/server.py 331
1083         # since all shares get the same lease data, we just grab the leases
1084         # from the first share
1085         try:
1086-            shnum, filename = self._get_bucket_shares(storage_index).next()
1087+            shnum, filename = self._get_shares(storageindex).next()
1088             sf = ShareFile(filename)
1089             return sf.get_leases()
1090         except StopIteration:
1091hunk ./src/allmydata/storage/server.py 337
1092             return iter([])
1093 
1094-    def remote_slot_testv_and_readv_and_writev(self, storage_index,
1095+    #  XXX  As far as Zancas' grockery has gotten.
1096+    def remote_slot_testv_and_readv_and_writev(self, storageindex,
1097                                                secrets,
1098                                                test_and_write_vectors,
1099                                                read_vector):
1100hunk ./src/allmydata/storage/server.py 344
1101         start = time.time()
1102         self.count("writev")
1103-        si_s = si_b2a(storage_index)
1104+        si_s = si_b2a(storageindex)
1105         log.msg("storage: slot_writev %s" % si_s)
1106hunk ./src/allmydata/storage/server.py 346
1107-        si_dir = storage_index_to_dir(storage_index)
1108+       
1109         (write_enabler, renew_secret, cancel_secret) = secrets
1110         # shares exist if there is a file for them
1111hunk ./src/allmydata/storage/server.py 349
1112-        bucketdir = os.path.join(self.sharedir, si_dir)
1113+        bucketdir = si_si2dir(self.sharedir, storageindex)
1114         shares = {}
1115         if os.path.isdir(bucketdir):
1116             for sharenum_s in os.listdir(bucketdir):
1117hunk ./src/allmydata/storage/server.py 432
1118                                          self)
1119         return share
1120 
1121-    def remote_slot_readv(self, storage_index, shares, readv):
1122+    def remote_slot_readv(self, storageindex, shares, readv):
1123         start = time.time()
1124         self.count("readv")
1125hunk ./src/allmydata/storage/server.py 435
1126-        si_s = si_b2a(storage_index)
1127+        si_s = si_b2a(storageindex)
1128         lp = log.msg("storage: slot_readv %s %s" % (si_s, shares),
1129                      facility="tahoe.storage", level=log.OPERATIONAL)
1130hunk ./src/allmydata/storage/server.py 438
1131-        si_dir = storage_index_to_dir(storage_index)
1132         # shares exist if there is a file for them
1133hunk ./src/allmydata/storage/server.py 439
1134-        bucketdir = os.path.join(self.sharedir, si_dir)
1135+        bucketdir = si_si2dir(self.sharedir, storageindex)
1136         if not os.path.isdir(bucketdir):
1137             self.add_latency("readv", time.time() - start)
1138             return {}
1139hunk ./src/allmydata/storage/server.py 458
1140         self.add_latency("readv", time.time() - start)
1141         return datavs
1142 
1143-    def remote_advise_corrupt_share(self, share_type, storage_index, shnum,
1144+    def remote_advise_corrupt_share(self, share_type, storageindex, shnum,
1145                                     reason):
1146         fileutil.make_dirs(self.corruption_advisory_dir)
1147         now = time_format.iso_utc(sep="T")
1148hunk ./src/allmydata/storage/server.py 462
1149-        si_s = si_b2a(storage_index)
1150+        si_s = si_b2a(storageindex)
1151         # windows can't handle colons in the filename
1152         fn = os.path.join(self.corruption_advisory_dir,
1153                           "%s--%s-%d" % (now, si_s, shnum)).replace(":","")
1154hunk ./src/allmydata/storage/server.py 469
1155         f = open(fn, "w")
1156         f.write("report: Share Corruption\n")
1157         f.write("type: %s\n" % share_type)
1158-        f.write("storage_index: %s\n" % si_s)
1159+        f.write("storageindex: %s\n" % si_s)
1160         f.write("share_number: %d\n" % shnum)
1161         f.write("\n")
1162         f.write(reason)
1163}
1164[modify null/core.py such that the correct interfaces are implemented
1165wilcoxjg@gmail.com**20110809201822
1166 Ignore-this: 3c64580592474f71633287d1b6beeb6b
1167] hunk ./src/allmydata/storage/backends/null/core.py 4
1168 from allmydata.storage.backends.base import Backend
1169 from allmydata.storage.immutable import BucketWriter, BucketReader
1170 from zope.interface import implements
1171+from allmydata.interfaces import IStorageBackend, IStorageBackendShare
1172 
1173 class NullCore(Backend):
1174     implements(IStorageBackend)
1175[make changes to storage/immutable.py most changes are part of movement to DAS specific backend.
1176wilcoxjg@gmail.com**20110809202232
1177 Ignore-this: 70c7c6ea6be2418d70556718a050714
1178] {
1179hunk ./src/allmydata/storage/immutable.py 1
1180-import os, stat, struct, time
1181+import os, time
1182 
1183 from foolscap.api import Referenceable
1184 
1185hunk ./src/allmydata/storage/immutable.py 7
1186 from zope.interface import implements
1187 from allmydata.interfaces import RIBucketWriter, RIBucketReader
1188-from allmydata.util import base32, fileutil, log
1189+from allmydata.util import base32, log
1190 from allmydata.util.assertutil import precondition
1191 from allmydata.util.hashutil import constant_time_compare
1192 from allmydata.storage.lease import LeaseInfo
1193hunk ./src/allmydata/storage/immutable.py 14
1194 from allmydata.storage.common import UnknownImmutableContainerVersionError, \
1195      DataTooLargeError
1196 
1197-# each share file (in storage/shares/$SI/$SHNUM) contains lease information
1198-# and share data. The share data is accessed by RIBucketWriter.write and
1199-# RIBucketReader.read . The lease information is not accessible through these
1200-# interfaces.
1201-
1202-# The share file has the following layout:
1203-#  0x00: share file version number, four bytes, current version is 1
1204-#  0x04: share data length, four bytes big-endian = A # See Footnote 1 below.
1205-#  0x08: number of leases, four bytes big-endian
1206-#  0x0c: beginning of share data (see immutable.layout.WriteBucketProxy)
1207-#  A+0x0c = B: first lease. Lease format is:
1208-#   B+0x00: owner number, 4 bytes big-endian, 0 is reserved for no-owner
1209-#   B+0x04: renew secret, 32 bytes (SHA256)
1210-#   B+0x24: cancel secret, 32 bytes (SHA256)
1211-#   B+0x44: expiration time, 4 bytes big-endian seconds-since-epoch
1212-#   B+0x48: next lease, or end of record
1213-
1214-# Footnote 1: as of Tahoe v1.3.0 this field is not used by storage servers,
1215-# but it is still filled in by storage servers in case the storage server
1216-# software gets downgraded from >= Tahoe v1.3.0 to < Tahoe v1.3.0, or the
1217-# share file is moved from one storage server to another. The value stored in
1218-# this field is truncated, so if the actual share data length is >= 2**32,
1219-# then the value stored in this field will be the actual share data length
1220-# modulo 2**32.
1221-
1222-class ShareFile:
1223-    LEASE_SIZE = struct.calcsize(">L32s32sL")
1224-    sharetype = "immutable"
1225-
1226-    def __init__(self, filename, max_size=None, create=False):
1227-        """ 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. """
1228-        precondition((max_size is not None) or (not create), max_size, create)
1229-        self.home = filename
1230-        self._max_size = max_size
1231-        if create:
1232-            # touch the file, so later callers will see that we're working on
1233-            # it. Also construct the metadata.
1234-            assert not os.path.exists(self.home)
1235-            fileutil.make_dirs(os.path.dirname(self.home))
1236-            f = open(self.home, 'wb')
1237-            # The second field -- the four-byte share data length -- is no
1238-            # longer used as of Tahoe v1.3.0, but we continue to write it in
1239-            # there in case someone downgrades a storage server from >=
1240-            # Tahoe-1.3.0 to < Tahoe-1.3.0, or moves a share file from one
1241-            # server to another, etc. We do saturation -- a share data length
1242-            # larger than 2**32-1 (what can fit into the field) is marked as
1243-            # the largest length that can fit into the field. That way, even
1244-            # if this does happen, the old < v1.3.0 server will still allow
1245-            # clients to read the first part of the share.
1246-            f.write(struct.pack(">LLL", 1, min(2**32-1, max_size), 0))
1247-            f.close()
1248-            self._lease_offset = max_size + 0x0c
1249-            self._num_leases = 0
1250-        else:
1251-            f = open(self.home, 'rb')
1252-            filesize = os.path.getsize(self.home)
1253-            (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1254-            f.close()
1255-            if version != 1:
1256-                msg = "sharefile %s had version %d but we wanted 1" % \
1257-                      (filename, version)
1258-                raise UnknownImmutableContainerVersionError(msg)
1259-            self._num_leases = num_leases
1260-            self._lease_offset = filesize - (num_leases * self.LEASE_SIZE)
1261-        self._data_offset = 0xc
1262-
1263-    def unlink(self):
1264-        os.unlink(self.home)
1265-
1266-    def read_share_data(self, offset, length):
1267-        precondition(offset >= 0)
1268-        # reads beyond the end of the data are truncated. Reads that start
1269-        # beyond the end of the data return an empty string. I wonder why
1270-        # Python doesn't do the following computation for me?
1271-        seekpos = self._data_offset+offset
1272-        fsize = os.path.getsize(self.home)
1273-        actuallength = max(0, min(length, fsize-seekpos))
1274-        if actuallength == 0:
1275-            return ""
1276-        f = open(self.home, 'rb')
1277-        f.seek(seekpos)
1278-        return f.read(actuallength)
1279-
1280-    def write_share_data(self, offset, data):
1281-        length = len(data)
1282-        precondition(offset >= 0, offset)
1283-        if self._max_size is not None and offset+length > self._max_size:
1284-            raise DataTooLargeError(self._max_size, offset, length)
1285-        f = open(self.home, 'rb+')
1286-        real_offset = self._data_offset+offset
1287-        f.seek(real_offset)
1288-        assert f.tell() == real_offset
1289-        f.write(data)
1290-        f.close()
1291-
1292-    def _write_lease_record(self, f, lease_number, lease_info):
1293-        offset = self._lease_offset + lease_number * self.LEASE_SIZE
1294-        f.seek(offset)
1295-        assert f.tell() == offset
1296-        f.write(lease_info.to_immutable_data())
1297-
1298-    def _read_num_leases(self, f):
1299-        f.seek(0x08)
1300-        (num_leases,) = struct.unpack(">L", f.read(4))
1301-        return num_leases
1302-
1303-    def _write_num_leases(self, f, num_leases):
1304-        f.seek(0x08)
1305-        f.write(struct.pack(">L", num_leases))
1306-
1307-    def _truncate_leases(self, f, num_leases):
1308-        f.truncate(self._lease_offset + num_leases * self.LEASE_SIZE)
1309-
1310-    def get_leases(self):
1311-        """Yields a LeaseInfo instance for all leases."""
1312-        f = open(self.home, 'rb')
1313-        (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1314-        f.seek(self._lease_offset)
1315-        for i in range(num_leases):
1316-            data = f.read(self.LEASE_SIZE)
1317-            if data:
1318-                yield LeaseInfo().from_immutable_data(data)
1319-
1320-    def add_lease(self, lease_info):
1321-        f = open(self.home, 'rb+')
1322-        num_leases = self._read_num_leases(f)
1323-        self._write_lease_record(f, num_leases, lease_info)
1324-        self._write_num_leases(f, num_leases+1)
1325-        f.close()
1326-
1327-    def renew_lease(self, renew_secret, new_expire_time):
1328-        for i,lease in enumerate(self.get_leases()):
1329-            if constant_time_compare(lease.renew_secret, renew_secret):
1330-                # yup. See if we need to update the owner time.
1331-                if new_expire_time > lease.expiration_time:
1332-                    # yes
1333-                    lease.expiration_time = new_expire_time
1334-                    f = open(self.home, 'rb+')
1335-                    self._write_lease_record(f, i, lease)
1336-                    f.close()
1337-                return
1338-        raise IndexError("unable to renew non-existent lease")
1339-
1340-    def add_or_renew_lease(self, lease_info):
1341-        try:
1342-            self.renew_lease(lease_info.renew_secret,
1343-                             lease_info.expiration_time)
1344-        except IndexError:
1345-            self.add_lease(lease_info)
1346-
1347-
1348-    def cancel_lease(self, cancel_secret):
1349-        """Remove a lease with the given cancel_secret. If the last lease is
1350-        cancelled, the file will be removed. Return the number of bytes that
1351-        were freed (by truncating the list of leases, and possibly by
1352-        deleting the file. Raise IndexError if there was no lease with the
1353-        given cancel_secret.
1354-        """
1355-
1356-        leases = list(self.get_leases())
1357-        num_leases_removed = 0
1358-        for i,lease in enumerate(leases):
1359-            if constant_time_compare(lease.cancel_secret, cancel_secret):
1360-                leases[i] = None
1361-                num_leases_removed += 1
1362-        if not num_leases_removed:
1363-            raise IndexError("unable to find matching lease to cancel")
1364-        if num_leases_removed:
1365-            # pack and write out the remaining leases. We write these out in
1366-            # the same order as they were added, so that if we crash while
1367-            # doing this, we won't lose any non-cancelled leases.
1368-            leases = [l for l in leases if l] # remove the cancelled leases
1369-            f = open(self.home, 'rb+')
1370-            for i,lease in enumerate(leases):
1371-                self._write_lease_record(f, i, lease)
1372-            self._write_num_leases(f, len(leases))
1373-            self._truncate_leases(f, len(leases))
1374-            f.close()
1375-        space_freed = self.LEASE_SIZE * num_leases_removed
1376-        if not len(leases):
1377-            space_freed += os.stat(self.home)[stat.ST_SIZE]
1378-            self.unlink()
1379-        return space_freed
1380-
1381-
1382 class BucketWriter(Referenceable):
1383     implements(RIBucketWriter)
1384 
1385hunk ./src/allmydata/storage/immutable.py 17
1386-    def __init__(self, ss, incominghome, finalhome, max_size, lease_info, canary):
1387+    def __init__(self, ss, immutableshare, max_size, lease_info, canary):
1388         self.ss = ss
1389hunk ./src/allmydata/storage/immutable.py 19
1390-        self.incominghome = incominghome
1391-        self.finalhome = finalhome
1392-        self._max_size = max_size # don't allow the client to write more than this
1393+        self._max_size = max_size # don't allow the client to write more than this        print self.ss._active_writers.keys()
1394         self._canary = canary
1395         self._disconnect_marker = canary.notifyOnDisconnect(self._disconnected)
1396         self.closed = False
1397hunk ./src/allmydata/storage/immutable.py 24
1398         self.throw_out_all_data = False
1399-        self._sharefile = ShareFile(incominghome, create=True, max_size=max_size)
1400+        self._sharefile = immutableshare
1401         # also, add our lease to the file now, so that other ones can be
1402         # added by simultaneous uploaders
1403         self._sharefile.add_lease(lease_info)
1404hunk ./src/allmydata/storage/immutable.py 45
1405         precondition(not self.closed)
1406         start = time.time()
1407 
1408-        fileutil.make_dirs(os.path.dirname(self.finalhome))
1409-        fileutil.rename(self.incominghome, self.finalhome)
1410-        try:
1411-            # self.incominghome is like storage/shares/incoming/ab/abcde/4 .
1412-            # We try to delete the parent (.../ab/abcde) to avoid leaving
1413-            # these directories lying around forever, but the delete might
1414-            # fail if we're working on another share for the same storage
1415-            # index (like ab/abcde/5). The alternative approach would be to
1416-            # use a hierarchy of objects (PrefixHolder, BucketHolder,
1417-            # ShareWriter), each of which is responsible for a single
1418-            # directory on disk, and have them use reference counting of
1419-            # their children to know when they should do the rmdir. This
1420-            # approach is simpler, but relies on os.rmdir refusing to delete
1421-            # a non-empty directory. Do *not* use fileutil.rm_dir() here!
1422-            os.rmdir(os.path.dirname(self.incominghome))
1423-            # we also delete the grandparent (prefix) directory, .../ab ,
1424-            # again to avoid leaving directories lying around. This might
1425-            # fail if there is another bucket open that shares a prefix (like
1426-            # ab/abfff).
1427-            os.rmdir(os.path.dirname(os.path.dirname(self.incominghome)))
1428-            # we leave the great-grandparent (incoming/) directory in place.
1429-        except EnvironmentError:
1430-            # ignore the "can't rmdir because the directory is not empty"
1431-            # exceptions, those are normal consequences of the
1432-            # above-mentioned conditions.
1433-            pass
1434+        self._sharefile.close()
1435+        filelen = self._sharefile.stat()
1436         self._sharefile = None
1437hunk ./src/allmydata/storage/immutable.py 48
1438+
1439         self.closed = True
1440         self._canary.dontNotifyOnDisconnect(self._disconnect_marker)
1441 
1442hunk ./src/allmydata/storage/immutable.py 52
1443-        filelen = os.stat(self.finalhome)[stat.ST_SIZE]
1444         self.ss.bucket_writer_closed(self, filelen)
1445         self.ss.add_latency("close", time.time() - start)
1446         self.ss.count("close")
1447hunk ./src/allmydata/storage/immutable.py 90
1448 class BucketReader(Referenceable):
1449     implements(RIBucketReader)
1450 
1451-    def __init__(self, ss, sharefname, storage_index=None, shnum=None):
1452+    def __init__(self, ss, share):
1453         self.ss = ss
1454hunk ./src/allmydata/storage/immutable.py 92
1455-        self._share_file = ShareFile(sharefname)
1456-        self.storage_index = storage_index
1457-        self.shnum = shnum
1458+        self._share_file = share
1459+        self.storageindex = share.storageindex
1460+        self.shnum = share.shnum
1461 
1462     def __repr__(self):
1463         return "<%s %s %s>" % (self.__class__.__name__,
1464hunk ./src/allmydata/storage/immutable.py 98
1465-                               base32.b2a_l(self.storage_index[:8], 60),
1466+                               base32.b2a_l(self.storageindex[:8], 60),
1467                                self.shnum)
1468 
1469     def remote_read(self, offset, length):
1470hunk ./src/allmydata/storage/immutable.py 110
1471 
1472     def remote_advise_corrupt_share(self, reason):
1473         return self.ss.remote_advise_corrupt_share("immutable",
1474-                                                   self.storage_index,
1475+                                                   self.storageindex,
1476                                                    self.shnum,
1477                                                    reason)
1478}
1479[creates backends/das/core.py
1480wilcoxjg@gmail.com**20110809202620
1481 Ignore-this: 2ea937f8d02aa85396135903be91ed67
1482] {
1483adddir ./src/allmydata/storage/backends/das
1484addfile ./src/allmydata/storage/backends/das/core.py
1485hunk ./src/allmydata/storage/backends/das/core.py 1
1486+import re, weakref, struct, time, stat
1487+from twisted.application import service
1488+from twisted.python.filepath import UnlistableError
1489+from twisted.python import filepath
1490+from twisted.python.filepath import FilePath
1491+from zope.interface import implements
1492+
1493+import allmydata # for __full_version__
1494+from allmydata.interfaces import IStorageBackend
1495+from allmydata.storage.backends.base import Backend
1496+from allmydata.storage.common import si_b2a, si_a2b, si_si2dir
1497+from allmydata.util.assertutil import precondition
1498+from allmydata.interfaces import IStatsProducer, IShareStore# XXX, RIStorageServer
1499+from allmydata.util import fileutil, idlib, log, time_format
1500+from allmydata.util.fileutil import fp_make_dirs
1501+from allmydata.storage.lease import LeaseInfo
1502+from allmydata.storage.mutable import MutableShareFile, EmptyShare, \
1503+     create_mutable_sharefile
1504+from allmydata.storage.immutable import BucketWriter, BucketReader
1505+from allmydata.storage.crawler import BucketCountingCrawler
1506+from allmydata.util.hashutil import constant_time_compare
1507+from allmydata.storage.backends.das.expirer import LeaseCheckingCrawler
1508+_pyflakes_hush = [si_b2a, si_a2b, si_si2dir] # re-exported
1509+
1510+# storage/
1511+# storage/shares/incoming
1512+#   incoming/ holds temp dirs named $START/$STORAGEINDEX/$SHARENUM which will
1513+#   be moved to storage/shares/$START/$STORAGEINDEX/$SHARENUM upon success
1514+# storage/shares/$START/$STORAGEINDEX
1515+# storage/shares/$START/$STORAGEINDEX/$SHARENUM
1516+
1517+# Where "$START" denotes the first 10 bits worth of $STORAGEINDEX (that's 2
1518+# base-32 chars).
1519+# $SHARENUM matches this regex:
1520+NUM_RE=re.compile("^[0-9]+$")
1521+
1522+class DASCore(Backend):
1523+    implements(IStorageBackend)
1524+    def __init__(self, storedir, expiration_policy, readonly=False, reserved_space=0):
1525+        Backend.__init__(self)
1526+        self._setup_storage(storedir, readonly, reserved_space)
1527+        self._setup_corruption_advisory()
1528+        self._setup_bucket_counter()
1529+        self._setup_lease_checkerf(expiration_policy)
1530+
1531+    def _setup_storage(self, storedir, readonly, reserved_space):
1532+        precondition(isinstance(storedir, FilePath), storedir, FilePath) 
1533+        self.storedir = storedir
1534+        self.readonly = readonly
1535+        self.reserved_space = int(reserved_space)
1536+        self.sharedir = self.storedir.child("shares")
1537+        fileutil.fp_make_dirs(self.sharedir)
1538+        self.incomingdir = self.sharedir.child('incoming')
1539+        self._clean_incomplete()
1540+        if self.reserved_space and (self.get_available_space() is None):
1541+            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",
1542+                    umid="0wZ27w", level=log.UNUSUAL)
1543+
1544+
1545+    def _clean_incomplete(self):
1546+        fileutil.fp_remove(self.incomingdir)
1547+        fileutil.fp_make_dirs(self.incomingdir)
1548+
1549+    def _setup_corruption_advisory(self):
1550+        # we don't actually create the corruption-advisory dir until necessary
1551+        self.corruption_advisory_dir = self.storedir.child("corruption-advisories")
1552+
1553+    def _setup_bucket_counter(self):
1554+        statefname = self.storedir.child("bucket_counter.state")
1555+        self.bucket_counter = BucketCountingCrawler(statefname)
1556+        self.bucket_counter.setServiceParent(self)
1557+
1558+    def _setup_lease_checkerf(self, expiration_policy):
1559+        statefile = self.storedir.child("lease_checker.state")
1560+        historyfile = self.storedir.child("lease_checker.history")
1561+        self.lease_checker = LeaseCheckingCrawler(statefile, historyfile, expiration_policy)
1562+        self.lease_checker.setServiceParent(self)
1563+
1564+    def get_incoming_shnums(self, storageindex):
1565+        """ Return a frozenset of the shnum (as ints) of incoming shares. """
1566+        incomingthissi = si_si2dir(self.incomingdir, storageindex)
1567+        try:
1568+            childfps = [ fp for fp in incomingthissi.children() if NUM_RE.match(fp.basename()) ]
1569+            shnums = [ int(fp.basename()) for fp in childfps]
1570+            return frozenset(shnums)
1571+        except UnlistableError:
1572+            # There is no shares directory at all.
1573+            return frozenset()
1574+           
1575+    def get_shares(self, storageindex):
1576+        """ Generate ImmutableShare objects for shares we have for this
1577+        storageindex. ("Shares we have" means completed ones, excluding
1578+        incoming ones.)"""
1579+        finalstoragedir = si_si2dir(self.sharedir, storageindex)
1580+        try:
1581+            for fp in finalstoragedir.children():
1582+                fpshnumstr = fp.basename()
1583+                if NUM_RE.match(fpshnumstr):
1584+                    finalhome = finalstoragedir.child(fpshnumstr)
1585+                    yield ImmutableShare(storageindex, fpshnumstr, finalhome)
1586+        except UnlistableError:
1587+            # There is no shares directory at all.
1588+            pass
1589+       
1590+    def get_available_space(self):
1591+        if self.readonly:
1592+            return 0
1593+        return fileutil.get_available_space(self.storedir, self.reserved_space)
1594+
1595+    def make_bucket_writer(self, storageindex, shnum, max_space_per_bucket, lease_info, canary):
1596+        finalhome = si_si2dir(self.sharedir, storageindex).child(str(shnum))
1597+        incominghome = si_si2dir(self.incomingdir, storageindex).child(str(shnum))
1598+        immsh = ImmutableShare(storageindex, shnum, finalhome, incominghome, max_size=max_space_per_bucket, create=True)
1599+        bw = BucketWriter(self.ss, immsh, max_space_per_bucket, lease_info, canary)
1600+        return bw
1601+
1602+    def make_bucket_reader(self, share):
1603+        return BucketReader(self.ss, share)
1604+
1605+    def set_storage_server(self, ss):
1606+        self.ss = ss
1607+       
1608+
1609+# each share file (in storage/shares/$SI/$SHNUM) contains lease information
1610+# and share data. The share data is accessed by RIBucketWriter.write and
1611+# RIBucketReader.read . The lease information is not accessible through these
1612+# interfaces.
1613+
1614+# The share file has the following layout:
1615+#  0x00: share file version number, four bytes, current version is 1
1616+#  0x04: share data length, four bytes big-endian = A # See Footnote 1 below.
1617+#  0x08: number of leases, four bytes big-endian
1618+#  0x0c: beginning of share data (see immutable.layout.WriteBucketProxy)
1619+#  A+0x0c = B: first lease. Lease format is:
1620+#   B+0x00: owner number, 4 bytes big-endian, 0 is reserved for no-owner
1621+#   B+0x04: renew secret, 32 bytes (SHA256)
1622+#   B+0x24: cancel secret, 32 bytes (SHA256)
1623+#   B+0x44: expiration time, 4 bytes big-endian seconds-since-epoch
1624+#   B+0x48: next lease, or end of record
1625+
1626+# Footnote 1: as of Tahoe v1.3.0 this field is not used by storage servers,
1627+# but it is still filled in by storage servers in case the storage server
1628+# software gets downgraded from >= Tahoe v1.3.0 to < Tahoe v1.3.0, or the
1629+# share file is moved from one storage server to another. The value stored in
1630+# this field is truncated, so if the actual share data length is >= 2**32,
1631+# then the value stored in this field will be the actual share data length
1632+# modulo 2**32.
1633+
1634+class ImmutableShare(object):
1635+    LEASE_SIZE = struct.calcsize(">L32s32sL")
1636+    sharetype = "immutable"
1637+
1638+    def __init__(self, storageindex, shnum, finalhome=None, incominghome=None, max_size=None, create=False):
1639+        """ If max_size is not None then I won't allow more than
1640+        max_size to be written to me. If create=True then max_size
1641+        must not be None. """
1642+        precondition((max_size is not None) or (not create), max_size, create)
1643+        self.storageindex = storageindex
1644+        self._max_size = max_size
1645+        self.incominghome = incominghome
1646+        self.finalhome = finalhome
1647+        self.shnum = shnum
1648+        if create:
1649+            # touch the file, so later callers will see that we're working on
1650+            # it. Also construct the metadata.
1651+            assert not finalhome.exists()
1652+            fp_make_dirs(self.incominghome.parent())
1653+            # The second field -- the four-byte share data length -- is no
1654+            # longer used as of Tahoe v1.3.0, but we continue to write it in
1655+            # there in case someone downgrades a storage server from >=
1656+            # Tahoe-1.3.0 to < Tahoe-1.3.0, or moves a share file from one
1657+            # server to another, etc. We do saturation -- a share data length
1658+            # larger than 2**32-1 (what can fit into the field) is marked as
1659+            # the largest length that can fit into the field. That way, even
1660+            # if this does happen, the old < v1.3.0 server will still allow
1661+            # clients to read the first part of the share.
1662+            self.incominghome.setContent(struct.pack(">LLL", 1, min(2**32-1, max_size), 0) )
1663+            self._lease_offset = max_size + 0x0c
1664+            self._num_leases = 0
1665+        else:
1666+            fh = self.finalhome.open(mode='rb')
1667+            try:
1668+                (version, unused, num_leases) = struct.unpack(">LLL", fh.read(0xc))
1669+            finally:
1670+                fh.close()
1671+            filesize = self.finalhome.getsize()
1672+            if version != 1:
1673+                msg = "sharefile %s had version %d but we wanted 1" % \
1674+                      (self.finalhome, version)
1675+                raise UnknownImmutableContainerVersionError(msg)
1676+            self._num_leases = num_leases
1677+            self._lease_offset = filesize - (num_leases * self.LEASE_SIZE)
1678+        self._data_offset = 0xc
1679+
1680+    def close(self):
1681+        fileutil.fp_make_dirs(self.finalhome.parent())
1682+        self.incominghome.moveTo(self.finalhome)
1683+        try:
1684+            # self.incominghome is like storage/shares/incoming/ab/abcde/4 .
1685+            # We try to delete the parent (.../ab/abcde) to avoid leaving
1686+            # these directories lying around forever, but the delete might
1687+            # fail if we're working on another share for the same storage
1688+            # index (like ab/abcde/5). The alternative approach would be to
1689+            # use a hierarchy of objects (PrefixHolder, BucketHolder,
1690+            # ShareWriter), each of which is responsible for a single
1691+            # directory on disk, and have them use reference counting of
1692+            # their children to know when they should do the rmdir. This
1693+            # approach is simpler, but relies on os.rmdir refusing to delete
1694+            # a non-empty directory. Do *not* use fileutil.rm_dir() here!
1695+            fileutil.fp_rmdir_if_empty(self.incominghome.parent())
1696+            # we also delete the grandparent (prefix) directory, .../ab ,
1697+            # again to avoid leaving directories lying around. This might
1698+            # fail if there is another bucket open that shares a prefix (like
1699+            # ab/abfff).
1700+            fileutil.fp_rmdir_if_empty(self.incominghome.parent().parent())
1701+            # we leave the great-grandparent (incoming/) directory in place.
1702+        except EnvironmentError:
1703+            # ignore the "can't rmdir because the directory is not empty"
1704+            # exceptions, those are normal consequences of the
1705+            # above-mentioned conditions.
1706+            pass
1707+        pass
1708+       
1709+    def stat(self):
1710+        return filepath.stat(self.finalhome.path)[stat.ST_SIZE]
1711+
1712+    def get_shnum(self):
1713+        return self.shnum
1714+
1715+    def unlink(self):
1716+        self.finalhome.remove()
1717+
1718+    def read_share_data(self, offset, length):
1719+        precondition(offset >= 0)
1720+        # Reads beyond the end of the data are truncated. Reads that start
1721+        # beyond the end of the data return an empty string.
1722+        seekpos = self._data_offset+offset
1723+        fsize = self.finalhome.getsize()
1724+        actuallength = max(0, min(length, fsize-seekpos))
1725+        if actuallength == 0:
1726+            return ""
1727+        fh = self.finalhome.open(mode='rb')
1728+        try:
1729+            fh.seek(seekpos)
1730+            sharedata = fh.read(actuallength)
1731+        finally:
1732+            fh.close()
1733+        return sharedata
1734+
1735+    def write_share_data(self, offset, data):
1736+        length = len(data)
1737+        precondition(offset >= 0, offset)
1738+        if self._max_size is not None and offset+length > self._max_size:
1739+            raise DataTooLargeError(self._max_size, offset, length)
1740+        fh = self.incominghome.open(mode='rb+')
1741+        try:
1742+            real_offset = self._data_offset+offset
1743+            fh.seek(real_offset)
1744+            assert fh.tell() == real_offset
1745+            fh.write(data)
1746+        finally:
1747+            fh.close()
1748+
1749+    def _write_lease_record(self, f, lease_number, lease_info):
1750+        offset = self._lease_offset + lease_number * self.LEASE_SIZE
1751+        fh = f.open()
1752+        try:
1753+            fh.seek(offset)
1754+            assert fh.tell() == offset
1755+            fh.write(lease_info.to_immutable_data())
1756+        finally:
1757+            fh.close()
1758+
1759+    def _read_num_leases(self, f):
1760+        fh = f.open() #XXX  Should be mocking FilePath.open()
1761+        try:
1762+            fh.seek(0x08)
1763+            ro = fh.read(4)
1764+            (num_leases,) = struct.unpack(">L", ro)
1765+        finally:
1766+            fh.close()
1767+        return num_leases
1768+
1769+    def _write_num_leases(self, f, num_leases):
1770+        fh = f.open()
1771+        try:
1772+            fh.seek(0x08)
1773+            fh.write(struct.pack(">L", num_leases))
1774+        finally:
1775+            fh.close()
1776+
1777+    def _truncate_leases(self, f, num_leases):
1778+        f.truncate(self._lease_offset + num_leases * self.LEASE_SIZE)
1779+
1780+    def get_leases(self):
1781+        """Yields a LeaseInfo instance for all leases."""
1782+        fh = self.finalhome.open(mode='rb')
1783+        (version, unused, num_leases) = struct.unpack(">LLL", fh.read(0xc))
1784+        fh.seek(self._lease_offset)
1785+        for i in range(num_leases):
1786+            data = fh.read(self.LEASE_SIZE)
1787+            if data:
1788+                yield LeaseInfo().from_immutable_data(data)
1789+
1790+    def add_lease(self, lease_info):
1791+        num_leases = self._read_num_leases(self.incominghome)
1792+        self._write_lease_record(self.incominghome, num_leases, lease_info)
1793+        self._write_num_leases(self.incominghome, num_leases+1)
1794+       
1795+    def renew_lease(self, renew_secret, new_expire_time):
1796+        for i,lease in enumerate(self.get_leases()):
1797+            if constant_time_compare(lease.renew_secret, renew_secret):
1798+                # yup. See if we need to update the owner time.
1799+                if new_expire_time > lease.expiration_time:
1800+                    # yes
1801+                    lease.expiration_time = new_expire_time
1802+                    f = open(self.finalhome, 'rb+')
1803+                    self._write_lease_record(f, i, lease)
1804+                    f.close()
1805+                return
1806+        raise IndexError("unable to renew non-existent lease")
1807+
1808+    def add_or_renew_lease(self, lease_info):
1809+        try:
1810+            self.renew_lease(lease_info.renew_secret,
1811+                             lease_info.expiration_time)
1812+        except IndexError:
1813+            self.add_lease(lease_info)
1814+
1815+    def cancel_lease(self, cancel_secret):
1816+        """Remove a lease with the given cancel_secret. If the last lease is
1817+        cancelled, the file will be removed. Return the number of bytes that
1818+        were freed (by truncating the list of leases, and possibly by
1819+        deleting the file. Raise IndexError if there was no lease with the
1820+        given cancel_secret.
1821+        """
1822+
1823+        leases = list(self.get_leases())
1824+        num_leases_removed = 0
1825+        for i,lease in enumerate(leases):
1826+            if constant_time_compare(lease.cancel_secret, cancel_secret):
1827+                leases[i] = None
1828+                num_leases_removed += 1
1829+        if not num_leases_removed:
1830+            raise IndexError("unable to find matching lease to cancel")
1831+        if num_leases_removed:
1832+            # pack and write out the remaining leases. We write these out in
1833+            # the same order as they were added, so that if we crash while
1834+            # doing this, we won't lose any non-cancelled leases.
1835+            leases = [l for l in leases if l] # remove the cancelled leases
1836+            f = open(self.finalhome, 'rb+')
1837+            for i,lease in enumerate(leases):
1838+                self._write_lease_record(f, i, lease)
1839+            self._write_num_leases(f, len(leases))
1840+            self._truncate_leases(f, len(leases))
1841+            f.close()
1842+        space_freed = self.LEASE_SIZE * num_leases_removed
1843+        if not len(leases):
1844+            space_freed += os.stat(self.finalhome)[stat.ST_SIZE]
1845+            self.unlink()
1846+        return space_freed
1847}
1848[change backends/das/core.py to correct which interfaces are implemented
1849wilcoxjg@gmail.com**20110809203123
1850 Ignore-this: 7f9331a04b55f7feee4335abee011e14
1851] hunk ./src/allmydata/storage/backends/das/core.py 13
1852 from allmydata.storage.backends.base import Backend
1853 from allmydata.storage.common import si_b2a, si_a2b, si_si2dir
1854 from allmydata.util.assertutil import precondition
1855-from allmydata.interfaces import IStatsProducer, IShareStore# XXX, RIStorageServer
1856+from allmydata.interfaces import IStorageBackend
1857 from allmydata.util import fileutil, idlib, log, time_format
1858 from allmydata.util.fileutil import fp_make_dirs
1859 from allmydata.storage.lease import LeaseInfo
1860[util/fileutil.py now expects and manipulates twisted.python.filepath.FilePath objects
1861wilcoxjg@gmail.com**20110809203321
1862 Ignore-this: 12c8aa13424ed51a5df09b92a454627
1863] {
1864hunk ./src/allmydata/util/fileutil.py 5
1865 Futz with files like a pro.
1866 """
1867 
1868-import sys, exceptions, os, stat, tempfile, time, binascii
1869+import errno, sys, exceptions, os, stat, tempfile, time, binascii
1870+
1871+from allmydata.util.assertutil import precondition
1872 
1873 from twisted.python import log
1874hunk ./src/allmydata/util/fileutil.py 10
1875+from twisted.python.filepath import FilePath, UnlistableError
1876 
1877 from pycryptopp.cipher.aes import AES
1878 
1879hunk ./src/allmydata/util/fileutil.py 189
1880             raise tx
1881         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...
1882 
1883-def rm_dir(dirname):
1884+def fp_make_dirs(dirfp):
1885+    """
1886+    An idempotent version of FilePath.makedirs().  If the dir already
1887+    exists, do nothing and return without raising an exception.  If this
1888+    call creates the dir, return without raising an exception.  If there is
1889+    an error that prevents creation or if the directory gets deleted after
1890+    fp_make_dirs() creates it and before fp_make_dirs() checks that it
1891+    exists, raise an exception.
1892+    """
1893+    log.msg( "xxx 0 %s" % (dirfp,))
1894+    tx = None
1895+    try:
1896+        dirfp.makedirs()
1897+    except OSError, x:
1898+        tx = x
1899+
1900+    if not dirfp.isdir():
1901+        if tx:
1902+            raise tx
1903+        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...
1904+
1905+def fp_rmdir_if_empty(dirfp):
1906+    """ Remove the directory if it is empty. """
1907+    try:
1908+        os.rmdir(dirfp.path)
1909+    except OSError, e:
1910+        if e.errno != errno.ENOTEMPTY:
1911+            raise
1912+    else:
1913+        dirfp.changed()
1914+
1915+def rmtree(dirname):
1916     """
1917     A threadsafe and idempotent version of shutil.rmtree().  If the dir is
1918     already gone, do nothing and return without raising an exception.  If this
1919hunk ./src/allmydata/util/fileutil.py 239
1920             else:
1921                 remove(fullname)
1922         os.rmdir(dirname)
1923-    except Exception, le:
1924-        # Ignore "No such file or directory"
1925-        if (not isinstance(le, OSError)) or le.args[0] != 2:
1926+    except EnvironmentError, le:
1927+        # Ignore "No such file or directory", collect any other exception.
1928+        if (le.args[0] != 2 and le.args[0] != 3) or (le.args[0] != errno.ENOENT):
1929             excs.append(le)
1930hunk ./src/allmydata/util/fileutil.py 243
1931+    except Exception, le:
1932+        excs.append(le)
1933 
1934     # Okay, now we've recursively removed everything, ignoring any "No
1935     # such file or directory" errors, and collecting any other errors.
1936hunk ./src/allmydata/util/fileutil.py 256
1937             raise OSError, "Failed to remove dir for unknown reason."
1938         raise OSError, excs
1939 
1940+def fp_remove(dirfp):
1941+    """
1942+    An idempotent version of shutil.rmtree().  If the dir is already gone,
1943+    do nothing and return without raising an exception.  If this call
1944+    removes the dir, return without raising an exception.  If there is an
1945+    error that prevents removal or if the directory gets created again by
1946+    someone else after this deletes it and before this checks that it is
1947+    gone, raise an exception.
1948+    """
1949+    try:
1950+        dirfp.remove()
1951+    except UnlistableError, e:
1952+        if e.originalException.errno != errno.ENOENT:
1953+            raise
1954+    except OSError, e:
1955+        if e.errno != errno.ENOENT:
1956+            raise
1957+
1958+def rm_dir(dirname):
1959+    # Renamed to be like shutil.rmtree and unlike rmdir.
1960+    return rmtree(dirname)
1961 
1962 def remove_if_possible(f):
1963     try:
1964hunk ./src/allmydata/util/fileutil.py 387
1965         import traceback
1966         traceback.print_exc()
1967 
1968-def get_disk_stats(whichdir, reserved_space=0):
1969+def get_disk_stats(whichdirfp, reserved_space=0):
1970     """Return disk statistics for the storage disk, in the form of a dict
1971     with the following fields.
1972       total:            total bytes on disk
1973hunk ./src/allmydata/util/fileutil.py 408
1974     you can pass how many bytes you would like to leave unused on this
1975     filesystem as reserved_space.
1976     """
1977+    precondition(isinstance(whichdirfp, FilePath), whichdirfp)
1978 
1979     if have_GetDiskFreeSpaceExW:
1980         # If this is a Windows system and GetDiskFreeSpaceExW is available, use it.
1981hunk ./src/allmydata/util/fileutil.py 419
1982         n_free_for_nonroot = c_ulonglong(0)
1983         n_total            = c_ulonglong(0)
1984         n_free_for_root    = c_ulonglong(0)
1985-        retval = GetDiskFreeSpaceExW(whichdir, byref(n_free_for_nonroot),
1986+        retval = GetDiskFreeSpaceExW(whichdirfp.path, byref(n_free_for_nonroot),
1987                                                byref(n_total),
1988                                                byref(n_free_for_root))
1989         if retval == 0:
1990hunk ./src/allmydata/util/fileutil.py 424
1991             raise OSError("Windows error %d attempting to get disk statistics for %r"
1992-                          % (GetLastError(), whichdir))
1993+                          % (GetLastError(), whichdirfp.path))
1994         free_for_nonroot = n_free_for_nonroot.value
1995         total            = n_total.value
1996         free_for_root    = n_free_for_root.value
1997hunk ./src/allmydata/util/fileutil.py 433
1998         # <http://docs.python.org/library/os.html#os.statvfs>
1999         # <http://opengroup.org/onlinepubs/7990989799/xsh/fstatvfs.html>
2000         # <http://opengroup.org/onlinepubs/7990989799/xsh/sysstatvfs.h.html>
2001-        s = os.statvfs(whichdir)
2002+        s = os.statvfs(whichdirfp.path)
2003 
2004         # on my mac laptop:
2005         #  statvfs(2) is a wrapper around statfs(2).
2006hunk ./src/allmydata/util/fileutil.py 460
2007              'avail': avail,
2008            }
2009 
2010-def get_available_space(whichdir, reserved_space):
2011+def get_available_space(whichdirfp, reserved_space):
2012     """Returns available space for share storage in bytes, or None if no
2013     API to get this information is available.
2014 
2015hunk ./src/allmydata/util/fileutil.py 472
2016     you can pass how many bytes you would like to leave unused on this
2017     filesystem as reserved_space.
2018     """
2019+    precondition(isinstance(whichdirfp, FilePath), whichdirfp)
2020     try:
2021hunk ./src/allmydata/util/fileutil.py 474
2022-        return get_disk_stats(whichdir, reserved_space)['avail']
2023+        return get_disk_stats(whichdirfp, reserved_space)['avail']
2024     except AttributeError:
2025         return None
2026hunk ./src/allmydata/util/fileutil.py 477
2027-    except EnvironmentError:
2028-        log.msg("OS call to get disk statistics failed")
2029-        return 0
2030}
2031[add expirer.py
2032wilcoxjg@gmail.com**20110809203519
2033 Ignore-this: b09d460593f0e0aa065e867d5159455b
2034] {
2035addfile ./src/allmydata/storage/backends/das/expirer.py
2036hunk ./src/allmydata/storage/backends/das/expirer.py 1
2037+import time, os, pickle, struct # os, pickle, and struct will almost certainly be migrated to the backend...
2038+from allmydata.storage.crawler import ShareCrawler
2039+from allmydata.storage.common import UnknownMutableContainerVersionError, \
2040+     UnknownImmutableContainerVersionError
2041+from twisted.python import log as twlog
2042+
2043+class LeaseCheckingCrawler(ShareCrawler):
2044+    """I examine the leases on all shares, determining which are still valid
2045+    and which have expired. I can remove the expired leases (if so
2046+    configured), and the share will be deleted when the last lease is
2047+    removed.
2048+
2049+    I collect statistics on the leases and make these available to a web
2050+    status page, including:
2051+
2052+    Space recovered during this cycle-so-far:
2053+     actual (only if expiration_enabled=True):
2054+      num-buckets, num-shares, sum of share sizes, real disk usage
2055+      ('real disk usage' means we use stat(fn).st_blocks*512 and include any
2056+       space used by the directory)
2057+     what it would have been with the original lease expiration time
2058+     what it would have been with our configured expiration time
2059+
2060+    Prediction of space that will be recovered during the rest of this cycle
2061+    Prediction of space that will be recovered by the entire current cycle.
2062+
2063+    Space recovered during the last 10 cycles  <-- saved in separate pickle
2064+
2065+    Shares/buckets examined:
2066+     this cycle-so-far
2067+     prediction of rest of cycle
2068+     during last 10 cycles <-- separate pickle
2069+    start/finish time of last 10 cycles  <-- separate pickle
2070+    expiration time used for last 10 cycles <-- separate pickle
2071+
2072+    Histogram of leases-per-share:
2073+     this-cycle-to-date
2074+     last 10 cycles <-- separate pickle
2075+    Histogram of lease ages, buckets = 1day
2076+     cycle-to-date
2077+     last 10 cycles <-- separate pickle
2078+
2079+    All cycle-to-date values remain valid until the start of the next cycle.
2080+
2081+    """
2082+
2083+    slow_start = 360 # wait 6 minutes after startup
2084+    minimum_cycle_time = 12*60*60 # not more than twice per day
2085+
2086+    def __init__(self, statefile, historyfp, expiration_policy):
2087+        self.historyfp = historyfp
2088+        self.expiration_enabled = expiration_policy['enabled']
2089+        self.mode = expiration_policy['mode']
2090+        self.override_lease_duration = None
2091+        self.cutoff_date = None
2092+        if self.mode == "age":
2093+            assert isinstance(expiration_policy['override_lease_duration'], (int, type(None)))
2094+            self.override_lease_duration = expiration_policy['override_lease_duration']# seconds
2095+        elif self.mode == "cutoff-date":
2096+            assert isinstance(expiration_policy['cutoff_date'], int) # seconds-since-epoch
2097+            assert cutoff_date is not None
2098+            self.cutoff_date = expiration_policy['cutoff_date']
2099+        else:
2100+            raise ValueError("GC mode '%s' must be 'age' or 'cutoff-date'" % expiration_policy['mode'])
2101+        self.sharetypes_to_expire = expiration_policy['sharetypes']
2102+        ShareCrawler.__init__(self, statefile)
2103+
2104+    def add_initial_state(self):
2105+        # we fill ["cycle-to-date"] here (even though they will be reset in
2106+        # self.started_cycle) just in case someone grabs our state before we
2107+        # get started: unit tests do this
2108+        so_far = self.create_empty_cycle_dict()
2109+        self.state.setdefault("cycle-to-date", so_far)
2110+        # in case we upgrade the code while a cycle is in progress, update
2111+        # the keys individually
2112+        for k in so_far:
2113+            self.state["cycle-to-date"].setdefault(k, so_far[k])
2114+
2115+        # initialize history
2116+        if not self.historyfp.exists():
2117+            history = {} # cyclenum -> dict
2118+            self.historyfp.setContent(pickle.dumps(history))
2119+
2120+    def create_empty_cycle_dict(self):
2121+        recovered = self.create_empty_recovered_dict()
2122+        so_far = {"corrupt-shares": [],
2123+                  "space-recovered": recovered,
2124+                  "lease-age-histogram": {}, # (minage,maxage)->count
2125+                  "leases-per-share-histogram": {}, # leasecount->numshares
2126+                  }
2127+        return so_far
2128+
2129+    def create_empty_recovered_dict(self):
2130+        recovered = {}
2131+        for a in ("actual", "original", "configured", "examined"):
2132+            for b in ("buckets", "shares", "sharebytes", "diskbytes"):
2133+                recovered[a+"-"+b] = 0
2134+                recovered[a+"-"+b+"-mutable"] = 0
2135+                recovered[a+"-"+b+"-immutable"] = 0
2136+        return recovered
2137+
2138+    def started_cycle(self, cycle):
2139+        self.state["cycle-to-date"] = self.create_empty_cycle_dict()
2140+
2141+    def stat(self, fn):
2142+        return os.stat(fn)
2143+
2144+    def process_bucket(self, cycle, prefix, prefixdir, storage_index_b32):
2145+        bucketdir = os.path.join(prefixdir, storage_index_b32)
2146+        s = self.stat(bucketdir)
2147+        would_keep_shares = []
2148+        wks = None
2149+
2150+        for fn in os.listdir(bucketdir):
2151+            try:
2152+                shnum = int(fn)
2153+            except ValueError:
2154+                continue # non-numeric means not a sharefile
2155+            sharefile = os.path.join(bucketdir, fn)
2156+            try:
2157+                wks = self.process_share(sharefile)
2158+            except (UnknownMutableContainerVersionError,
2159+                    UnknownImmutableContainerVersionError,
2160+                    struct.error):
2161+                twlog.msg("lease-checker error processing %s" % sharefile)
2162+                twlog.err()
2163+                which = (storage_index_b32, shnum)
2164+                self.state["cycle-to-date"]["corrupt-shares"].append(which)
2165+                wks = (1, 1, 1, "unknown")
2166+            would_keep_shares.append(wks)
2167+
2168+        sharetype = None
2169+        if wks:
2170+            # use the last share's sharetype as the buckettype
2171+            sharetype = wks[3]
2172+        rec = self.state["cycle-to-date"]["space-recovered"]
2173+        self.increment(rec, "examined-buckets", 1)
2174+        if sharetype:
2175+            self.increment(rec, "examined-buckets-"+sharetype, 1)
2176+
2177+        try:
2178+            bucket_diskbytes = s.st_blocks * 512
2179+        except AttributeError:
2180+            bucket_diskbytes = 0 # no stat().st_blocks on windows
2181+        if sum([wks[0] for wks in would_keep_shares]) == 0:
2182+            self.increment_bucketspace("original", bucket_diskbytes, sharetype)
2183+        if sum([wks[1] for wks in would_keep_shares]) == 0:
2184+            self.increment_bucketspace("configured", bucket_diskbytes, sharetype)
2185+        if sum([wks[2] for wks in would_keep_shares]) == 0:
2186+            self.increment_bucketspace("actual", bucket_diskbytes, sharetype)
2187+
2188+    def process_share(self, sharefilename):
2189+        # first, find out what kind of a share it is
2190+        f = open(sharefilename, "rb")
2191+        prefix = f.read(32)
2192+        f.close()
2193+        if prefix == MutableShareFile.MAGIC:
2194+            sf = MutableShareFile(sharefilename)
2195+        else:
2196+            # otherwise assume it's immutable
2197+            sf = FSBShare(sharefilename)
2198+        sharetype = sf.sharetype
2199+        now = time.time()
2200+        s = self.stat(sharefilename)
2201+
2202+        num_leases = 0
2203+        num_valid_leases_original = 0
2204+        num_valid_leases_configured = 0
2205+        expired_leases_configured = []
2206+
2207+        for li in sf.get_leases():
2208+            num_leases += 1
2209+            original_expiration_time = li.get_expiration_time()
2210+            grant_renew_time = li.get_grant_renew_time_time()
2211+            age = li.get_age()
2212+            self.add_lease_age_to_histogram(age)
2213+
2214+            #  expired-or-not according to original expiration time
2215+            if original_expiration_time > now:
2216+                num_valid_leases_original += 1
2217+
2218+            #  expired-or-not according to our configured age limit
2219+            expired = False
2220+            if self.mode == "age":
2221+                age_limit = original_expiration_time
2222+                if self.override_lease_duration is not None:
2223+                    age_limit = self.override_lease_duration
2224+                if age > age_limit:
2225+                    expired = True
2226+            else:
2227+                assert self.mode == "cutoff-date"
2228+                if grant_renew_time < self.cutoff_date:
2229+                    expired = True
2230+            if sharetype not in self.sharetypes_to_expire:
2231+                expired = False
2232+
2233+            if expired:
2234+                expired_leases_configured.append(li)
2235+            else:
2236+                num_valid_leases_configured += 1
2237+
2238+        so_far = self.state["cycle-to-date"]
2239+        self.increment(so_far["leases-per-share-histogram"], num_leases, 1)
2240+        self.increment_space("examined", s, sharetype)
2241+
2242+        would_keep_share = [1, 1, 1, sharetype]
2243+
2244+        if self.expiration_enabled:
2245+            for li in expired_leases_configured:
2246+                sf.cancel_lease(li.cancel_secret)
2247+
2248+        if num_valid_leases_original == 0:
2249+            would_keep_share[0] = 0
2250+            self.increment_space("original", s, sharetype)
2251+
2252+        if num_valid_leases_configured == 0:
2253+            would_keep_share[1] = 0
2254+            self.increment_space("configured", s, sharetype)
2255+            if self.expiration_enabled:
2256+                would_keep_share[2] = 0
2257+                self.increment_space("actual", s, sharetype)
2258+
2259+        return would_keep_share
2260+
2261+    def increment_space(self, a, s, sharetype):
2262+        sharebytes = s.st_size
2263+        try:
2264+            # note that stat(2) says that st_blocks is 512 bytes, and that
2265+            # st_blksize is "optimal file sys I/O ops blocksize", which is
2266+            # independent of the block-size that st_blocks uses.
2267+            diskbytes = s.st_blocks * 512
2268+        except AttributeError:
2269+            # the docs say that st_blocks is only on linux. I also see it on
2270+            # MacOS. But it isn't available on windows.
2271+            diskbytes = sharebytes
2272+        so_far_sr = self.state["cycle-to-date"]["space-recovered"]
2273+        self.increment(so_far_sr, a+"-shares", 1)
2274+        self.increment(so_far_sr, a+"-sharebytes", sharebytes)
2275+        self.increment(so_far_sr, a+"-diskbytes", diskbytes)
2276+        if sharetype:
2277+            self.increment(so_far_sr, a+"-shares-"+sharetype, 1)
2278+            self.increment(so_far_sr, a+"-sharebytes-"+sharetype, sharebytes)
2279+            self.increment(so_far_sr, a+"-diskbytes-"+sharetype, diskbytes)
2280+
2281+    def increment_bucketspace(self, a, bucket_diskbytes, sharetype):
2282+        rec = self.state["cycle-to-date"]["space-recovered"]
2283+        self.increment(rec, a+"-diskbytes", bucket_diskbytes)
2284+        self.increment(rec, a+"-buckets", 1)
2285+        if sharetype:
2286+            self.increment(rec, a+"-diskbytes-"+sharetype, bucket_diskbytes)
2287+            self.increment(rec, a+"-buckets-"+sharetype, 1)
2288+
2289+    def increment(self, d, k, delta=1):
2290+        if k not in d:
2291+            d[k] = 0
2292+        d[k] += delta
2293+
2294+    def add_lease_age_to_histogram(self, age):
2295+        bucket_interval = 24*60*60
2296+        bucket_number = int(age/bucket_interval)
2297+        bucket_start = bucket_number * bucket_interval
2298+        bucket_end = bucket_start + bucket_interval
2299+        k = (bucket_start, bucket_end)
2300+        self.increment(self.state["cycle-to-date"]["lease-age-histogram"], k, 1)
2301+
2302+    def convert_lease_age_histogram(self, lah):
2303+        # convert { (minage,maxage) : count } into [ (minage,maxage,count) ]
2304+        # since the former is not JSON-safe (JSON dictionaries must have
2305+        # string keys).
2306+        json_safe_lah = []
2307+        for k in sorted(lah):
2308+            (minage,maxage) = k
2309+            json_safe_lah.append( (minage, maxage, lah[k]) )
2310+        return json_safe_lah
2311+
2312+    def finished_cycle(self, cycle):
2313+        # add to our history state, prune old history
2314+        h = {}
2315+
2316+        start = self.state["current-cycle-start-time"]
2317+        now = time.time()
2318+        h["cycle-start-finish-times"] = (start, now)
2319+        h["expiration-enabled"] = self.expiration_enabled
2320+        h["configured-expiration-mode"] = (self.mode,
2321+                                           self.override_lease_duration,
2322+                                           self.cutoff_date,
2323+                                           self.sharetypes_to_expire)
2324+
2325+        s = self.state["cycle-to-date"]
2326+
2327+        # state["lease-age-histogram"] is a dictionary (mapping
2328+        # (minage,maxage) tuple to a sharecount), but we report
2329+        # self.get_state()["lease-age-histogram"] as a list of
2330+        # (min,max,sharecount) tuples, because JSON can handle that better.
2331+        # We record the list-of-tuples form into the history for the same
2332+        # reason.
2333+        lah = self.convert_lease_age_histogram(s["lease-age-histogram"])
2334+        h["lease-age-histogram"] = lah
2335+        h["leases-per-share-histogram"] = s["leases-per-share-histogram"].copy()
2336+        h["corrupt-shares"] = s["corrupt-shares"][:]
2337+        # note: if ["shares-recovered"] ever acquires an internal dict, this
2338+        # copy() needs to become a deepcopy
2339+        h["space-recovered"] = s["space-recovered"].copy()
2340+
2341+        history = pickle.load(self.historyfp.getContent())
2342+        history[cycle] = h
2343+        while len(history) > 10:
2344+            oldcycles = sorted(history.keys())
2345+            del history[oldcycles[0]]
2346+        self.historyfp.setContent(pickle.dumps(history))
2347+
2348+    def get_state(self):
2349+        """In addition to the crawler state described in
2350+        ShareCrawler.get_state(), I return the following keys which are
2351+        specific to the lease-checker/expirer. Note that the non-history keys
2352+        (with 'cycle' in their names) are only present if a cycle is
2353+        currently running. If the crawler is between cycles, it appropriate
2354+        to show the latest item in the 'history' key instead. Also note that
2355+        each history item has all the data in the 'cycle-to-date' value, plus
2356+        cycle-start-finish-times.
2357+
2358+         cycle-to-date:
2359+          expiration-enabled
2360+          configured-expiration-mode
2361+          lease-age-histogram (list of (minage,maxage,sharecount) tuples)
2362+          leases-per-share-histogram
2363+          corrupt-shares (list of (si_b32,shnum) tuples, minimal verification)
2364+          space-recovered
2365+
2366+         estimated-remaining-cycle:
2367+          # Values may be None if not enough data has been gathered to
2368+          # produce an estimate.
2369+          space-recovered
2370+
2371+         estimated-current-cycle:
2372+          # cycle-to-date plus estimated-remaining. Values may be None if
2373+          # not enough data has been gathered to produce an estimate.
2374+          space-recovered
2375+
2376+         history: maps cyclenum to a dict with the following keys:
2377+          cycle-start-finish-times
2378+          expiration-enabled
2379+          configured-expiration-mode
2380+          lease-age-histogram
2381+          leases-per-share-histogram
2382+          corrupt-shares
2383+          space-recovered
2384+
2385+         The 'space-recovered' structure is a dictionary with the following
2386+         keys:
2387+          # 'examined' is what was looked at
2388+          examined-buckets, examined-buckets-mutable, examined-buckets-immutable
2389+          examined-shares, -mutable, -immutable
2390+          examined-sharebytes, -mutable, -immutable
2391+          examined-diskbytes, -mutable, -immutable
2392+
2393+          # 'actual' is what was actually deleted
2394+          actual-buckets, -mutable, -immutable
2395+          actual-shares, -mutable, -immutable
2396+          actual-sharebytes, -mutable, -immutable
2397+          actual-diskbytes, -mutable, -immutable
2398+
2399+          # would have been deleted, if the original lease timer was used
2400+          original-buckets, -mutable, -immutable
2401+          original-shares, -mutable, -immutable
2402+          original-sharebytes, -mutable, -immutable
2403+          original-diskbytes, -mutable, -immutable
2404+
2405+          # would have been deleted, if our configured max_age was used
2406+          configured-buckets, -mutable, -immutable
2407+          configured-shares, -mutable, -immutable
2408+          configured-sharebytes, -mutable, -immutable
2409+          configured-diskbytes, -mutable, -immutable
2410+
2411+        """
2412+        progress = self.get_progress()
2413+
2414+        state = ShareCrawler.get_state(self) # does a shallow copy
2415+        history = pickle.load(self.historyfp.getContent())
2416+        state["history"] = history
2417+
2418+        if not progress["cycle-in-progress"]:
2419+            del state["cycle-to-date"]
2420+            return state
2421+
2422+        so_far = state["cycle-to-date"].copy()
2423+        state["cycle-to-date"] = so_far
2424+
2425+        lah = so_far["lease-age-histogram"]
2426+        so_far["lease-age-histogram"] = self.convert_lease_age_histogram(lah)
2427+        so_far["expiration-enabled"] = self.expiration_enabled
2428+        so_far["configured-expiration-mode"] = (self.mode,
2429+                                                self.override_lease_duration,
2430+                                                self.cutoff_date,
2431+                                                self.sharetypes_to_expire)
2432+
2433+        so_far_sr = so_far["space-recovered"]
2434+        remaining_sr = {}
2435+        remaining = {"space-recovered": remaining_sr}
2436+        cycle_sr = {}
2437+        cycle = {"space-recovered": cycle_sr}
2438+
2439+        if progress["cycle-complete-percentage"] > 0.0:
2440+            pc = progress["cycle-complete-percentage"] / 100.0
2441+            m = (1-pc)/pc
2442+            for a in ("actual", "original", "configured", "examined"):
2443+                for b in ("buckets", "shares", "sharebytes", "diskbytes"):
2444+                    for c in ("", "-mutable", "-immutable"):
2445+                        k = a+"-"+b+c
2446+                        remaining_sr[k] = m * so_far_sr[k]
2447+                        cycle_sr[k] = so_far_sr[k] + remaining_sr[k]
2448+        else:
2449+            for a in ("actual", "original", "configured", "examined"):
2450+                for b in ("buckets", "shares", "sharebytes", "diskbytes"):
2451+                    for c in ("", "-mutable", "-immutable"):
2452+                        k = a+"-"+b+c
2453+                        remaining_sr[k] = None
2454+                        cycle_sr[k] = None
2455+
2456+        state["estimated-remaining-cycle"] = remaining
2457+        state["estimated-current-cycle"] = cycle
2458+        return state
2459}
2460[Changes I have made that aren't necessary for the test_backends.py suite to pass.
2461wilcoxjg@gmail.com**20110809203811
2462 Ignore-this: 117d49047456013f382ffc0559f00c40
2463] {
2464hunk ./src/allmydata/storage/crawler.py 1
2465-
2466 import os, time, struct
2467 import cPickle as pickle
2468 from twisted.internet import reactor
2469hunk ./src/allmydata/storage/crawler.py 6
2470 from twisted.application import service
2471 from allmydata.storage.common import si_b2a
2472-from allmydata.util import fileutil
2473 
2474 class TimeSliceExceeded(Exception):
2475     pass
2476hunk ./src/allmydata/storage/crawler.py 11
2477 
2478 class ShareCrawler(service.MultiService):
2479-    """A ShareCrawler subclass is attached to a StorageServer, and
2480+    """A subclass of ShareCrawler is attached to a StorageServer, and
2481     periodically walks all of its shares, processing each one in some
2482     fashion. This crawl is rate-limited, to reduce the IO burden on the host,
2483     since large servers can easily have a terabyte of shares, in several
2484hunk ./src/allmydata/storage/crawler.py 29
2485     We assume that the normal upload/download/get_buckets traffic of a tahoe
2486     grid will cause the prefixdir contents to be mostly cached in the kernel,
2487     or that the number of buckets in each prefixdir will be small enough to
2488-    load quickly. A 1TB allmydata.com server was measured to have 2.56M
2489+    load quickly. A 1TB allmydata.com server was measured to have 2.56 * 10^6
2490     buckets, spread into the 1024 prefixdirs, with about 2500 buckets per
2491     prefix. On this server, each prefixdir took 130ms-200ms to list the first
2492     time, and 17ms to list the second time.
2493hunk ./src/allmydata/storage/crawler.py 66
2494     cpu_slice = 1.0 # use up to 1.0 seconds before yielding
2495     minimum_cycle_time = 300 # don't run a cycle faster than this
2496 
2497-    def __init__(self, server, statefile, allowed_cpu_percentage=None):
2498+    def __init__(self, statefp, allowed_cpu_percentage=None):
2499         service.MultiService.__init__(self)
2500         if allowed_cpu_percentage is not None:
2501             self.allowed_cpu_percentage = allowed_cpu_percentage
2502hunk ./src/allmydata/storage/crawler.py 70
2503-        self.server = server
2504-        self.sharedir = server.sharedir
2505-        self.statefile = statefile
2506+        self.statefp = statefp
2507         self.prefixes = [si_b2a(struct.pack(">H", i << (16-10)))[:2]
2508                          for i in range(2**10)]
2509         self.prefixes.sort()
2510hunk ./src/allmydata/storage/crawler.py 190
2511         #                            of the last bucket to be processed, or
2512         #                            None if we are sleeping between cycles
2513         try:
2514-            f = open(self.statefile, "rb")
2515-            state = pickle.load(f)
2516-            f.close()
2517+            state = pickle.loads(self.statefp.getContent())
2518         except EnvironmentError:
2519             state = {"version": 1,
2520                      "last-cycle-finished": None,
2521hunk ./src/allmydata/storage/crawler.py 226
2522         else:
2523             last_complete_prefix = self.prefixes[lcpi]
2524         self.state["last-complete-prefix"] = last_complete_prefix
2525-        tmpfile = self.statefile + ".tmp"
2526-        f = open(tmpfile, "wb")
2527-        pickle.dump(self.state, f)
2528-        f.close()
2529-        fileutil.move_into_place(tmpfile, self.statefile)
2530+        self.statefp.setContent(pickle.dumps(self.state))
2531 
2532     def startService(self):
2533         # arrange things to look like we were just sleeping, so
2534hunk ./src/allmydata/storage/crawler.py 438
2535 
2536     minimum_cycle_time = 60*60 # we don't need this more than once an hour
2537 
2538-    def __init__(self, server, statefile, num_sample_prefixes=1):
2539-        ShareCrawler.__init__(self, server, statefile)
2540+    def __init__(self, statefp, num_sample_prefixes=1):
2541+        ShareCrawler.__init__(self, statefp)
2542         self.num_sample_prefixes = num_sample_prefixes
2543 
2544     def add_initial_state(self):
2545hunk ./src/allmydata/storage/crawler.py 478
2546             old_cycle,buckets = self.state["storage-index-samples"][prefix]
2547             if old_cycle != cycle:
2548                 del self.state["storage-index-samples"][prefix]
2549-
2550hunk ./src/allmydata/storage/lease.py 17
2551 
2552     def get_expiration_time(self):
2553         return self.expiration_time
2554+
2555     def get_grant_renew_time_time(self):
2556         # hack, based upon fixed 31day expiration period
2557         return self.expiration_time - 31*24*60*60
2558hunk ./src/allmydata/storage/lease.py 21
2559+
2560     def get_age(self):
2561         return time.time() - self.get_grant_renew_time_time()
2562 
2563hunk ./src/allmydata/storage/lease.py 32
2564          self.expiration_time) = struct.unpack(">L32s32sL", data)
2565         self.nodeid = None
2566         return self
2567+
2568     def to_immutable_data(self):
2569         return struct.pack(">L32s32sL",
2570                            self.owner_num,
2571hunk ./src/allmydata/storage/lease.py 45
2572                            int(self.expiration_time),
2573                            self.renew_secret, self.cancel_secret,
2574                            self.nodeid)
2575+
2576     def from_mutable_data(self, data):
2577         (self.owner_num,
2578          self.expiration_time,
2579}
2580[add __init__.py to backend and core and null
2581wilcoxjg@gmail.com**20110810033751
2582 Ignore-this: 1c72bc54951033ab433c38de58bdc39c
2583] {
2584addfile ./src/allmydata/storage/backends/__init__.py
2585addfile ./src/allmydata/storage/backends/null/__init__.py
2586}
2587[whitespace-cleanup
2588wilcoxjg@gmail.com**20110810170847
2589 Ignore-this: 7a278e7c87c6fcd2e5ed783667c8b746
2590] {
2591hunk ./src/allmydata/interfaces.py 1
2592-
2593 from zope.interface import Interface
2594 from foolscap.api import StringConstraint, ListOf, TupleOf, SetOf, DictOf, \
2595      ChoiceOf, IntegerConstraint, Any, RemoteInterface, Referenceable
2596hunk ./src/allmydata/storage/backends/das/core.py 47
2597         self._setup_lease_checkerf(expiration_policy)
2598 
2599     def _setup_storage(self, storedir, readonly, reserved_space):
2600-        precondition(isinstance(storedir, FilePath), storedir, FilePath) 
2601+        precondition(isinstance(storedir, FilePath), storedir, FilePath)
2602         self.storedir = storedir
2603         self.readonly = readonly
2604         self.reserved_space = int(reserved_space)
2605hunk ./src/allmydata/storage/backends/das/core.py 89
2606         except UnlistableError:
2607             # There is no shares directory at all.
2608             return frozenset()
2609-           
2610+
2611     def get_shares(self, storageindex):
2612         """ Generate ImmutableShare objects for shares we have for this
2613         storageindex. ("Shares we have" means completed ones, excluding
2614hunk ./src/allmydata/storage/backends/das/core.py 104
2615         except UnlistableError:
2616             # There is no shares directory at all.
2617             pass
2618-       
2619+
2620     def get_available_space(self):
2621         if self.readonly:
2622             return 0
2623hunk ./src/allmydata/storage/backends/das/core.py 122
2624 
2625     def set_storage_server(self, ss):
2626         self.ss = ss
2627-       
2628+
2629 
2630 # each share file (in storage/shares/$SI/$SHNUM) contains lease information
2631 # and share data. The share data is accessed by RIBucketWriter.write and
2632hunk ./src/allmydata/storage/backends/das/core.py 223
2633             # above-mentioned conditions.
2634             pass
2635         pass
2636-       
2637+
2638     def stat(self):
2639         return filepath.stat(self.finalhome.path)[stat.ST_SIZE]
2640 
2641hunk ./src/allmydata/storage/backends/das/core.py 309
2642         num_leases = self._read_num_leases(self.incominghome)
2643         self._write_lease_record(self.incominghome, num_leases, lease_info)
2644         self._write_num_leases(self.incominghome, num_leases+1)
2645-       
2646+
2647     def renew_lease(self, renew_secret, new_expire_time):
2648         for i,lease in enumerate(self.get_leases()):
2649             if constant_time_compare(lease.renew_secret, renew_secret):
2650hunk ./src/allmydata/storage/common.py 1
2651-
2652 import os.path
2653 from allmydata.util import base32
2654 
2655hunk ./src/allmydata/storage/server.py 149
2656 
2657         if self.readonly_storage:
2658             return 0
2659-        return fileutil.get_available_space(self.storedir, self.reserved_space)
2660+        return fileutil.get_available_space(self.sharedir, self.reserved_space)
2661 
2662     def allocated_size(self):
2663         space = 0
2664hunk ./src/allmydata/storage/server.py 346
2665         self.count("writev")
2666         si_s = si_b2a(storageindex)
2667         log.msg("storage: slot_writev %s" % si_s)
2668-       
2669+
2670         (write_enabler, renew_secret, cancel_secret) = secrets
2671         # shares exist if there is a file for them
2672         bucketdir = si_si2dir(self.sharedir, storageindex)
2673}
2674[das/__init__.py
2675wilcoxjg@gmail.com**20110810173849
2676 Ignore-this: bdb730cba1d53d8827ef5fef65958471
2677] addfile ./src/allmydata/storage/backends/das/__init__.py
2678[test_backends.py: cleaned whitespace and removed unused variables
2679wilcoxjg@gmail.com**20110810201041
2680 Ignore-this: d000d4a7d3a0793464306e9d09437be6
2681] {
2682hunk ./src/allmydata/test/test_backends.py 13
2683 from allmydata.storage.common import si_si2dir
2684 # The following share file content was generated with
2685 # storage.immutable.ShareFile from Tahoe-LAFS v1.8.2
2686-# with share data == 'a'. The total size of this input
2687+# with share data == 'a'. The total size of this input
2688 # is 85 bytes.
2689 shareversionnumber = '\x00\x00\x00\x01'
2690 sharedatalength = '\x00\x00\x00\x01'
2691hunk ./src/allmydata/test/test_backends.py 29
2692     cancelsecret + expirationtime + nextlease
2693 share_data = containerdata + client_data
2694 testnodeid = 'testnodeidxxxxxxxxxx'
2695-expiration_policy = {'enabled' : False,
2696+expiration_policy = {'enabled' : False,
2697                      'mode' : 'age',
2698                      'override_lease_duration' : None,
2699                      'cutoff_date' : None,
2700hunk ./src/allmydata/test/test_backends.py 37
2701 
2702 
2703 class MockFileSystem(unittest.TestCase):
2704-    """ I simulate a filesystem that the code under test can use. I simulate
2705-    just the parts of the filesystem that the current implementation of DAS
2706+    """ I simulate a filesystem that the code under test can use. I simulate
2707+    just the parts of the filesystem that the current implementation of DAS
2708     backend needs. """
2709     def setUp(self):
2710         # Make patcher, patch, and make effects for fs using functions.
2711hunk ./src/allmydata/test/test_backends.py 43
2712         msg( "%s.setUp()" % (self,))
2713-        self.mockedfilepaths = {}
2714+        self.mockedfilepaths = {}
2715         #keys are pathnames, values are MockFilePath objects. This is necessary because
2716         #MockFilePath behavior sometimes depends on the filesystem. Where it does,
2717         #self.mockedfilepaths has the relevent info.
2718hunk ./src/allmydata/test/test_backends.py 56
2719         self.sharefinalname = self.sharedirfinalname.child('0')
2720 
2721         self.FilePathFake = mock.patch('allmydata.storage.backends.das.core.FilePath', new = MockFilePath )
2722-        FakePath = self.FilePathFake.__enter__()
2723+        self.FilePathFake.__enter__()
2724 
2725         self.BCountingCrawler = mock.patch('allmydata.storage.backends.das.core.BucketCountingCrawler')
2726         FakeBCC = self.BCountingCrawler.__enter__()
2727hunk ./src/allmydata/test/test_backends.py 89
2728 
2729     def tearDown(self):
2730         msg( "%s.tearDown()" % (self,))
2731-        FakePath = self.FilePathFake.__exit__()       
2732+        self.FilePathFake.__exit__()
2733         self.mockedfilepaths = {}
2734 
2735 
2736hunk ./src/allmydata/test/test_backends.py 116
2737         self.mockedfilepaths[self.path].fileobject = self.fileobject
2738         self.mockedfilepaths[self.path].existance = self.existance
2739         self.setparents()
2740-       
2741+
2742     def create(self):
2743         # This method chokes if there's a pre-existing file!
2744         if self.mockedfilepaths[self.path].fileobject:
2745hunk ./src/allmydata/test/test_backends.py 122
2746             raise OSError
2747         else:
2748-            self.fileobject = MockFileObject(contentstring)
2749             self.existance = True
2750             self.mockedfilepaths[self.path].fileobject = self.fileobject
2751             self.mockedfilepaths[self.path].existance = self.existance
2752hunk ./src/allmydata/test/test_backends.py 125
2753-            self.setparents()       
2754+            self.setparents()
2755 
2756     def open(self, mode='r'):
2757         # XXX Makes no use of mode.
2758hunk ./src/allmydata/test/test_backends.py 151
2759         childrenfromffs = [ffp for ffp in childrenfromffs if not ffp.path.endswith(self.path)]
2760         childrenfromffs = [ffp for ffp in childrenfromffs if ffp.exists()]
2761         self.spawn = frozenset(childrenfromffs)
2762-        return self.spawn 
2763+        return self.spawn
2764 
2765     def parent(self):
2766         if self.mockedfilepaths.has_key(self.antecedent):
2767hunk ./src/allmydata/test/test_backends.py 163
2768     def parents(self):
2769         antecedents = []
2770         def f(fps, antecedents):
2771-            newfps = os.path.split(fps)[0]
2772+            newfps = os.path.split(fps)[0]
2773             if newfps:
2774                 antecedents.append(newfps)
2775                 f(newfps, antecedents)
2776hunk ./src/allmydata/test/test_backends.py 256
2777     @mock.patch('os.listdir')
2778     @mock.patch('os.path.isdir')
2779     def test_write_share(self, mockisdir, mocklistdir, mockopen, mockmkdir):
2780-        """ Write a new share. """
2781+        """ Write a new share. This tests that StorageServer's remote_allocate_buckets generates the correct return types when given test-vector arguments.  that bs is of the correct type is verified by bs[0] exercising remote_write without error. """
2782 
2783         alreadygot, bs = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
2784         bs[0].remote_write(0, 'a')
2785hunk ./src/allmydata/test/test_backends.py 275
2786 
2787 
2788 class TestServerAndFSBackend(MockFileSystem, ReallyEqualMixin):
2789-    """ This tests both the StorageServer and the DAS backend together. """   
2790+    """ This tests both the StorageServer and the DAS backend together. """
2791     def setUp(self):
2792         MockFileSystem.setUp(self)
2793         try:
2794hunk ./src/allmydata/test/test_backends.py 292
2795     @mock.patch('allmydata.util.fileutil.get_available_space')
2796     def test_out_of_space(self, mockget_available_space, mocktime):
2797         mocktime.return_value = 0
2798-       
2799+
2800         def call_get_available_space(dir, reserve):
2801             return 0
2802 
2803hunk ./src/allmydata/test/test_backends.py 310
2804         mocktime.return_value = 0
2805         # Inspect incoming and fail unless it's empty.
2806         incomingset = self.ss.backend.get_incoming_shnums('teststorage_index')
2807-       
2808+
2809         self.failUnlessReallyEqual(incomingset, frozenset())
2810hunk ./src/allmydata/test/test_backends.py 312
2811-       
2812+
2813         # Populate incoming with the sharenum: 0.
2814         alreadygot, bs = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, frozenset((0,)), 1, mock.Mock())
2815 
2816hunk ./src/allmydata/test/test_backends.py 329
2817         # has been called.
2818         self.failIf(bsa)
2819 
2820-        # Test allocated size.
2821+        # Test allocated size.
2822         spaceint = self.ss.allocated_size()
2823         self.failUnlessReallyEqual(spaceint, 1)
2824 
2825hunk ./src/allmydata/test/test_backends.py 335
2826         # Write 'a' to shnum 0. Only tested together with close and read.
2827         bs[0].remote_write(0, 'a')
2828-       
2829+
2830         # Preclose: Inspect final, failUnless nothing there.
2831         self.failUnlessReallyEqual(len(list(self.backend.get_shares('teststorage_index'))), 0)
2832         bs[0].remote_close()
2833hunk ./src/allmydata/test/test_backends.py 349
2834         # Exercise the case that the share we're asking to allocate is
2835         # already (completely) uploaded.
2836         self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
2837-       
2838+
2839 
2840     def test_read_old_share(self):
2841         """ This tests whether the code correctly finds and reads
2842hunk ./src/allmydata/test/test_backends.py 360
2843         StorageServer object. """
2844         # Contruct a file with the appropriate contents in the mockfilesystem.
2845         datalen = len(share_data)
2846-        finalhome = si_si2dir(self.basedir, 'teststorage_index').child(str(0))
2847+        finalhome = si_si2dir(self.basedir, 'teststorage_index').child(str(0))
2848         finalhome.setContent(share_data)
2849 
2850         # Now begin the test.
2851}
2852[test_backends.py, backends/das -> backends/disk: renaming backend das to disk
2853wilcoxjg@gmail.com**20110829184834
2854 Ignore-this: c65f84cceb14e6001c6f6b1ddc9b508d
2855] {
2856move ./src/allmydata/storage/backends/das ./src/allmydata/storage/backends/disk
2857hunk ./src/allmydata/storage/backends/disk/core.py 22
2858 from allmydata.storage.immutable import BucketWriter, BucketReader
2859 from allmydata.storage.crawler import BucketCountingCrawler
2860 from allmydata.util.hashutil import constant_time_compare
2861-from allmydata.storage.backends.das.expirer import LeaseCheckingCrawler
2862+from allmydata.storage.backends.disk.expirer import LeaseCheckingCrawler
2863 _pyflakes_hush = [si_b2a, si_a2b, si_si2dir] # re-exported
2864 
2865 # storage/
2866hunk ./src/allmydata/storage/backends/disk/core.py 37
2867 # $SHARENUM matches this regex:
2868 NUM_RE=re.compile("^[0-9]+$")
2869 
2870-class DASCore(Backend):
2871+class DiskCore(Backend):
2872     implements(IStorageBackend)
2873     def __init__(self, storedir, expiration_policy, readonly=False, reserved_space=0):
2874         Backend.__init__(self)
2875hunk ./src/allmydata/test/test_backends.py 8
2876 import mock
2877 # This is the code that we're going to be testing.
2878 from allmydata.storage.server import StorageServer
2879-from allmydata.storage.backends.das.core import DASCore
2880+from allmydata.storage.backends.disk.core import DiskCore
2881 from allmydata.storage.backends.null.core import NullCore
2882 from allmydata.storage.common import si_si2dir
2883 # The following share file content was generated with
2884hunk ./src/allmydata/test/test_backends.py 38
2885 
2886 class MockFileSystem(unittest.TestCase):
2887     """ I simulate a filesystem that the code under test can use. I simulate
2888-    just the parts of the filesystem that the current implementation of DAS
2889+    just the parts of the filesystem that the current implementation of Disk
2890     backend needs. """
2891     def setUp(self):
2892         # Make patcher, patch, and make effects for fs using functions.
2893hunk ./src/allmydata/test/test_backends.py 55
2894         self.shareincomingname = self.sharedirincomingname.child('0')
2895         self.sharefinalname = self.sharedirfinalname.child('0')
2896 
2897-        self.FilePathFake = mock.patch('allmydata.storage.backends.das.core.FilePath', new = MockFilePath )
2898+        self.FilePathFake = mock.patch('allmydata.storage.backends.disk.core.FilePath', new = MockFilePath )
2899         self.FilePathFake.__enter__()
2900 
2901hunk ./src/allmydata/test/test_backends.py 58
2902-        self.BCountingCrawler = mock.patch('allmydata.storage.backends.das.core.BucketCountingCrawler')
2903+        self.BCountingCrawler = mock.patch('allmydata.storage.backends.disk.core.BucketCountingCrawler')
2904         FakeBCC = self.BCountingCrawler.__enter__()
2905         FakeBCC.side_effect = self.call_FakeBCC
2906 
2907hunk ./src/allmydata/test/test_backends.py 62
2908-        self.LeaseCheckingCrawler = mock.patch('allmydata.storage.backends.das.core.LeaseCheckingCrawler')
2909+        self.LeaseCheckingCrawler = mock.patch('allmydata.storage.backends.disk.core.LeaseCheckingCrawler')
2910         FakeLCC = self.LeaseCheckingCrawler.__enter__()
2911         FakeLCC.side_effect = self.call_FakeLCC
2912 
2913hunk ./src/allmydata/test/test_backends.py 70
2914         GetSpace = self.get_available_space.__enter__()
2915         GetSpace.side_effect = self.call_get_available_space
2916 
2917-        self.statforsize = mock.patch('allmydata.storage.backends.das.core.filepath.stat')
2918+        self.statforsize = mock.patch('allmydata.storage.backends.disk.core.filepath.stat')
2919         getsize = self.statforsize.__enter__()
2920         getsize.side_effect = self.call_statforsize
2921 
2922hunk ./src/allmydata/test/test_backends.py 271
2923         """ This tests whether a server instance can be constructed with a
2924         filesystem backend. To pass the test, it mustn't use the filesystem
2925         outside of its configured storedir. """
2926-        StorageServer(testnodeid, backend=DASCore(self.storedir, expiration_policy))
2927+        StorageServer(testnodeid, backend=DiskCore(self.storedir, expiration_policy))
2928 
2929 
2930 class TestServerAndFSBackend(MockFileSystem, ReallyEqualMixin):
2931hunk ./src/allmydata/test/test_backends.py 275
2932-    """ This tests both the StorageServer and the DAS backend together. """
2933+    """ This tests both the StorageServer and the Disk backend together. """
2934     def setUp(self):
2935         MockFileSystem.setUp(self)
2936         try:
2937hunk ./src/allmydata/test/test_backends.py 279
2938-            self.backend = DASCore(self.storedir, expiration_policy)
2939+            self.backend = DiskCore(self.storedir, expiration_policy)
2940             self.ss = StorageServer(testnodeid, self.backend)
2941 
2942hunk ./src/allmydata/test/test_backends.py 282
2943-            self.backendwithreserve = DASCore(self.storedir, expiration_policy, reserved_space = 1)
2944+            self.backendwithreserve = DiskCore(self.storedir, expiration_policy, reserved_space = 1)
2945             self.sswithreserve = StorageServer(testnodeid, self.backendwithreserve)
2946         except:
2947             MockFileSystem.tearDown(self)
2948}
2949[disk/core.py: slips past pyflakes without causing errors
2950wilcoxjg@gmail.com**20110829213631
2951 Ignore-this: a3758ee3bd5da2d4d76fd3cd0de64476
2952] {
2953hunk ./src/allmydata/storage/backends/disk/core.py 1
2954-import re, weakref, struct, time, stat
2955-from twisted.application import service
2956+import re, struct, stat, os
2957 from twisted.python.filepath import UnlistableError
2958 from twisted.python import filepath
2959 from twisted.python.filepath import FilePath
2960hunk ./src/allmydata/storage/backends/disk/core.py 7
2961 from zope.interface import implements
2962 
2963-import allmydata # for __full_version__
2964 from allmydata.interfaces import IStorageBackend
2965 from allmydata.storage.backends.base import Backend
2966 from allmydata.storage.common import si_b2a, si_a2b, si_si2dir
2967hunk ./src/allmydata/storage/backends/disk/core.py 11
2968 from allmydata.util.assertutil import precondition
2969-from allmydata.interfaces import IStorageBackend
2970-from allmydata.util import fileutil, idlib, log, time_format
2971+from allmydata.util import fileutil, log
2972 from allmydata.util.fileutil import fp_make_dirs
2973 from allmydata.storage.lease import LeaseInfo
2974hunk ./src/allmydata/storage/backends/disk/core.py 14
2975-from allmydata.storage.mutable import MutableShareFile, EmptyShare, \
2976-     create_mutable_sharefile
2977 from allmydata.storage.immutable import BucketWriter, BucketReader
2978 from allmydata.storage.crawler import BucketCountingCrawler
2979 from allmydata.util.hashutil import constant_time_compare
2980hunk ./src/allmydata/storage/backends/disk/core.py 18
2981 from allmydata.storage.backends.disk.expirer import LeaseCheckingCrawler
2982+from allmydata.storage.common import UnknownImmutableContainerVersionError, DataTooLargeError
2983 _pyflakes_hush = [si_b2a, si_a2b, si_si2dir] # re-exported
2984 
2985 # storage/
2986}
2987[null/core.py, storage/common.py, storage/immutable.py: pyflaked clean
2988wilcoxjg@gmail.com**20110829214816
2989 Ignore-this: 526f1df98928f52a6df718d7fb510911
2990] {
2991hunk ./src/allmydata/storage/backends/null/core.py 1
2992+import os, struct, stat
2993+from allmydata.util.assertutil import precondition
2994+from allmydata.storage.lease import LeaseInfo
2995+from allmydata.util.hashutil import constant_time_compare
2996 from allmydata.storage.backends.base import Backend
2997hunk ./src/allmydata/storage/backends/null/core.py 6
2998-from allmydata.storage.immutable import BucketWriter, BucketReader
2999+from allmydata.storage.immutable import BucketWriter
3000 from zope.interface import implements
3001 from allmydata.interfaces import IStorageBackend, IStorageBackendShare
3002 
3003hunk ./src/allmydata/storage/common.py 1
3004-import os.path
3005 from allmydata.util import base32
3006 
3007 class DataTooLargeError(Exception):
3008hunk ./src/allmydata/storage/immutable.py 9
3009 from allmydata.interfaces import RIBucketWriter, RIBucketReader
3010 from allmydata.util import base32, log
3011 from allmydata.util.assertutil import precondition
3012-from allmydata.util.hashutil import constant_time_compare
3013-from allmydata.storage.lease import LeaseInfo
3014-from allmydata.storage.common import UnknownImmutableContainerVersionError, \
3015-     DataTooLargeError
3016 
3017 class BucketWriter(Referenceable):
3018     implements(RIBucketWriter)
3019}
3020
3021Context:
3022
3023[test_mutable.Update: only upload the files needed for each test. refs #1500
3024Brian Warner <warner@lothar.com>**20110829072717
3025 Ignore-this: 4d2ab4c7523af9054af7ecca9c3d9dc7
3026 
3027 This first step shaves 15% off the runtime: from 139s to 119s on my laptop.
3028 It also fixes a couple of places where a Deferred was being dropped, which
3029 would cause two tests to run in parallel and also confuse error reporting.
3030]
3031[Let Uploader retain History instead of passing it into upload(). Fixes #1079.
3032Brian Warner <warner@lothar.com>**20110829063246
3033 Ignore-this: 3902c58ec12bd4b2d876806248e19f17
3034 
3035 This consistently records all immutable uploads in the Recent Uploads And
3036 Downloads page, regardless of code path. Previously, certain webapi upload
3037 operations (like PUT /uri/$DIRCAP/newchildname) failed to pass the History
3038 object and were left out.
3039]
3040[Fix mutable publish/retrieve timing status displays. Fixes #1505.
3041Brian Warner <warner@lothar.com>**20110828232221
3042 Ignore-this: 4080ce065cf481b2180fd711c9772dd6
3043 
3044 publish:
3045 * encrypt and encode times are cumulative, not just current-segment
3046 
3047 retrieve:
3048 * same for decrypt and decode times
3049 * update "current status" to include segment number
3050 * set status to Finished/Failed when download is complete
3051 * set progress to 1.0 when complete
3052 
3053 More improvements to consider:
3054 * progress is currently 0% or 100%: should calculate how many segments are
3055   involved (remembering retrieve can be less than the whole file) and set it
3056   to a fraction
3057 * "fetch" time is fuzzy: what we want is to know how much of the delay is not
3058   our own fault, but since we do decode/decrypt work while waiting for more
3059   shares, it's not straightforward
3060]
3061[Teach 'tahoe debug catalog-shares about MDMF. Closes #1507.
3062Brian Warner <warner@lothar.com>**20110828080931
3063 Ignore-this: 56ef2951db1a648353d7daac6a04c7d1
3064]
3065[debug.py: remove some dead comments
3066Brian Warner <warner@lothar.com>**20110828074556
3067 Ignore-this: 40e74040dd4d14fd2f4e4baaae506b31
3068]
3069[hush pyflakes
3070Brian Warner <warner@lothar.com>**20110828074254
3071 Ignore-this: bef9d537a969fa82fe4decc4ba2acb09
3072]
3073[MutableFileNode.set_downloader_hints: never depend upon order of dict.values()
3074Brian Warner <warner@lothar.com>**20110828074103
3075 Ignore-this: caaf1aa518dbdde4d797b7f335230faa
3076 
3077 The old code was calculating the "extension parameters" (a list) from the
3078 downloader hints (a dictionary) with hints.values(), which is not stable, and
3079 would result in corrupted filecaps (with the 'k' and 'segsize' hints
3080 occasionally swapped). The new code always uses [k,segsize].
3081]
3082[layout.py: fix MDMF share layout documentation
3083Brian Warner <warner@lothar.com>**20110828073921
3084 Ignore-this: 3f13366fed75b5e31b51ae895450a225
3085]
3086[teach 'tahoe debug dump-share' about MDMF and offsets. refs #1507
3087Brian Warner <warner@lothar.com>**20110828073834
3088 Ignore-this: 3a9d2ef9c47a72bf1506ba41199a1dea
3089]
3090[test_mutable.Version.test_debug: use splitlines() to fix buildslaves
3091Brian Warner <warner@lothar.com>**20110828064728
3092 Ignore-this: c7f6245426fc80b9d1ae901d5218246a
3093 
3094 Any slave running in a directory with spaces in the name was miscounting
3095 shares, causing the test to fail.
3096]
3097[test_mutable.Version: exercise 'tahoe debug find-shares' on MDMF. refs #1507
3098Brian Warner <warner@lothar.com>**20110828005542
3099 Ignore-this: cb20bea1c28bfa50a72317d70e109672
3100 
3101 Also changes NoNetworkGrid to put shares in storage/shares/ .
3102]
3103[test_mutable.py: oops, missed a .todo
3104Brian Warner <warner@lothar.com>**20110828002118
3105 Ignore-this: fda09ae86481352b7a627c278d2a3940
3106]
3107[test_mutable: merge davidsarah's patch with my Version refactorings
3108warner@lothar.com**20110827235707
3109 Ignore-this: b5aaf481c90d99e33827273b5d118fd0
3110]
3111[Make the immutable/read-only constraint checking for MDMF URIs identical to that for SSK URIs. refs #393
3112david-sarah@jacaranda.org**20110823012720
3113 Ignore-this: e1f59d7ff2007c81dbef2aeb14abd721
3114]
3115[Additional tests for MDMF URIs and for zero-length files. refs #393
3116david-sarah@jacaranda.org**20110823011532
3117 Ignore-this: a7cc0c09d1d2d72413f9cd227c47a9d5
3118]
3119[Additional tests for zero-length partial reads and updates to mutable versions. refs #393
3120david-sarah@jacaranda.org**20110822014111
3121 Ignore-this: 5fc6f4d06e11910124e4a277ec8a43ea
3122]
3123[test_mutable.Version: factor out some expensive uploads, save 25% runtime
3124Brian Warner <warner@lothar.com>**20110827232737
3125 Ignore-this: ea37383eb85ea0894b254fe4dfb45544
3126]
3127[SDMF: update filenode with correct k/N after Retrieve. Fixes #1510.
3128Brian Warner <warner@lothar.com>**20110827225031
3129 Ignore-this: b50ae6e1045818c400079f118b4ef48
3130 
3131 Without this, we get a regression when modifying a mutable file that was
3132 created with more shares (larger N) than our current tahoe.cfg . The
3133 modification attempt creates new versions of the (0,1,..,newN-1) shares, but
3134 leaves the old versions of the (newN,..,oldN-1) shares alone (and throws a
3135 assertion error in SDMFSlotWriteProxy.finish_publishing in the process).
3136 
3137 The mixed versions that result (some shares with e.g. N=10, some with N=20,
3138 such that both versions are recoverable) cause problems for the Publish code,
3139 even before MDMF landed. Might be related to refs #1390 and refs #1042.
3140]
3141[layout.py: annotate assertion to figure out 'tahoe backup' failure
3142Brian Warner <warner@lothar.com>**20110827195253
3143 Ignore-this: 9b92b954e3ed0d0f80154fff1ff674e5
3144]
3145[Add 'tahoe debug dump-cap' support for MDMF, DIR2-CHK, DIR2-MDMF. refs #1507.
3146Brian Warner <warner@lothar.com>**20110827195048
3147 Ignore-this: 61c6af5e33fc88e0251e697a50addb2c
3148 
3149 This also adds tests for all those cases, and fixes an omission in uri.py
3150 that broke parsing of DIR2-MDMF-Verifier and DIR2-CHK-Verifier.
3151]
3152[MDMF: more writable/writeable consistentifications
3153warner@lothar.com**20110827190602
3154 Ignore-this: 22492a9e20c1819ddb12091062888b55
3155]
3156[MDMF: s/Writable/Writeable/g, for consistency with existing SDMF code
3157warner@lothar.com**20110827183357
3158 Ignore-this: 9dd312acedbdb2fc2f7bef0d0fb17c0b
3159]
3160[setup.cfg: remove no-longer-supported test_mac_diskimage alias. refs #1479
3161david-sarah@jacaranda.org**20110826230345
3162 Ignore-this: 40e908b8937322a290fb8012bfcad02a
3163]
3164[test_mutable.Update: increase timeout from 120s to 400s, slaves are failing
3165Brian Warner <warner@lothar.com>**20110825230140
3166 Ignore-this: 101b1924a30cdbda9b2e419e95ca15ec
3167]
3168[tests: fix check_memory test
3169zooko@zooko.com**20110825201116
3170 Ignore-this: 4d66299fa8cb61d2ca04b3f45344d835
3171 fixes #1503
3172]
3173[TAG allmydata-tahoe-1.9.0a1
3174warner@lothar.com**20110825161122
3175 Ignore-this: 3cbf49f00dbda58189f893c427f65605
3176]
3177Patch bundle hash:
3178296daba999522f770510cf12485f85b0e4eccf46