ef40113a24a97b771218f4996dbb595239ffac74
[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 from __future__ import division, print_function, unicode_literals
22
23 import os, sys, tempfile
24 import boto
25 from boto.exception import S3ResponseError
26 from boto.s3.bucket import Bucket
27 from boto.s3.key import Key
28
29 import cumulus.store
30
31 def throw_notfound(method):
32     """Decorator to convert a 404 error into a cumulus.store.NoutFoundError."""
33     def f(*args, **kwargs):
34         try:
35             return method(*args, **kwargs)
36         except S3ResponseError as e:
37             if e.status == 404:
38                 print("Got a 404:", e)
39                 raise cumulus.store.NotFoundError(e)
40             else:
41                 raise
42     return f
43
44 class S3Store(cumulus.store.Store):
45     def __init__(self, url, **kw):
46         # Old versions of the Python urlparse library will take a URL like
47         # s3://bucket/path/ and include the bucket with the path, while new
48         # versions (2.6 and later) treat it as the netloc (which seems more
49         # correct).
50         #
51         # But, so that we can work with either behavior, for now just combine
52         # the netloc and path together before we do any further processing
53         # (which will then split the combined path apart into a bucket and path
54         # again).  If we didn't want to support Python 2.5, this would be
55         # easier as we could just use the netloc as the bucket directly.
56         path = self.netloc + '/' + self.path
57         (bucket, prefix) = path.lstrip("/").split("/", 1)
58         self.conn = boto.connect_s3(is_secure=False)
59         self.bucket = self.conn.create_bucket(bucket)
60         self.prefix = prefix.strip("/")
61         self.scan_cache = {}
62
63     def _get_key(self, path):
64         k = Key(self.bucket)
65         k.key = "%s/%s" % (self.prefix, path)
66         return k
67
68     @throw_notfound
69     def scan(self, path):
70         prefix = "%s/%s/" % (self.prefix, path)
71         for i in self.bucket.list(prefix):
72             assert i.key.startswith(prefix)
73             self.scan_cache[i.key] = i
74
75     @throw_notfound
76     def list(self, path):
77         prefix = "%s/%s/" % (self.prefix, path)
78         for i in self.bucket.list(prefix):
79             assert i.key.startswith(prefix)
80             yield i.key[len(prefix):]
81
82     @throw_notfound
83     def get(self, path):
84         fp = tempfile.TemporaryFile()
85         k = self._get_key(path)
86         k.get_file(fp)
87         fp.seek(0)
88         return fp
89
90     @throw_notfound
91     def put(self, path, fp):
92         k = self._get_key(path)
93         k.set_contents_from_file(fp)
94
95     @throw_notfound
96     def delete(self, path):
97         self.bucket.delete_key("%s/%s" % (self.prefix, path))
98
99     def stat(self, path):
100         path = "%s/%s" % (self.prefix, path)
101         if path in self.scan_cache:
102             k = self.scan_cache[path]
103         else:
104             k = self.bucket.get_key(path)
105         if k is None:
106             raise cumulus.store.NotFoundError
107
108         return {'size': int(k.size)}
109
110 Store = S3Store