Revision 5a96180b
b/MANIFEST.in | ||
---|---|---|
1 | 1 |
global-include */templates/* */fixtures/* */static/* |
2 | 2 |
global-exclude */.DS_Store |
3 |
exclude */settings.py |
|
4 |
include */settings.py.dist |
|
3 |
include pithos/settings.d/* |
|
5 | 4 |
prune docs |
6 |
prune tools |
|
5 |
prune other |
b/README | ||
---|---|---|
44 | 44 |
|
45 | 45 |
This server is useful during development, but should not be used for deployment. |
46 | 46 |
To deploy Pithos using Apache, take a look at the Administrator Guide in docs. |
47 |
|
|
48 |
Using the tools |
|
49 |
--------------- |
|
50 |
|
|
51 |
In the pithos/tools directory you will find the following utilities: |
|
52 |
|
|
53 |
pithos-sh Pithos shell |
|
54 |
pithos-sync Pithos synchronization client |
|
55 |
pithos-fs Pithos FUSE implementation |
|
56 |
pithos-test Pithos server tests |
|
57 |
|
|
58 |
Also, the pithos/lib directory contains a python library that can be used |
|
59 |
to access Pithos and manage stored objects. All tools use the included lib. |
|
60 |
|
|
61 |
Connection options can be set via environmental variables: |
|
62 |
|
|
63 |
PITHOS_USER Login user |
|
64 |
PITHOS_AUTH Login token |
|
65 |
PITHOS_SERVER Pithos server (default: plus.pithos.grnet.gr) |
|
66 |
PITHOS_API Pithos server path (default: v1) |
|
67 |
PITHOS_SYNC_CONTAINER Container to sync with (default: pithos) |
b/docs/source/backends.rst | ||
---|---|---|
33 | 33 |
:members: |
34 | 34 |
:undoc-members: |
35 | 35 |
|
36 |
Hashfiler
|
|
37 |
~~~~~~~~~
|
|
36 |
Store
|
|
37 |
~~~~~ |
|
38 | 38 |
|
39 |
.. automodule:: pithos.backends.lib.hashfiler |
|
39 |
.. automodule:: pithos.backends.lib.hashfiler.store
|
|
40 | 40 |
:show-inheritance: |
41 | 41 |
:members: |
42 | 42 |
:undoc-members: |
b/docs/source/clientlib.rst | ||
---|---|---|
1 | 1 |
Client Library |
2 | 2 |
============== |
3 | 3 |
|
4 |
.. automodule:: tools.lib.client
|
|
4 |
.. automodule:: pithos.lib.client
|
|
5 | 5 |
:show-inheritance: |
6 | 6 |
:members: |
7 | 7 |
:undoc-members: |
b/other/invite.py | ||
---|---|---|
1 |
#!/usr/bin/env python |
|
2 |
|
|
3 |
import sys |
|
4 |
|
|
5 |
if len(sys.argv) != 4: |
|
6 |
print "Usage: %s <inviter token> <invitee name> <invitee email>" % (sys.argv[0],) |
|
7 |
sys.exit(-1) |
|
8 |
|
|
9 |
import httplib2 |
|
10 |
http = httplib2.Http(disable_ssl_certificate_validation=True) |
|
11 |
|
|
12 |
url = 'https://pithos.dev.grnet.gr/im/invite' |
|
13 |
|
|
14 |
import urllib |
|
15 |
params = urllib.urlencode({ |
|
16 |
'uniq': sys.argv[3], |
|
17 |
'realname': sys.argv[2] |
|
18 |
}) |
|
19 |
|
|
20 |
response, content = http.request(url, 'POST', params, |
|
21 |
headers={'Content-type': 'application/x-www-form-urlencoded', 'X-Auth-Token': sys.argv[1]} |
|
22 |
) |
|
23 |
|
|
24 |
if response['status'] == '200': |
|
25 |
print 'OK' |
|
26 |
sys.exit(0) |
|
27 |
else: |
|
28 |
print response, content |
|
29 |
sys.exit(-1) |
b/other/migrate-data | ||
---|---|---|
1 |
#!/usr/bin/env python |
|
2 |
|
|
3 |
# Copyright 2011 GRNET S.A. All rights reserved. |
|
4 |
# |
|
5 |
# Redistribution and use in source and binary forms, with or |
|
6 |
# without modification, are permitted provided that the following |
|
7 |
# conditions are met: |
|
8 |
# |
|
9 |
# 1. Redistributions of source code must retain the above |
|
10 |
# copyright notice, this list of conditions and the following |
|
11 |
# disclaimer. |
|
12 |
# |
|
13 |
# 2. Redistributions in binary form must reproduce the above |
|
14 |
# copyright notice, this list of conditions and the following |
|
15 |
# disclaimer in the documentation and/or other materials |
|
16 |
# provided with the distribution. |
|
17 |
# |
|
18 |
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS |
|
19 |
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
20 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|
21 |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR |
|
22 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
23 |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
24 |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
|
25 |
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
|
26 |
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
|
27 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
|
28 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|
29 |
# POSSIBILITY OF SUCH DAMAGE. |
|
30 |
# |
|
31 |
# The views and conclusions contained in the software and |
|
32 |
# documentation are those of the authors and should not be |
|
33 |
# interpreted as representing official policies, either expressed |
|
34 |
# or implied, of GRNET S.A. |
|
35 |
|
|
36 |
from binascii import hexlify |
|
37 |
|
|
38 |
from sqlalchemy import Table |
|
39 |
from sqlalchemy.sql import select |
|
40 |
|
|
41 |
from pithos import settings |
|
42 |
from pithos.backends.modular import ModularBackend |
|
43 |
|
|
44 |
from pithos.lib.hashmap import HashMap |
|
45 |
|
|
46 |
from migrate import Migration, Cache |
|
47 |
|
|
48 |
import os |
|
49 |
|
|
50 |
class DataMigration(Migration): |
|
51 |
def __init__(self, pithosdb, db): |
|
52 |
Migration.__init__(self, pithosdb) |
|
53 |
self.cache = Cache(db) |
|
54 |
|
|
55 |
def retrieve_files(self): |
|
56 |
# Loop for all available files. |
|
57 |
filebody = Table('filebody', self.metadata, autoload=True) |
|
58 |
s = select([filebody.c.storedfilepath]) |
|
59 |
rp = self.conn.execute(s) |
|
60 |
path = rp.fetchone() |
|
61 |
while path: |
|
62 |
yield path |
|
63 |
path = rp.fetchone() |
|
64 |
rp.close() |
|
65 |
|
|
66 |
def execute(self): |
|
67 |
blocksize = self.backend.block_size |
|
68 |
blockhash = self.backend.hash_algorithm |
|
69 |
|
|
70 |
for (path,) in self.retrieve_files(): |
|
71 |
map = HashMap(blocksize, blockhash) |
|
72 |
try: |
|
73 |
map.load(open(path)) |
|
74 |
except Exception, e: |
|
75 |
print e |
|
76 |
continue |
|
77 |
hash = hexlify(map.hash()) |
|
78 |
|
|
79 |
if hash != self.cache.get(path): |
|
80 |
missing = self.backend.blocker.block_ping(map) # XXX Backend hack... |
|
81 |
status = '[>] ' + path |
|
82 |
if missing: |
|
83 |
status += ' - %d block(s) missing' % len(missing) |
|
84 |
with open(path) as fp: |
|
85 |
for h in missing: |
|
86 |
offset = map.index(h) * blocksize |
|
87 |
fp.seek(offset) |
|
88 |
block = fp.read(blocksize) |
|
89 |
self.backend.put_block(block) |
|
90 |
else: |
|
91 |
status += ' - no blocks missing' |
|
92 |
self.cache.put(path, hash) |
|
93 |
else: |
|
94 |
status = '[-] ' + path |
|
95 |
print status |
|
96 |
|
|
97 |
if __name__ == "__main__": |
|
98 |
pithosdb = 'postgresql://gss@127.0.0.1/pithos' |
|
99 |
db = 'sqlite:///migrate.db' |
|
100 |
|
|
101 |
dt = DataMigration(pithosdb, db) |
|
102 |
dt.execute() |
b/other/migrate-db | ||
---|---|---|
1 |
#!/usr/bin/env python |
|
2 |
|
|
3 |
# Copyright 2011 GRNET S.A. All rights reserved. |
|
4 |
# |
|
5 |
# Redistribution and use in source and binary forms, with or |
|
6 |
# without modification, are permitted provided that the following |
|
7 |
# conditions are met: |
|
8 |
# |
|
9 |
# 1. Redistributions of source code must retain the above |
|
10 |
# copyright notice, this list of conditions and the following |
|
11 |
# disclaimer. |
|
12 |
# |
|
13 |
# 2. Redistributions in binary form must reproduce the above |
|
14 |
# copyright notice, this list of conditions and the following |
|
15 |
# disclaimer in the documentation and/or other materials |
|
16 |
# provided with the distribution. |
|
17 |
# |
|
18 |
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS |
|
19 |
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
20 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|
21 |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR |
|
22 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
23 |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
24 |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
|
25 |
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
|
26 |
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
|
27 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
|
28 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|
29 |
# POSSIBILITY OF SUCH DAMAGE. |
|
30 |
# |
|
31 |
# The views and conclusions contained in the software and |
|
32 |
# documentation are those of the authors and should not be |
|
33 |
# interpreted as representing official policies, either expressed |
|
34 |
# or implied, of GRNET S.A. |
|
35 |
|
|
36 |
from sqlalchemy import Table |
|
37 |
from sqlalchemy.sql import select, and_ |
|
38 |
|
|
39 |
from binascii import hexlify |
|
40 |
|
|
41 |
from pithos.backends.lib.hashfiler import Blocker |
|
42 |
from pithos.backends.lib.sqlalchemy import Node |
|
43 |
|
|
44 |
from django.conf import settings |
|
45 |
|
|
46 |
from pithos.backends.modular import CLUSTER_NORMAL, CLUSTER_HISTORY, CLUSTER_DELETED |
|
47 |
from pithos.backends.lib.sqlalchemy.node import Node, ROOTNODE |
|
48 |
|
|
49 |
from pithos.lib.transfer import upload |
|
50 |
from pithos.lib.hashmap import HashMap |
|
51 |
from pithos.lib.client import Fault |
|
52 |
|
|
53 |
from migrate import Migration, Cache |
|
54 |
|
|
55 |
from calendar import timegm |
|
56 |
from decimal import Decimal |
|
57 |
from collections import defaultdict |
|
58 |
|
|
59 |
import json |
|
60 |
import os |
|
61 |
import sys |
|
62 |
import hashlib |
|
63 |
import mimetypes |
|
64 |
import time |
|
65 |
import datetime |
|
66 |
|
|
67 |
(ID, CREATIONDATE, MODIFICATIONDATE, DELETED, ICON, NAME, VERSION, CREATEDBY_ID, MODIFIEDBY_ID, OWNER_ID, PARENT_ID, READFORALL, SHARED, USER) = range(14) |
|
68 |
|
|
69 |
class ObjectMigration(Migration): |
|
70 |
def __init__(self, old_db, db, f): |
|
71 |
Migration.__init__(self, old_db) |
|
72 |
self.cache = Cache(db) |
|
73 |
|
|
74 |
def create_node(self, username, container, object): |
|
75 |
node = self.backend.node.node_lookup(object) |
|
76 |
if not node: |
|
77 |
parent_path = '%s/%s' %(username, container) |
|
78 |
parent_node = self.backend.node.node_lookup(parent_path) |
|
79 |
if not parent_node: |
|
80 |
raise Exception('Missing node') |
|
81 |
node = self.backend.node.node_create(parent_node, object) |
|
82 |
return node |
|
83 |
|
|
84 |
def create_history(self, header_id, node_id, deleted=False): |
|
85 |
i = 0 |
|
86 |
map = HashMap(self.backend.block_size, self.backend.hash_algorithm) |
|
87 |
v = [] |
|
88 |
stored_versions = self.backend.node.node_get_versions(node_id, ['mtime']) |
|
89 |
stored_versions_mtime = [datetime.datetime.utcfromtimestamp(elem[0]) for elem in stored_versions] |
|
90 |
for t, rowcount in self.retrieve_node_versions(header_id): |
|
91 |
size, modyfied_by, filepath, mimetype, mdate = t |
|
92 |
if mdate in stored_versions_mtime: |
|
93 |
continue |
|
94 |
cluster = CLUSTER_HISTORY if i < rowcount - 1 else CLUSTER_NORMAL |
|
95 |
cluster = cluster if not deleted else CLUSTER_DELETED |
|
96 |
hash = self.cache.get(filepath) |
|
97 |
if hash == None: |
|
98 |
raise Exception("Missing hash") |
|
99 |
args = node_id, hash, size, modyfied_by, cluster, mimetype, mdate |
|
100 |
v.append(self.create_version(*args)) |
|
101 |
i += 1 |
|
102 |
return v |
|
103 |
|
|
104 |
def create_version(self, node_id, hash, size, modyfied_by, cluster, mimetype, mdate): |
|
105 |
args = (node_id, hash, size, None, modyfied_by, cluster) |
|
106 |
serial = self.backend.node.version_create(*args)[0] |
|
107 |
meta = {'hash':hash, |
|
108 |
'content-type':mimetype} |
|
109 |
self.backend.node.attribute_set(serial, ((k, v) for k, v in meta.iteritems())) |
|
110 |
timestamp = timegm(mdate.timetuple()) |
|
111 |
microseconds = mdate.time().microsecond |
|
112 |
values = timestamp, microseconds, serial |
|
113 |
f.write('update versions set mtime=\'%10d.%6d\' where serial=%s;' %values) |
|
114 |
return serial |
|
115 |
|
|
116 |
def create_tags(self, header_id, node_id, vserials): |
|
117 |
tags = self.retrieve_tags(header_id) |
|
118 |
if not tags: |
|
119 |
return |
|
120 |
for v in vserials: |
|
121 |
self.backend.node.attribute_set(v, (('X-Object-Meta-Tag', tags),)) |
|
122 |
|
|
123 |
def create_permissions(self, fid, path, owner, is_folder=True): |
|
124 |
fpath, fpermissions = self.backend.permissions.access_inherit(path) |
|
125 |
permissions = self.retrieve_permissions(fid, is_folder) |
|
126 |
if not fpermissions: |
|
127 |
keys = ('read', 'write') |
|
128 |
for k in keys: |
|
129 |
if owner in permissions[k]: |
|
130 |
permissions[k].remove(owner) |
|
131 |
self.backend.permissions.access_set(path, permissions) |
|
132 |
else: |
|
133 |
keys = ('read', 'write') |
|
134 |
common_p = {} |
|
135 |
for k in keys: |
|
136 |
if owner in permissions[k]: |
|
137 |
permissions[k].remove(owner) |
|
138 |
common = set(fpermissions[k]).intersection(set(permissions[k])) |
|
139 |
common_p[k] = list(common) |
|
140 |
#keep only the common permissions |
|
141 |
#trade off for securing access only to explicitly authorized users |
|
142 |
self.backend.permissions.access_set(fpath, common_p) |
|
143 |
|
|
144 |
def create_objects(self): |
|
145 |
for t in self.retrieve_current_nodes(): |
|
146 |
username, headerid, folderid, filename, deleted, filepath, mimetype, public, owner_id = t |
|
147 |
containers = ['pithos', 'trash'] |
|
148 |
|
|
149 |
for c in containers: |
|
150 |
#create container if it does not exist |
|
151 |
try: |
|
152 |
self.backend._lookup_container(username, c) |
|
153 |
except NameError, e: |
|
154 |
self.backend.put_container(username, username, c) |
|
155 |
|
|
156 |
container = 'pithos' if not deleted else 'trash' |
|
157 |
path = self.build_path(folderid) |
|
158 |
#create node |
|
159 |
object = '%s/%s' %(username, container) |
|
160 |
object = '%s/%s/%s' %(object, path, filename) if path else '%s/%s' %(object, filename) |
|
161 |
args = username, container, object |
|
162 |
nodeid = self.create_node(*args) |
|
163 |
#create node history |
|
164 |
vserials = self.create_history(headerid, nodeid, deleted) |
|
165 |
#set object tags |
|
166 |
self.create_tags(headerid, nodeid, vserials) |
|
167 |
#set object's publicity |
|
168 |
if public: |
|
169 |
self.backend.permissions.public_set(object) |
|
170 |
#set object's permissions |
|
171 |
self.create_permissions(headerid, object, username, is_folder=False) |
|
172 |
|
|
173 |
def build_path(self, child_id): |
|
174 |
folder = Table('folder', self.metadata, autoload=True) |
|
175 |
user = Table('gss_user', self.metadata, autoload=True) |
|
176 |
j = folder.join(user, folder.c.owner_id == user.c.id) |
|
177 |
s = select([folder, user.c.username], from_obj=j) |
|
178 |
s = s.where(folder.c.id == child_id) |
|
179 |
s.order_by(folder.c.modificationdate) |
|
180 |
rp = self.conn.execute(s) |
|
181 |
t = rp.fetchone() |
|
182 |
md5 = hashlib.md5() |
|
183 |
hash = md5.hexdigest().lower() |
|
184 |
size = 0 |
|
185 |
if not t[PARENT_ID]: |
|
186 |
return '' |
|
187 |
else: |
|
188 |
container_path = t[USER] |
|
189 |
container_path += '/trash' if t[DELETED] else '/pithos' |
|
190 |
parent_node = self.backend.node.node_lookup(container_path) |
|
191 |
if not parent_node: |
|
192 |
raise Exception('Missing node:', container_path) |
|
193 |
parent_path = self.build_path(t[PARENT_ID]) |
|
194 |
path = '%s/%s/%s' %(container_path, parent_path, t[NAME]) if parent_path else '%s/%s' %(container_path, t[NAME]) |
|
195 |
node = self.backend.node.node_lookup(path) |
|
196 |
if not node: |
|
197 |
node = self.backend.node.node_create(parent_node, path) |
|
198 |
if not node: |
|
199 |
raise Exception('Unable to create node:', path) |
|
200 |
|
|
201 |
#create versions |
|
202 |
v = self.create_version(node, hash, size, t[USER], CLUSTER_NORMAL, 'application/directory', t[CREATIONDATE]) |
|
203 |
if t[CREATIONDATE] != t[MODIFICATIONDATE]: |
|
204 |
self.backend.node.version_recluster(v, CLUSTER_HISTORY) |
|
205 |
self.create_version(node, hash, size, t[USER], CLUSTER_NORMAL, 'application/directory', t[MODIFICATIONDATE]) |
|
206 |
|
|
207 |
#set permissions |
|
208 |
self.create_permissions(t[ID], path, t[USER], is_folder=True) |
|
209 |
return '%s/%s' %(parent_path, t[NAME]) if parent_path else t[NAME] |
|
210 |
|
|
211 |
def retrieve_current_nodes(self): |
|
212 |
fileheader = Table('fileheader', self.metadata, autoload=True) |
|
213 |
filebody = Table('filebody', self.metadata, autoload=True) |
|
214 |
folder = Table('folder', self.metadata, autoload=True) |
|
215 |
gss_user = Table('gss_user', self.metadata, autoload=True) |
|
216 |
j = filebody.join(fileheader, filebody.c.id == fileheader.c.currentbody_id) |
|
217 |
j = j.join(folder, fileheader.c.folder_id == folder.c.id) |
|
218 |
j = j.join(gss_user, fileheader.c.owner_id == gss_user.c.id) |
|
219 |
s = select([gss_user.c.username, fileheader.c.id, fileheader.c.folder_id, |
|
220 |
fileheader.c.name, fileheader.c.deleted, |
|
221 |
filebody.c.storedfilepath, filebody.c.mimetype, |
|
222 |
fileheader.c.readforall, fileheader.c.owner_id], from_obj=j) |
|
223 |
rp = self.conn.execute(s) |
|
224 |
object = rp.fetchone() |
|
225 |
while object: |
|
226 |
yield object |
|
227 |
object = rp.fetchone() |
|
228 |
rp.close() |
|
229 |
|
|
230 |
def retrieve_node_versions(self, header_id): |
|
231 |
filebody = Table('filebody', self.metadata, autoload=True) |
|
232 |
gss_user = Table('gss_user', self.metadata, autoload=True) |
|
233 |
j = filebody.join(gss_user, filebody.c.modifiedby_id == gss_user.c.id) |
|
234 |
s = select([filebody.c.filesize, gss_user.c.username, |
|
235 |
filebody.c.storedfilepath, filebody.c.mimetype, |
|
236 |
filebody.c.modificationdate], from_obj=j) |
|
237 |
s = s.where(filebody.c.header_id == header_id) |
|
238 |
s = s.order_by(filebody.c.version) |
|
239 |
rp = self.conn.execute(s) |
|
240 |
version = rp.fetchone() |
|
241 |
while version: |
|
242 |
yield version, rp.rowcount |
|
243 |
version = rp.fetchone() |
|
244 |
rp.close() |
|
245 |
|
|
246 |
def retrieve_tags(self, header_id): |
|
247 |
filetag = Table('filetag', self.metadata, autoload=True) |
|
248 |
s = select([filetag.c.tag], filetag.c.fileid == header_id) |
|
249 |
rp = self.conn.execute(s) |
|
250 |
tags = rp.fetchall() if rp.returns_rows else [] |
|
251 |
tags = [elem[0] for elem in tags] |
|
252 |
rp.close() |
|
253 |
return ','.join(tags) if tags else '' |
|
254 |
|
|
255 |
def retrieve_permissions(self, id, is_folder=True): |
|
256 |
permissions = {} |
|
257 |
if is_folder: |
|
258 |
ftable = Table('folder_permission', self.metadata, autoload=True) |
|
259 |
else: |
|
260 |
ftable = Table('fileheader_permission', self.metadata, autoload=True) |
|
261 |
permission = Table('permission', self.metadata, autoload=True) |
|
262 |
group = Table('gss_group', self.metadata, autoload=True) |
|
263 |
user = Table('gss_user', self.metadata, autoload=True) |
|
264 |
j = ftable.join(permission, ftable.c.permissions_id == permission.c.id) |
|
265 |
j1 = j.join(group, group.c.id == permission.c.group_id) |
|
266 |
j2 = j.join(user, user.c.id == permission.c.user_id) |
|
267 |
|
|
268 |
permissions = defaultdict(list) |
|
269 |
|
|
270 |
def _get_permissions(self, action='read', get_groups=True): |
|
271 |
if get_groups: |
|
272 |
col, j = group.c.name, j1 |
|
273 |
cond2 = permission.c.group_id != None |
|
274 |
else: |
|
275 |
col, j = user.c.username, j2 |
|
276 |
cond2 = permission.c.user_id != None |
|
277 |
s = select([col], from_obj=j) |
|
278 |
if is_folder: |
|
279 |
s = s.where(ftable.c.folder_id == id) |
|
280 |
else: |
|
281 |
s = s.where(ftable.c.fileheader_id == id) |
|
282 |
if action == 'read': |
|
283 |
cond1 = permission.c.read == True |
|
284 |
else: |
|
285 |
cond1 = permission.c.write == True |
|
286 |
s = s.where(and_(cond1, cond2)) |
|
287 |
print '>', s, s.compile().params |
|
288 |
rp = self.conn.execute(s) |
|
289 |
p = permissions[action].extend([e[0] for e in rp.fetchall()]) |
|
290 |
rp.close() |
|
291 |
return p |
|
292 |
|
|
293 |
#get object read groups |
|
294 |
_get_permissions(self, action='read', get_groups=True) |
|
295 |
|
|
296 |
#get object read users |
|
297 |
_get_permissions(self, action='read', get_groups=False) |
|
298 |
|
|
299 |
#get object write groups |
|
300 |
_get_permissions(self, action='write', get_groups=True) |
|
301 |
|
|
302 |
#get object write groups |
|
303 |
_get_permissions(self, action='write', get_groups=False) |
|
304 |
|
|
305 |
return permissions |
|
306 |
|
|
307 |
if __name__ == "__main__": |
|
308 |
old_db = '' |
|
309 |
db = '' |
|
310 |
|
|
311 |
f = open('fixdates.sql', 'w') |
|
312 |
ot = ObjectMigration(old_db, db, f) |
|
313 |
ot.create_objects() |
|
314 |
f.close() |
|
315 |
|
|
316 |
|
b/other/migrate-users | ||
---|---|---|
1 |
#!/usr/bin/env python |
|
2 |
|
|
3 |
# Copyright 2011 GRNET S.A. All rights reserved. |
|
4 |
# |
|
5 |
# Redistribution and use in source and binary forms, with or |
|
6 |
# without modification, are permitted provided that the following |
|
7 |
# conditions are met: |
|
8 |
# |
|
9 |
# 1. Redistributions of source code must retain the above |
|
10 |
# copyright notice, this list of conditions and the following |
|
11 |
# disclaimer. |
|
12 |
# |
|
13 |
# 2. Redistributions in binary form must reproduce the above |
|
14 |
# copyright notice, this list of conditions and the following |
|
15 |
# disclaimer in the documentation and/or other materials |
|
16 |
# provided with the distribution. |
|
17 |
# |
|
18 |
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS |
|
19 |
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
20 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|
21 |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR |
|
22 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
23 |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
24 |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
|
25 |
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
|
26 |
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
|
27 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
|
28 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|
29 |
# POSSIBILITY OF SUCH DAMAGE. |
|
30 |
# |
|
31 |
# The views and conclusions contained in the software and |
|
32 |
# documentation are those of the authors and should not be |
|
33 |
# interpreted as representing official policies, either expressed |
|
34 |
# or implied, of GRNET S.A. |
|
35 |
|
|
36 |
from sqlalchemy import Table |
|
37 |
from sqlalchemy.sql import select |
|
38 |
|
|
39 |
from pithos.im.models import User |
|
40 |
|
|
41 |
from migrate import Migration |
|
42 |
|
|
43 |
import base64 |
|
44 |
|
|
45 |
class UserMigration(Migration): |
|
46 |
def __init__(self, db): |
|
47 |
Migration.__init__(self, db) |
|
48 |
self.gss_users = Table('gss_user', self.metadata, autoload=True) |
|
49 |
|
|
50 |
def execute(self): |
|
51 |
for u in self.retrieve_users(): |
|
52 |
user = User() |
|
53 |
user.pk = u['id'] |
|
54 |
user.uniq = u['username'] |
|
55 |
user.realname = u['name'] |
|
56 |
user.affiliation = u['homeorganization'] if u['homeorganization'] else '' |
|
57 |
user.auth_token = base64.b64encode(u['authtoken']) |
|
58 |
user.auth_token_created = u['creationdate'] |
|
59 |
user.auth_token_expires = u['authtokenexpirydate'] |
|
60 |
user.created = u['creationdate'] |
|
61 |
user.updated = u['modificationdate'] |
|
62 |
user.email = u['email'] |
|
63 |
user.active = 'ACTIVE' if u['active'] else 'SUSPENDED' |
|
64 |
print '#', user |
|
65 |
user.save(update_timestamps=False) |
|
66 |
|
|
67 |
#create user groups |
|
68 |
for (owner, group, members) in self.retrieve_groups(u['username']): |
|
69 |
self.backend.permissions.group_addmany(owner, group, members) |
|
70 |
|
|
71 |
|
|
72 |
def retrieve_users(self): |
|
73 |
s = self.gss_users.select() |
|
74 |
rp = self.conn.execute(s) |
|
75 |
user = rp.fetchone() |
|
76 |
while user: |
|
77 |
yield user |
|
78 |
user = rp.fetchone() |
|
79 |
rp.close() |
|
80 |
|
|
81 |
def retrieve_groups(self, owner): |
|
82 |
gss_group = Table('gss_group', self.metadata, autoload=True) |
|
83 |
gss_user = Table('gss_user', self.metadata, autoload=True) |
|
84 |
group_user = Table('gss_group_gss_user', self.metadata, autoload=True) |
|
85 |
j1 = gss_group.join(gss_user, gss_group.c.owner_id == gss_user.c.id) |
|
86 |
j2 = group_user.join(gss_user, group_user.c.members_id == gss_user.c.id) |
|
87 |
s = select([gss_group.c.id, gss_group.c.name, gss_user.c.username], from_obj=j1) |
|
88 |
s = s.where(gss_user.c.username == owner) |
|
89 |
rp = self.conn.execute(s) |
|
90 |
gr = rp.fetchone() |
|
91 |
while gr: |
|
92 |
id, group, owner = gr |
|
93 |
s = select([gss_user.c.username], from_obj=j2) |
|
94 |
s = s.where(group_user.c.groupsmember_id == id) |
|
95 |
rp2 = self.conn.execute(s) |
|
96 |
members = rp2.fetchall() |
|
97 |
rp2.close() |
|
98 |
yield owner, group, (m[0] for m in members) |
|
99 |
gr = rp.fetchone() |
|
100 |
rp.close() |
|
101 |
|
|
102 |
if __name__ == "__main__": |
|
103 |
db = '' |
|
104 |
m = UserMigration(db) |
|
105 |
m.execute() |
b/other/migrate.py | ||
---|---|---|
1 |
#!/usr/bin/env python |
|
2 |
|
|
3 |
# Copyright 2011 GRNET S.A. All rights reserved. |
|
4 |
# |
|
5 |
# Redistribution and use in source and binary forms, with or |
|
6 |
# without modification, are permitted provided that the following |
|
7 |
# conditions are met: |
|
8 |
# |
|
9 |
# 1. Redistributions of source code must retain the above |
|
10 |
# copyright notice, this list of conditions and the following |
|
11 |
# disclaimer. |
|
12 |
# |
|
13 |
# 2. Redistributions in binary form must reproduce the above |
|
14 |
# copyright notice, this list of conditions and the following |
|
15 |
# disclaimer in the documentation and/or other materials |
|
16 |
# provided with the distribution. |
|
17 |
# |
|
18 |
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS |
|
19 |
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
20 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|
21 |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR |
|
22 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
23 |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
24 |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
|
25 |
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
|
26 |
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
|
27 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
|
28 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|
29 |
# POSSIBILITY OF SUCH DAMAGE. |
|
30 |
# |
|
31 |
# The views and conclusions contained in the software and |
|
32 |
# documentation are those of the authors and should not be |
|
33 |
# interpreted as representing official policies, either expressed |
|
34 |
# or implied, of GRNET S.A. |
|
35 |
|
|
36 |
from sqlalchemy import create_engine |
|
37 |
from sqlalchemy import Table, Column, String, MetaData |
|
38 |
from sqlalchemy.sql import select |
|
39 |
|
|
40 |
from django.conf import settings |
|
41 |
|
|
42 |
from pithos.backends.modular import ModularBackend |
|
43 |
|
|
44 |
class Migration(object): |
|
45 |
def __init__(self, db): |
|
46 |
self.engine = create_engine(db) |
|
47 |
self.metadata = MetaData(self.engine) |
|
48 |
#self.engine.echo = True |
|
49 |
self.conn = self.engine.connect() |
|
50 |
|
|
51 |
options = getattr(settings, 'BACKEND', None)[1] |
|
52 |
self.backend = ModularBackend(*options) |
|
53 |
|
|
54 |
def execute(self): |
|
55 |
pass |
|
56 |
|
|
57 |
class Cache(): |
|
58 |
def __init__(self, db): |
|
59 |
self.engine = create_engine(db) |
|
60 |
metadata = MetaData(self.engine) |
|
61 |
|
|
62 |
columns=[] |
|
63 |
columns.append(Column('path', String(2048), primary_key=True)) |
|
64 |
columns.append(Column('hash', String(255))) |
|
65 |
self.files = Table('files', metadata, *columns) |
|
66 |
self.conn = self.engine.connect() |
|
67 |
self.engine.echo = True |
|
68 |
metadata.create_all(self.engine) |
|
69 |
|
|
70 |
def put(self, path, hash): |
|
71 |
# Insert or replace. |
|
72 |
s = self.files.delete().where(self.files.c.path==path) |
|
73 |
r = self.conn.execute(s) |
|
74 |
r.close() |
|
75 |
s = self.files.insert() |
|
76 |
r = self.conn.execute(s, {'path': path, 'hash': hash}) |
|
77 |
r.close() |
|
78 |
|
|
79 |
def get(self, path): |
|
80 |
s = select([self.files.c.hash], self.files.c.path == path) |
|
81 |
r = self.conn.execute(s) |
|
82 |
l = r.fetchone() |
|
83 |
r.close() |
|
84 |
if not l: |
|
85 |
return l |
|
86 |
return l[0] |
b/pithos/__init__.py | ||
---|---|---|
1 |
# Copyright (c) Django Software Foundation and individual contributors. |
|
2 |
# All rights reserved. |
|
3 |
# |
|
4 |
# Redistribution and use in source and binary forms, with or without modification, |
|
5 |
# are permitted provided that the following conditions are met: |
|
6 |
# |
|
7 |
# 1. Redistributions of source code must retain the above copyright notice, |
|
8 |
# this list of conditions and the following disclaimer. |
|
9 |
# |
|
10 |
# 2. Redistributions in binary form must reproduce the above copyright |
|
11 |
# notice, this list of conditions and the following disclaimer in the |
|
12 |
# documentation and/or other materials provided with the distribution. |
|
13 |
# |
|
14 |
# 3. Neither the name of Django nor the names of its contributors may be used |
|
15 |
# to endorse or promote products derived from this software without |
|
16 |
# specific prior written permission. |
|
17 |
# |
|
18 |
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND |
|
19 |
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
20 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
|
21 |
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR |
|
22 |
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
|
23 |
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
|
24 |
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON |
|
25 |
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|
26 |
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|
27 |
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
28 |
|
|
29 |
VERSION = (0, 8, 2, 'alpha', 0) |
|
30 |
|
|
31 |
def get_version(): |
|
32 |
version = '%s.%s' % (VERSION[0], VERSION[1]) |
|
33 |
if VERSION[2]: |
|
34 |
version = '%s.%s' % (version, VERSION[2]) |
|
35 |
if VERSION[3:] == ('alpha', 0): |
|
36 |
version = '%s pre-alpha' % version |
|
37 |
else: |
|
38 |
if VERSION[3] != 'final': |
|
39 |
version = '%s %s %s' % (version, VERSION[3], VERSION[4]) |
|
40 |
return version |
/dev/null | ||
---|---|---|
1 |
# Copyright (c) Django Software Foundation and individual contributors. |
|
2 |
# All rights reserved. |
|
3 |
# |
|
4 |
# Redistribution and use in source and binary forms, with or without modification, |
|
5 |
# are permitted provided that the following conditions are met: |
|
6 |
# |
|
7 |
# 1. Redistributions of source code must retain the above copyright notice, |
|
8 |
# this list of conditions and the following disclaimer. |
|
9 |
# |
|
10 |
# 2. Redistributions in binary form must reproduce the above copyright |
|
11 |
# notice, this list of conditions and the following disclaimer in the |
|
12 |
# documentation and/or other materials provided with the distribution. |
|
13 |
# |
|
14 |
# 3. Neither the name of Django nor the names of its contributors may be used |
|
15 |
# to endorse or promote products derived from this software without |
|
16 |
# specific prior written permission. |
|
17 |
# |
|
18 |
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND |
|
19 |
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
20 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
|
21 |
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR |
|
22 |
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
|
23 |
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
|
24 |
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON |
|
25 |
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|
26 |
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|
27 |
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
28 |
|
|
29 |
import re |
|
30 |
import datetime |
|
31 |
import calendar |
|
32 |
|
|
33 |
MONTHS = 'jan feb mar apr may jun jul aug sep oct nov dec'.split() |
|
34 |
__D = r'(?P<day>\d{2})' |
|
35 |
__D2 = r'(?P<day>[ \d]\d)' |
|
36 |
__M = r'(?P<mon>\w{3})' |
|
37 |
__Y = r'(?P<year>\d{4})' |
|
38 |
__Y2 = r'(?P<year>\d{2})' |
|
39 |
__T = r'(?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})' |
|
40 |
RFC1123_DATE = re.compile(r'^\w{3}, %s %s %s %s GMT$' % (__D, __M, __Y, __T)) |
|
41 |
RFC850_DATE = re.compile(r'^\w{6,9}, %s-%s-%s %s GMT$' % (__D, __M, __Y2, __T)) |
|
42 |
ASCTIME_DATE = re.compile(r'^\w{3} %s %s %s %s$' % (__M, __D2, __T, __Y)) |
|
43 |
|
|
44 |
def parse_http_date(date): |
|
45 |
""" |
|
46 |
Parses a date format as specified by HTTP RFC2616 section 3.3.1. |
|
47 |
|
|
48 |
The three formats allowed by the RFC are accepted, even if only the first |
|
49 |
one is still in widespread use. |
|
50 |
|
|
51 |
Returns an floating point number expressed in seconds since the epoch, in |
|
52 |
UTC. |
|
53 |
""" |
|
54 |
# emails.Util.parsedate does the job for RFC1123 dates; unfortunately |
|
55 |
# RFC2616 makes it mandatory to support RFC850 dates too. So we roll |
|
56 |
# our own RFC-compliant parsing. |
|
57 |
for regex in RFC1123_DATE, RFC850_DATE, ASCTIME_DATE: |
|
58 |
m = regex.match(date) |
|
59 |
if m is not None: |
|
60 |
break |
|
61 |
else: |
|
62 |
raise ValueError("%r is not in a valid HTTP date format" % date) |
|
63 |
try: |
|
64 |
year = int(m.group('year')) |
|
65 |
if year < 100: |
|
66 |
if year < 70: |
|
67 |
year += 2000 |
|
68 |
else: |
|
69 |
year += 1900 |
|
70 |
month = MONTHS.index(m.group('mon').lower()) + 1 |
|
71 |
day = int(m.group('day')) |
|
72 |
hour = int(m.group('hour')) |
|
73 |
min = int(m.group('min')) |
|
74 |
sec = int(m.group('sec')) |
|
75 |
result = datetime.datetime(year, month, day, hour, min, sec) |
|
76 |
return calendar.timegm(result.utctimetuple()) |
|
77 |
except Exception: |
|
78 |
raise ValueError("%r is not a valid date" % date) |
|
79 |
|
|
80 |
def parse_http_date_safe(date): |
|
81 |
""" |
|
82 |
Same as parse_http_date, but returns None if the input is invalid. |
|
83 |
""" |
|
84 |
try: |
|
85 |
return parse_http_date(date) |
|
86 |
except Exception: |
|
87 |
pass |
b/pithos/api/util.py | ||
---|---|---|
47 | 47 |
from django.core.files.uploadhandler import FileUploadHandler |
48 | 48 |
from django.core.files.uploadedfile import UploadedFile |
49 | 49 |
|
50 |
from pithos.api.compat import parse_http_date_safe, parse_http_date |
|
50 |
from pithos.lib.compat import parse_http_date_safe, parse_http_date |
|
51 |
|
|
51 | 52 |
from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, Forbidden, ItemNotFound, |
52 | 53 |
Conflict, LengthRequired, PreconditionFailed, RequestEntityTooLarge, |
53 | 54 |
RangeNotSatisfiable, ServiceUnavailable) |
b/pithos/backends/modular.py | ||
---|---|---|
35 | 35 |
import os |
36 | 36 |
import time |
37 | 37 |
import logging |
38 |
import hashlib |
|
39 | 38 |
import binascii |
40 | 39 |
|
41 | 40 |
from base import NotAllowedError, QuotaError, BaseBackend |
42 | 41 |
|
42 |
from pithos.lib.hashmap import HashMap |
|
43 |
|
|
43 | 44 |
( CLUSTER_NORMAL, CLUSTER_HISTORY, CLUSTER_DELETED ) = range(3) |
44 | 45 |
|
45 | 46 |
inf = float('inf') |
... | ... | |
50 | 51 |
logger = logging.getLogger(__name__) |
51 | 52 |
|
52 | 53 |
|
53 |
class HashMap(list): |
|
54 |
|
|
55 |
def __init__(self, blocksize, blockhash): |
|
56 |
super(HashMap, self).__init__() |
|
57 |
self.blocksize = blocksize |
|
58 |
self.blockhash = blockhash |
|
59 |
|
|
60 |
def _hash_raw(self, v): |
|
61 |
h = hashlib.new(self.blockhash) |
|
62 |
h.update(v) |
|
63 |
return h.digest() |
|
64 |
|
|
65 |
def hash(self): |
|
66 |
if len(self) == 0: |
|
67 |
return self._hash_raw('') |
|
68 |
if len(self) == 1: |
|
69 |
return self.__getitem__(0) |
|
70 |
|
|
71 |
h = list(self) |
|
72 |
s = 2 |
|
73 |
while s < len(h): |
|
74 |
s = s * 2 |
|
75 |
h += [('\x00' * len(h[0]))] * (s - len(h)) |
|
76 |
while len(h) > 1: |
|
77 |
h = [self._hash_raw(h[x] + h[x + 1]) for x in range(0, len(h), 2)] |
|
78 |
return h[0] |
|
79 |
|
|
80 |
|
|
81 | 54 |
def backend_method(func=None, autocommit=1): |
82 | 55 |
if func is None: |
83 | 56 |
def fn(func): |
b/pithos/lib/client.py | ||
---|---|---|
1 |
# Copyright 2011 GRNET S.A. All rights reserved. |
|
2 |
# |
|
3 |
# Redistribution and use in source and binary forms, with or |
|
4 |
# without modification, are permitted provided that the following |
|
5 |
# conditions are met: |
|
6 |
# |
|
7 |
# 1. Redistributions of source code must retain the above |
|
8 |
# copyright notice, this list of conditions and the following |
|
9 |
# disclaimer. |
|
10 |
# |
|
11 |
# 2. Redistributions in binary form must reproduce the above |
|
12 |
# copyright notice, this list of conditions and the following |
|
13 |
# disclaimer in the documentation and/or other materials |
|
14 |
# provided with the distribution. |
|
15 |
# |
|
16 |
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS |
|
17 |
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
18 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|
19 |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR |
|
20 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
21 |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
22 |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
|
23 |
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
|
24 |
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
|
25 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
|
26 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|
27 |
# POSSIBILITY OF SUCH DAMAGE. |
|
28 |
# |
|
29 |
# The views and conclusions contained in the software and |
|
30 |
# documentation are those of the authors and should not be |
|
31 |
# interpreted as representing official policies, either expressed |
|
32 |
# or implied, of GRNET S.A. |
|
33 |
|
|
34 |
from httplib import HTTPConnection, HTTP |
|
35 |
from sys import stdin |
|
36 |
from xml.dom import minidom |
|
37 |
from StringIO import StringIO |
|
38 |
from urllib import quote, unquote |
|
39 |
|
|
40 |
import json |
|
41 |
import types |
|
42 |
import socket |
|
43 |
import urllib |
|
44 |
import datetime |
|
45 |
|
|
46 |
ERROR_CODES = {304:'Not Modified', |
|
47 |
400:'Bad Request', |
|
48 |
401:'Unauthorized', |
|
49 |
403:'Forbidden', |
|
50 |
404:'Not Found', |
|
51 |
409:'Conflict', |
|
52 |
411:'Length Required', |
|
53 |
412:'Precondition Failed', |
|
54 |
413:'Request Entity Too Large', |
|
55 |
416:'Range Not Satisfiable', |
|
56 |
422:'Unprocessable Entity', |
|
57 |
503:'Service Unavailable', |
|
58 |
} |
|
59 |
|
|
60 |
class Fault(Exception): |
|
61 |
def __init__(self, data='', status=None): |
|
62 |
if data == '' and status in ERROR_CODES.keys(): |
|
63 |
data = ERROR_CODES[status] |
|
64 |
Exception.__init__(self, data) |
|
65 |
self.data = data |
|
66 |
self.status = status |
|
67 |
|
|
68 |
class Client(object): |
|
69 |
def __init__(self, host, token, account, api='v1', verbose=False, debug=False): |
|
70 |
"""`host` can also include a port, e.g '127.0.0.1:8000'.""" |
|
71 |
|
|
72 |
self.host = host |
|
73 |
self.account = account |
|
74 |
self.api = api |
|
75 |
self.verbose = verbose or debug |
|
76 |
self.debug = debug |
|
77 |
self.token = token |
|
78 |
|
|
79 |
def _req(self, method, path, body=None, headers={}, format='text', params={}): |
|
80 |
slash = '/' if self.api else '' |
|
81 |
full_path = '%s%s%s?format=%s' % (slash, self.api, quote(path), format) |
|
82 |
|
|
83 |
for k,v in params.items(): |
|
84 |
if v: |
|
85 |
full_path = '%s&%s=%s' %(full_path, quote(k), quote(str(v))) |
|
86 |
else: |
|
87 |
full_path = '%s&%s=' %(full_path, k) |
|
88 |
conn = HTTPConnection(self.host) |
|
89 |
|
|
90 |
kwargs = {} |
|
91 |
for k,v in headers.items(): |
|
92 |
headers.pop(k) |
|
93 |
k = k.replace('_', '-') |
|
94 |
headers[quote(k)] = quote(v, safe='/=,:@ *"') if type(v) == types.StringType else v |
|
95 |
|
|
96 |
kwargs['headers'] = headers |
|
97 |
kwargs['headers']['X-Auth-Token'] = self.token |
|
98 |
if body: |
|
99 |
kwargs['body'] = body |
|
100 |
kwargs['headers'].setdefault('content-type', 'application/octet-stream') |
|
101 |
kwargs['headers'].setdefault('content-length', len(body) if body else 0) |
|
102 |
|
|
103 |
#print '#', method, full_path, kwargs |
|
104 |
t1 = datetime.datetime.utcnow() |
|
105 |
conn.request(method, full_path, **kwargs) |
|
106 |
|
|
107 |
resp = conn.getresponse() |
|
108 |
t2 = datetime.datetime.utcnow() |
|
109 |
#print 'response time:', str(t2-t1) |
|
110 |
headers = resp.getheaders() |
|
111 |
headers = dict((unquote(h), unquote(v)) for h,v in headers) |
|
112 |
|
|
113 |
if self.verbose: |
|
114 |
print '%d %s' % (resp.status, resp.reason) |
|
115 |
for key, val in headers.items(): |
|
116 |
print '%s: %s' % (key.capitalize(), val) |
|
117 |
|
|
118 |
|
|
119 |
length = resp.getheader('content-length', None) |
|
120 |
data = resp.read(length) |
|
121 |
if self.debug: |
|
122 |
print data |
|
123 |
|
|
124 |
|
|
125 |
if int(resp.status) in ERROR_CODES.keys(): |
|
126 |
raise Fault(data, int(resp.status)) |
|
127 |
|
|
128 |
#print '**', resp.status, headers, data, '\n' |
|
129 |
return resp.status, headers, data |
|
130 |
|
|
131 |
def _chunked_transfer(self, path, method='PUT', f=stdin, headers=None, |
|
132 |
blocksize=1024, params={}): |
|
133 |
"""perfomrs a chunked request""" |
|
134 |
http = HTTPConnection(self.host) |
|
135 |
|
|
136 |
# write header |
|
137 |
full_path = '/%s%s?' % (self.api, path) |
|
138 |
|
|
139 |
for k,v in params.items(): |
|
140 |
if v: |
|
141 |
full_path = '%s&%s=%s' %(full_path, k, v) |
|
142 |
else: |
|
143 |
full_path = '%s&%s=' %(full_path, k) |
|
144 |
|
|
145 |
full_path = urllib.quote(full_path, '?&:=/') |
|
146 |
|
|
147 |
http.putrequest(method, full_path) |
|
148 |
http.putheader('x-auth-token', self.token) |
|
149 |
http.putheader('content-type', 'application/octet-stream') |
|
150 |
http.putheader('transfer-encoding', 'chunked') |
|
151 |
if headers: |
|
152 |
for header,value in headers.items(): |
|
153 |
http.putheader(header, value) |
|
154 |
http.endheaders() |
|
155 |
|
|
156 |
# write body |
|
157 |
data = '' |
|
158 |
while True: |
|
159 |
if f.closed: |
|
160 |
break |
|
161 |
block = f.read(blocksize) |
|
162 |
if block == '': |
|
163 |
break |
|
164 |
data = '%x\r\n%s\r\n' % (len(block), block) |
|
165 |
try: |
|
166 |
http.send(data) |
|
167 |
except: |
|
168 |
#retry |
|
169 |
http.send(data) |
|
170 |
data = '0\r\n\r\n' |
|
171 |
try: |
|
172 |
http.send(data) |
|
173 |
except: |
|
174 |
#retry |
|
175 |
http.send(data) |
|
176 |
|
|
177 |
# get response |
|
178 |
resp = http.getresponse() |
|
179 |
|
|
180 |
headers = dict(resp.getheaders()) |
|
181 |
|
|
182 |
if self.verbose: |
|
183 |
print '%d %s' % (resp.status, resp.reason) |
|
184 |
for key, val in headers.items(): |
|
185 |
print '%s: %s' % (key.capitalize(), val) |
|
186 |
|
|
187 |
|
|
188 |
length = resp.getheader('Content-length', None) |
|
189 |
data = resp.read(length) |
|
190 |
if self.debug: |
|
191 |
print data |
|
192 |
|
|
193 |
|
|
194 |
if int(resp.status) in ERROR_CODES.keys(): |
|
195 |
raise Fault(data, int(resp.status)) |
|
196 |
|
|
197 |
#print '*', resp.status, headers, data |
|
198 |
return resp.status, headers, data |
|
199 |
|
|
200 |
def delete(self, path, format='text', params={}): |
|
201 |
return self._req('DELETE', path, format=format, params=params) |
|
202 |
|
|
203 |
def get(self, path, format='text', headers={}, params={}): |
|
204 |
return self._req('GET', path, headers=headers, format=format, |
|
205 |
params=params) |
|
206 |
|
|
207 |
def head(self, path, format='text', params={}): |
|
208 |
return self._req('HEAD', path, format=format, params=params) |
|
209 |
|
|
210 |
def post(self, path, body=None, format='text', headers=None, params={}): |
|
211 |
return self._req('POST', path, body, headers=headers, format=format, |
|
212 |
params=params) |
|
213 |
|
|
214 |
def put(self, path, body=None, format='text', headers=None, params={}): |
|
215 |
return self._req('PUT', path, body, headers=headers, format=format, |
|
216 |
params=params) |
|
217 |
|
|
218 |
def _list(self, path, format='text', params={}, **headers): |
|
219 |
status, headers, data = self.get(path, format=format, headers=headers, |
|
220 |
params=params) |
|
221 |
if format == 'json': |
|
222 |
data = json.loads(data) if data else '' |
|
223 |
elif format == 'xml': |
|
224 |
data = minidom.parseString(data) |
|
225 |
else: |
|
226 |
data = data.split('\n')[:-1] if data else '' |
|
227 |
return data |
|
228 |
|
|
229 |
def _get_metadata(self, path, prefix=None, params={}): |
|
230 |
status, headers, data = self.head(path, params=params) |
|
231 |
prefixlen = len(prefix) if prefix else 0 |
|
232 |
meta = {} |
|
233 |
for key, val in headers.items(): |
|
234 |
if prefix and not key.startswith(prefix): |
|
235 |
continue |
|
236 |
elif prefix and key.startswith(prefix): |
|
237 |
key = key[prefixlen:] |
|
238 |
meta[key] = val |
|
239 |
return meta |
|
240 |
|
|
241 |
def _filter(self, l, d): |
|
242 |
""" |
|
243 |
filter out from l elements having the metadata values provided |
|
244 |
""" |
|
245 |
ll = l |
|
246 |
for elem in l: |
|
247 |
if type(elem) == types.DictionaryType: |
|
248 |
for key in d.keys(): |
|
249 |
k = 'x_object_meta_%s' % key |
|
250 |
if k in elem.keys() and elem[k] == d[key]: |
|
251 |
ll.remove(elem) |
|
252 |
break |
|
253 |
return ll |
|
254 |
|
|
255 |
class OOS_Client(Client): |
|
256 |
"""Openstack Object Storage Client""" |
|
257 |
|
|
258 |
def _update_metadata(self, path, entity, **meta): |
|
259 |
"""adds new and updates the values of previously set metadata""" |
|
260 |
ex_meta = self.retrieve_account_metadata(restricted=True) |
|
261 |
ex_meta.update(meta) |
|
262 |
headers = {} |
|
263 |
prefix = 'x-%s-meta-' % entity |
|
264 |
for k,v in ex_meta.items(): |
|
265 |
k = '%s%s' % (prefix, k) |
|
266 |
headers[k] = v |
|
267 |
return self.post(path, headers=headers) |
|
268 |
|
|
269 |
def _reset_metadata(self, path, entity, **meta): |
|
270 |
""" |
|
271 |
overwrites all user defined metadata |
|
272 |
""" |
|
273 |
headers = {} |
|
274 |
prefix = 'x-%s-meta-' % entity |
|
275 |
for k,v in meta.items(): |
|
276 |
k = '%s%s' % (prefix, k) |
|
277 |
headers[k] = v |
|
278 |
return self.post(path, headers=headers) |
|
279 |
|
|
280 |
def _delete_metadata(self, path, entity, meta=[]): |
|
281 |
"""delete previously set metadata""" |
|
282 |
ex_meta = self.retrieve_account_metadata(restricted=True) |
|
283 |
headers = {} |
|
284 |
prefix = 'x-%s-meta-' % entity |
|
285 |
for k in ex_meta.keys(): |
|
286 |
if k in meta: |
|
287 |
headers['%s%s' % (prefix, k)] = ex_meta[k] |
|
288 |
return self.post(path, headers=headers) |
|
289 |
|
|
290 |
# Storage Account Services |
|
291 |
|
|
292 |
def list_containers(self, format='text', limit=None, |
|
293 |
marker=None, params={}, account=None, **headers): |
|
294 |
"""lists containers""" |
|
295 |
account = account or self.account |
|
296 |
path = '/%s' % account |
|
297 |
params.update({'limit':limit, 'marker':marker}) |
|
298 |
return self._list(path, format, params, **headers) |
|
299 |
|
|
300 |
def retrieve_account_metadata(self, restricted=False, account=None, **params): |
|
301 |
"""returns the account metadata""" |
|
302 |
account = account or self.account |
|
303 |
path = '/%s' % account |
|
304 |
prefix = 'x-account-meta-' if restricted else None |
|
305 |
return self._get_metadata(path, prefix, params) |
|
306 |
|
|
307 |
def update_account_metadata(self, account=None, **meta): |
|
308 |
"""updates the account metadata""" |
|
309 |
account = account or self.account |
|
310 |
path = '/%s' % account |
|
311 |
return self._update_metadata(path, 'account', **meta) |
|
312 |
|
|
313 |
def delete_account_metadata(self, meta=[], account=None): |
|
314 |
"""deletes the account metadata""" |
|
315 |
account = account or self.account |
|
316 |
path = '/%s' % account |
|
317 |
return self._delete_metadata(path, 'account', meta) |
|
318 |
|
|
319 |
def reset_account_metadata(self, account=None, **meta): |
|
320 |
"""resets account metadata""" |
|
321 |
account = account or self.account |
|
322 |
path = '/%s' % account |
|
323 |
return self._reset_metadata(path, 'account', **meta) |
|
324 |
|
|
325 |
# Storage Container Services |
|
326 |
|
|
327 |
def _filter_trashed(self, l): |
|
328 |
return self._filter(l, {'trash':'true'}) |
|
329 |
|
|
330 |
def list_objects(self, container, format='text', |
|
331 |
limit=None, marker=None, prefix=None, delimiter=None, |
|
332 |
path=None, include_trashed=False, params={}, account=None, |
|
333 |
**headers): |
|
334 |
"""returns a list with the container objects""" |
|
335 |
account = account or self.account |
|
336 |
params.update({'limit':limit, 'marker':marker, 'prefix':prefix, |
|
337 |
'delimiter':delimiter, 'path':path}) |
|
338 |
l = self._list('/%s/%s' % (account, container), format, params, |
|
339 |
**headers) |
|
340 |
#TODO support filter trashed with xml also |
|
341 |
if format != 'xml' and not include_trashed: |
|
342 |
l = self._filter_trashed(l) |
|
343 |
return l |
|
344 |
|
|
345 |
def create_container(self, container, account=None, **meta): |
|
346 |
"""creates a container""" |
|
347 |
account = account or self.account |
|
348 |
headers = {} |
|
349 |
for k,v in meta.items(): |
|
350 |
headers['x-container-meta-%s' %k.strip().upper()] = v.strip() |
|
351 |
status, header, data = self.put('/%s/%s' % (account, container), |
|
352 |
headers=headers) |
|
353 |
if status == 202: |
|
354 |
return False |
|
355 |
elif status != 201: |
|
356 |
raise Fault(data, int(status)) |
|
357 |
return True |
|
358 |
|
|
359 |
def delete_container(self, container, params={}, account=None): |
|
360 |
"""deletes a container""" |
|
361 |
account = account or self.account |
|
362 |
return self.delete('/%s/%s' % (account, container), params=params) |
|
363 |
|
|
364 |
def retrieve_container_metadata(self, container, restricted=False, |
|
365 |
account=None, **params): |
|
366 |
"""returns the container metadata""" |
|
367 |
account = account or self.account |
|
368 |
prefix = 'x-container-meta-' if restricted else None |
|
369 |
return self._get_metadata('/%s/%s' % (account, container), prefix, |
|
370 |
params) |
|
371 |
|
|
372 |
def update_container_metadata(self, container, account=None, **meta): |
|
373 |
"""unpdates the container metadata""" |
|
374 |
account = account or self.account |
|
375 |
return self._update_metadata('/%s/%s' % (account, container), |
|
376 |
'container', **meta) |
|
377 |
|
|
378 |
def delete_container_metadata(self, container, meta=[], account=None): |
|
379 |
"""deletes the container metadata""" |
|
380 |
account = account or self.account |
|
381 |
path = '/%s/%s' % (account, container) |
|
382 |
return self._delete_metadata(path, 'container', meta) |
|
383 |
|
|
384 |
# Storage Object Services |
|
385 |
|
|
386 |
def request_object(self, container, object, format='text', params={}, |
|
387 |
account=None, **headers): |
|
388 |
"""returns tuple containing the status, headers and data response for an object request""" |
|
389 |
account = account or self.account |
|
390 |
path = '/%s/%s/%s' % (account, container, object) |
|
391 |
status, headers, data = self.get(path, format, headers, params) |
|
392 |
return status, headers, data |
|
393 |
|
|
394 |
def retrieve_object(self, container, object, format='text', params={}, |
|
395 |
account=None, **headers): |
|
396 |
"""returns an object's data""" |
|
397 |
account = account or self.account |
|
398 |
t = self.request_object(container, object, format, params, account, |
|
399 |
**headers) |
|
400 |
data = t[2] |
|
401 |
if format == 'json': |
|
402 |
data = json.loads(data) if data else '' |
|
403 |
elif format == 'xml': |
|
404 |
data = minidom.parseString(data) |
|
405 |
return data |
|
406 |
|
|
407 |
def retrieve_object_hashmap(self, container, object, params={}, |
|
408 |
account=None, **headers): |
|
409 |
"""returns the hashmap representing object's data""" |
|
410 |
args = locals().copy() |
|
411 |
for elem in ['self', 'container', 'object']: |
|
412 |
args.pop(elem) |
|
413 |
return self.retrieve_object(container, object, format='json', **args) |
|
414 |
|
|
415 |
def create_directory_marker(self, container, object, account=None): |
|
416 |
"""creates a dierectory marker""" |
|
417 |
account = account or self.account |
|
418 |
if not object: |
|
419 |
raise Fault('Directory markers have to be nested in a container') |
|
420 |
h = {'content_type':'application/directory'} |
|
421 |
return self.create_zero_length_object(container, object, account=account, |
|
422 |
**h) |
|
423 |
|
|
424 |
def create_object(self, container, object, f=stdin, format='text', meta={}, |
|
425 |
params={}, etag=None, content_type=None, content_encoding=None, |
|
426 |
content_disposition=None, account=None, **headers): |
|
427 |
"""creates a zero-length object""" |
|
428 |
account = account or self.account |
|
429 |
path = '/%s/%s/%s' % (account, container, object) |
|
430 |
for k, v in headers.items(): |
|
431 |
if v == None: |
|
432 |
headers.pop(k) |
|
433 |
|
|
434 |
l = ['etag', 'content_encoding', 'content_disposition', 'content_type'] |
|
435 |
l = [elem for elem in l if eval(elem)] |
|
436 |
for elem in l: |
|
437 |
headers.update({elem:eval(elem)}) |
|
438 |
headers.setdefault('content-type', 'application/octet-stream') |
|
439 |
|
|
440 |
for k,v in meta.items(): |
|
441 |
headers['x-object-meta-%s' %k.strip()] = v.strip() |
|
442 |
data = f.read() if f else None |
|
443 |
return self.put(path, data, format, headers=headers, params=params) |
|
444 |
|
|
445 |
def create_zero_length_object(self, container, object, meta={}, etag=None, |
|
446 |
content_type=None, content_encoding=None, |
|
447 |
content_disposition=None, account=None, |
|
448 |
**headers): |
|
449 |
account = account or self.account |
|
450 |
args = locals().copy() |
|
451 |
for elem in ['self', 'container', 'headers', 'account']: |
|
452 |
args.pop(elem) |
|
453 |
args.update(headers) |
|
454 |
return self.create_object(container, account=account, f=None, **args) |
|
455 |
|
|
456 |
def update_object(self, container, object, f=stdin, |
|
457 |
offset=None, meta={}, params={}, content_length=None, |
|
458 |
content_type=None, content_encoding=None, |
|
459 |
content_disposition=None, account=None, **headers): |
|
460 |
account = account or self.account |
|
461 |
path = '/%s/%s/%s' % (account, container, object) |
|
462 |
for k, v in headers.items(): |
|
463 |
if v == None: |
|
464 |
headers.pop(k) |
|
465 |
|
|
466 |
l = ['content_encoding', 'content_disposition', 'content_type', |
|
467 |
'content_length'] |
|
468 |
l = [elem for elem in l if eval(elem)] |
|
469 |
for elem in l: |
|
470 |
headers.update({elem:eval(elem)}) |
|
471 |
|
|
472 |
if 'content_range' not in headers.keys(): |
|
473 |
if offset != None: |
|
474 |
headers['content_range'] = 'bytes %s-/*' % offset |
|
475 |
else: |
|
476 |
headers['content_range'] = 'bytes */*' |
|
477 |
|
|
478 |
for k,v in meta.items(): |
|
479 |
headers['x-object-meta-%s' %k.strip()] = v.strip() |
|
480 |
data = f.read() if f else None |
|
481 |
return self.post(path, data, headers=headers, params=params) |
|
482 |
|
|
483 |
def update_object_using_chunks(self, container, object, f=stdin, |
|
484 |
blocksize=1024, offset=None, meta={}, |
|
485 |
params={}, content_type=None, content_encoding=None, |
|
486 |
content_disposition=None, account=None, **headers): |
|
487 |
"""updates an object (incremental upload)""" |
|
488 |
account = account or self.account |
|
489 |
path = '/%s/%s/%s' % (account, container, object) |
|
490 |
headers = headers if not headers else {} |
|
491 |
l = ['content_type', 'content_encoding', 'content_disposition'] |
|
492 |
l = [elem for elem in l if eval(elem)] |
|
493 |
for elem in l: |
|
494 |
headers.update({elem:eval(elem)}) |
|
495 |
|
|
496 |
if offset != None: |
|
497 |
headers['content_range'] = 'bytes %s-/*' % offset |
|
498 |
else: |
|
499 |
headers['content_range'] = 'bytes */*' |
|
500 |
|
|
501 |
for k,v in meta.items(): |
|
502 |
v = v.strip() |
|
503 |
headers['x-object-meta-%s' %k.strip()] = v |
|
504 |
return self._chunked_transfer(path, 'POST', f, headers=headers, |
|
505 |
blocksize=blocksize, params=params) |
|
506 |
|
|
507 |
def _change_obj_location(self, src_container, src_object, dst_container, |
|
508 |
dst_object, remove=False, meta={}, account=None, |
|
509 |
content_type=None, **headers): |
|
510 |
account = account or self.account |
|
511 |
path = '/%s/%s/%s' % (account, dst_container, dst_object) |
|
512 |
headers = {} if not headers else headers |
|
513 |
for k, v in meta.items(): |
|
514 |
headers['x-object-meta-%s' % k] = v |
|
515 |
if remove: |
|
516 |
headers['x-move-from'] = '/%s/%s' % (src_container, src_object) |
|
517 |
else: |
|
518 |
headers['x-copy-from'] = '/%s/%s' % (src_container, src_object) |
|
519 |
headers['content_length'] = 0 |
|
520 |
if content_type: |
|
521 |
headers['content_type'] = content_type |
|
522 |
return self.put(path, headers=headers) |
|
523 |
|
|
524 |
def copy_object(self, src_container, src_object, dst_container, dst_object, |
|
525 |
meta={}, account=None, content_type=None, **headers): |
|
526 |
"""copies an object""" |
|
527 |
account = account or self.account |
|
528 |
return self._change_obj_location(src_container, src_object, |
|
529 |
dst_container, dst_object, account=account, |
|
530 |
remove=False, meta=meta, |
|
531 |
content_type=content_type, **headers) |
|
532 |
|
|
533 |
def move_object(self, src_container, src_object, dst_container, |
|
534 |
dst_object, meta={}, account=None, |
|
535 |
content_type=None, **headers): |
|
536 |
"""moves an object""" |
|
537 |
account = account or self.account |
Also available in: Unified diff