Ticket #999: checkpoint4.darcs.patch

File checkpoint4.darcs.patch, 109.3 KB (added by arch_o_median, at 2011-06-28T20:24:26Z)
Line 
1Fri Mar 25 14:35:14 MDT 2011  wilcoxjg@gmail.com
2  * storage: new mocking tests of storage server read and write
3  There are already tests of read and functionality in test_storage.py, but those tests let the code under test use a real filesystem whereas these tests mock all file system calls.
4
5Fri Jun 24 14:28:50 MDT 2011  wilcoxjg@gmail.com
6  * server.py, test_backends.py, interfaces.py, immutable.py (others?): working patch for implementation of backends plugin
7  sloppy not for production
8
9Sat Jun 25 23:27:32 MDT 2011  wilcoxjg@gmail.com
10  * a temp patch used as a snapshot
11
12Sat Jun 25 23:32:44 MDT 2011  wilcoxjg@gmail.com
13  * snapshot of progress on backend implementation (not suitable for trunk)
14
15Sun Jun 26 10:57:15 MDT 2011  wilcoxjg@gmail.com
16  * checkpoint patch
17
18Tue Jun 28 14:22:02 MDT 2011  wilcoxjg@gmail.com
19  * checkpoint4
20
21New patches:
22
23[storage: new mocking tests of storage server read and write
24wilcoxjg@gmail.com**20110325203514
25 Ignore-this: df65c3c4f061dd1516f88662023fdb41
26 There are already tests of read and functionality in test_storage.py, but those tests let the code under test use a real filesystem whereas these tests mock all file system calls.
27] {
28addfile ./src/allmydata/test/test_server.py
29hunk ./src/allmydata/test/test_server.py 1
30+from twisted.trial import unittest
31+
32+from StringIO import StringIO
33+
34+from allmydata.test.common_util import ReallyEqualMixin
35+
36+import mock
37+
38+# This is the code that we're going to be testing.
39+from allmydata.storage.server import StorageServer
40+
41+# The following share file contents was generated with
42+# storage.immutable.ShareFile from Tahoe-LAFS v1.8.2
43+# with share data == 'a'.
44+share_data = 'a\x00\x00\x00\x00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy\x00(\xde\x80'
45+share_file_data = '\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01' + share_data
46+
47+sharefname = 'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a/0'
48+
49+class TestServerConstruction(unittest.TestCase, ReallyEqualMixin):
50+    @mock.patch('__builtin__.open')
51+    def test_create_server(self, mockopen):
52+        """ This tests whether a server instance can be constructed. """
53+
54+        def call_open(fname, mode):
55+            if fname == 'testdir/bucket_counter.state':
56+                raise IOError(2, "No such file or directory: 'testdir/bucket_counter.state'")
57+            elif fname == 'testdir/lease_checker.state':
58+                raise IOError(2, "No such file or directory: 'testdir/lease_checker.state'")
59+            elif fname == 'testdir/lease_checker.history':
60+                return StringIO()
61+        mockopen.side_effect = call_open
62+
63+        # Now begin the test.
64+        s = StorageServer('testdir', 'testnodeidxxxxxxxxxx')
65+
66+        # You passed!
67+
68+class TestServer(unittest.TestCase, ReallyEqualMixin):
69+    @mock.patch('__builtin__.open')
70+    def setUp(self, mockopen):
71+        def call_open(fname, mode):
72+            if fname == 'testdir/bucket_counter.state':
73+                raise IOError(2, "No such file or directory: 'testdir/bucket_counter.state'")
74+            elif fname == 'testdir/lease_checker.state':
75+                raise IOError(2, "No such file or directory: 'testdir/lease_checker.state'")
76+            elif fname == 'testdir/lease_checker.history':
77+                return StringIO()
78+        mockopen.side_effect = call_open
79+
80+        self.s = StorageServer('testdir', 'testnodeidxxxxxxxxxx')
81+
82+
83+    @mock.patch('time.time')
84+    @mock.patch('os.mkdir')
85+    @mock.patch('__builtin__.open')
86+    @mock.patch('os.listdir')
87+    @mock.patch('os.path.isdir')
88+    def test_write_share(self, mockisdir, mocklistdir, mockopen, mockmkdir, mocktime):
89+        """Handle a report of corruption."""
90+
91+        def call_listdir(dirname):
92+            self.failUnlessReallyEqual(dirname, 'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a')
93+            raise OSError(2, "No such file or directory: 'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a'")
94+
95+        mocklistdir.side_effect = call_listdir
96+
97+        class MockFile:
98+            def __init__(self):
99+                self.buffer = ''
100+                self.pos = 0
101+            def write(self, instring):
102+                begin = self.pos
103+                padlen = begin - len(self.buffer)
104+                if padlen > 0:
105+                    self.buffer += '\x00' * padlen
106+                end = self.pos + len(instring)
107+                self.buffer = self.buffer[:begin]+instring+self.buffer[end:]
108+                self.pos = end
109+            def close(self):
110+                pass
111+            def seek(self, pos):
112+                self.pos = pos
113+            def read(self, numberbytes):
114+                return self.buffer[self.pos:self.pos+numberbytes]
115+            def tell(self):
116+                return self.pos
117+
118+        mocktime.return_value = 0
119+
120+        sharefile = MockFile()
121+        def call_open(fname, mode):
122+            self.failUnlessReallyEqual(fname, 'testdir/shares/incoming/or/orsxg5dtorxxeylhmvpws3temv4a/0' )
123+            return sharefile
124+
125+        mockopen.side_effect = call_open
126+        # Now begin the test.
127+        alreadygot, bs = self.s.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
128+        print bs
129+        bs[0].remote_write(0, 'a')
130+        self.failUnlessReallyEqual(sharefile.buffer, share_file_data)
131+
132+
133+    @mock.patch('os.path.exists')
134+    @mock.patch('os.path.getsize')
135+    @mock.patch('__builtin__.open')
136+    @mock.patch('os.listdir')
137+    def test_read_share(self, mocklistdir, mockopen, mockgetsize, mockexists):
138+        """ This tests whether the code correctly finds and reads
139+        shares written out by old (Tahoe-LAFS <= v1.8.2)
140+        servers. There is a similar test in test_download, but that one
141+        is from the perspective of the client and exercises a deeper
142+        stack of code. This one is for exercising just the
143+        StorageServer object. """
144+
145+        def call_listdir(dirname):
146+            self.failUnlessReallyEqual(dirname,'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a')
147+            return ['0']
148+
149+        mocklistdir.side_effect = call_listdir
150+
151+        def call_open(fname, mode):
152+            self.failUnlessReallyEqual(fname, sharefname)
153+            self.failUnless('r' in mode, mode)
154+            self.failUnless('b' in mode, mode)
155+
156+            return StringIO(share_file_data)
157+        mockopen.side_effect = call_open
158+
159+        datalen = len(share_file_data)
160+        def call_getsize(fname):
161+            self.failUnlessReallyEqual(fname, sharefname)
162+            return datalen
163+        mockgetsize.side_effect = call_getsize
164+
165+        def call_exists(fname):
166+            self.failUnlessReallyEqual(fname, sharefname)
167+            return True
168+        mockexists.side_effect = call_exists
169+
170+        # Now begin the test.
171+        bs = self.s.remote_get_buckets('teststorage_index')
172+
173+        self.failUnlessEqual(len(bs), 1)
174+        b = bs[0]
175+        self.failUnlessReallyEqual(b.remote_read(0, datalen), share_data)
176+        # If you try to read past the end you get the as much data as is there.
177+        self.failUnlessReallyEqual(b.remote_read(0, datalen+20), share_data)
178+        # If you start reading past the end of the file you get the empty string.
179+        self.failUnlessReallyEqual(b.remote_read(datalen+1, 3), '')
180}
181[server.py, test_backends.py, interfaces.py, immutable.py (others?): working patch for implementation of backends plugin
182wilcoxjg@gmail.com**20110624202850
183 Ignore-this: ca6f34987ee3b0d25cac17c1fc22d50c
184 sloppy not for production
185] {
186move ./src/allmydata/test/test_server.py ./src/allmydata/test/test_backends.py
187hunk ./src/allmydata/storage/crawler.py 13
188     pass
189 
190 class ShareCrawler(service.MultiService):
191-    """A ShareCrawler subclass is attached to a StorageServer, and
192+    """A subcless of ShareCrawler is attached to a StorageServer, and
193     periodically walks all of its shares, processing each one in some
194     fashion. This crawl is rate-limited, to reduce the IO burden on the host,
195     since large servers can easily have a terabyte of shares, in several
196hunk ./src/allmydata/storage/crawler.py 31
197     We assume that the normal upload/download/get_buckets traffic of a tahoe
198     grid will cause the prefixdir contents to be mostly cached in the kernel,
199     or that the number of buckets in each prefixdir will be small enough to
200-    load quickly. A 1TB allmydata.com server was measured to have 2.56M
201+    load quickly. A 1TB allmydata.com server was measured to have 2.56 * 10^6
202     buckets, spread into the 1024 prefixdirs, with about 2500 buckets per
203     prefix. On this server, each prefixdir took 130ms-200ms to list the first
204     time, and 17ms to list the second time.
205hunk ./src/allmydata/storage/crawler.py 68
206     cpu_slice = 1.0 # use up to 1.0 seconds before yielding
207     minimum_cycle_time = 300 # don't run a cycle faster than this
208 
209-    def __init__(self, server, statefile, allowed_cpu_percentage=None):
210+    def __init__(self, backend, statefile, allowed_cpu_percentage=None):
211         service.MultiService.__init__(self)
212         if allowed_cpu_percentage is not None:
213             self.allowed_cpu_percentage = allowed_cpu_percentage
214hunk ./src/allmydata/storage/crawler.py 72
215-        self.server = server
216-        self.sharedir = server.sharedir
217-        self.statefile = statefile
218+        self.backend = backend
219         self.prefixes = [si_b2a(struct.pack(">H", i << (16-10)))[:2]
220                          for i in range(2**10)]
221         self.prefixes.sort()
222hunk ./src/allmydata/storage/crawler.py 446
223 
224     minimum_cycle_time = 60*60 # we don't need this more than once an hour
225 
226-    def __init__(self, server, statefile, num_sample_prefixes=1):
227-        ShareCrawler.__init__(self, server, statefile)
228+    def __init__(self, statefile, num_sample_prefixes=1):
229+        ShareCrawler.__init__(self, statefile)
230         self.num_sample_prefixes = num_sample_prefixes
231 
232     def add_initial_state(self):
233hunk ./src/allmydata/storage/expirer.py 15
234     removed.
235 
236     I collect statistics on the leases and make these available to a web
237-    status page, including::
238+    status page, including:
239 
240     Space recovered during this cycle-so-far:
241      actual (only if expiration_enabled=True):
242hunk ./src/allmydata/storage/expirer.py 51
243     slow_start = 360 # wait 6 minutes after startup
244     minimum_cycle_time = 12*60*60 # not more than twice per day
245 
246-    def __init__(self, server, statefile, historyfile,
247+    def __init__(self, statefile, historyfile,
248                  expiration_enabled, mode,
249                  override_lease_duration, # used if expiration_mode=="age"
250                  cutoff_date, # used if expiration_mode=="cutoff-date"
251hunk ./src/allmydata/storage/expirer.py 71
252         else:
253             raise ValueError("GC mode '%s' must be 'age' or 'cutoff-date'" % mode)
254         self.sharetypes_to_expire = sharetypes
255-        ShareCrawler.__init__(self, server, statefile)
256+        ShareCrawler.__init__(self, statefile)
257 
258     def add_initial_state(self):
259         # we fill ["cycle-to-date"] here (even though they will be reset in
260hunk ./src/allmydata/storage/immutable.py 44
261     sharetype = "immutable"
262 
263     def __init__(self, filename, max_size=None, create=False):
264-        """ 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. """
265+        """ If max_size is not None then I won't allow more than
266+        max_size to be written to me. If create=True then max_size
267+        must not be None. """
268         precondition((max_size is not None) or (not create), max_size, create)
269         self.home = filename
270         self._max_size = max_size
271hunk ./src/allmydata/storage/immutable.py 87
272 
273     def read_share_data(self, offset, length):
274         precondition(offset >= 0)
275-        # reads beyond the end of the data are truncated. Reads that start
276-        # beyond the end of the data return an empty string. I wonder why
277-        # Python doesn't do the following computation for me?
278+        # Reads beyond the end of the data are truncated. Reads that start
279+        # beyond the end of the data return an empty string.
280         seekpos = self._data_offset+offset
281         fsize = os.path.getsize(self.home)
282         actuallength = max(0, min(length, fsize-seekpos))
283hunk ./src/allmydata/storage/immutable.py 198
284             space_freed += os.stat(self.home)[stat.ST_SIZE]
285             self.unlink()
286         return space_freed
287+class NullBucketWriter(Referenceable):
288+    implements(RIBucketWriter)
289 
290hunk ./src/allmydata/storage/immutable.py 201
291+    def remote_write(self, offset, data):
292+        return
293 
294 class BucketWriter(Referenceable):
295     implements(RIBucketWriter)
296hunk ./src/allmydata/storage/server.py 7
297 from twisted.application import service
298 
299 from zope.interface import implements
300-from allmydata.interfaces import RIStorageServer, IStatsProducer
301+from allmydata.interfaces import RIStorageServer, IStatsProducer, IShareStore
302 from allmydata.util import fileutil, idlib, log, time_format
303 import allmydata # for __full_version__
304 
305hunk ./src/allmydata/storage/server.py 16
306 from allmydata.storage.lease import LeaseInfo
307 from allmydata.storage.mutable import MutableShareFile, EmptyShare, \
308      create_mutable_sharefile
309-from allmydata.storage.immutable import ShareFile, BucketWriter, BucketReader
310+from allmydata.storage.immutable import ShareFile, NullBucketWriter, BucketWriter, BucketReader
311 from allmydata.storage.crawler import BucketCountingCrawler
312 from allmydata.storage.expirer import LeaseCheckingCrawler
313 
314hunk ./src/allmydata/storage/server.py 20
315+from zope.interface import implements
316+
317+# A Backend is a MultiService so that its server's crawlers (if the server has any) can
318+# be started and stopped.
319+class Backend(service.MultiService):
320+    implements(IStatsProducer)
321+    def __init__(self):
322+        service.MultiService.__init__(self)
323+
324+    def get_bucket_shares(self):
325+        """XXX"""
326+        raise NotImplementedError
327+
328+    def get_share(self):
329+        """XXX"""
330+        raise NotImplementedError
331+
332+    def make_bucket_writer(self):
333+        """XXX"""
334+        raise NotImplementedError
335+
336+class NullBackend(Backend):
337+    def __init__(self):
338+        Backend.__init__(self)
339+
340+    def get_available_space(self):
341+        return None
342+
343+    def get_bucket_shares(self, storage_index):
344+        return set()
345+
346+    def get_share(self, storage_index, sharenum):
347+        return None
348+
349+    def make_bucket_writer(self, storage_index, shnum, max_space_per_bucket, lease_info, canary):
350+        return NullBucketWriter()
351+
352+class FSBackend(Backend):
353+    def __init__(self, storedir, readonly=False, reserved_space=0):
354+        Backend.__init__(self)
355+
356+        self._setup_storage(storedir, readonly, reserved_space)
357+        self._setup_corruption_advisory()
358+        self._setup_bucket_counter()
359+        self._setup_lease_checkerf()
360+
361+    def _setup_storage(self, storedir, readonly, reserved_space):
362+        self.storedir = storedir
363+        self.readonly = readonly
364+        self.reserved_space = int(reserved_space)
365+        if self.reserved_space:
366+            if self.get_available_space() is None:
367+                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",
368+                        umid="0wZ27w", level=log.UNUSUAL)
369+
370+        self.sharedir = os.path.join(self.storedir, "shares")
371+        fileutil.make_dirs(self.sharedir)
372+        self.incomingdir = os.path.join(self.sharedir, 'incoming')
373+        self._clean_incomplete()
374+
375+    def _clean_incomplete(self):
376+        fileutil.rm_dir(self.incomingdir)
377+        fileutil.make_dirs(self.incomingdir)
378+
379+    def _setup_corruption_advisory(self):
380+        # we don't actually create the corruption-advisory dir until necessary
381+        self.corruption_advisory_dir = os.path.join(self.storedir,
382+                                                    "corruption-advisories")
383+
384+    def _setup_bucket_counter(self):
385+        statefile = os.path.join(self.storedir, "bucket_counter.state")
386+        self.bucket_counter = BucketCountingCrawler(statefile)
387+        self.bucket_counter.setServiceParent(self)
388+
389+    def _setup_lease_checkerf(self):
390+        statefile = os.path.join(self.storedir, "lease_checker.state")
391+        historyfile = os.path.join(self.storedir, "lease_checker.history")
392+        self.lease_checker = LeaseCheckingCrawler(statefile, historyfile,
393+                                   expiration_enabled, expiration_mode,
394+                                   expiration_override_lease_duration,
395+                                   expiration_cutoff_date,
396+                                   expiration_sharetypes)
397+        self.lease_checker.setServiceParent(self)
398+
399+    def get_available_space(self):
400+        if self.readonly:
401+            return 0
402+        return fileutil.get_available_space(self.storedir, self.reserved_space)
403+
404+    def get_bucket_shares(self, storage_index):
405+        """Return a list of (shnum, pathname) tuples for files that hold
406+        shares for this storage_index. In each tuple, 'shnum' will always be
407+        the integer form of the last component of 'pathname'."""
408+        storagedir = os.path.join(self.sharedir, storage_index_to_dir(storage_index))
409+        try:
410+            for f in os.listdir(storagedir):
411+                if NUM_RE.match(f):
412+                    filename = os.path.join(storagedir, f)
413+                    yield (int(f), filename)
414+        except OSError:
415+            # Commonly caused by there being no buckets at all.
416+            pass
417+
418 # storage/
419 # storage/shares/incoming
420 #   incoming/ holds temp dirs named $START/$STORAGEINDEX/$SHARENUM which will
421hunk ./src/allmydata/storage/server.py 143
422     name = 'storage'
423     LeaseCheckerClass = LeaseCheckingCrawler
424 
425-    def __init__(self, storedir, nodeid, reserved_space=0,
426-                 discard_storage=False, readonly_storage=False,
427+    def __init__(self, nodeid, backend, reserved_space=0,
428+                 readonly_storage=False,
429                  stats_provider=None,
430                  expiration_enabled=False,
431                  expiration_mode="age",
432hunk ./src/allmydata/storage/server.py 155
433         assert isinstance(nodeid, str)
434         assert len(nodeid) == 20
435         self.my_nodeid = nodeid
436-        self.storedir = storedir
437-        sharedir = os.path.join(storedir, "shares")
438-        fileutil.make_dirs(sharedir)
439-        self.sharedir = sharedir
440-        # we don't actually create the corruption-advisory dir until necessary
441-        self.corruption_advisory_dir = os.path.join(storedir,
442-                                                    "corruption-advisories")
443-        self.reserved_space = int(reserved_space)
444-        self.no_storage = discard_storage
445-        self.readonly_storage = readonly_storage
446         self.stats_provider = stats_provider
447         if self.stats_provider:
448             self.stats_provider.register_producer(self)
449hunk ./src/allmydata/storage/server.py 158
450-        self.incomingdir = os.path.join(sharedir, 'incoming')
451-        self._clean_incomplete()
452-        fileutil.make_dirs(self.incomingdir)
453         self._active_writers = weakref.WeakKeyDictionary()
454hunk ./src/allmydata/storage/server.py 159
455+        self.backend = backend
456+        self.backend.setServiceParent(self)
457         log.msg("StorageServer created", facility="tahoe.storage")
458 
459hunk ./src/allmydata/storage/server.py 163
460-        if reserved_space:
461-            if self.get_available_space() is None:
462-                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",
463-                        umin="0wZ27w", level=log.UNUSUAL)
464-
465         self.latencies = {"allocate": [], # immutable
466                           "write": [],
467                           "close": [],
468hunk ./src/allmydata/storage/server.py 174
469                           "renew": [],
470                           "cancel": [],
471                           }
472-        self.add_bucket_counter()
473-
474-        statefile = os.path.join(self.storedir, "lease_checker.state")
475-        historyfile = os.path.join(self.storedir, "lease_checker.history")
476-        klass = self.LeaseCheckerClass
477-        self.lease_checker = klass(self, statefile, historyfile,
478-                                   expiration_enabled, expiration_mode,
479-                                   expiration_override_lease_duration,
480-                                   expiration_cutoff_date,
481-                                   expiration_sharetypes)
482-        self.lease_checker.setServiceParent(self)
483 
484     def __repr__(self):
485         return "<StorageServer %s>" % (idlib.shortnodeid_b2a(self.my_nodeid),)
486hunk ./src/allmydata/storage/server.py 178
487 
488-    def add_bucket_counter(self):
489-        statefile = os.path.join(self.storedir, "bucket_counter.state")
490-        self.bucket_counter = BucketCountingCrawler(self, statefile)
491-        self.bucket_counter.setServiceParent(self)
492-
493     def count(self, name, delta=1):
494         if self.stats_provider:
495             self.stats_provider.count("storage_server." + name, delta)
496hunk ./src/allmydata/storage/server.py 233
497             kwargs["facility"] = "tahoe.storage"
498         return log.msg(*args, **kwargs)
499 
500-    def _clean_incomplete(self):
501-        fileutil.rm_dir(self.incomingdir)
502-
503     def get_stats(self):
504         # remember: RIStatsProvider requires that our return dict
505         # contains numeric values.
506hunk ./src/allmydata/storage/server.py 269
507             stats['storage_server.total_bucket_count'] = bucket_count
508         return stats
509 
510-    def get_available_space(self):
511-        """Returns available space for share storage in bytes, or None if no
512-        API to get this information is available."""
513-
514-        if self.readonly_storage:
515-            return 0
516-        return fileutil.get_available_space(self.storedir, self.reserved_space)
517-
518     def allocated_size(self):
519         space = 0
520         for bw in self._active_writers:
521hunk ./src/allmydata/storage/server.py 276
522         return space
523 
524     def remote_get_version(self):
525-        remaining_space = self.get_available_space()
526+        remaining_space = self.backend.get_available_space()
527         if remaining_space is None:
528             # We're on a platform that has no API to get disk stats.
529             remaining_space = 2**64
530hunk ./src/allmydata/storage/server.py 301
531         self.count("allocate")
532         alreadygot = set()
533         bucketwriters = {} # k: shnum, v: BucketWriter
534-        si_dir = storage_index_to_dir(storage_index)
535-        si_s = si_b2a(storage_index)
536 
537hunk ./src/allmydata/storage/server.py 302
538+        si_s = si_b2a(storage_index)
539         log.msg("storage: allocate_buckets %s" % si_s)
540 
541         # in this implementation, the lease information (including secrets)
542hunk ./src/allmydata/storage/server.py 316
543 
544         max_space_per_bucket = allocated_size
545 
546-        remaining_space = self.get_available_space()
547+        remaining_space = self.backend.get_available_space()
548         limited = remaining_space is not None
549         if limited:
550             # this is a bit conservative, since some of this allocated_size()
551hunk ./src/allmydata/storage/server.py 329
552         # they asked about: this will save them a lot of work. Add or update
553         # leases for all of them: if they want us to hold shares for this
554         # file, they'll want us to hold leases for this file.
555-        for (shnum, fn) in self._get_bucket_shares(storage_index):
556+        for (shnum, fn) in self.backend.get_bucket_shares(storage_index):
557             alreadygot.add(shnum)
558             sf = ShareFile(fn)
559             sf.add_or_renew_lease(lease_info)
560hunk ./src/allmydata/storage/server.py 335
561 
562         for shnum in sharenums:
563-            incominghome = os.path.join(self.incomingdir, si_dir, "%d" % shnum)
564-            finalhome = os.path.join(self.sharedir, si_dir, "%d" % shnum)
565-            if os.path.exists(finalhome):
566+            share = self.backend.get_share(storage_index, shnum)
567+
568+            if not share:
569+                if (not limited) or (remaining_space >= max_space_per_bucket):
570+                    # ok! we need to create the new share file.
571+                    bw = self.backend.make_bucket_writer(storage_index, shnum,
572+                                      max_space_per_bucket, lease_info, canary)
573+                    bucketwriters[shnum] = bw
574+                    self._active_writers[bw] = 1
575+                    if limited:
576+                        remaining_space -= max_space_per_bucket
577+                else:
578+                    # bummer! not enough space to accept this bucket
579+                    pass
580+
581+            elif share.is_complete():
582                 # great! we already have it. easy.
583                 pass
584hunk ./src/allmydata/storage/server.py 353
585-            elif os.path.exists(incominghome):
586+            elif not share.is_complete():
587                 # Note that we don't create BucketWriters for shnums that
588                 # have a partial share (in incoming/), so if a second upload
589                 # occurs while the first is still in progress, the second
590hunk ./src/allmydata/storage/server.py 359
591                 # uploader will use different storage servers.
592                 pass
593-            elif (not limited) or (remaining_space >= max_space_per_bucket):
594-                # ok! we need to create the new share file.
595-                bw = BucketWriter(self, incominghome, finalhome,
596-                                  max_space_per_bucket, lease_info, canary)
597-                if self.no_storage:
598-                    bw.throw_out_all_data = True
599-                bucketwriters[shnum] = bw
600-                self._active_writers[bw] = 1
601-                if limited:
602-                    remaining_space -= max_space_per_bucket
603-            else:
604-                # bummer! not enough space to accept this bucket
605-                pass
606-
607-        if bucketwriters:
608-            fileutil.make_dirs(os.path.join(self.sharedir, si_dir))
609 
610         self.add_latency("allocate", time.time() - start)
611         return alreadygot, bucketwriters
612hunk ./src/allmydata/storage/server.py 437
613             self.stats_provider.count('storage_server.bytes_added', consumed_size)
614         del self._active_writers[bw]
615 
616-    def _get_bucket_shares(self, storage_index):
617-        """Return a list of (shnum, pathname) tuples for files that hold
618-        shares for this storage_index. In each tuple, 'shnum' will always be
619-        the integer form of the last component of 'pathname'."""
620-        storagedir = os.path.join(self.sharedir, storage_index_to_dir(storage_index))
621-        try:
622-            for f in os.listdir(storagedir):
623-                if NUM_RE.match(f):
624-                    filename = os.path.join(storagedir, f)
625-                    yield (int(f), filename)
626-        except OSError:
627-            # Commonly caused by there being no buckets at all.
628-            pass
629 
630     def remote_get_buckets(self, storage_index):
631         start = time.time()
632hunk ./src/allmydata/storage/server.py 444
633         si_s = si_b2a(storage_index)
634         log.msg("storage: get_buckets %s" % si_s)
635         bucketreaders = {} # k: sharenum, v: BucketReader
636-        for shnum, filename in self._get_bucket_shares(storage_index):
637+        for shnum, filename in self.backend.get_bucket_shares(storage_index):
638             bucketreaders[shnum] = BucketReader(self, filename,
639                                                 storage_index, shnum)
640         self.add_latency("get", time.time() - start)
641hunk ./src/allmydata/test/test_backends.py 10
642 import mock
643 
644 # This is the code that we're going to be testing.
645-from allmydata.storage.server import StorageServer
646+from allmydata.storage.server import StorageServer, FSBackend, NullBackend
647 
648 # The following share file contents was generated with
649 # storage.immutable.ShareFile from Tahoe-LAFS v1.8.2
650hunk ./src/allmydata/test/test_backends.py 21
651 sharefname = 'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a/0'
652 
653 class TestServerConstruction(unittest.TestCase, ReallyEqualMixin):
654+    @mock.patch('time.time')
655+    @mock.patch('os.mkdir')
656+    @mock.patch('__builtin__.open')
657+    @mock.patch('os.listdir')
658+    @mock.patch('os.path.isdir')
659+    def test_create_server_null_backend(self, mockisdir, mocklistdir, mockopen, mockmkdir, mocktime):
660+        """ This tests whether a server instance can be constructed
661+        with a null backend. The server instance fails the test if it
662+        tries to read or write to the file system. """
663+
664+        # Now begin the test.
665+        s = StorageServer('testnodeidxxxxxxxxxx', backend=NullBackend())
666+
667+        self.failIf(mockisdir.called)
668+        self.failIf(mocklistdir.called)
669+        self.failIf(mockopen.called)
670+        self.failIf(mockmkdir.called)
671+
672+        # You passed!
673+
674+    @mock.patch('time.time')
675+    @mock.patch('os.mkdir')
676     @mock.patch('__builtin__.open')
677hunk ./src/allmydata/test/test_backends.py 44
678-    def test_create_server(self, mockopen):
679-        """ This tests whether a server instance can be constructed. """
680+    @mock.patch('os.listdir')
681+    @mock.patch('os.path.isdir')
682+    def test_create_server_fs_backend(self, mockisdir, mocklistdir, mockopen, mockmkdir, mocktime):
683+        """ This tests whether a server instance can be constructed
684+        with a filesystem backend. To pass the test, it has to use the
685+        filesystem in only the prescribed ways. """
686 
687         def call_open(fname, mode):
688             if fname == 'testdir/bucket_counter.state':
689hunk ./src/allmydata/test/test_backends.py 58
690                 raise IOError(2, "No such file or directory: 'testdir/lease_checker.state'")
691             elif fname == 'testdir/lease_checker.history':
692                 return StringIO()
693+            else:
694+                self.fail("Server with FS backend tried to open '%s' in mode '%s'" % (fname, mode))
695         mockopen.side_effect = call_open
696 
697         # Now begin the test.
698hunk ./src/allmydata/test/test_backends.py 63
699-        s = StorageServer('testdir', 'testnodeidxxxxxxxxxx')
700+        s = StorageServer('testnodeidxxxxxxxxxx', backend=FSBackend('teststoredir'))
701+
702+        self.failIf(mockisdir.called)
703+        self.failIf(mocklistdir.called)
704+        self.failIf(mockopen.called)
705+        self.failIf(mockmkdir.called)
706+        self.failIf(mocktime.called)
707 
708         # You passed!
709 
710hunk ./src/allmydata/test/test_backends.py 73
711-class TestServer(unittest.TestCase, ReallyEqualMixin):
712+class TestServerNullBackend(unittest.TestCase, ReallyEqualMixin):
713+    def setUp(self):
714+        self.s = StorageServer('testnodeidxxxxxxxxxx', backend=NullBackend())
715+
716+    @mock.patch('os.mkdir')
717+    @mock.patch('__builtin__.open')
718+    @mock.patch('os.listdir')
719+    @mock.patch('os.path.isdir')
720+    def test_write_share(self, mockisdir, mocklistdir, mockopen, mockmkdir):
721+        """ Write a new share. """
722+
723+        # Now begin the test.
724+        alreadygot, bs = self.s.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
725+        bs[0].remote_write(0, 'a')
726+        self.failIf(mockisdir.called)
727+        self.failIf(mocklistdir.called)
728+        self.failIf(mockopen.called)
729+        self.failIf(mockmkdir.called)
730+
731+    @mock.patch('os.path.exists')
732+    @mock.patch('os.path.getsize')
733+    @mock.patch('__builtin__.open')
734+    @mock.patch('os.listdir')
735+    def test_read_share(self, mocklistdir, mockopen, mockgetsize, mockexists):
736+        """ This tests whether the code correctly finds and reads
737+        shares written out by old (Tahoe-LAFS <= v1.8.2)
738+        servers. There is a similar test in test_download, but that one
739+        is from the perspective of the client and exercises a deeper
740+        stack of code. This one is for exercising just the
741+        StorageServer object. """
742+
743+        # Now begin the test.
744+        bs = self.s.remote_get_buckets('teststorage_index')
745+
746+        self.failUnlessEqual(len(bs), 0)
747+        self.failIf(mocklistdir.called)
748+        self.failIf(mockopen.called)
749+        self.failIf(mockgetsize.called)
750+        self.failIf(mockexists.called)
751+
752+
753+class TestServerFSBackend(unittest.TestCase, ReallyEqualMixin):
754     @mock.patch('__builtin__.open')
755     def setUp(self, mockopen):
756         def call_open(fname, mode):
757hunk ./src/allmydata/test/test_backends.py 126
758                 return StringIO()
759         mockopen.side_effect = call_open
760 
761-        self.s = StorageServer('testdir', 'testnodeidxxxxxxxxxx')
762-
763+        self.s = StorageServer('testnodeidxxxxxxxxxx', backend=FSBackend('teststoredir'))
764 
765     @mock.patch('time.time')
766     @mock.patch('os.mkdir')
767hunk ./src/allmydata/test/test_backends.py 134
768     @mock.patch('os.listdir')
769     @mock.patch('os.path.isdir')
770     def test_write_share(self, mockisdir, mocklistdir, mockopen, mockmkdir, mocktime):
771-        """Handle a report of corruption."""
772+        """ Write a new share. """
773 
774         def call_listdir(dirname):
775             self.failUnlessReallyEqual(dirname, 'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a')
776hunk ./src/allmydata/test/test_backends.py 173
777         mockopen.side_effect = call_open
778         # Now begin the test.
779         alreadygot, bs = self.s.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
780-        print bs
781         bs[0].remote_write(0, 'a')
782         self.failUnlessReallyEqual(sharefile.buffer, share_file_data)
783 
784hunk ./src/allmydata/test/test_backends.py 176
785-
786     @mock.patch('os.path.exists')
787     @mock.patch('os.path.getsize')
788     @mock.patch('__builtin__.open')
789hunk ./src/allmydata/test/test_backends.py 218
790 
791         self.failUnlessEqual(len(bs), 1)
792         b = bs[0]
793+        # These should match by definition, the next two cases cover cases without (completely) unambiguous behaviors.
794         self.failUnlessReallyEqual(b.remote_read(0, datalen), share_data)
795         # If you try to read past the end you get the as much data as is there.
796         self.failUnlessReallyEqual(b.remote_read(0, datalen+20), share_data)
797hunk ./src/allmydata/test/test_backends.py 224
798         # If you start reading past the end of the file you get the empty string.
799         self.failUnlessReallyEqual(b.remote_read(datalen+1, 3), '')
800+
801+
802}
803[a temp patch used as a snapshot
804wilcoxjg@gmail.com**20110626052732
805 Ignore-this: 95f05e314eaec870afa04c76d979aa44
806] {
807hunk ./docs/configuration.rst 637
808   [storage]
809   enabled = True
810   readonly = True
811-  sizelimit = 10000000000
812 
813 
814   [helper]
815hunk ./docs/garbage-collection.rst 16
816 
817 When a file or directory in the virtual filesystem is no longer referenced,
818 the space that its shares occupied on each storage server can be freed,
819-making room for other shares. Tahoe currently uses a garbage collection
820+making room for other shares. Tahoe uses a garbage collection
821 ("GC") mechanism to implement this space-reclamation process. Each share has
822 one or more "leases", which are managed by clients who want the
823 file/directory to be retained. The storage server accepts each share for a
824hunk ./docs/garbage-collection.rst 34
825 the `<lease-tradeoffs.svg>`_ diagram to get an idea for the tradeoffs involved.
826 If lease renewal occurs quickly and with 100% reliability, than any renewal
827 time that is shorter than the lease duration will suffice, but a larger ratio
828-of duration-over-renewal-time will be more robust in the face of occasional
829+of lease duration to renewal time will be more robust in the face of occasional
830 delays or failures.
831 
832 The current recommended values for a small Tahoe grid are to renew the leases
833replace ./docs/garbage-collection.rst [A-Za-z_0-9\-\.] Tahoe Tahoe-LAFS
834hunk ./src/allmydata/client.py 260
835             sharetypes.append("mutable")
836         expiration_sharetypes = tuple(sharetypes)
837 
838+        if self.get_config("storage", "backend", "filesystem") == "filesystem":
839+            xyz
840+        xyz
841         ss = StorageServer(storedir, self.nodeid,
842                            reserved_space=reserved,
843                            discard_storage=discard,
844hunk ./src/allmydata/storage/crawler.py 234
845         f = open(tmpfile, "wb")
846         pickle.dump(self.state, f)
847         f.close()
848-        fileutil.move_into_place(tmpfile, self.statefile)
849+        fileutil.move_into_place(tmpfile, self.statefname)
850 
851     def startService(self):
852         # arrange things to look like we were just sleeping, so
853}
854[snapshot of progress on backend implementation (not suitable for trunk)
855wilcoxjg@gmail.com**20110626053244
856 Ignore-this: 50c764af791c2b99ada8289546806a0a
857] {
858adddir ./src/allmydata/storage/backends
859adddir ./src/allmydata/storage/backends/das
860move ./src/allmydata/storage/expirer.py ./src/allmydata/storage/backends/das/expirer.py
861adddir ./src/allmydata/storage/backends/null
862hunk ./src/allmydata/interfaces.py 270
863         store that on disk.
864         """
865 
866+class IStorageBackend(Interface):
867+    """
868+    Objects of this kind live on the server side and are used by the
869+    storage server object.
870+    """
871+    def get_available_space(self, reserved_space):
872+        """ Returns available space for share storage in bytes, or
873+        None if this information is not available or if the available
874+        space is unlimited.
875+
876+        If the backend is configured for read-only mode then this will
877+        return 0.
878+
879+        reserved_space is how many bytes to subtract from the answer, so
880+        you can pass how many bytes you would like to leave unused on this
881+        filesystem as reserved_space. """
882+
883+    def get_bucket_shares(self):
884+        """XXX"""
885+
886+    def get_share(self):
887+        """XXX"""
888+
889+    def make_bucket_writer(self):
890+        """XXX"""
891+
892+class IStorageBackendShare(Interface):
893+    """
894+    This object contains as much as all of the share data.  It is intended
895+    for lazy evaluation such that in many use cases substantially less than
896+    all of the share data will be accessed.
897+    """
898+    def is_complete(self):
899+        """
900+        Returns the share state, or None if the share does not exist.
901+        """
902+
903 class IStorageBucketWriter(Interface):
904     """
905     Objects of this kind live on the client side.
906hunk ./src/allmydata/interfaces.py 2492
907 
908 class EmptyPathnameComponentError(Exception):
909     """The webapi disallows empty pathname components."""
910+
911+class IShareStore(Interface):
912+    pass
913+
914addfile ./src/allmydata/storage/backends/__init__.py
915addfile ./src/allmydata/storage/backends/das/__init__.py
916addfile ./src/allmydata/storage/backends/das/core.py
917hunk ./src/allmydata/storage/backends/das/core.py 1
918+from allmydata.interfaces import IStorageBackend
919+from allmydata.storage.backends.base import Backend
920+from allmydata.storage.common import si_b2a, si_a2b, storage_index_to_dir
921+from allmydata.util.assertutil import precondition
922+
923+import os, re, weakref, struct, time
924+
925+from foolscap.api import Referenceable
926+from twisted.application import service
927+
928+from zope.interface import implements
929+from allmydata.interfaces import RIStorageServer, IStatsProducer, IShareStore
930+from allmydata.util import fileutil, idlib, log, time_format
931+import allmydata # for __full_version__
932+
933+from allmydata.storage.common import si_b2a, si_a2b, storage_index_to_dir
934+_pyflakes_hush = [si_b2a, si_a2b, storage_index_to_dir] # re-exported
935+from allmydata.storage.lease import LeaseInfo
936+from allmydata.storage.mutable import MutableShareFile, EmptyShare, \
937+     create_mutable_sharefile
938+from allmydata.storage.backends.das.immutable import NullBucketWriter, BucketWriter, BucketReader
939+from allmydata.storage.crawler import FSBucketCountingCrawler
940+from allmydata.storage.backends.das.expirer import FSLeaseCheckingCrawler
941+
942+from zope.interface import implements
943+
944+class DASCore(Backend):
945+    implements(IStorageBackend)
946+    def __init__(self, storedir, expiration_policy, readonly=False, reserved_space=0):
947+        Backend.__init__(self)
948+
949+        self._setup_storage(storedir, readonly, reserved_space)
950+        self._setup_corruption_advisory()
951+        self._setup_bucket_counter()
952+        self._setup_lease_checkerf(expiration_policy)
953+
954+    def _setup_storage(self, storedir, readonly, reserved_space):
955+        self.storedir = storedir
956+        self.readonly = readonly
957+        self.reserved_space = int(reserved_space)
958+        if self.reserved_space:
959+            if self.get_available_space() is None:
960+                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",
961+                        umid="0wZ27w", level=log.UNUSUAL)
962+
963+        self.sharedir = os.path.join(self.storedir, "shares")
964+        fileutil.make_dirs(self.sharedir)
965+        self.incomingdir = os.path.join(self.sharedir, 'incoming')
966+        self._clean_incomplete()
967+
968+    def _clean_incomplete(self):
969+        fileutil.rm_dir(self.incomingdir)
970+        fileutil.make_dirs(self.incomingdir)
971+
972+    def _setup_corruption_advisory(self):
973+        # we don't actually create the corruption-advisory dir until necessary
974+        self.corruption_advisory_dir = os.path.join(self.storedir,
975+                                                    "corruption-advisories")
976+
977+    def _setup_bucket_counter(self):
978+        statefname = os.path.join(self.storedir, "bucket_counter.state")
979+        self.bucket_counter = FSBucketCountingCrawler(statefname)
980+        self.bucket_counter.setServiceParent(self)
981+
982+    def _setup_lease_checkerf(self, expiration_policy):
983+        statefile = os.path.join(self.storedir, "lease_checker.state")
984+        historyfile = os.path.join(self.storedir, "lease_checker.history")
985+        self.lease_checker = FSLeaseCheckingCrawler(statefile, historyfile, expiration_policy)
986+        self.lease_checker.setServiceParent(self)
987+
988+    def get_available_space(self):
989+        if self.readonly:
990+            return 0
991+        return fileutil.get_available_space(self.storedir, self.reserved_space)
992+
993+    def get_shares(self, storage_index):
994+        """Return a list of the FSBShare objects that correspond to the passed storage_index."""
995+        finalstoragedir = os.path.join(self.sharedir, storage_index_to_dir(storage_index))
996+        try:
997+            for f in os.listdir(finalstoragedir):
998+                if NUM_RE.match(f):
999+                    filename = os.path.join(finalstoragedir, f)
1000+                    yield FSBShare(filename, int(f))
1001+        except OSError:
1002+            # Commonly caused by there being no buckets at all.
1003+            pass
1004+       
1005+    def make_bucket_writer(self, storage_index, shnum, max_space_per_bucket, lease_info, canary):
1006+        immsh = ImmutableShare(self.sharedir, storage_index, shnum, max_size=max_space_per_bucket, create=True)
1007+        bw = BucketWriter(self.ss, immsh, max_space_per_bucket, lease_info, canary)
1008+        return bw
1009+       
1010+
1011+# each share file (in storage/shares/$SI/$SHNUM) contains lease information
1012+# and share data. The share data is accessed by RIBucketWriter.write and
1013+# RIBucketReader.read . The lease information is not accessible through these
1014+# interfaces.
1015+
1016+# The share file has the following layout:
1017+#  0x00: share file version number, four bytes, current version is 1
1018+#  0x04: share data length, four bytes big-endian = A # See Footnote 1 below.
1019+#  0x08: number of leases, four bytes big-endian
1020+#  0x0c: beginning of share data (see immutable.layout.WriteBucketProxy)
1021+#  A+0x0c = B: first lease. Lease format is:
1022+#   B+0x00: owner number, 4 bytes big-endian, 0 is reserved for no-owner
1023+#   B+0x04: renew secret, 32 bytes (SHA256)
1024+#   B+0x24: cancel secret, 32 bytes (SHA256)
1025+#   B+0x44: expiration time, 4 bytes big-endian seconds-since-epoch
1026+#   B+0x48: next lease, or end of record
1027+
1028+# Footnote 1: as of Tahoe v1.3.0 this field is not used by storage servers,
1029+# but it is still filled in by storage servers in case the storage server
1030+# software gets downgraded from >= Tahoe v1.3.0 to < Tahoe v1.3.0, or the
1031+# share file is moved from one storage server to another. The value stored in
1032+# this field is truncated, so if the actual share data length is >= 2**32,
1033+# then the value stored in this field will be the actual share data length
1034+# modulo 2**32.
1035+
1036+class ImmutableShare:
1037+    LEASE_SIZE = struct.calcsize(">L32s32sL")
1038+    sharetype = "immutable"
1039+
1040+    def __init__(self, sharedir, storageindex, shnum, max_size=None, create=False):
1041+        """ If max_size is not None then I won't allow more than
1042+        max_size to be written to me. If create=True then max_size
1043+        must not be None. """
1044+        precondition((max_size is not None) or (not create), max_size, create)
1045+        self.shnum = shnum
1046+        self.fname = os.path.join(sharedir, storage_index_to_dir(storageindex), str(shnum))
1047+        self._max_size = max_size
1048+        if create:
1049+            # touch the file, so later callers will see that we're working on
1050+            # it. Also construct the metadata.
1051+            assert not os.path.exists(self.fname)
1052+            fileutil.make_dirs(os.path.dirname(self.fname))
1053+            f = open(self.fname, 'wb')
1054+            # The second field -- the four-byte share data length -- is no
1055+            # longer used as of Tahoe v1.3.0, but we continue to write it in
1056+            # there in case someone downgrades a storage server from >=
1057+            # Tahoe-1.3.0 to < Tahoe-1.3.0, or moves a share file from one
1058+            # server to another, etc. We do saturation -- a share data length
1059+            # larger than 2**32-1 (what can fit into the field) is marked as
1060+            # the largest length that can fit into the field. That way, even
1061+            # if this does happen, the old < v1.3.0 server will still allow
1062+            # clients to read the first part of the share.
1063+            f.write(struct.pack(">LLL", 1, min(2**32-1, max_size), 0))
1064+            f.close()
1065+            self._lease_offset = max_size + 0x0c
1066+            self._num_leases = 0
1067+        else:
1068+            f = open(self.fname, 'rb')
1069+            filesize = os.path.getsize(self.fname)
1070+            (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1071+            f.close()
1072+            if version != 1:
1073+                msg = "sharefile %s had version %d but we wanted 1" % \
1074+                      (self.fname, version)
1075+                raise UnknownImmutableContainerVersionError(msg)
1076+            self._num_leases = num_leases
1077+            self._lease_offset = filesize - (num_leases * self.LEASE_SIZE)
1078+        self._data_offset = 0xc
1079+
1080+    def unlink(self):
1081+        os.unlink(self.fname)
1082+
1083+    def read_share_data(self, offset, length):
1084+        precondition(offset >= 0)
1085+        # Reads beyond the end of the data are truncated. Reads that start
1086+        # beyond the end of the data return an empty string.
1087+        seekpos = self._data_offset+offset
1088+        fsize = os.path.getsize(self.fname)
1089+        actuallength = max(0, min(length, fsize-seekpos))
1090+        if actuallength == 0:
1091+            return ""
1092+        f = open(self.fname, 'rb')
1093+        f.seek(seekpos)
1094+        return f.read(actuallength)
1095+
1096+    def write_share_data(self, offset, data):
1097+        length = len(data)
1098+        precondition(offset >= 0, offset)
1099+        if self._max_size is not None and offset+length > self._max_size:
1100+            raise DataTooLargeError(self._max_size, offset, length)
1101+        f = open(self.fname, 'rb+')
1102+        real_offset = self._data_offset+offset
1103+        f.seek(real_offset)
1104+        assert f.tell() == real_offset
1105+        f.write(data)
1106+        f.close()
1107+
1108+    def _write_lease_record(self, f, lease_number, lease_info):
1109+        offset = self._lease_offset + lease_number * self.LEASE_SIZE
1110+        f.seek(offset)
1111+        assert f.tell() == offset
1112+        f.write(lease_info.to_immutable_data())
1113+
1114+    def _read_num_leases(self, f):
1115+        f.seek(0x08)
1116+        (num_leases,) = struct.unpack(">L", f.read(4))
1117+        return num_leases
1118+
1119+    def _write_num_leases(self, f, num_leases):
1120+        f.seek(0x08)
1121+        f.write(struct.pack(">L", num_leases))
1122+
1123+    def _truncate_leases(self, f, num_leases):
1124+        f.truncate(self._lease_offset + num_leases * self.LEASE_SIZE)
1125+
1126+    def get_leases(self):
1127+        """Yields a LeaseInfo instance for all leases."""
1128+        f = open(self.fname, 'rb')
1129+        (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1130+        f.seek(self._lease_offset)
1131+        for i in range(num_leases):
1132+            data = f.read(self.LEASE_SIZE)
1133+            if data:
1134+                yield LeaseInfo().from_immutable_data(data)
1135+
1136+    def add_lease(self, lease_info):
1137+        f = open(self.fname, 'rb+')
1138+        num_leases = self._read_num_leases(f)
1139+        self._write_lease_record(f, num_leases, lease_info)
1140+        self._write_num_leases(f, num_leases+1)
1141+        f.close()
1142+
1143+    def renew_lease(self, renew_secret, new_expire_time):
1144+        for i,lease in enumerate(self.get_leases()):
1145+            if constant_time_compare(lease.renew_secret, renew_secret):
1146+                # yup. See if we need to update the owner time.
1147+                if new_expire_time > lease.expiration_time:
1148+                    # yes
1149+                    lease.expiration_time = new_expire_time
1150+                    f = open(self.fname, 'rb+')
1151+                    self._write_lease_record(f, i, lease)
1152+                    f.close()
1153+                return
1154+        raise IndexError("unable to renew non-existent lease")
1155+
1156+    def add_or_renew_lease(self, lease_info):
1157+        try:
1158+            self.renew_lease(lease_info.renew_secret,
1159+                             lease_info.expiration_time)
1160+        except IndexError:
1161+            self.add_lease(lease_info)
1162+
1163+
1164+    def cancel_lease(self, cancel_secret):
1165+        """Remove a lease with the given cancel_secret. If the last lease is
1166+        cancelled, the file will be removed. Return the number of bytes that
1167+        were freed (by truncating the list of leases, and possibly by
1168+        deleting the file. Raise IndexError if there was no lease with the
1169+        given cancel_secret.
1170+        """
1171+
1172+        leases = list(self.get_leases())
1173+        num_leases_removed = 0
1174+        for i,lease in enumerate(leases):
1175+            if constant_time_compare(lease.cancel_secret, cancel_secret):
1176+                leases[i] = None
1177+                num_leases_removed += 1
1178+        if not num_leases_removed:
1179+            raise IndexError("unable to find matching lease to cancel")
1180+        if num_leases_removed:
1181+            # pack and write out the remaining leases. We write these out in
1182+            # the same order as they were added, so that if we crash while
1183+            # doing this, we won't lose any non-cancelled leases.
1184+            leases = [l for l in leases if l] # remove the cancelled leases
1185+            f = open(self.fname, 'rb+')
1186+            for i,lease in enumerate(leases):
1187+                self._write_lease_record(f, i, lease)
1188+            self._write_num_leases(f, len(leases))
1189+            self._truncate_leases(f, len(leases))
1190+            f.close()
1191+        space_freed = self.LEASE_SIZE * num_leases_removed
1192+        if not len(leases):
1193+            space_freed += os.stat(self.fname)[stat.ST_SIZE]
1194+            self.unlink()
1195+        return space_freed
1196hunk ./src/allmydata/storage/backends/das/expirer.py 2
1197 import time, os, pickle, struct
1198-from allmydata.storage.crawler import ShareCrawler
1199-from allmydata.storage.shares import get_share_file
1200+from allmydata.storage.crawler import FSShareCrawler
1201 from allmydata.storage.common import UnknownMutableContainerVersionError, \
1202      UnknownImmutableContainerVersionError
1203 from twisted.python import log as twlog
1204hunk ./src/allmydata/storage/backends/das/expirer.py 7
1205 
1206-class LeaseCheckingCrawler(ShareCrawler):
1207+class FSLeaseCheckingCrawler(FSShareCrawler):
1208     """I examine the leases on all shares, determining which are still valid
1209     and which have expired. I can remove the expired leases (if so
1210     configured), and the share will be deleted when the last lease is
1211hunk ./src/allmydata/storage/backends/das/expirer.py 50
1212     slow_start = 360 # wait 6 minutes after startup
1213     minimum_cycle_time = 12*60*60 # not more than twice per day
1214 
1215-    def __init__(self, statefile, historyfile,
1216-                 expiration_enabled, mode,
1217-                 override_lease_duration, # used if expiration_mode=="age"
1218-                 cutoff_date, # used if expiration_mode=="cutoff-date"
1219-                 sharetypes):
1220+    def __init__(self, statefile, historyfile, expiration_policy):
1221         self.historyfile = historyfile
1222hunk ./src/allmydata/storage/backends/das/expirer.py 52
1223-        self.expiration_enabled = expiration_enabled
1224-        self.mode = mode
1225+        self.expiration_enabled = expiration_policy['enabled']
1226+        self.mode = expiration_policy['mode']
1227         self.override_lease_duration = None
1228         self.cutoff_date = None
1229         if self.mode == "age":
1230hunk ./src/allmydata/storage/backends/das/expirer.py 57
1231-            assert isinstance(override_lease_duration, (int, type(None)))
1232-            self.override_lease_duration = override_lease_duration # seconds
1233+            assert isinstance(expiration_policy['override_lease_duration'], (int, type(None)))
1234+            self.override_lease_duration = expiration_policy['override_lease_duration']# seconds
1235         elif self.mode == "cutoff-date":
1236hunk ./src/allmydata/storage/backends/das/expirer.py 60
1237-            assert isinstance(cutoff_date, int) # seconds-since-epoch
1238+            assert isinstance(expiration_policy['cutoff_date'], int) # seconds-since-epoch
1239             assert cutoff_date is not None
1240hunk ./src/allmydata/storage/backends/das/expirer.py 62
1241-            self.cutoff_date = cutoff_date
1242+            self.cutoff_date = expiration_policy['cutoff_date']
1243         else:
1244hunk ./src/allmydata/storage/backends/das/expirer.py 64
1245-            raise ValueError("GC mode '%s' must be 'age' or 'cutoff-date'" % mode)
1246-        self.sharetypes_to_expire = sharetypes
1247-        ShareCrawler.__init__(self, statefile)
1248+            raise ValueError("GC mode '%s' must be 'age' or 'cutoff-date'" % expiration_policy['mode'])
1249+        self.sharetypes_to_expire = expiration_policy['sharetypes']
1250+        FSShareCrawler.__init__(self, statefile)
1251 
1252     def add_initial_state(self):
1253         # we fill ["cycle-to-date"] here (even though they will be reset in
1254hunk ./src/allmydata/storage/backends/das/expirer.py 156
1255 
1256     def process_share(self, sharefilename):
1257         # first, find out what kind of a share it is
1258-        sf = get_share_file(sharefilename)
1259+        f = open(sharefilename, "rb")
1260+        prefix = f.read(32)
1261+        f.close()
1262+        if prefix == MutableShareFile.MAGIC:
1263+            sf = MutableShareFile(sharefilename)
1264+        else:
1265+            # otherwise assume it's immutable
1266+            sf = FSBShare(sharefilename)
1267         sharetype = sf.sharetype
1268         now = time.time()
1269         s = self.stat(sharefilename)
1270addfile ./src/allmydata/storage/backends/null/__init__.py
1271addfile ./src/allmydata/storage/backends/null/core.py
1272hunk ./src/allmydata/storage/backends/null/core.py 1
1273+from allmydata.storage.backends.base import Backend
1274+
1275+class NullCore(Backend):
1276+    def __init__(self):
1277+        Backend.__init__(self)
1278+
1279+    def get_available_space(self):
1280+        return None
1281+
1282+    def get_shares(self, storage_index):
1283+        return set()
1284+
1285+    def get_share(self, storage_index, sharenum):
1286+        return None
1287+
1288+    def make_bucket_writer(self, storage_index, shnum, max_space_per_bucket, lease_info, canary):
1289+        return NullBucketWriter()
1290hunk ./src/allmydata/storage/crawler.py 12
1291 class TimeSliceExceeded(Exception):
1292     pass
1293 
1294-class ShareCrawler(service.MultiService):
1295+class FSShareCrawler(service.MultiService):
1296     """A subcless of ShareCrawler is attached to a StorageServer, and
1297     periodically walks all of its shares, processing each one in some
1298     fashion. This crawl is rate-limited, to reduce the IO burden on the host,
1299hunk ./src/allmydata/storage/crawler.py 68
1300     cpu_slice = 1.0 # use up to 1.0 seconds before yielding
1301     minimum_cycle_time = 300 # don't run a cycle faster than this
1302 
1303-    def __init__(self, backend, statefile, allowed_cpu_percentage=None):
1304+    def __init__(self, statefname, allowed_cpu_percentage=None):
1305         service.MultiService.__init__(self)
1306         if allowed_cpu_percentage is not None:
1307             self.allowed_cpu_percentage = allowed_cpu_percentage
1308hunk ./src/allmydata/storage/crawler.py 72
1309-        self.backend = backend
1310+        self.statefname = statefname
1311         self.prefixes = [si_b2a(struct.pack(">H", i << (16-10)))[:2]
1312                          for i in range(2**10)]
1313         self.prefixes.sort()
1314hunk ./src/allmydata/storage/crawler.py 192
1315         #                            of the last bucket to be processed, or
1316         #                            None if we are sleeping between cycles
1317         try:
1318-            f = open(self.statefile, "rb")
1319+            f = open(self.statefname, "rb")
1320             state = pickle.load(f)
1321             f.close()
1322         except EnvironmentError:
1323hunk ./src/allmydata/storage/crawler.py 230
1324         else:
1325             last_complete_prefix = self.prefixes[lcpi]
1326         self.state["last-complete-prefix"] = last_complete_prefix
1327-        tmpfile = self.statefile + ".tmp"
1328+        tmpfile = self.statefname + ".tmp"
1329         f = open(tmpfile, "wb")
1330         pickle.dump(self.state, f)
1331         f.close()
1332hunk ./src/allmydata/storage/crawler.py 433
1333         pass
1334 
1335 
1336-class BucketCountingCrawler(ShareCrawler):
1337+class FSBucketCountingCrawler(FSShareCrawler):
1338     """I keep track of how many buckets are being managed by this server.
1339     This is equivalent to the number of distributed files and directories for
1340     which I am providing storage. The actual number of files+directories in
1341hunk ./src/allmydata/storage/crawler.py 446
1342 
1343     minimum_cycle_time = 60*60 # we don't need this more than once an hour
1344 
1345-    def __init__(self, statefile, num_sample_prefixes=1):
1346-        ShareCrawler.__init__(self, statefile)
1347+    def __init__(self, statefname, num_sample_prefixes=1):
1348+        FSShareCrawler.__init__(self, statefname)
1349         self.num_sample_prefixes = num_sample_prefixes
1350 
1351     def add_initial_state(self):
1352hunk ./src/allmydata/storage/immutable.py 14
1353 from allmydata.storage.common import UnknownImmutableContainerVersionError, \
1354      DataTooLargeError
1355 
1356-# each share file (in storage/shares/$SI/$SHNUM) contains lease information
1357-# and share data. The share data is accessed by RIBucketWriter.write and
1358-# RIBucketReader.read . The lease information is not accessible through these
1359-# interfaces.
1360-
1361-# The share file has the following layout:
1362-#  0x00: share file version number, four bytes, current version is 1
1363-#  0x04: share data length, four bytes big-endian = A # See Footnote 1 below.
1364-#  0x08: number of leases, four bytes big-endian
1365-#  0x0c: beginning of share data (see immutable.layout.WriteBucketProxy)
1366-#  A+0x0c = B: first lease. Lease format is:
1367-#   B+0x00: owner number, 4 bytes big-endian, 0 is reserved for no-owner
1368-#   B+0x04: renew secret, 32 bytes (SHA256)
1369-#   B+0x24: cancel secret, 32 bytes (SHA256)
1370-#   B+0x44: expiration time, 4 bytes big-endian seconds-since-epoch
1371-#   B+0x48: next lease, or end of record
1372-
1373-# Footnote 1: as of Tahoe v1.3.0 this field is not used by storage servers,
1374-# but it is still filled in by storage servers in case the storage server
1375-# software gets downgraded from >= Tahoe v1.3.0 to < Tahoe v1.3.0, or the
1376-# share file is moved from one storage server to another. The value stored in
1377-# this field is truncated, so if the actual share data length is >= 2**32,
1378-# then the value stored in this field will be the actual share data length
1379-# modulo 2**32.
1380-
1381-class ShareFile:
1382-    LEASE_SIZE = struct.calcsize(">L32s32sL")
1383-    sharetype = "immutable"
1384-
1385-    def __init__(self, filename, max_size=None, create=False):
1386-        """ If max_size is not None then I won't allow more than
1387-        max_size to be written to me. If create=True then max_size
1388-        must not be None. """
1389-        precondition((max_size is not None) or (not create), max_size, create)
1390-        self.home = filename
1391-        self._max_size = max_size
1392-        if create:
1393-            # touch the file, so later callers will see that we're working on
1394-            # it. Also construct the metadata.
1395-            assert not os.path.exists(self.home)
1396-            fileutil.make_dirs(os.path.dirname(self.home))
1397-            f = open(self.home, 'wb')
1398-            # The second field -- the four-byte share data length -- is no
1399-            # longer used as of Tahoe v1.3.0, but we continue to write it in
1400-            # there in case someone downgrades a storage server from >=
1401-            # Tahoe-1.3.0 to < Tahoe-1.3.0, or moves a share file from one
1402-            # server to another, etc. We do saturation -- a share data length
1403-            # larger than 2**32-1 (what can fit into the field) is marked as
1404-            # the largest length that can fit into the field. That way, even
1405-            # if this does happen, the old < v1.3.0 server will still allow
1406-            # clients to read the first part of the share.
1407-            f.write(struct.pack(">LLL", 1, min(2**32-1, max_size), 0))
1408-            f.close()
1409-            self._lease_offset = max_size + 0x0c
1410-            self._num_leases = 0
1411-        else:
1412-            f = open(self.home, 'rb')
1413-            filesize = os.path.getsize(self.home)
1414-            (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1415-            f.close()
1416-            if version != 1:
1417-                msg = "sharefile %s had version %d but we wanted 1" % \
1418-                      (filename, version)
1419-                raise UnknownImmutableContainerVersionError(msg)
1420-            self._num_leases = num_leases
1421-            self._lease_offset = filesize - (num_leases * self.LEASE_SIZE)
1422-        self._data_offset = 0xc
1423-
1424-    def unlink(self):
1425-        os.unlink(self.home)
1426-
1427-    def read_share_data(self, offset, length):
1428-        precondition(offset >= 0)
1429-        # Reads beyond the end of the data are truncated. Reads that start
1430-        # beyond the end of the data return an empty string.
1431-        seekpos = self._data_offset+offset
1432-        fsize = os.path.getsize(self.home)
1433-        actuallength = max(0, min(length, fsize-seekpos))
1434-        if actuallength == 0:
1435-            return ""
1436-        f = open(self.home, 'rb')
1437-        f.seek(seekpos)
1438-        return f.read(actuallength)
1439-
1440-    def write_share_data(self, offset, data):
1441-        length = len(data)
1442-        precondition(offset >= 0, offset)
1443-        if self._max_size is not None and offset+length > self._max_size:
1444-            raise DataTooLargeError(self._max_size, offset, length)
1445-        f = open(self.home, 'rb+')
1446-        real_offset = self._data_offset+offset
1447-        f.seek(real_offset)
1448-        assert f.tell() == real_offset
1449-        f.write(data)
1450-        f.close()
1451-
1452-    def _write_lease_record(self, f, lease_number, lease_info):
1453-        offset = self._lease_offset + lease_number * self.LEASE_SIZE
1454-        f.seek(offset)
1455-        assert f.tell() == offset
1456-        f.write(lease_info.to_immutable_data())
1457-
1458-    def _read_num_leases(self, f):
1459-        f.seek(0x08)
1460-        (num_leases,) = struct.unpack(">L", f.read(4))
1461-        return num_leases
1462-
1463-    def _write_num_leases(self, f, num_leases):
1464-        f.seek(0x08)
1465-        f.write(struct.pack(">L", num_leases))
1466-
1467-    def _truncate_leases(self, f, num_leases):
1468-        f.truncate(self._lease_offset + num_leases * self.LEASE_SIZE)
1469-
1470-    def get_leases(self):
1471-        """Yields a LeaseInfo instance for all leases."""
1472-        f = open(self.home, 'rb')
1473-        (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1474-        f.seek(self._lease_offset)
1475-        for i in range(num_leases):
1476-            data = f.read(self.LEASE_SIZE)
1477-            if data:
1478-                yield LeaseInfo().from_immutable_data(data)
1479-
1480-    def add_lease(self, lease_info):
1481-        f = open(self.home, 'rb+')
1482-        num_leases = self._read_num_leases(f)
1483-        self._write_lease_record(f, num_leases, lease_info)
1484-        self._write_num_leases(f, num_leases+1)
1485-        f.close()
1486-
1487-    def renew_lease(self, renew_secret, new_expire_time):
1488-        for i,lease in enumerate(self.get_leases()):
1489-            if constant_time_compare(lease.renew_secret, renew_secret):
1490-                # yup. See if we need to update the owner time.
1491-                if new_expire_time > lease.expiration_time:
1492-                    # yes
1493-                    lease.expiration_time = new_expire_time
1494-                    f = open(self.home, 'rb+')
1495-                    self._write_lease_record(f, i, lease)
1496-                    f.close()
1497-                return
1498-        raise IndexError("unable to renew non-existent lease")
1499-
1500-    def add_or_renew_lease(self, lease_info):
1501-        try:
1502-            self.renew_lease(lease_info.renew_secret,
1503-                             lease_info.expiration_time)
1504-        except IndexError:
1505-            self.add_lease(lease_info)
1506-
1507-
1508-    def cancel_lease(self, cancel_secret):
1509-        """Remove a lease with the given cancel_secret. If the last lease is
1510-        cancelled, the file will be removed. Return the number of bytes that
1511-        were freed (by truncating the list of leases, and possibly by
1512-        deleting the file. Raise IndexError if there was no lease with the
1513-        given cancel_secret.
1514-        """
1515-
1516-        leases = list(self.get_leases())
1517-        num_leases_removed = 0
1518-        for i,lease in enumerate(leases):
1519-            if constant_time_compare(lease.cancel_secret, cancel_secret):
1520-                leases[i] = None
1521-                num_leases_removed += 1
1522-        if not num_leases_removed:
1523-            raise IndexError("unable to find matching lease to cancel")
1524-        if num_leases_removed:
1525-            # pack and write out the remaining leases. We write these out in
1526-            # the same order as they were added, so that if we crash while
1527-            # doing this, we won't lose any non-cancelled leases.
1528-            leases = [l for l in leases if l] # remove the cancelled leases
1529-            f = open(self.home, 'rb+')
1530-            for i,lease in enumerate(leases):
1531-                self._write_lease_record(f, i, lease)
1532-            self._write_num_leases(f, len(leases))
1533-            self._truncate_leases(f, len(leases))
1534-            f.close()
1535-        space_freed = self.LEASE_SIZE * num_leases_removed
1536-        if not len(leases):
1537-            space_freed += os.stat(self.home)[stat.ST_SIZE]
1538-            self.unlink()
1539-        return space_freed
1540-class NullBucketWriter(Referenceable):
1541-    implements(RIBucketWriter)
1542-
1543-    def remote_write(self, offset, data):
1544-        return
1545-
1546 class BucketWriter(Referenceable):
1547     implements(RIBucketWriter)
1548 
1549hunk ./src/allmydata/storage/immutable.py 17
1550-    def __init__(self, ss, incominghome, finalhome, max_size, lease_info, canary):
1551+    def __init__(self, ss, immutableshare, max_size, lease_info, canary):
1552         self.ss = ss
1553hunk ./src/allmydata/storage/immutable.py 19
1554-        self.incominghome = incominghome
1555-        self.finalhome = finalhome
1556         self._max_size = max_size # don't allow the client to write more than this
1557         self._canary = canary
1558         self._disconnect_marker = canary.notifyOnDisconnect(self._disconnected)
1559hunk ./src/allmydata/storage/immutable.py 24
1560         self.closed = False
1561         self.throw_out_all_data = False
1562-        self._sharefile = ShareFile(incominghome, create=True, max_size=max_size)
1563+        self._sharefile = immutableshare
1564         # also, add our lease to the file now, so that other ones can be
1565         # added by simultaneous uploaders
1566         self._sharefile.add_lease(lease_info)
1567hunk ./src/allmydata/storage/server.py 16
1568 from allmydata.storage.lease import LeaseInfo
1569 from allmydata.storage.mutable import MutableShareFile, EmptyShare, \
1570      create_mutable_sharefile
1571-from allmydata.storage.immutable import ShareFile, NullBucketWriter, BucketWriter, BucketReader
1572-from allmydata.storage.crawler import BucketCountingCrawler
1573-from allmydata.storage.expirer import LeaseCheckingCrawler
1574 
1575 from zope.interface import implements
1576 
1577hunk ./src/allmydata/storage/server.py 19
1578-# A Backend is a MultiService so that its server's crawlers (if the server has any) can
1579-# be started and stopped.
1580-class Backend(service.MultiService):
1581-    implements(IStatsProducer)
1582-    def __init__(self):
1583-        service.MultiService.__init__(self)
1584-
1585-    def get_bucket_shares(self):
1586-        """XXX"""
1587-        raise NotImplementedError
1588-
1589-    def get_share(self):
1590-        """XXX"""
1591-        raise NotImplementedError
1592-
1593-    def make_bucket_writer(self):
1594-        """XXX"""
1595-        raise NotImplementedError
1596-
1597-class NullBackend(Backend):
1598-    def __init__(self):
1599-        Backend.__init__(self)
1600-
1601-    def get_available_space(self):
1602-        return None
1603-
1604-    def get_bucket_shares(self, storage_index):
1605-        return set()
1606-
1607-    def get_share(self, storage_index, sharenum):
1608-        return None
1609-
1610-    def make_bucket_writer(self, storage_index, shnum, max_space_per_bucket, lease_info, canary):
1611-        return NullBucketWriter()
1612-
1613-class FSBackend(Backend):
1614-    def __init__(self, storedir, readonly=False, reserved_space=0):
1615-        Backend.__init__(self)
1616-
1617-        self._setup_storage(storedir, readonly, reserved_space)
1618-        self._setup_corruption_advisory()
1619-        self._setup_bucket_counter()
1620-        self._setup_lease_checkerf()
1621-
1622-    def _setup_storage(self, storedir, readonly, reserved_space):
1623-        self.storedir = storedir
1624-        self.readonly = readonly
1625-        self.reserved_space = int(reserved_space)
1626-        if self.reserved_space:
1627-            if self.get_available_space() is None:
1628-                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",
1629-                        umid="0wZ27w", level=log.UNUSUAL)
1630-
1631-        self.sharedir = os.path.join(self.storedir, "shares")
1632-        fileutil.make_dirs(self.sharedir)
1633-        self.incomingdir = os.path.join(self.sharedir, 'incoming')
1634-        self._clean_incomplete()
1635-
1636-    def _clean_incomplete(self):
1637-        fileutil.rm_dir(self.incomingdir)
1638-        fileutil.make_dirs(self.incomingdir)
1639-
1640-    def _setup_corruption_advisory(self):
1641-        # we don't actually create the corruption-advisory dir until necessary
1642-        self.corruption_advisory_dir = os.path.join(self.storedir,
1643-                                                    "corruption-advisories")
1644-
1645-    def _setup_bucket_counter(self):
1646-        statefile = os.path.join(self.storedir, "bucket_counter.state")
1647-        self.bucket_counter = BucketCountingCrawler(statefile)
1648-        self.bucket_counter.setServiceParent(self)
1649-
1650-    def _setup_lease_checkerf(self):
1651-        statefile = os.path.join(self.storedir, "lease_checker.state")
1652-        historyfile = os.path.join(self.storedir, "lease_checker.history")
1653-        self.lease_checker = LeaseCheckingCrawler(statefile, historyfile,
1654-                                   expiration_enabled, expiration_mode,
1655-                                   expiration_override_lease_duration,
1656-                                   expiration_cutoff_date,
1657-                                   expiration_sharetypes)
1658-        self.lease_checker.setServiceParent(self)
1659-
1660-    def get_available_space(self):
1661-        if self.readonly:
1662-            return 0
1663-        return fileutil.get_available_space(self.storedir, self.reserved_space)
1664-
1665-    def get_bucket_shares(self, storage_index):
1666-        """Return a list of (shnum, pathname) tuples for files that hold
1667-        shares for this storage_index. In each tuple, 'shnum' will always be
1668-        the integer form of the last component of 'pathname'."""
1669-        storagedir = os.path.join(self.sharedir, storage_index_to_dir(storage_index))
1670-        try:
1671-            for f in os.listdir(storagedir):
1672-                if NUM_RE.match(f):
1673-                    filename = os.path.join(storagedir, f)
1674-                    yield (int(f), filename)
1675-        except OSError:
1676-            # Commonly caused by there being no buckets at all.
1677-            pass
1678-
1679 # storage/
1680 # storage/shares/incoming
1681 #   incoming/ holds temp dirs named $START/$STORAGEINDEX/$SHARENUM which will
1682hunk ./src/allmydata/storage/server.py 32
1683 # $SHARENUM matches this regex:
1684 NUM_RE=re.compile("^[0-9]+$")
1685 
1686-
1687-
1688 class StorageServer(service.MultiService, Referenceable):
1689     implements(RIStorageServer, IStatsProducer)
1690     name = 'storage'
1691hunk ./src/allmydata/storage/server.py 35
1692-    LeaseCheckerClass = LeaseCheckingCrawler
1693 
1694     def __init__(self, nodeid, backend, reserved_space=0,
1695                  readonly_storage=False,
1696hunk ./src/allmydata/storage/server.py 38
1697-                 stats_provider=None,
1698-                 expiration_enabled=False,
1699-                 expiration_mode="age",
1700-                 expiration_override_lease_duration=None,
1701-                 expiration_cutoff_date=None,
1702-                 expiration_sharetypes=("mutable", "immutable")):
1703+                 stats_provider=None ):
1704         service.MultiService.__init__(self)
1705         assert isinstance(nodeid, str)
1706         assert len(nodeid) == 20
1707hunk ./src/allmydata/storage/server.py 217
1708         # they asked about: this will save them a lot of work. Add or update
1709         # leases for all of them: if they want us to hold shares for this
1710         # file, they'll want us to hold leases for this file.
1711-        for (shnum, fn) in self.backend.get_bucket_shares(storage_index):
1712-            alreadygot.add(shnum)
1713-            sf = ShareFile(fn)
1714-            sf.add_or_renew_lease(lease_info)
1715-
1716-        for shnum in sharenums:
1717-            share = self.backend.get_share(storage_index, shnum)
1718+        for share in self.backend.get_shares(storage_index):
1719+            alreadygot.add(share.shnum)
1720+            share.add_or_renew_lease(lease_info)
1721 
1722hunk ./src/allmydata/storage/server.py 221
1723-            if not share:
1724-                if (not limited) or (remaining_space >= max_space_per_bucket):
1725-                    # ok! we need to create the new share file.
1726-                    bw = self.backend.make_bucket_writer(storage_index, shnum,
1727-                                      max_space_per_bucket, lease_info, canary)
1728-                    bucketwriters[shnum] = bw
1729-                    self._active_writers[bw] = 1
1730-                    if limited:
1731-                        remaining_space -= max_space_per_bucket
1732-                else:
1733-                    # bummer! not enough space to accept this bucket
1734-                    pass
1735+        for shnum in (sharenums - alreadygot):
1736+            if (not limited) or (remaining_space >= max_space_per_bucket):
1737+                #XXX or should the following line occur in storage server construtor? ok! we need to create the new share file.
1738+                self.backend.set_storage_server(self)
1739+                bw = self.backend.make_bucket_writer(storage_index, shnum,
1740+                                                     max_space_per_bucket, lease_info, canary)
1741+                bucketwriters[shnum] = bw
1742+                self._active_writers[bw] = 1
1743+                if limited:
1744+                    remaining_space -= max_space_per_bucket
1745 
1746hunk ./src/allmydata/storage/server.py 232
1747-            elif share.is_complete():
1748-                # great! we already have it. easy.
1749-                pass
1750-            elif not share.is_complete():
1751-                # Note that we don't create BucketWriters for shnums that
1752-                # have a partial share (in incoming/), so if a second upload
1753-                # occurs while the first is still in progress, the second
1754-                # uploader will use different storage servers.
1755-                pass
1756+        #XXX We SHOULD DOCUMENT LATER.
1757 
1758         self.add_latency("allocate", time.time() - start)
1759         return alreadygot, bucketwriters
1760hunk ./src/allmydata/storage/server.py 238
1761 
1762     def _iter_share_files(self, storage_index):
1763-        for shnum, filename in self._get_bucket_shares(storage_index):
1764+        for shnum, filename in self._get_shares(storage_index):
1765             f = open(filename, 'rb')
1766             header = f.read(32)
1767             f.close()
1768hunk ./src/allmydata/storage/server.py 318
1769         si_s = si_b2a(storage_index)
1770         log.msg("storage: get_buckets %s" % si_s)
1771         bucketreaders = {} # k: sharenum, v: BucketReader
1772-        for shnum, filename in self.backend.get_bucket_shares(storage_index):
1773+        for shnum, filename in self.backend.get_shares(storage_index):
1774             bucketreaders[shnum] = BucketReader(self, filename,
1775                                                 storage_index, shnum)
1776         self.add_latency("get", time.time() - start)
1777hunk ./src/allmydata/storage/server.py 334
1778         # since all shares get the same lease data, we just grab the leases
1779         # from the first share
1780         try:
1781-            shnum, filename = self._get_bucket_shares(storage_index).next()
1782+            shnum, filename = self._get_shares(storage_index).next()
1783             sf = ShareFile(filename)
1784             return sf.get_leases()
1785         except StopIteration:
1786hunk ./src/allmydata/storage/shares.py 1
1787-#! /usr/bin/python
1788-
1789-from allmydata.storage.mutable import MutableShareFile
1790-from allmydata.storage.immutable import ShareFile
1791-
1792-def get_share_file(filename):
1793-    f = open(filename, "rb")
1794-    prefix = f.read(32)
1795-    f.close()
1796-    if prefix == MutableShareFile.MAGIC:
1797-        return MutableShareFile(filename)
1798-    # otherwise assume it's immutable
1799-    return ShareFile(filename)
1800-
1801rmfile ./src/allmydata/storage/shares.py
1802hunk ./src/allmydata/test/common_util.py 20
1803 
1804 def flip_one_bit(s, offset=0, size=None):
1805     """ flip one random bit of the string s, in a byte greater than or equal to offset and less
1806-    than offset+size. """
1807+    than offset+size. Return the new string. """
1808     if size is None:
1809         size=len(s)-offset
1810     i = randrange(offset, offset+size)
1811hunk ./src/allmydata/test/test_backends.py 7
1812 
1813 from allmydata.test.common_util import ReallyEqualMixin
1814 
1815-import mock
1816+import mock, os
1817 
1818 # This is the code that we're going to be testing.
1819hunk ./src/allmydata/test/test_backends.py 10
1820-from allmydata.storage.server import StorageServer, FSBackend, NullBackend
1821+from allmydata.storage.server import StorageServer
1822+
1823+from allmydata.storage.backends.das.core import DASCore
1824+from allmydata.storage.backends.null.core import NullCore
1825+
1826 
1827 # The following share file contents was generated with
1828 # storage.immutable.ShareFile from Tahoe-LAFS v1.8.2
1829hunk ./src/allmydata/test/test_backends.py 22
1830 share_data = 'a\x00\x00\x00\x00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy\x00(\xde\x80'
1831 share_file_data = '\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01' + share_data
1832 
1833-sharefname = 'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a/0'
1834+tempdir = 'teststoredir'
1835+sharedirname = os.path.join(tempdir, 'shares', 'or', 'orsxg5dtorxxeylhmvpws3temv4a')
1836+sharefname = os.path.join(sharedirname, '0')
1837 
1838 class TestServerConstruction(unittest.TestCase, ReallyEqualMixin):
1839     @mock.patch('time.time')
1840hunk ./src/allmydata/test/test_backends.py 58
1841         filesystem in only the prescribed ways. """
1842 
1843         def call_open(fname, mode):
1844-            if fname == 'testdir/bucket_counter.state':
1845-                raise IOError(2, "No such file or directory: 'testdir/bucket_counter.state'")
1846-            elif fname == 'testdir/lease_checker.state':
1847-                raise IOError(2, "No such file or directory: 'testdir/lease_checker.state'")
1848-            elif fname == 'testdir/lease_checker.history':
1849+            if fname == os.path.join(tempdir,'bucket_counter.state'):
1850+                raise IOError(2, "No such file or directory: '%s'" % os.path.join(tempdir, 'bucket_counter.state'))
1851+            elif fname == os.path.join(tempdir, 'lease_checker.state'):
1852+                raise IOError(2, "No such file or directory: '%s'" % os.path.join(tempdir, 'lease_checker.state'))
1853+            elif fname == os.path.join(tempdir, 'lease_checker.history'):
1854                 return StringIO()
1855             else:
1856                 self.fail("Server with FS backend tried to open '%s' in mode '%s'" % (fname, mode))
1857hunk ./src/allmydata/test/test_backends.py 124
1858     @mock.patch('__builtin__.open')
1859     def setUp(self, mockopen):
1860         def call_open(fname, mode):
1861-            if fname == 'testdir/bucket_counter.state':
1862-                raise IOError(2, "No such file or directory: 'testdir/bucket_counter.state'")
1863-            elif fname == 'testdir/lease_checker.state':
1864-                raise IOError(2, "No such file or directory: 'testdir/lease_checker.state'")
1865-            elif fname == 'testdir/lease_checker.history':
1866+            if fname == os.path.join(tempdir, 'bucket_counter.state'):
1867+                raise IOError(2, "No such file or directory: '%s'" % os.path.join(tempdir, 'bucket_counter.state'))
1868+            elif fname == os.path.join(tempdir, 'lease_checker.state'):
1869+                raise IOError(2, "No such file or directory: '%s'" % os.path.join(tempdir, 'lease_checker.state'))
1870+            elif fname == os.path.join(tempdir, 'lease_checker.history'):
1871                 return StringIO()
1872         mockopen.side_effect = call_open
1873hunk ./src/allmydata/test/test_backends.py 131
1874-
1875-        self.s = StorageServer('testnodeidxxxxxxxxxx', backend=FSBackend('teststoredir'))
1876+        expiration_policy = {'enabled' : False,
1877+                             'mode' : 'age',
1878+                             'override_lease_duration' : None,
1879+                             'cutoff_date' : None,
1880+                             'sharetypes' : None}
1881+        testbackend = DASCore(tempdir, expiration_policy)
1882+        self.s = StorageServer('testnodeidxxxxxxxxxx', backend=DASCore(tempdir, expiration_policy) )
1883 
1884     @mock.patch('time.time')
1885     @mock.patch('os.mkdir')
1886hunk ./src/allmydata/test/test_backends.py 148
1887         """ Write a new share. """
1888 
1889         def call_listdir(dirname):
1890-            self.failUnlessReallyEqual(dirname, 'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a')
1891-            raise OSError(2, "No such file or directory: 'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a'")
1892+            self.failUnlessReallyEqual(dirname, sharedirname)
1893+            raise OSError(2, "No such file or directory: '%s'" % os.path.join(tempdir, 'shares/or/orsxg5dtorxxeylhmvpws3temv4a'))
1894 
1895         mocklistdir.side_effect = call_listdir
1896 
1897hunk ./src/allmydata/test/test_backends.py 178
1898 
1899         sharefile = MockFile()
1900         def call_open(fname, mode):
1901-            self.failUnlessReallyEqual(fname, 'testdir/shares/incoming/or/orsxg5dtorxxeylhmvpws3temv4a/0' )
1902+            self.failUnlessReallyEqual(fname, os.path.join(tempdir, 'shares', 'or', 'orsxg5dtorxxeylhmvpws3temv4a', '0' ))
1903             return sharefile
1904 
1905         mockopen.side_effect = call_open
1906hunk ./src/allmydata/test/test_backends.py 200
1907         StorageServer object. """
1908 
1909         def call_listdir(dirname):
1910-            self.failUnlessReallyEqual(dirname,'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a')
1911+            self.failUnlessReallyEqual(dirname, os.path.join(tempdir, 'shares', 'or', 'orsxg5dtorxxeylhmvpws3temv4a'))
1912             return ['0']
1913 
1914         mocklistdir.side_effect = call_listdir
1915}
1916[checkpoint patch
1917wilcoxjg@gmail.com**20110626165715
1918 Ignore-this: fbfce2e8a1c1bb92715793b8ad6854d5
1919] {
1920hunk ./src/allmydata/storage/backends/das/core.py 21
1921 from allmydata.storage.lease import LeaseInfo
1922 from allmydata.storage.mutable import MutableShareFile, EmptyShare, \
1923      create_mutable_sharefile
1924-from allmydata.storage.backends.das.immutable import NullBucketWriter, BucketWriter, BucketReader
1925+from allmydata.storage.immutable import BucketWriter, BucketReader
1926 from allmydata.storage.crawler import FSBucketCountingCrawler
1927 from allmydata.storage.backends.das.expirer import FSLeaseCheckingCrawler
1928 
1929hunk ./src/allmydata/storage/backends/das/core.py 27
1930 from zope.interface import implements
1931 
1932+# $SHARENUM matches this regex:
1933+NUM_RE=re.compile("^[0-9]+$")
1934+
1935 class DASCore(Backend):
1936     implements(IStorageBackend)
1937     def __init__(self, storedir, expiration_policy, readonly=False, reserved_space=0):
1938hunk ./src/allmydata/storage/backends/das/core.py 80
1939         return fileutil.get_available_space(self.storedir, self.reserved_space)
1940 
1941     def get_shares(self, storage_index):
1942-        """Return a list of the FSBShare objects that correspond to the passed storage_index."""
1943+        """Return a list of the ImmutableShare objects that correspond to the passed storage_index."""
1944         finalstoragedir = os.path.join(self.sharedir, storage_index_to_dir(storage_index))
1945         try:
1946             for f in os.listdir(finalstoragedir):
1947hunk ./src/allmydata/storage/backends/das/core.py 86
1948                 if NUM_RE.match(f):
1949                     filename = os.path.join(finalstoragedir, f)
1950-                    yield FSBShare(filename, int(f))
1951+                    yield ImmutableShare(self.sharedir, storage_index, int(f))
1952         except OSError:
1953             # Commonly caused by there being no buckets at all.
1954             pass
1955hunk ./src/allmydata/storage/backends/das/core.py 95
1956         immsh = ImmutableShare(self.sharedir, storage_index, shnum, max_size=max_space_per_bucket, create=True)
1957         bw = BucketWriter(self.ss, immsh, max_space_per_bucket, lease_info, canary)
1958         return bw
1959+
1960+    def set_storage_server(self, ss):
1961+        self.ss = ss
1962         
1963 
1964 # each share file (in storage/shares/$SI/$SHNUM) contains lease information
1965hunk ./src/allmydata/storage/server.py 29
1966 # Where "$START" denotes the first 10 bits worth of $STORAGEINDEX (that's 2
1967 # base-32 chars).
1968 
1969-# $SHARENUM matches this regex:
1970-NUM_RE=re.compile("^[0-9]+$")
1971 
1972 class StorageServer(service.MultiService, Referenceable):
1973     implements(RIStorageServer, IStatsProducer)
1974}
1975[checkpoint4
1976wilcoxjg@gmail.com**20110628202202
1977 Ignore-this: 9778596c10bb066b58fc211f8c1707b7
1978] {
1979hunk ./src/allmydata/storage/backends/das/core.py 96
1980         bw = BucketWriter(self.ss, immsh, max_space_per_bucket, lease_info, canary)
1981         return bw
1982 
1983+    def make_bucket_reader(self, share):
1984+        return BucketReader(self.ss, share)
1985+
1986     def set_storage_server(self, ss):
1987         self.ss = ss
1988         
1989hunk ./src/allmydata/storage/backends/das/core.py 138
1990         must not be None. """
1991         precondition((max_size is not None) or (not create), max_size, create)
1992         self.shnum = shnum
1993+        self.storage_index = storageindex
1994         self.fname = os.path.join(sharedir, storage_index_to_dir(storageindex), str(shnum))
1995         self._max_size = max_size
1996         if create:
1997hunk ./src/allmydata/storage/backends/das/core.py 173
1998             self._lease_offset = filesize - (num_leases * self.LEASE_SIZE)
1999         self._data_offset = 0xc
2000 
2001+    def get_shnum(self):
2002+        return self.shnum
2003+
2004     def unlink(self):
2005         os.unlink(self.fname)
2006 
2007hunk ./src/allmydata/storage/backends/null/core.py 2
2008 from allmydata.storage.backends.base import Backend
2009+from allmydata.storage.immutable import BucketWriter, BucketReader
2010 
2011 class NullCore(Backend):
2012     def __init__(self):
2013hunk ./src/allmydata/storage/backends/null/core.py 17
2014     def get_share(self, storage_index, sharenum):
2015         return None
2016 
2017-    def make_bucket_writer(self, storage_index, shnum, max_space_per_bucket, lease_info, canary):
2018-        return NullBucketWriter()
2019+    def make_bucket_writer(self, storageindex, shnum, max_space_per_bucket, lease_info, canary):
2020+       
2021+        return BucketWriter(self.ss, immutableshare, max_space_per_bucket, lease_info, canary)
2022+
2023+    def set_storage_server(self, ss):
2024+        self.ss = ss
2025+
2026+class ImmutableShare:
2027+    sharetype = "immutable"
2028+
2029+    def __init__(self, sharedir, storageindex, shnum, max_size=None, create=False):
2030+        """ If max_size is not None then I won't allow more than
2031+        max_size to be written to me. If create=True then max_size
2032+        must not be None. """
2033+        precondition((max_size is not None) or (not create), max_size, create)
2034+        self.shnum = shnum
2035+        self.storage_index = storageindex
2036+        self.fname = os.path.join(sharedir, storage_index_to_dir(storageindex), str(shnum))
2037+        self._max_size = max_size
2038+        if create:
2039+            # touch the file, so later callers will see that we're working on
2040+            # it. Also construct the metadata.
2041+            assert not os.path.exists(self.fname)
2042+            fileutil.make_dirs(os.path.dirname(self.fname))
2043+            f = open(self.fname, 'wb')
2044+            # The second field -- the four-byte share data length -- is no
2045+            # longer used as of Tahoe v1.3.0, but we continue to write it in
2046+            # there in case someone downgrades a storage server from >=
2047+            # Tahoe-1.3.0 to < Tahoe-1.3.0, or moves a share file from one
2048+            # server to another, etc. We do saturation -- a share data length
2049+            # larger than 2**32-1 (what can fit into the field) is marked as
2050+            # the largest length that can fit into the field. That way, even
2051+            # if this does happen, the old < v1.3.0 server will still allow
2052+            # clients to read the first part of the share.
2053+            f.write(struct.pack(">LLL", 1, min(2**32-1, max_size), 0))
2054+            f.close()
2055+            self._lease_offset = max_size + 0x0c
2056+            self._num_leases = 0
2057+        else:
2058+            f = open(self.fname, 'rb')
2059+            filesize = os.path.getsize(self.fname)
2060+            (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
2061+            f.close()
2062+            if version != 1:
2063+                msg = "sharefile %s had version %d but we wanted 1" % \
2064+                      (self.fname, version)
2065+                raise UnknownImmutableContainerVersionError(msg)
2066+            self._num_leases = num_leases
2067+            self._lease_offset = filesize - (num_leases * self.LEASE_SIZE)
2068+        self._data_offset = 0xc
2069+
2070+    def get_shnum(self):
2071+        return self.shnum
2072+
2073+    def unlink(self):
2074+        os.unlink(self.fname)
2075+
2076+    def read_share_data(self, offset, length):
2077+        precondition(offset >= 0)
2078+        # Reads beyond the end of the data are truncated. Reads that start
2079+        # beyond the end of the data return an empty string.
2080+        seekpos = self._data_offset+offset
2081+        fsize = os.path.getsize(self.fname)
2082+        actuallength = max(0, min(length, fsize-seekpos))
2083+        if actuallength == 0:
2084+            return ""
2085+        f = open(self.fname, 'rb')
2086+        f.seek(seekpos)
2087+        return f.read(actuallength)
2088+
2089+    def write_share_data(self, offset, data):
2090+        length = len(data)
2091+        precondition(offset >= 0, offset)
2092+        if self._max_size is not None and offset+length > self._max_size:
2093+            raise DataTooLargeError(self._max_size, offset, length)
2094+        f = open(self.fname, 'rb+')
2095+        real_offset = self._data_offset+offset
2096+        f.seek(real_offset)
2097+        assert f.tell() == real_offset
2098+        f.write(data)
2099+        f.close()
2100+
2101+    def _write_lease_record(self, f, lease_number, lease_info):
2102+        offset = self._lease_offset + lease_number * self.LEASE_SIZE
2103+        f.seek(offset)
2104+        assert f.tell() == offset
2105+        f.write(lease_info.to_immutable_data())
2106+
2107+    def _read_num_leases(self, f):
2108+        f.seek(0x08)
2109+        (num_leases,) = struct.unpack(">L", f.read(4))
2110+        return num_leases
2111+
2112+    def _write_num_leases(self, f, num_leases):
2113+        f.seek(0x08)
2114+        f.write(struct.pack(">L", num_leases))
2115+
2116+    def _truncate_leases(self, f, num_leases):
2117+        f.truncate(self._lease_offset + num_leases * self.LEASE_SIZE)
2118+
2119+    def get_leases(self):
2120+        """Yields a LeaseInfo instance for all leases."""
2121+        f = open(self.fname, 'rb')
2122+        (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
2123+        f.seek(self._lease_offset)
2124+        for i in range(num_leases):
2125+            data = f.read(self.LEASE_SIZE)
2126+            if data:
2127+                yield LeaseInfo().from_immutable_data(data)
2128+
2129+    def add_lease(self, lease_info):
2130+        f = open(self.fname, 'rb+')
2131+        num_leases = self._read_num_leases(f)
2132+        self._write_lease_record(f, num_leases, lease_info)
2133+        self._write_num_leases(f, num_leases+1)
2134+        f.close()
2135+
2136+    def renew_lease(self, renew_secret, new_expire_time):
2137+        for i,lease in enumerate(self.get_leases()):
2138+            if constant_time_compare(lease.renew_secret, renew_secret):
2139+                # yup. See if we need to update the owner time.
2140+                if new_expire_time > lease.expiration_time:
2141+                    # yes
2142+                    lease.expiration_time = new_expire_time
2143+                    f = open(self.fname, 'rb+')
2144+                    self._write_lease_record(f, i, lease)
2145+                    f.close()
2146+                return
2147+        raise IndexError("unable to renew non-existent lease")
2148+
2149+    def add_or_renew_lease(self, lease_info):
2150+        try:
2151+            self.renew_lease(lease_info.renew_secret,
2152+                             lease_info.expiration_time)
2153+        except IndexError:
2154+            self.add_lease(lease_info)
2155+
2156+
2157+    def cancel_lease(self, cancel_secret):
2158+        """Remove a lease with the given cancel_secret. If the last lease is
2159+        cancelled, the file will be removed. Return the number of bytes that
2160+        were freed (by truncating the list of leases, and possibly by
2161+        deleting the file. Raise IndexError if there was no lease with the
2162+        given cancel_secret.
2163+        """
2164+
2165+        leases = list(self.get_leases())
2166+        num_leases_removed = 0
2167+        for i,lease in enumerate(leases):
2168+            if constant_time_compare(lease.cancel_secret, cancel_secret):
2169+                leases[i] = None
2170+                num_leases_removed += 1
2171+        if not num_leases_removed:
2172+            raise IndexError("unable to find matching lease to cancel")
2173+        if num_leases_removed:
2174+            # pack and write out the remaining leases. We write these out in
2175+            # the same order as they were added, so that if we crash while
2176+            # doing this, we won't lose any non-cancelled leases.
2177+            leases = [l for l in leases if l] # remove the cancelled leases
2178+            f = open(self.fname, 'rb+')
2179+            for i,lease in enumerate(leases):
2180+                self._write_lease_record(f, i, lease)
2181+            self._write_num_leases(f, len(leases))
2182+            self._truncate_leases(f, len(leases))
2183+            f.close()
2184+        space_freed = self.LEASE_SIZE * num_leases_removed
2185+        if not len(leases):
2186+            space_freed += os.stat(self.fname)[stat.ST_SIZE]
2187+            self.unlink()
2188+        return space_freed
2189hunk ./src/allmydata/storage/immutable.py 114
2190 class BucketReader(Referenceable):
2191     implements(RIBucketReader)
2192 
2193-    def __init__(self, ss, sharefname, storage_index=None, shnum=None):
2194+    def __init__(self, ss, share):
2195         self.ss = ss
2196hunk ./src/allmydata/storage/immutable.py 116
2197-        self._share_file = ShareFile(sharefname)
2198-        self.storage_index = storage_index
2199-        self.shnum = shnum
2200+        self._share_file = share
2201+        self.storage_index = share.storage_index
2202+        self.shnum = share.shnum
2203 
2204     def __repr__(self):
2205         return "<%s %s %s>" % (self.__class__.__name__,
2206hunk ./src/allmydata/storage/server.py 316
2207         si_s = si_b2a(storage_index)
2208         log.msg("storage: get_buckets %s" % si_s)
2209         bucketreaders = {} # k: sharenum, v: BucketReader
2210-        for shnum, filename in self.backend.get_shares(storage_index):
2211-            bucketreaders[shnum] = BucketReader(self, filename,
2212-                                                storage_index, shnum)
2213+        self.backend.set_storage_server(self)
2214+        for share in self.backend.get_shares(storage_index):
2215+            bucketreaders[share.get_shnum()] = self.backend.make_bucket_reader(share)
2216         self.add_latency("get", time.time() - start)
2217         return bucketreaders
2218 
2219hunk ./src/allmydata/test/test_backends.py 25
2220 tempdir = 'teststoredir'
2221 sharedirname = os.path.join(tempdir, 'shares', 'or', 'orsxg5dtorxxeylhmvpws3temv4a')
2222 sharefname = os.path.join(sharedirname, '0')
2223+expiration_policy = {'enabled' : False,
2224+                     'mode' : 'age',
2225+                     'override_lease_duration' : None,
2226+                     'cutoff_date' : None,
2227+                     'sharetypes' : None}
2228 
2229 class TestServerConstruction(unittest.TestCase, ReallyEqualMixin):
2230     @mock.patch('time.time')
2231hunk ./src/allmydata/test/test_backends.py 43
2232         tries to read or write to the file system. """
2233 
2234         # Now begin the test.
2235-        s = StorageServer('testnodeidxxxxxxxxxx', backend=NullBackend())
2236+        s = StorageServer('testnodeidxxxxxxxxxx', backend=NullCore())
2237 
2238         self.failIf(mockisdir.called)
2239         self.failIf(mocklistdir.called)
2240hunk ./src/allmydata/test/test_backends.py 74
2241         mockopen.side_effect = call_open
2242 
2243         # Now begin the test.
2244-        s = StorageServer('testnodeidxxxxxxxxxx', backend=FSBackend('teststoredir'))
2245+        s = StorageServer('testnodeidxxxxxxxxxx', backend=DASCore('teststoredir', expiration_policy))
2246 
2247         self.failIf(mockisdir.called)
2248         self.failIf(mocklistdir.called)
2249hunk ./src/allmydata/test/test_backends.py 86
2250 
2251 class TestServerNullBackend(unittest.TestCase, ReallyEqualMixin):
2252     def setUp(self):
2253-        self.s = StorageServer('testnodeidxxxxxxxxxx', backend=NullBackend())
2254+        self.s = StorageServer('testnodeidxxxxxxxxxx', backend=NullCore())
2255 
2256     @mock.patch('os.mkdir')
2257     @mock.patch('__builtin__.open')
2258hunk ./src/allmydata/test/test_backends.py 136
2259             elif fname == os.path.join(tempdir, 'lease_checker.history'):
2260                 return StringIO()
2261         mockopen.side_effect = call_open
2262-        expiration_policy = {'enabled' : False,
2263-                             'mode' : 'age',
2264-                             'override_lease_duration' : None,
2265-                             'cutoff_date' : None,
2266-                             'sharetypes' : None}
2267         testbackend = DASCore(tempdir, expiration_policy)
2268         self.s = StorageServer('testnodeidxxxxxxxxxx', backend=DASCore(tempdir, expiration_policy) )
2269 
2270}
2271
2272Context:
2273
2274[Makefile: add 'make check' as an alias for 'make test'. Also remove an unnecessary dependency of 'test' on 'build' and 'src/allmydata/_version.py'. fixes #1344
2275david-sarah@jacaranda.org**20110623205528
2276 Ignore-this: c63e23146c39195de52fb17c7c49b2da
2277]
2278[Rename test_package_initialization.py to (much shorter) test_import.py .
2279Brian Warner <warner@lothar.com>**20110611190234
2280 Ignore-this: 3eb3dbac73600eeff5cfa6b65d65822
2281 
2282 The former name was making my 'ls' listings hard to read, by forcing them
2283 down to just two columns.
2284]
2285[tests: fix tests to accomodate [20110611153758-92b7f-0ba5e4726fb6318dac28fb762a6512a003f4c430]
2286zooko@zooko.com**20110611163741
2287 Ignore-this: 64073a5f39e7937e8e5e1314c1a302d1
2288 Apparently none of the two authors (stercor, terrell), three reviewers (warner, davidsarah, terrell), or one committer (me) actually ran the tests. This is presumably due to #20.
2289 fixes #1412
2290]
2291[wui: right-align the size column in the WUI
2292zooko@zooko.com**20110611153758
2293 Ignore-this: 492bdaf4373c96f59f90581c7daf7cd7
2294 Thanks to Ted "stercor" Rolle Jr. and Terrell Russell.
2295 fixes #1412
2296]
2297[docs: three minor fixes
2298zooko@zooko.com**20110610121656
2299 Ignore-this: fec96579eb95aceb2ad5fc01a814c8a2
2300 CREDITS for arc for stats tweak
2301 fix link to .zip file in quickstart.rst (thanks to ChosenOne for noticing)
2302 English usage tweak
2303]
2304[docs/running.rst: fix stray HTML (not .rst) link noticed by ChosenOne.
2305david-sarah@jacaranda.org**20110609223719
2306 Ignore-this: fc50ac9c94792dcac6f1067df8ac0d4a
2307]
2308[server.py:  get_latencies now reports percentiles _only_ if there are sufficient observations for the interpretation of the percentile to be unambiguous.
2309wilcoxjg@gmail.com**20110527120135
2310 Ignore-this: 2e7029764bffc60e26f471d7c2b6611e
2311 interfaces.py:  modified the return type of RIStatsProvider.get_stats to allow for None as a return value
2312 NEWS.rst, stats.py: documentation of change to get_latencies
2313 stats.rst: now documents percentile modification in get_latencies
2314 test_storage.py:  test_latencies now expects None in output categories that contain too few samples for the associated percentile to be unambiguously reported.
2315 fixes #1392
2316]
2317[docs: revert link in relnotes.txt from NEWS.rst to NEWS, since the former did not exist at revision 5000.
2318david-sarah@jacaranda.org**20110517011214
2319 Ignore-this: 6a5be6e70241e3ec0575641f64343df7
2320]
2321[docs: convert NEWS to NEWS.rst and change all references to it.
2322david-sarah@jacaranda.org**20110517010255
2323 Ignore-this: a820b93ea10577c77e9c8206dbfe770d
2324]
2325[docs: remove out-of-date docs/testgrid/introducer.furl and containing directory. fixes #1404
2326david-sarah@jacaranda.org**20110512140559
2327 Ignore-this: 784548fc5367fac5450df1c46890876d
2328]
2329[scripts/common.py: don't assume that the default alias is always 'tahoe' (it is, but the API of get_alias doesn't say so). refs #1342
2330david-sarah@jacaranda.org**20110130164923
2331 Ignore-this: a271e77ce81d84bb4c43645b891d92eb
2332]
2333[setup: don't catch all Exception from check_requirement(), but only PackagingError and ImportError
2334zooko@zooko.com**20110128142006
2335 Ignore-this: 57d4bc9298b711e4bc9dc832c75295de
2336 I noticed this because I had accidentally inserted a bug which caused AssertionError to be raised from check_requirement().
2337]
2338[M-x whitespace-cleanup
2339zooko@zooko.com**20110510193653
2340 Ignore-this: dea02f831298c0f65ad096960e7df5c7
2341]
2342[docs: fix typo in running.rst, thanks to arch_o_median
2343zooko@zooko.com**20110510193633
2344 Ignore-this: ca06de166a46abbc61140513918e79e8
2345]
2346[relnotes.txt: don't claim to work on Cygwin (which has been untested for some time). refs #1342
2347david-sarah@jacaranda.org**20110204204902
2348 Ignore-this: 85ef118a48453d93fa4cddc32d65b25b
2349]
2350[relnotes.txt: forseeable -> foreseeable. refs #1342
2351david-sarah@jacaranda.org**20110204204116
2352 Ignore-this: 746debc4d82f4031ebf75ab4031b3a9
2353]
2354[replace remaining .html docs with .rst docs
2355zooko@zooko.com**20110510191650
2356 Ignore-this: d557d960a986d4ac8216d1677d236399
2357 Remove install.html (long since deprecated).
2358 Also replace some obsolete references to install.html with references to quickstart.rst.
2359 Fix some broken internal references within docs/historical/historical_known_issues.txt.
2360 Thanks to Ravi Pinjala and Patrick McDonald.
2361 refs #1227
2362]
2363[docs: FTP-and-SFTP.rst: fix a minor error and update the information about which version of Twisted fixes #1297
2364zooko@zooko.com**20110428055232
2365 Ignore-this: b63cfb4ebdbe32fb3b5f885255db4d39
2366]
2367[munin tahoe_files plugin: fix incorrect file count
2368francois@ctrlaltdel.ch**20110428055312
2369 Ignore-this: 334ba49a0bbd93b4a7b06a25697aba34
2370 fixes #1391
2371]
2372[corrected "k must never be smaller than N" to "k must never be greater than N"
2373secorp@allmydata.org**20110425010308
2374 Ignore-this: 233129505d6c70860087f22541805eac
2375]
2376[Fix a test failure in test_package_initialization on Python 2.4.x due to exceptions being stringified differently than in later versions of Python. refs #1389
2377david-sarah@jacaranda.org**20110411190738
2378 Ignore-this: 7847d26bc117c328c679f08a7baee519
2379]
2380[tests: add test for including the ImportError message and traceback entry in the summary of errors from importing dependencies. refs #1389
2381david-sarah@jacaranda.org**20110410155844
2382 Ignore-this: fbecdbeb0d06a0f875fe8d4030aabafa
2383]
2384[allmydata/__init__.py: preserve the message and last traceback entry (file, line number, function, and source line) of ImportErrors in the package versions string. fixes #1389
2385david-sarah@jacaranda.org**20110410155705
2386 Ignore-this: 2f87b8b327906cf8bfca9440a0904900
2387]
2388[remove unused variable detected by pyflakes
2389zooko@zooko.com**20110407172231
2390 Ignore-this: 7344652d5e0720af822070d91f03daf9
2391]
2392[allmydata/__init__.py: Nicer reporting of unparseable version numbers in dependencies. fixes #1388
2393david-sarah@jacaranda.org**20110401202750
2394 Ignore-this: 9c6bd599259d2405e1caadbb3e0d8c7f
2395]
2396[update FTP-and-SFTP.rst: the necessary patch is included in Twisted-10.1
2397Brian Warner <warner@lothar.com>**20110325232511
2398 Ignore-this: d5307faa6900f143193bfbe14e0f01a
2399]
2400[control.py: remove all uses of s.get_serverid()
2401warner@lothar.com**20110227011203
2402 Ignore-this: f80a787953bd7fa3d40e828bde00e855
2403]
2404[web: remove some uses of s.get_serverid(), not all
2405warner@lothar.com**20110227011159
2406 Ignore-this: a9347d9cf6436537a47edc6efde9f8be
2407]
2408[immutable/downloader/fetcher.py: remove all get_serverid() calls
2409warner@lothar.com**20110227011156
2410 Ignore-this: fb5ef018ade1749348b546ec24f7f09a
2411]
2412[immutable/downloader/fetcher.py: fix diversity bug in server-response handling
2413warner@lothar.com**20110227011153
2414 Ignore-this: bcd62232c9159371ae8a16ff63d22c1b
2415 
2416 When blocks terminate (either COMPLETE or CORRUPT/DEAD/BADSEGNUM), the
2417 _shares_from_server dict was being popped incorrectly (using shnum as the
2418 index instead of serverid). I'm still thinking through the consequences of
2419 this bug. It was probably benign and really hard to detect. I think it would
2420 cause us to incorrectly believe that we're pulling too many shares from a
2421 server, and thus prefer a different server rather than asking for a second
2422 share from the first server. The diversity code is intended to spread out the
2423 number of shares simultaneously being requested from each server, but with
2424 this bug, it might be spreading out the total number of shares requested at
2425 all, not just simultaneously. (note that SegmentFetcher is scoped to a single
2426 segment, so the effect doesn't last very long).
2427]
2428[immutable/downloader/share.py: reduce get_serverid(), one left, update ext deps
2429warner@lothar.com**20110227011150
2430 Ignore-this: d8d56dd8e7b280792b40105e13664554
2431 
2432 test_download.py: create+check MyShare instances better, make sure they share
2433 Server objects, now that finder.py cares
2434]
2435[immutable/downloader/finder.py: reduce use of get_serverid(), one left
2436warner@lothar.com**20110227011146
2437 Ignore-this: 5785be173b491ae8a78faf5142892020
2438]
2439[immutable/offloaded.py: reduce use of get_serverid() a bit more
2440warner@lothar.com**20110227011142
2441 Ignore-this: b48acc1b2ae1b311da7f3ba4ffba38f
2442]
2443[immutable/upload.py: reduce use of get_serverid()
2444warner@lothar.com**20110227011138
2445 Ignore-this: ffdd7ff32bca890782119a6e9f1495f6
2446]
2447[immutable/checker.py: remove some uses of s.get_serverid(), not all
2448warner@lothar.com**20110227011134
2449 Ignore-this: e480a37efa9e94e8016d826c492f626e
2450]
2451[add remaining get_* methods to storage_client.Server, NoNetworkServer, and
2452warner@lothar.com**20110227011132
2453 Ignore-this: 6078279ddf42b179996a4b53bee8c421
2454 MockIServer stubs
2455]
2456[upload.py: rearrange _make_trackers a bit, no behavior changes
2457warner@lothar.com**20110227011128
2458 Ignore-this: 296d4819e2af452b107177aef6ebb40f
2459]
2460[happinessutil.py: finally rename merge_peers to merge_servers
2461warner@lothar.com**20110227011124
2462 Ignore-this: c8cd381fea1dd888899cb71e4f86de6e
2463]
2464[test_upload.py: factor out FakeServerTracker
2465warner@lothar.com**20110227011120
2466 Ignore-this: 6c182cba90e908221099472cc159325b
2467]
2468[test_upload.py: server-vs-tracker cleanup
2469warner@lothar.com**20110227011115
2470 Ignore-this: 2915133be1a3ba456e8603885437e03
2471]
2472[happinessutil.py: server-vs-tracker cleanup
2473warner@lothar.com**20110227011111
2474 Ignore-this: b856c84033562d7d718cae7cb01085a9
2475]
2476[upload.py: more tracker-vs-server cleanup
2477warner@lothar.com**20110227011107
2478 Ignore-this: bb75ed2afef55e47c085b35def2de315
2479]
2480[upload.py: fix var names to avoid confusion between 'trackers' and 'servers'
2481warner@lothar.com**20110227011103
2482 Ignore-this: 5d5e3415b7d2732d92f42413c25d205d
2483]
2484[refactor: s/peer/server/ in immutable/upload, happinessutil.py, test_upload
2485warner@lothar.com**20110227011100
2486 Ignore-this: 7ea858755cbe5896ac212a925840fe68
2487 
2488 No behavioral changes, just updating variable/method names and log messages.
2489 The effects outside these three files should be minimal: some exception
2490 messages changed (to say "server" instead of "peer"), and some internal class
2491 names were changed. A few things still use "peer" to minimize external
2492 changes, like UploadResults.timings["peer_selection"] and
2493 happinessutil.merge_peers, which can be changed later.
2494]
2495[storage_client.py: clean up test_add_server/test_add_descriptor, remove .test_servers
2496warner@lothar.com**20110227011056
2497 Ignore-this: efad933e78179d3d5fdcd6d1ef2b19cc
2498]
2499[test_client.py, upload.py:: remove KiB/MiB/etc constants, and other dead code
2500warner@lothar.com**20110227011051
2501 Ignore-this: dc83c5794c2afc4f81e592f689c0dc2d
2502]
2503[test: increase timeout on a network test because Francois's ARM machine hit that timeout
2504zooko@zooko.com**20110317165909
2505 Ignore-this: 380c345cdcbd196268ca5b65664ac85b
2506 I'm skeptical that the test was proceeding correctly but ran out of time. It seems more likely that it had gotten hung. But if we raise the timeout to an even more extravagant number then we can be even more certain that the test was never going to finish.
2507]
2508[docs/configuration.rst: add a "Frontend Configuration" section
2509Brian Warner <warner@lothar.com>**20110222014323
2510 Ignore-this: 657018aa501fe4f0efef9851628444ca
2511 
2512 this points to docs/frontends/*.rst, which were previously underlinked
2513]
2514[web/filenode.py: avoid calling req.finish() on closed HTTP connections. Closes #1366
2515"Brian Warner <warner@lothar.com>"**20110221061544
2516 Ignore-this: 799d4de19933f2309b3c0c19a63bb888
2517]
2518[Add unit tests for cross_check_pkg_resources_versus_import, and a regression test for ref #1355. This requires a little refactoring to make it testable.
2519david-sarah@jacaranda.org**20110221015817
2520 Ignore-this: 51d181698f8c20d3aca58b057e9c475a
2521]
2522[allmydata/__init__.py: .name was used in place of the correct .__name__ when printing an exception. Also, robustify string formatting by using %r instead of %s in some places. fixes #1355.
2523david-sarah@jacaranda.org**20110221020125
2524 Ignore-this: b0744ed58f161bf188e037bad077fc48
2525]
2526[Refactor StorageFarmBroker handling of servers
2527Brian Warner <warner@lothar.com>**20110221015804
2528 Ignore-this: 842144ed92f5717699b8f580eab32a51
2529 
2530 Pass around IServer instance instead of (peerid, rref) tuple. Replace
2531 "descriptor" with "server". Other replacements:
2532 
2533  get_all_servers -> get_connected_servers/get_known_servers
2534  get_servers_for_index -> get_servers_for_psi (now returns IServers)
2535 
2536 This change still needs to be pushed further down: lots of code is now
2537 getting the IServer and then distributing (peerid, rref) internally.
2538 Instead, it ought to distribute the IServer internally and delay
2539 extracting a serverid or rref until the last moment.
2540 
2541 no_network.py was updated to retain parallelism.
2542]
2543[TAG allmydata-tahoe-1.8.2
2544warner@lothar.com**20110131020101]
2545Patch bundle hash:
2546b38fc06b9d1020dc478d04dfdfad49ba834d299a