Commit 1ca52eb4 authored by Antony Chazapis's avatar Antony Chazapis
Browse files

Added GET/PUT/COPY object API.

parent 82ea7d7a
......@@ -13,6 +13,9 @@ class Fault(Exception):
self.details = details
self.name = name or camelCase(self.__class__.__name__)
class NotModified(Fault):
code = 304
class BadRequest(Fault):
code = 400
......@@ -25,5 +28,17 @@ class ResizeNotAllowed(Fault):
class ItemNotFound(Fault):
code = 404
class LengthRequired(Fault):
code = 411
class PreconditionFailed(Fault):
code = 412
class RangeNotSatisfiable(Fault):
code = 416
class UnprocessableEntity(Fault):
code = 422
class ServiceUnavailable(Fault):
code = 503
......@@ -5,9 +5,15 @@
from django.http import HttpResponse
from django.template.loader import render_to_string
from django.utils import simplejson as json
from django.utils.http import http_date, parse_etags
from pithos.api.faults import Fault, BadRequest, Unauthorized
from pithos.api.util import api_method
try:
from django.utils.http import parse_http_date_safe
except:
from pithos.api.util import parse_http_date_safe
from pithos.api.faults import Fault, NotModified, BadRequest, Unauthorized, LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity
from pithos.api.util import get_object_meta, get_range, api_method
from pithos.backends.dummy_debug import *
......@@ -61,6 +67,8 @@ def object_demux(request, v_account, v_container, v_object):
return object_read(request, v_account, v_container, v_object)
elif request.method == 'PUT':
return object_write(request, v_account, v_container, v_object)
elif request.method == 'COPY':
return object_copy(request, v_account, v_container, v_object)
elif request.method == 'POST':
return object_update(request, v_account, v_container, v_object)
elif request.method == 'DELETE':
......@@ -210,8 +218,7 @@ def object_meta(request, v_account, v_container, v_object):
response['ETag'] = info['hash']
response['Content-Length'] = info['bytes']
response['Content-Type'] = info['content_type']
# TODO: Format time.
response['Last-Modified'] = info['last_modified']
response['Last-Modified'] = http_date(info['last_modified'])
for k, v in info['meta'].iteritems():
response['X-Object-Meta-%s' % k.capitalize()] = v
......@@ -219,11 +226,160 @@ def object_meta(request, v_account, v_container, v_object):
@api_method('GET')
def object_read(request, v_account, v_container, v_object):
return HttpResponse("object_read: %s %s %s" % (v_account, v_container, v_object))
# Normal Response Codes: 200, 206
# Error Response Codes: serviceUnavailable (503),
# rangeNotSatisfiable (416),
# preconditionFailed (412),
# itemNotFound (404),
# unauthorized (401),
# badRequest (400),
# notModified (304)
info = get_object_meta(request.user, v_container, v_object)
response = HttpResponse()
response['ETag'] = info['hash']
response['Content-Type'] = info['content_type']
response['Last-Modified'] = http_date(info['last_modified'])
# Range handling.
range = get_range(request)
if range is not None:
offset, length = range
if not length:
length = 0
if offset + length > info['bytes']:
raise RangeNotSatisfiable()
response['Content-Length'] = length
response.status_code = 206
else:
offset = 0
length = 0
response['Content-Length'] = info['bytes']
response.status_code = 200
# Conditions (according to RFC2616 must be evaluated at the end).
if_match = request.META.get('HTTP_IF_MATCH')
if if_match is not None and if_match != '*':
if info['hash'] not in parse_etags(if_match):
raise PreconditionFailed()
if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
# if if_none_match is not None:
# if if_none_match = '*' or info['hash'] in parse_etags(if_none_match):
# raise NotModified()
if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
if if_modified_since is not None:
if_modified_since = parse_http_date_safe(if_modified_since)
if if_modified_since is not None and info['last_modified'] <= if_modified_since:
raise NotModified()
if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
if if_unmodified_since is not None:
if_unmodified_since = parse_http_date_safe(if_unmodified_since)
if if_unmodified_since is not None and info['last_modified'] > if_unmodified_since:
raise PreconditionFailed()
response.content = get_object_data(request.user, v_container, v_object, offset, length)
return response
@api_method('PUT')
def object_write(request, v_account, v_container, v_object):
return HttpResponse("object_write: %s %s %s" % (v_account, v_container, v_object))
# Normal Response Codes: 201
# Error Response Codes: serviceUnavailable (503),
# unprocessableEntity (422),
# lengthRequired (411),
# itemNotFound (404),
# unauthorized (401),
# badRequest (400)
copy_from = request.META.get('HTTP_X_COPY_FROM')
if copy_from:
parts = copy_from.split('/')
if len(parts) < 3 or parts[0] != '':
raise BadRequest('Bad X-Copy-From path.')
copy_container = parts[1]
copy_name = '/'.join(parts[2:])
info = get_object_meta(request.user, copy_container, copy_name)
content_length = request.META.get('CONTENT_LENGTH')
content_type = request.META.get('CONTENT_TYPE')
if not content_length:
raise LengthRequired()
if content_type:
info['content_type'] = content_type
meta = get_object_meta(request)
for k, v in meta.iteritems():
info['meta'][k] = v
copy_object(request.user, copy_container, copy_name, v_container, v_object)
update_object_meta(request.user, v_container, v_object, info)
response = HttpResponse(status = 201)
else:
content_length = request.META.get('CONTENT_LENGTH')
content_type = request.META.get('CONTENT_TYPE')
if not content_length or not content_type:
raise LengthRequired()
meta = get_object_meta(request)
info = {'bytes': content_length, 'content_type': content_type, 'meta': meta}
etag = request.META.get('HTTP_ETAG')
if etag:
etag = parse_etags(etag)[0] # TODO: Unescape properly.
info['hash'] = etag
data = request.read()
# TODO: Hash function.
# etag = hash(data)
# if info.get('hash') and info['hash'] != etag:
# raise UnprocessableEntity()
update_object_data(request.user, v_container, v_name, info, data)
response = HttpResponse(status = 201)
# response['ETag'] = etag
return response
@api_method('COPY')
def object_copy(request, v_account, v_container, v_object):
# Normal Response Codes: 201
# Error Response Codes: serviceUnavailable (503),
# itemNotFound (404),
# unauthorized (401),
# badRequest (400)
destination = request.META.get('HTTP_DESTINATION')
if not destination:
raise BadRequest('Missing Destination.');
parts = destination.split('/')
if len(parts) < 3 or parts[0] != '':
raise BadRequest('Bad Destination path.')
dest_container = parts[1]
dest_name = '/'.join(parts[2:])
info = get_object_meta(request.user, v_container, v_object)
content_type = request.META.get('CONTENT_TYPE')
if content_type:
info['content_type'] = content_type
meta = get_object_meta(request)
for k, v in meta.iteritems():
info['meta'][k] = v
copy_object(request.user, v_container, v_object, dest_container, dest_name)
update_object_meta(request.user, dest_container, dest_name, info)
response = HttpResponse(status = 201)
@api_method('POST')
def object_update(request, v_account, v_container, v_object):
......@@ -233,8 +389,7 @@ def object_update(request, v_account, v_container, v_object):
# unauthorized (401),
# badRequest (400)
prefix = 'X-Object-Meta-'
meta = dict([(k[len(prefix):].lower(), v) for k, v in request.POST.iteritems() if k.startswith(prefix)])
meta = get_object_meta(request)
update_object_meta(request.user, v_container, v_object, meta)
return HttpResponse(status = 202)
......
......@@ -22,55 +22,115 @@ import datetime
import dateutil.parser
import logging
# class UTC(tzinfo):
# def utcoffset(self, dt):
# return timedelta(0)
#
# def tzname(self, dt):
# return 'UTC'
#
# def dst(self, dt):
# return timedelta(0)
#
#
# def isoformat(d):
# """Return an ISO8601 date string that includes a timezon."""
#
# return d.replace(tzinfo=UTC()).isoformat()
#
# def isoparse(s):
# """Parse an ISO8601 date string into a datetime object."""
#
# if not s:
# return None
#
# try:
# since = dateutil.parser.parse(s)
# utc_since = since.astimezone(UTC()).replace(tzinfo=None)
# except ValueError:
# raise BadRequest('Invalid changes-since parameter.')
#
# now = datetime.datetime.now()
# if utc_since > now:
# raise BadRequest('changes-since value set in the future.')
#
# if now - utc_since > timedelta(seconds=settings.POLL_LIMIT):
# raise BadRequest('Too old changes-since value.')
#
# return utc_since
#
# def random_password(length=8):
# pool = ascii_letters + digits
# return ''.join(choice(pool) for i in range(length))
#
#
# def get_user():
# # XXX Placeholder function, everything belongs to a single SynnefoUser for now
# try:
# return SynnefoUser.objects.all()[0]
# except IndexError:
# raise Unauthorized
#
import re
import calendar
# Part of newer Django versions.
__D = r'(?P<day>\d{2})'
__D2 = r'(?P<day>[ \d]\d)'
__M = r'(?P<mon>\w{3})'
__Y = r'(?P<year>\d{4})'
__Y2 = r'(?P<year>\d{2})'
__T = r'(?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})'
RFC1123_DATE = re.compile(r'^\w{3}, %s %s %s %s GMT$' % (__D, __M, __Y, __T))
RFC850_DATE = re.compile(r'^\w{6,9}, %s-%s-%s %s GMT$' % (__D, __M, __Y2, __T))
ASCTIME_DATE = re.compile(r'^\w{3} %s %s %s %s$' % (__M, __D2, __T, __Y))
def parse_http_date(date):
"""
Parses a date format as specified by HTTP RFC2616 section 3.3.1.
The three formats allowed by the RFC are accepted, even if only the first
one is still in widespread use.
Returns an floating point number expressed in seconds since the epoch, in
UTC.
"""
# emails.Util.parsedate does the job for RFC1123 dates; unfortunately
# RFC2616 makes it mandatory to support RFC850 dates too. So we roll
# our own RFC-compliant parsing.
for regex in RFC1123_DATE, RFC850_DATE, ASCTIME_DATE:
m = regex.match(date)
if m is not None:
break
else:
raise ValueError("%r is not in a valid HTTP date format" % date)
try:
year = int(m.group('year'))
if year < 100:
if year < 70:
year += 2000
else:
year += 1900
month = MONTHS.index(m.group('mon').lower()) + 1
day = int(m.group('day'))
hour = int(m.group('hour'))
min = int(m.group('min'))
sec = int(m.group('sec'))
result = datetime.datetime(year, month, day, hour, min, sec)
return calendar.timegm(result.utctimetuple())
except Exception:
raise ValueError("%r is not a valid date" % date)
def parse_http_date_safe(date):
"""
Same as parse_http_date, but returns None if the input is invalid.
"""
try:
return parse_http_date(date)
except Exception:
pass
def get_object_meta(request):
"""
Get all X-Object-Meta-* headers in a dict.
"""
prefix = 'HTTP_X_OBJECT_META_'
return dict([(k[len(prefix):].lower(), v) for k, v in request.META.iteritems() if k.startswith(prefix)])
def get_range(request):
"""
Parse a Range header from the request.
Either returns None, or an (offset, length) tuple.
If no offset is defined offset equals 0.
If no length is defined length is None.
"""
range = request.GET.get('range')
if not range:
return None
range = range.replace(' ', '')
if not range.startswith('bytes='):
return None
parts = range.split('-')
if len(parts) != 2:
return None
offset, length = parts
if offset == '' and length == '':
return None
if offset != '':
try:
offset = int(offset)
except ValueError:
return None
else:
offset = 0
if length != '':
try:
length = int(length)
except ValueError:
return None
else:
length = None
return (offset, length)
# def get_vm(server_id):
# """Return a VirtualMachine instance or raise ItemNotFound."""
#
......@@ -133,21 +193,6 @@ def update_response_headers(request, response):
if settings.TEST:
response['Date'] = format_date_time(time())
# def render_metadata(request, metadata, use_values=False, status=200):
# if request.serialization == 'xml':
# data = render_to_string('metadata.xml', {'metadata': metadata})
# else:
# d = {'metadata': {'values': metadata}} if use_values else {'metadata': metadata}
# data = json.dumps(d)
# return HttpResponse(data, status=status)
#
# def render_meta(request, meta, status=200):
# if request.serialization == 'xml':
# data = render_to_string('meta.xml', {'meta': meta})
# else:
# data = json.dumps({'meta': {meta.meta_key: meta.meta_value}})
# return HttpResponse(data, status=status)
def render_fault(request, fault):
if settings.DEBUG or settings.TEST:
fault.details = format_exc(fault)
......@@ -164,9 +209,10 @@ def render_fault(request, fault):
return resp
def request_serialization(request, format_allowed=False):
"""Return the serialization format requested.
"""
Return the serialization format requested.
Valid formats are 'text' and 'json', 'xml' if `format_allowed` is True.
Valid formats are 'text' and 'json', 'xml' if `format_allowed` is True.
"""
if not format_allowed:
......@@ -178,6 +224,7 @@ def request_serialization(request, format_allowed=False):
elif format == 'xml':
return 'xml'
# TODO: Do we care of Accept headers?
# for item in request.META.get('HTTP_ACCEPT', '').split(','):
# accept, sep, rest = item.strip().partition(';')
# if accept == 'application/json':
......@@ -188,7 +235,9 @@ def request_serialization(request, format_allowed=False):
return 'text'
def api_method(http_method = None, format_allowed = False):
"""Decorator function for views that implement an API method."""
"""
Decorator function for views that implement an API method.
"""
def decorator(func):
@wraps(func)
......
......@@ -118,12 +118,15 @@ def update_object_meta(account, container, name, meta):
return
def get_object_data(account, container, name, offset=0, length=0):
logging.debug("get_object_data: %s %s %s %s %s", account, container, name, offset, length)
return ''
def update_object_data(account, container, name, meta, data):
logging.debug("update_object_data: %s %s %s %s %s", account, container, name, meta, data)
return
def copy_object(account, container, name, new_name):
def copy_object(account, container, name, new_container, new_name):
logging.debug("copy_object: %s %s %s %s %s", account, container, name, new_container, new_name)
return
def delete_object(account, container, name):
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment