From 9fd537a08245cf6a34c1abdf8b7c42bfd3669c74 Mon Sep 17 00:00:00 2001
From: Tim Burke <tim.burke@gmail.com>
Date: Tue, 8 Dec 2015 10:45:07 -0800
Subject: [PATCH] Use application/directory content-type for dir markers

Previously, we were using a content-type of text/directory, but that is
already defined in RFC 2425 and doesn't reflect our usage:

   The text/directory Content-Type is defined for holding a variety
   of directory information, for example, name, or email address,
   or logo.

(From there it goes on to describe a superset of the vCard format
defined in RFC 2426.)

application/directory, on the other hand, is used by Static Web [1] and
is used by cloudfuse [2]. Seems like as sane a choice as any to
standardize on.

[1] https://github.com/openstack/swift/blob/2.5.0/swift/common/middleware/staticweb.py#L71-L75
[2] https://github.com/redbo/cloudfuse/blob/1.0/README#L105-L106

Change-Id: I19e30484270886292d83f50e7ee997b6e1623ec7
---
 swiftclient/service.py     |  15 ++--
 tests/unit/test_service.py | 136 +++++++++++++++++++++++++++++++++++++
 2 files changed, 145 insertions(+), 6 deletions(-)

diff --git a/swiftclient/service.py b/swiftclient/service.py
index 99c833e0..232815ff 100644
--- a/swiftclient/service.py
+++ b/swiftclient/service.py
@@ -189,6 +189,10 @@ _default_local_options = {
 }
 
 POLICY = 'X-Storage-Policy'
+KNOWN_DIR_MARKERS = (
+    'application/directory',  # Preferred
+    'text/directory',  # Historically relevant
+)
 
 
 def get_from_queue(q, timeout=864000):
@@ -1130,9 +1134,8 @@ class SwiftService(object):
 
             fp = None
             try:
-                content_type = headers.get('content-type')
-                if (content_type and
-                   content_type.split(';', 1)[0] == 'text/directory'):
+                content_type = headers.get('content-type', '').split(';', 1)[0]
+                if content_type in KNOWN_DIR_MARKERS:
                     make_dir = not no_file and out_file != "-"
                     if make_dir and not isdir(path):
                         mkdirs(path)
@@ -1590,12 +1593,12 @@ class SwiftService(object):
         if options['changed']:
             try:
                 headers = conn.head_object(container, obj)
-                ct = headers.get('content-type')
+                ct = headers.get('content-type', '').split(';', 1)[0]
                 cl = int(headers.get('content-length'))
                 et = headers.get('etag')
                 mt = headers.get('x-object-meta-mtime')
 
-                if (ct.split(';', 1)[0] == 'text/directory' and
+                if (ct in KNOWN_DIR_MARKERS and
                         cl == 0 and
                         et == EMPTY_ETAG and
                         mt == put_headers['x-object-meta-mtime']):
@@ -1614,7 +1617,7 @@ class SwiftService(object):
                     return res
         try:
             conn.put_object(container, obj, '', content_length=0,
-                            content_type='text/directory',
+                            content_type=KNOWN_DIR_MARKERS[0],
                             headers=put_headers,
                             response_dict=results_dict)
             res.update({
diff --git a/tests/unit/test_service.py b/tests/unit/test_service.py
index e9310aa7..8651b057 100644
--- a/tests/unit/test_service.py
+++ b/tests/unit/test_service.py
@@ -1348,6 +1348,142 @@ class TestServiceUpload(_TestServiceBase):
                 errors.append(msg)
         self.assertFalse(errors, "\nERRORS:\n%s" % '\n'.join(errors))
 
+    def test_create_dir_marker_job_unchanged(self):
+        mock_conn = mock.Mock()
+        mock_conn.head_object.return_value = {
+            'content-type': 'application/directory',
+            'content-length': '0',
+            'x-object-meta-mtime': '1.234000',
+            'etag': md5().hexdigest()}
+
+        s = SwiftService()
+        with mock.patch('swiftclient.service.get_conn',
+                        return_value=mock_conn):
+            with mock.patch('swiftclient.service.getmtime',
+                            return_value=1.234):
+                r = s._create_dir_marker_job(conn=mock_conn,
+                                             container='test_c',
+                                             obj='test_o',
+                                             path='test',
+                                             options={'changed': True,
+                                                      'skip_identical': True,
+                                                      'leave_segments': True,
+                                                      'header': '',
+                                                      'segment_size': 10})
+        self.assertEqual({
+            'action': 'create_dir_marker',
+            'container': 'test_c',
+            'object': 'test_o',
+            'path': 'test',
+            'headers': {'x-object-meta-mtime': '1.234000'},
+            # NO response dict!
+            'success': True,
+        }, r)
+        self.assertEqual([], mock_conn.put_object.mock_calls)
+
+    def test_create_dir_marker_job_unchanged_old_type(self):
+        mock_conn = mock.Mock()
+        mock_conn.head_object.return_value = {
+            'content-type': 'text/directory',
+            'content-length': '0',
+            'x-object-meta-mtime': '1.000000',
+            'etag': md5().hexdigest()}
+
+        s = SwiftService()
+        with mock.patch('swiftclient.service.get_conn',
+                        return_value=mock_conn):
+            with mock.patch('swiftclient.service.time',
+                            return_value=1.234):
+                r = s._create_dir_marker_job(conn=mock_conn,
+                                             container='test_c',
+                                             obj='test_o',
+                                             options={'changed': True,
+                                                      'skip_identical': True,
+                                                      'leave_segments': True,
+                                                      'header': '',
+                                                      'segment_size': 10})
+        self.assertEqual({
+            'action': 'create_dir_marker',
+            'container': 'test_c',
+            'object': 'test_o',
+            'path': None,
+            'headers': {'x-object-meta-mtime': '1.000000'},
+            # NO response dict!
+            'success': True,
+        }, r)
+        self.assertEqual([], mock_conn.put_object.mock_calls)
+
+    def test_create_dir_marker_job_overwrites_bad_type(self):
+        mock_conn = mock.Mock()
+        mock_conn.head_object.return_value = {
+            'content-type': 'text/plain',
+            'content-length': '0',
+            'x-object-meta-mtime': '1.000000',
+            'etag': md5().hexdigest()}
+
+        s = SwiftService()
+        with mock.patch('swiftclient.service.get_conn',
+                        return_value=mock_conn):
+            with mock.patch('swiftclient.service.time',
+                            return_value=1.234):
+                r = s._create_dir_marker_job(conn=mock_conn,
+                                             container='test_c',
+                                             obj='test_o',
+                                             options={'changed': True,
+                                                      'skip_identical': True,
+                                                      'leave_segments': True,
+                                                      'header': '',
+                                                      'segment_size': 10})
+        self.assertEqual({
+            'action': 'create_dir_marker',
+            'container': 'test_c',
+            'object': 'test_o',
+            'path': None,
+            'headers': {'x-object-meta-mtime': '1.000000'},
+            'response_dict': {},
+            'success': True,
+        }, r)
+        self.assertEqual([mock.call(
+            'test_c', 'test_o', '',
+            content_length=0,
+            content_type='application/directory',
+            headers={'x-object-meta-mtime': '1.000000'},
+            response_dict={})], mock_conn.put_object.mock_calls)
+
+    def test_create_dir_marker_job_missing(self):
+        mock_conn = mock.Mock()
+        mock_conn.head_object.side_effect = \
+            ClientException('Not Found', http_status=404)
+
+        s = SwiftService()
+        with mock.patch('swiftclient.service.get_conn',
+                        return_value=mock_conn):
+            with mock.patch('swiftclient.service.time',
+                            return_value=1.234):
+                r = s._create_dir_marker_job(conn=mock_conn,
+                                             container='test_c',
+                                             obj='test_o',
+                                             options={'changed': True,
+                                                      'skip_identical': True,
+                                                      'leave_segments': True,
+                                                      'header': '',
+                                                      'segment_size': 10})
+        self.assertEqual({
+            'action': 'create_dir_marker',
+            'container': 'test_c',
+            'object': 'test_o',
+            'path': None,
+            'headers': {'x-object-meta-mtime': '1.000000'},
+            'response_dict': {},
+            'success': True,
+        }, r)
+        self.assertEqual([mock.call(
+            'test_c', 'test_o', '',
+            content_length=0,
+            content_type='application/directory',
+            headers={'x-object-meta-mtime': '1.000000'},
+            response_dict={})], mock_conn.put_object.mock_calls)
+
 
 class TestServiceDownload(_TestServiceBase):