diff --git a/swift/obj/server.py b/swift/obj/server.py index a5f90ef49c..b22b8221f8 100644 --- a/swift/obj/server.py +++ b/swift/obj/server.py @@ -21,6 +21,7 @@ import multiprocessing import time import traceback import socket +import math from datetime import datetime from swift import gettext_ as _ from hashlib import md5 @@ -492,7 +493,7 @@ class ObjectController(object): except (OverflowError, ValueError): # catches timestamps before the epoch return HTTPPreconditionFailed(request=request) - if if_modified_since and file_x_ts_utc < if_modified_since: + if if_modified_since and file_x_ts_utc <= if_modified_since: return HTTPNotModified(request=request) keep_cache = (self.keep_cache_private or ('X-Auth-Token' not in request.headers and @@ -507,7 +508,7 @@ class ObjectController(object): key.lower() in self.allowed_headers: response.headers[key] = value response.etag = metadata['ETag'] - response.last_modified = file_x_ts_flt + response.last_modified = math.ceil(file_x_ts_flt) response.content_length = obj_size try: response.content_encoding = metadata[ @@ -549,7 +550,7 @@ class ObjectController(object): response.headers[key] = value response.etag = metadata['ETag'] ts = metadata['X-Timestamp'] - response.last_modified = float(ts) + response.last_modified = math.ceil(float(ts)) # Needed for container sync feature response.headers['X-Timestamp'] = ts response.content_length = int(metadata['Content-Length']) diff --git a/swift/proxy/controllers/obj.py b/swift/proxy/controllers/obj.py index b230269e48..189e909756 100644 --- a/swift/proxy/controllers/obj.py +++ b/swift/proxy/controllers/obj.py @@ -28,6 +28,7 @@ import itertools import mimetypes import re import time +import math from datetime import datetime from swift import gettext_ as _ from urllib import unquote, quote @@ -1164,7 +1165,7 @@ class ObjectController(Controller): resp.headers['X-Copied-From-Last-Modified'] = \ source_resp.headers['last-modified'] copy_headers_into(req, resp) - resp.last_modified = float(req.headers['X-Timestamp']) + resp.last_modified = math.ceil(float(req.headers['X-Timestamp'])) return resp @public diff --git a/test/functional/swift_test_client.py b/test/functional/swift_test_client.py index 67d4472302..3ae4abdbf5 100644 --- a/test/functional/swift_test_client.py +++ b/test/functional/swift_test_client.py @@ -723,7 +723,8 @@ class File(Base): else: raise RuntimeError - def write(self, data='', hdrs={}, parms={}, callback=None, cfg={}): + def write(self, data='', hdrs={}, parms={}, callback=None, cfg={}, + return_resp=False): block_size = 2 ** 20 if isinstance(data, file): @@ -767,6 +768,9 @@ class File(Base): pass self.md5 = self.compute_md5sum(data) + if return_resp: + return self.conn.response + return True def write_random(self, size=None, hdrs={}, parms={}, cfg={}): @@ -776,3 +780,12 @@ class File(Base): self.conn.make_path(self.path)) self.md5 = self.compute_md5sum(StringIO.StringIO(data)) return data + + def write_random_return_resp(self, size=None, hdrs={}, parms={}, cfg={}): + data = self.random_data(size) + resp = self.write(data, hdrs=hdrs, parms=parms, cfg=cfg, + return_resp=True) + if not resp: + raise ResponseError(self.conn.response) + self.md5 = self.compute_md5sum(StringIO.StringIO(data)) + return resp diff --git a/test/functional/tests.py b/test/functional/tests.py index d1c626e12d..3a8a02f512 100644 --- a/test/functional/tests.py +++ b/test/functional/tests.py @@ -1732,6 +1732,28 @@ class TestFileComparison(Base): self.assertRaises(ResponseError, file_item.read, hdrs=hdrs) self.assert_status(412) + def testLastModified(self): + file_name = Utils.create_name() + content_type = Utils.create_name() + + file = self.env.container.file(file_name) + file.content_type = content_type + resp = file.write_random_return_resp(self.env.file_size) + put_last_modified = resp.getheader('last-modified') + + file = self.env.container.file(file_name) + info = file.info() + self.assert_('last_modified' in info) + last_modified = info['last_modified'] + self.assertEqual(put_last_modified, info['last_modified']) + + hdrs = {'If-Modified-Since': last_modified} + self.assertRaises(ResponseError, file.read, hdrs=hdrs) + self.assert_status(304) + + hdrs = {'If-Unmodified-Since': last_modified} + self.assert_(file.read(hdrs=hdrs)) + class TestFileComparisonUTF8(Base2, TestFileComparison): set_up = False diff --git a/test/unit/obj/test_server.py b/test/unit/obj/test_server.py index fb25f12abb..fd6c11e201 100755 --- a/test/unit/obj/test_server.py +++ b/test/unit/obj/test_server.py @@ -21,6 +21,7 @@ import operator import os import mock import unittest +import math from shutil import rmtree from StringIO import StringIO from time import gmtime, strftime, time @@ -739,7 +740,8 @@ class TestObjectController(unittest.TestCase): self.assertEquals(resp.headers['content-type'], 'application/x-test') self.assertEquals( resp.headers['last-modified'], - strftime('%a, %d %b %Y %H:%M:%S GMT', gmtime(float(timestamp)))) + strftime('%a, %d %b %Y %H:%M:%S GMT', + gmtime(math.ceil(float(timestamp))))) self.assertEquals(resp.headers['etag'], '"0b4c12d7e0a73840c1c4f148fda3b037"') self.assertEquals(resp.headers['x-object-meta-1'], 'One') @@ -841,7 +843,8 @@ class TestObjectController(unittest.TestCase): self.assertEquals(resp.headers['content-type'], 'application/x-test') self.assertEquals( resp.headers['last-modified'], - strftime('%a, %d %b %Y %H:%M:%S GMT', gmtime(float(timestamp)))) + strftime('%a, %d %b %Y %H:%M:%S GMT', + gmtime(math.ceil(float(timestamp))))) self.assertEquals(resp.headers['etag'], '"0b4c12d7e0a73840c1c4f148fda3b037"') self.assertEquals(resp.headers['x-object-meta-1'], 'One') @@ -1043,6 +1046,37 @@ class TestObjectController(unittest.TestCase): resp = req.get_response(self.object_controller) self.assertEquals(resp.status_int, 304) + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'HEAD'}) + resp = req.get_response(self.object_controller) + since = resp.headers['Last-Modified'] + self.assertEquals(since, strftime('%a, %d %b %Y %H:%M:%S GMT', + gmtime(math.ceil(float(timestamp))))) + + req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'GET'}, + headers={'If-Modified-Since': since}) + resp = self.object_controller.GET(req) + self.assertEquals(resp.status_int, 304) + + timestamp = normalize_timestamp(int(time())) + req = Request.blank('/sda1/p/a/c/o2', + environ={'REQUEST_METHOD': 'PUT'}, + headers={ + 'X-Timestamp': timestamp, + 'Content-Type': 'application/octet-stream', + 'Content-Length': '4'}) + req.body = 'test' + resp = req.get_response(self.object_controller) + self.assertEquals(resp.status_int, 201) + + since = strftime('%a, %d %b %Y %H:%M:%S GMT', + gmtime(float(timestamp))) + req = Request.blank('/sda1/p/a/c/o2', + environ={'REQUEST_METHOD': 'GET'}, + headers={'If-Modified-Since': since}) + resp = req.get_response(self.object_controller) + self.assertEquals(resp.status_int, 304) + def test_GET_if_unmodified_since(self): timestamp = normalize_timestamp(time()) req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, @@ -1079,6 +1113,18 @@ class TestObjectController(unittest.TestCase): resp = req.get_response(self.object_controller) self.assertEquals(resp.status_int, 200) + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'HEAD'}) + resp = req.get_response(self.object_controller) + since = resp.headers['Last-Modified'] + self.assertEquals(since, strftime('%a, %d %b %Y %H:%M:%S GMT', + gmtime(math.ceil(float(timestamp))))) + + req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'GET'}, + headers={'If-Unmodified-Since': since}) + resp = self.object_controller.GET(req) + self.assertEquals(resp.status_int, 200) + def test_GET_quarantine(self): # Test swift.obj.server.ObjectController.GET timestamp = normalize_timestamp(time()) diff --git a/test/unit/proxy/test_server.py b/test/unit/proxy/test_server.py index 7b00c56a60..8803e4a797 100644 --- a/test/unit/proxy/test_server.py +++ b/test/unit/proxy/test_server.py @@ -1036,6 +1036,56 @@ class TestObjectController(unittest.TestCase): finally: swift.proxy.controllers.obj.MAX_FILE_SIZE = MAX_FILE_SIZE + def test_PUT_last_modified(self): + prolis = _test_sockets[0] + sock = connect_tcp(('localhost', prolis.getsockname()[1])) + fd = sock.makefile() + fd.write('PUT /v1/a/c/o.last_modified HTTP/1.1\r\n' + 'Host: localhost\r\nConnection: close\r\n' + 'X-Storage-Token: t\r\nContent-Length: 0\r\n\r\n') + fd.flush() + headers = readuntil2crlfs(fd) + exp = 'HTTP/1.1 201' + lm_hdr = 'Last-Modified: ' + self.assertEqual(headers[:len(exp)], exp) + + last_modified_put = [line for line in headers.split('\r\n') + if lm_hdr in line][0][len(lm_hdr):] + sock = connect_tcp(('localhost', prolis.getsockname()[1])) + fd = sock.makefile() + fd.write('HEAD /v1/a/c/o.last_modified HTTP/1.1\r\n' + 'Host: localhost\r\nConnection: close\r\n' + 'X-Storage-Token: t\r\n\r\n') + fd.flush() + headers = readuntil2crlfs(fd) + exp = 'HTTP/1.1 200' + self.assertEqual(headers[:len(exp)], exp) + last_modified_head = [line for line in headers.split('\r\n') + if lm_hdr in line][0][len(lm_hdr):] + self.assertEqual(last_modified_put, last_modified_head) + + sock = connect_tcp(('localhost', prolis.getsockname()[1])) + fd = sock.makefile() + fd.write('GET /v1/a/c/o.last_modified HTTP/1.1\r\n' + 'Host: localhost\r\nConnection: close\r\n' + 'If-Modified-Since: %s\r\n' + 'X-Storage-Token: t\r\n\r\n' % last_modified_put) + fd.flush() + headers = readuntil2crlfs(fd) + exp = 'HTTP/1.1 304' + self.assertEqual(headers[:len(exp)], exp) + + sock = connect_tcp(('localhost', prolis.getsockname()[1])) + fd = sock.makefile() + fd.write('GET /v1/a/c/o.last_modified HTTP/1.1\r\n' + 'Host: localhost\r\nConnection: close\r\n' + 'If-Unmodified-Since: %s\r\n' + 'X-Storage-Token: t\r\n\r\n' % last_modified_put) + fd.flush() + headers = readuntil2crlfs(fd) + exp = 'HTTP/1.1 200' + self.assertEqual(headers[:len(exp)], exp) + def test_expirer_DELETE_on_versioned_object(self): test_errors = []