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