Add a Content-Type header when sending data
[pithos] / tools / store
1 #!/usr/bin/env python
2
3 from getpass import getuser
4 from httplib import HTTPConnection
5 from optparse import OptionParser
6 from os.path import basename
7 from sys import argv, exit, stdin
8
9 import json
10 import logging
11
12
13 DEFAULT_HOST = 'pithos.dev.grnet.gr'
14 DEFAULT_API = 'v1'
15
16
17 class Fault(Exception):
18     def __init__(self, data=''):
19         Exception.__init__(self, data)
20         self.data = data
21
22
23 class Client(object):
24     def __init__(self, host, account, api='v1', verbose=False, debug=False):
25         """`host` can also include a port, e.g '127.0.0.1:8000'."""
26         
27         self.host = host
28         self.account = account
29         self.api = api
30         self.verbose = verbose or debug
31         self.debug = debug
32     
33     def req(self, method, path, body=None, headers=None, format='text'):
34         full_path = '/%s/%s%s?format=%s' % (self.api, self.account, path, format)
35         conn = HTTPConnection(self.host)
36         
37         kwargs = {}
38         kwargs['headers'] = headers or {}
39         kwargs['headers']['Content-Length'] = len(body) if body else 0
40         if body:
41             kwargs['body'] = body
42             kwargs['headers']['Content-Type'] = 'application/octet-stream'
43         
44         conn.request(method, full_path, **kwargs)
45         resp = conn.getresponse()
46         headers = dict(resp.getheaders())
47         
48         if self.verbose:
49             print '%d %s' % (resp.status, resp.reason)
50             for key, val in headers.items():
51                 print '%s: %s' % (key.capitalize(), val)
52             print
53         
54         data = resp.read()
55         
56         if self.debug:
57             print data
58             print
59         
60         if format == 'json':
61             data = json.loads(data)
62         
63         return resp.status, headers, data
64     
65     def delete(self, path, format='text'):
66         return self.req('DELETE', path, format=format)
67     
68     def get(self, path, format='text'):
69         return self.req('GET', path, format=format)
70     
71     def head(self, path, format='text'):
72         return self.req('HEAD', path, format=format)
73     
74     def post(self, path, body=None, format='text', headers=None):
75         return self.req('POST', path, body, headers=headers, format=format)
76     
77     def put(self, path, body=None, format='text', headers=None):
78         return self.req('PUT', path, body, headers=headers, format=format)
79     
80     def _list(self, path, detail=False):
81         format = 'json' if detail else 'text'
82         status, headers, data = self.get(path, format=format)
83         return data
84     
85     def _get_metadata(self, path, prefix):
86         status, headers, data = self.head(path)
87         prefixlen = len(prefix)
88         meta = {}
89         for key, val in headers.items():
90             if key.startswith(prefix):
91                 key = key[prefixlen:]
92                 meta[key] = val
93         return meta
94     
95     
96     # Storage Account Services
97     
98     def list_containers(self, detail=False):
99         return self._list('', detail)
100     
101     def account_metadata(self):
102         return self._get_metadata('', 'x-account-')
103     
104     
105     # Storage Container Services
106     
107     def list_objects(self, container, detail=False):
108         return self._list('/' + container, detail)
109     
110     def create_container(self, container):
111         status, header, data = self.put('/' + container)
112         if status == 202:
113             return False
114         elif status != 201:
115             raise Fault(data)
116         return True
117     
118     def delete_container(self, container):
119         self.delete('/' + container)
120     
121     def retrieve_container_metadata(self, container):
122         return self._get_metadata('/%s' % container, 'x-container-')
123     
124     
125     # Storage Object Services
126     
127     def retrieve_object(self, container, object):
128         path = '/%s/%s' % (container, object)
129         status, headers, data = self.get(path)
130         return data
131     
132     def create_object(self, container, object, data):
133         path = '/%s/%s' % (container, object)
134         self.put(path, data)
135     
136     def copy_object(self, src_container, src_object, dst_container, dst_object):
137         path = '/%s/%s' % (dst_container, dst_object)
138         headers = {}
139         headers['X-Copy-From'] = '/%s/%s' % (src_container, src_object)
140         headers['Content-Length'] = 0
141         self.put(path, headers=headers)
142     
143     def delete_object(self, container, object):
144         self.delete('/%s/%s' % (container, object))
145     
146     def retrieve_object_metadata(self, container, object):
147         path = '/%s/%s' % (container, object)
148         return self._get_metadata(path, 'x-object-meta-')
149     
150     def update_object_metadata(self, container, object, **meta):
151         path = '/%s/%s' % (container, object)
152         headers = {}
153         for key, val in meta.items():
154             http_key = 'X-Object-Meta-' + key
155             headers[http_key] = val
156         self.post(path, headers=headers)
157
158
159 _cli_commands = {}
160
161 def cli_command(*args):
162     def decorator(cls):
163         cls.commands = args
164         for name in args:
165             _cli_commands[name] = cls
166         return cls
167     return decorator
168
169 def class_for_cli_command(name):
170     return _cli_commands[name]
171
172 def print_dict(d, header='name'):
173     if header:
174         print d.pop(header)
175     for key, val in sorted(d.items()):
176         print '%s: %s' % (key.rjust(15), val)
177
178
179 class Command(object):
180     def __init__(self, argv):
181         parser = OptionParser()
182         parser.add_option('--host', dest='host', metavar='HOST', default=DEFAULT_HOST,
183                             help='use server HOST')
184         parser.add_option('--user', dest='user', metavar='USERNAME', default=getuser(),
185                             help='use account USERNAME')
186         parser.add_option('--api', dest='api', metavar='API', default=DEFAULT_API,
187                             help='use api API')
188         parser.add_option('-v', action='store_true', dest='verbose', default=False,
189                             help='use verbose output')
190         parser.add_option('-d', action='store_true', dest='debug', default=False,
191                             help='use debug output')
192         self.add_options(parser)
193         options, args = parser.parse_args(argv)
194         
195         # Add options to self
196         for opt in parser.option_list:
197             key = opt.dest
198             if key:
199                 val = getattr(options, key)
200                 setattr(self, key, val)
201         
202         self.client = Client(self.host, self.user, self.api, self.verbose, self.debug)
203         
204         self.parser = parser
205         self.args = args
206     
207     def add_options(self, parser):
208         pass
209
210     def execute(self, *args):
211         pass
212
213
214 @cli_command('list', 'ls')
215 class List(Command):
216     syntax = '[container]'
217     description = 'list containers or objects'
218     
219     def add_options(self, parser):
220         parser.add_option('-l', action='store_true', dest='detail', default=False,
221                             help='show detailed output')
222     
223     def execute(self, container=None):
224         if container:
225             self.list_objects(container)
226         else:
227             self.list_containers()
228     
229     def list_containers(self):
230         if self.detail:
231             for container in self.client.list_containers(detail=True):
232                 print_dict(container)
233                 print
234         else:
235             print self.client.list_containers().strip()
236     
237     def list_objects(self, container):
238         if self.detail:
239             for obj in self.client.list_objects(container, detail=True):
240                 print_dict(obj)
241                 print
242         else:
243             print self.client.list_objects(container).strip()
244
245
246 @cli_command('meta')
247 class Meta(Command):
248     syntax = '[<container>[/<object>]]'
249     description = 'get the metadata of an account, a container or an object'
250
251     def execute(self, path=''):
252         container, sep, object = path.partition('/')
253         if object:
254             meta = self.client.retrieve_object_metadata(container, object)
255         elif container:
256             meta = self.client.retrieve_container_metadata(container)
257         else:
258             meta = self.client.account_metadata()
259         print_dict(meta, header=None)
260
261
262 @cli_command('create')
263 class CreateContainer(Command):
264     syntax = '<container>'
265     description = 'create a container'
266     
267     def execute(self, container):
268         ret = self.client.create_container(container)
269         if not ret:
270             print 'Container already exists'
271
272
273 @cli_command('delete', 'rm')
274 class Delete(Command):
275     syntax = '<container>[/<object>]'
276     description = 'delete a container or an object'
277     
278     def execute(self, path):
279         container, sep, object = path.partition('/')
280         if object:
281             self.client.delete_object(container, object)
282         else:
283             self.client.delete_container(container)
284
285
286 @cli_command('get')
287 class GetObject(Command):
288     syntax = '<container>/<object>'
289     description = 'get the data of an object'
290     
291     def execute(self, path):
292         container, sep, object = path.partition('/')
293         print self.client.retrieve_object(container, object)
294
295
296 @cli_command('put')
297 class PutObject(Command):
298     syntax = '<container>/<object> <path>'
299     description = 'create or update an object with contents of path'
300
301     def execute(self, path, srcpath):
302         container, sep, object = path.partition('/')
303         f = open(srcpath) if srcpath != '-' else stdin
304         data = f.read()
305         self.client.create_object(container, object, data)
306         f.close()
307
308
309 @cli_command('copy', 'cp')
310 class CopyObject(Command):
311     syntax = '<src container>/<src object> [<dst container>/]<dst object>'
312     description = 'copies an object to a different location'
313     
314     def execute(self, src, dst):
315         src_container, sep, src_object = src.partition('/')
316         dst_container, sep, dst_object = dst.partition('/')
317         if not sep:
318             dst_container = src_container
319             dst_object = dst
320         self.client.copy_object(src_container, src_object, dst_container, dst_object)
321
322
323 @cli_command('set')
324 class SetOjectMeta(Command):
325     syntax = '<container>/<object> key=val [key=val] [...]'
326     description = 'set object metadata'
327     
328     def execute(self, path, *args):
329         container, sep, object = path.partition('/')
330         meta = {}
331         for arg in args:
332             key, sep, val = arg.partition('=')
333             meta[key.strip()] = val.strip()
334         self.client.update_object_metadata(container, object, **meta)
335
336
337 def print_usage():
338     cmd = Command([])
339     parser = cmd.parser
340     parser.usage = '%prog <command> [options]'
341     parser.print_help()
342     
343     commands = []
344     for cls in set(_cli_commands.values()):
345         name = ', '.join(cls.commands)
346         description = getattr(cls, 'description', '')
347         commands.append('  %s %s' % (name.ljust(12), description))
348     print '\nCommands:\n' + '\n'.join(sorted(commands))
349
350
351
352 def main():
353     try:
354         name = argv[1]
355         cls = class_for_cli_command(name)
356     except (IndexError, KeyError):
357         print_usage()
358         exit(1)
359
360     cmd = cls(argv[2:])
361     
362     try:
363         cmd.execute(*cmd.args)
364     except TypeError:
365         cmd.parser.usage = '%%prog %s [options] %s' % (name, cmd.syntax)
366         cmd.parser.print_help()
367         exit(1)
368     except Fault, f:
369         print f.data
370
371
372 if __name__ == '__main__':
373     main()