# Copyright (C) 2012-2014 Aleksey Lim
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# pylint: disable-msg=W0611
import os
import re
import json
import shutil
import logging
from os.path import join, exists
from sugar_network import db, toolkit
from sugar_network.model import FrontRoutes
from sugar_network.node import model
from sugar_network.toolkit.router import ACL, File, route
from sugar_network.toolkit.router import fallbackroute, preroute, postroute
from sugar_network.toolkit.spec import parse_version
from sugar_network.toolkit.coroutine import this
from sugar_network.toolkit import http, coroutine, ranges, enforce
_GROUPED_DIFF_LIMIT = 1024
_GUID_RE = re.compile('[a-zA-Z0-9_+-.]+$')
_logger = logging.getLogger('node.routes')
class NodeRoutes(db.Routes, FrontRoutes):
def __init__(self, guid, auth=None, **kwargs):
db.Routes.__init__(self, **kwargs)
FrontRoutes.__init__(self)
self._guid = guid
self._auth = auth
self._batch_dir = join(self.volume.root, 'batch')
if not exists(self._batch_dir):
os.makedirs(self._batch_dir)
@property
def guid(self):
return self._guid
@preroute
def preroute(self, op):
request = this.request
if request.principal:
this.principal = request.principal
elif op.acl & ACL.AUTH:
this.principal = self._auth.logon(request)
else:
this.principal = None
if op.acl & ACL.AUTHOR and not this.principal.cap_author_override:
if request.resource == 'user':
allowed = (this.principal == request.guid)
else:
allowed = this.principal in this.resource['author']
enforce(allowed, http.Forbidden, 'Authors only')
if op.acl & ACL.AGG_AUTHOR and not this.principal.cap_author_override:
if this.resource.metadata[request.prop].acl & ACL.AUTHOR:
allowed = this.principal in this.resource['author']
elif request.key:
value = this.resource[request.prop].get(request.key)
allowed = value is None or this.principal in value['author']
else:
allowed = True
enforce(allowed, http.Forbidden, 'Authors only')
if op.acl & ACL.ADMIN:
enforce(this.principal.cap_admin, http.Forbidden, 'Admins only')
@postroute
def postroute(self, result, exception):
request = this.request
if not request.guid:
return result
pull = request.headers['pull']
if pull is None:
return result
this.response.content_type = 'application/octet-stream'
return model.diff_resource(pull)
@route('GET', cmd='logon', acl=ACL.AUTH)
def logon(self):
pass
@route('GET', cmd='whoami', mime_type='application/json')
def whoami(self):
return {'guid': this.principal,
'route': 'direct',
}
@route('GET', cmd='status', mime_type='application/json')
def status(self):
return {'guid': self.guid,
'seqno': {
'db': self.volume.seqno.value,
'releases': self.volume.release_seqno.value,
},
}
@route('POST', ['user'], mime_type='application/json')
def register(self):
# To avoid authentication while registering new user
self.create()
@fallbackroute('GET', ['packages'])
def route_packages(self):
path = this.request.path
if path and path[-1] == 'updates':
result = []
last_modified = 0
for blob in self.volume.blobs.diff(
[[this.request.if_modified_since + 1, None]],
join(*path[:-1]), recursive=False):
if '.' in blob.name:
continue
result.append(blob.name)
last_modified = max(last_modified, blob.mtime)
this.response.content_type = 'application/json'
if last_modified:
this.response.last_modified = last_modified
return result
blob = self.volume.blobs.get(join(*path))
if isinstance(blob, File):
return blob
else:
this.response.content_type = 'application/json'
return [i.name for i in blob if '.' not in i.name]
@route('POST', ['context'], cmd='submit',
arguments={'initial': False},
mime_type='application/json', acl=ACL.AUTH)
def submit_release(self, initial):
blob = self.volume.blobs.post(
this.request.content, this.request.content_type)
try:
context, release = model.load_bundle(blob, initial=initial)
except Exception:
self.volume.blobs.delete(blob.digest)
raise
this.call(method='POST', path=['context', context, 'releases'],
content_type='application/json', content=release)
return blob.digest
@route('GET', ['context', None], cmd='solve',
arguments={'requires': list, 'stability': list, 'assume': list},
mime_type='application/json')
def solve(self, assume=None):
assume_ = this.request['assume'] = {}
for item in assume or []:
enforce('-' in item, http.BadRequest,
"'assume' should be formed as '-")
context, version = item.split('-', 1)
assume_[context] = parse_version(version)
solution = model.solve(self.volume, this.request.guid, **this.request)
enforce(solution is not None, 'Failed to solve')
return solution
@route('GET', ['context', None], cmd='clone',
arguments={'requires': list, 'stability': list, 'assume': list})
def clone(self, assume=None):
solution = self.solve(assume)
return self.volume.blobs.get(solution[this.request.guid]['blob'])
@route('GET', [None, None], cmd='diff')
def diff_resource(self):
return model.diff_resource(this.request.headers['ranges'])
@route('GET', [None], cmd='diff', mime_type='application/json')
def grouped_diff(self, key):
request = this.request
enforce(request.resource != 'user', http.BadRequest,
'Not allowed for User resource')
if not key:
key = 'guid'
in_r = request.headers['ranges'] or [[1, None]]
diff = {}
for doc in self.volume[request.resource].diff(in_r):
out_r = diff.get(doc[key])
if out_r is None:
if len(diff) >= _GROUPED_DIFF_LIMIT:
break
out_r = diff[doc[key]] = []
ranges.include(out_r, doc['seqno'], doc['seqno'])
doc.diff(in_r, out_r)
return diff
@route('POST', cmd='apply', acl=ACL.AUTH)
def batched_post(self):
with toolkit.NamedTemporaryFile(dir=self._batch_dir,
prefix=this.principal, delete=False) as batch:
try:
shutil.copyfileobj(this.request.content, batch)
except Exception:
os.unlink(batch.name)
raise
with file(batch.name + '.meta', 'w') as f:
json.dump({'principal': this.principal.dump()}, f)
coroutine.spawn(model.apply_batch, batch.name)
def create(self):
if this.principal and this.principal.cap_create_with_guid:
guid = this.request.content.get('guid')
enforce(not guid or _GUID_RE.match(guid), http.BadRequest,
'Malformed GUID')
else:
enforce('guid' not in this.request.content, http.BadRequest,
'GUID should not be specified')
return db.Routes.create(self)
this.principal = None