Start a cleanup on the storage backends.
[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                 raise cumulus.store.NotFoundError(e)
39             else:
40                 raise
41     return f
42
43 class Store(cumulus.store.Store):
44     def __init__(self, url):
45         super(Store, self).__init__(url)
46         self.conn = boto.connect_s3(is_secure=False)
47         self.bucket = self.conn.create_bucket(url.hostname)
48         self.prefix = url.path
49         if not self.prefix.endswith("/"):
50             self.prefix += "/"
51         self.prefix = self.prefix.lstrip("/")
52         self.scan_cache = {}
53
54     def _fullpath(self, path, is_directory=False):
55         fullpath = self.prefix + path
56         if is_directory and not fullpath.endswith("/"):
57             fullpath += "/"
58         return fullpath
59
60     def _get_key(self, path):
61         k = Key(self.bucket)
62         k.key = self._fullpath(path)
63         return k
64
65     @throw_notfound
66     def scan(self, path):
67         prefix = self._fullpath(path, is_directory=True)
68         for i in self.bucket.list(prefix):
69             assert i.key.startswith(prefix)
70             self.scan_cache[i.key] = i
71
72     @throw_notfound
73     def list(self, path):
74         prefix = self._fullpath(path, is_directory=True)
75         # TODO: Should use a delimiter
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(self._fullpath(path))
96
97     def stat(self, path):
98         path = self._fullpath(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)}