Update backend code and cumulus-sync for the new backup layout.
[cumulus.git] / python / cumulus / store / s3.py
1 # Cumulus: Efficient Filesystem Backup to the Cloud
2 # Copyright (C) 2008-2010 The Cumulus Developers
3 # See the AUTHORS file for a list of contributors.
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
19 """Amazon S3 storage backend.  Uses a URL of the form s3://BUCKET/PATH/."""
20
21 import os, sys, tempfile
22 import boto
23 from boto.exception import S3ResponseError
24 from boto.s3.bucket import Bucket
25 from boto.s3.key import Key
26
27 import cumulus.store
28
29 def throw_notfound(method):
30     """Decorator to convert a 404 error into a cumulus.store.NoutFoundError."""
31     def f(*args, **kwargs):
32         try:
33             return method(*args, **kwargs)
34         except S3ResponseError as e:
35             if e.status == 404:
36                 print "Got a 404:", e
37                 raise cumulus.store.NotFoundError(e)
38             else:
39                 raise
40     return f
41
42 class S3Store(cumulus.store.Store):
43     def __init__(self, url, **kw):
44         # Old versions of the Python urlparse library will take a URL like
45         # s3://bucket/path/ and include the bucket with the path, while new
46         # versions (2.6 and later) treat it as the netloc (which seems more
47         # correct).
48         #
49         # But, so that we can work with either behavior, for now just combine
50         # the netloc and path together before we do any further processing
51         # (which will then split the combined path apart into a bucket and path
52         # again).  If we didn't want to support Python 2.5, this would be
53         # easier as we could just use the netloc as the bucket directly.
54         path = self.netloc + '/' + self.path
55         (bucket, prefix) = path.lstrip("/").split("/", 1)
56         self.conn = boto.connect_s3(is_secure=False)
57         self.bucket = self.conn.create_bucket(bucket)
58         self.prefix = prefix.strip("/")
59         self.scan_cache = {}
60
61     def _get_key(self, path):
62         k = Key(self.bucket)
63         k.key = "%s/%s" % (self.prefix, path)
64         return k
65
66     @throw_notfound
67     def scan(self, path):
68         prefix = "%s/%s/" % (self.prefix, path)
69         for i in self.bucket.list(prefix):
70             assert i.key.startswith(prefix)
71             self.scan_cache[i.key] = i
72
73     @throw_notfound
74     def list(self, path):
75         prefix = "%s/%s/" % (self.prefix, path)
76         for i in self.bucket.list(prefix):
77             assert i.key.startswith(prefix)
78             yield i.key[len(prefix):]
79
80     @throw_notfound
81     def get(self, path):
82         fp = tempfile.TemporaryFile()
83         k = self._get_key(path)
84         k.get_file(fp)
85         fp.seek(0)
86         return fp
87
88     @throw_notfound
89     def put(self, path, fp):
90         k = self._get_key(path)
91         k.set_contents_from_file(fp)
92
93     @throw_notfound
94     def delete(self, path):
95         self.bucket.delete_key("%s/%s" % (self.prefix, path))
96
97     def stat(self, path):
98         path = "%s/%s" % (self.prefix, path)
99         if path in self.scan_cache:
100             k = self.scan_cache[path]
101         else:
102             k = self.bucket.get_key(path)
103         if k is None:
104             raise cumulus.store.NotFoundError
105
106         return {'size': int(k.size)}
107
108 Store = S3Store