From ae35a29169ab460cdd5d6b8c26d376c000a5c10e Mon Sep 17 00:00:00 2001
From: Honza Pokorny <honza@redhat.com>
Date: Wed, 21 Jun 2017 12:53:38 -0300
Subject: [PATCH] Allow objects to be streamed to stdout

Change-Id: Icd8de6b2122fe77926d93da9bda08f56c3672a7a
---
 doc/source/cli/command-objects/object.rst     |  3 +-
 openstackclient/api/object_store_v1.py        | 15 ++++---
 openstackclient/object/v1/object.py           |  3 +-
 .../tests/functional/object/v1/test_object.py |  4 ++
 openstackclient/tests/unit/object/v1/fakes.py |  4 ++
 .../tests/unit/object/v1/test_object_all.py   | 43 +++++++++++++++++++
 .../notes/object-stdout-db76cc500948b0e8.yaml |  5 +++
 7 files changed, 70 insertions(+), 7 deletions(-)
 create mode 100644 releasenotes/notes/object-stdout-db76cc500948b0e8.yaml

diff --git a/doc/source/cli/command-objects/object.rst b/doc/source/cli/command-objects/object.rst
index 6323c4ed0b..4cba38ee59 100644
--- a/doc/source/cli/command-objects/object.rst
+++ b/doc/source/cli/command-objects/object.rst
@@ -114,7 +114,8 @@ Save object locally
 
 .. option:: --file <filename>
 
-    Destination filename (defaults to object name)
+    Destination filename (defaults to object name);
+    using - as the filename will print the file to stdout
 
 .. describe:: <container>
 
diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py
index 74c4a46f20..3103352503 100644
--- a/openstackclient/api/object_store_v1.py
+++ b/openstackclient/api/object_store_v1.py
@@ -16,6 +16,7 @@
 import io
 import logging
 import os
+import sys
 
 from osc_lib import utils
 import six
@@ -376,12 +377,16 @@ class APIv1(api.BaseAPI):
             stream=True,
         )
         if response.status_code == 200:
-            if not os.path.exists(os.path.dirname(file)):
-                if len(os.path.dirname(file)) > 0:
-                    os.makedirs(os.path.dirname(file))
-            with open(file, 'wb') as f:
+            if file == '-':
                 for chunk in response.iter_content(64 * 1024):
-                    f.write(chunk)
+                    sys.stdout.write(chunk)
+            else:
+                if not os.path.exists(os.path.dirname(file)):
+                    if len(os.path.dirname(file)) > 0:
+                        os.makedirs(os.path.dirname(file))
+                with open(file, 'wb') as f:
+                    for chunk in response.iter_content(64 * 1024):
+                        f.write(chunk)
 
     def object_set(
         self,
diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py
index e79cea48ea..aeb0253653 100644
--- a/openstackclient/object/v1/object.py
+++ b/openstackclient/object/v1/object.py
@@ -204,7 +204,8 @@ class SaveObject(command.Command):
         parser.add_argument(
             "--file",
             metavar="<filename>",
-            help=_("Destination filename (defaults to object name)"),
+            help=_("Destination filename (defaults to object name); using '-'"
+                   " as the filename will print the file to stdout"),
         )
         parser.add_argument(
             'container',
diff --git a/openstackclient/tests/functional/object/v1/test_object.py b/openstackclient/tests/functional/object/v1/test_object.py
index 776cf47c2c..0927d706dc 100644
--- a/openstackclient/tests/functional/object/v1/test_object.py
+++ b/openstackclient/tests/functional/object/v1/test_object.py
@@ -66,6 +66,10 @@ class ObjectTests(base.TestCase):
                        + ' ' + object_file + ' --file ' + tmp_file)
         # TODO(stevemar): Assert returned fields
 
+        raw_output = self.openstack('object save ' + self.CONTAINER_NAME
+                                    + ' ' + object_file + ' --file -')
+        self.assertEqual(raw_output, 'test content')
+
         self.openstack('object show ' + self.CONTAINER_NAME
                        + ' ' + object_file)
         # TODO(stevemar): Assert returned fields
diff --git a/openstackclient/tests/unit/object/v1/fakes.py b/openstackclient/tests/unit/object/v1/fakes.py
index 72646d25bb..5d65d1066f 100644
--- a/openstackclient/tests/unit/object/v1/fakes.py
+++ b/openstackclient/tests/unit/object/v1/fakes.py
@@ -13,6 +13,8 @@
 #   under the License.
 #
 
+import six
+
 from keystoneauth1 import session
 
 from openstackclient.api import object_store_v1 as object_store
@@ -67,6 +69,8 @@ OBJECT = {
     'last_modified': object_modified_1,
 }
 
+object_1_content = six.b('object 1 content')
+
 OBJECT_2 = {
     'name': object_name_2,
     'bytes': object_bytes_2,
diff --git a/openstackclient/tests/unit/object/v1/test_object_all.py b/openstackclient/tests/unit/object/v1/test_object_all.py
index f215836ed0..363f2ea21e 100644
--- a/openstackclient/tests/unit/object/v1/test_object_all.py
+++ b/openstackclient/tests/unit/object/v1/test_object_all.py
@@ -13,8 +13,10 @@
 
 import copy
 
+import mock
 from osc_lib import exceptions
 from requests_mock.contrib import fixture
+import six
 
 from openstackclient.object.v1 import object as object_cmds
 from openstackclient.tests.unit.object.v1 import fakes as object_fakes
@@ -202,3 +204,44 @@ class TestObjectShow(TestObjectAll):
             'manifest',
         )
         self.assertEqual(datalist, data)
+
+
+class TestObjectSave(TestObjectAll):
+
+    def setUp(self):
+        super(TestObjectSave, self).setUp()
+
+        # Get the command object to test
+        self.cmd = object_cmds.SaveObject(self.app, None)
+
+    def test_save_to_stdout(self):
+        self.requests_mock.register_uri(
+            'GET',
+            object_fakes.ENDPOINT +
+            '/' +
+            object_fakes.container_name +
+            '/' +
+            object_fakes.object_name_1,
+            status_code=200,
+            content=object_fakes.object_1_content
+        )
+
+        arglist = [
+            object_fakes.container_name,
+            object_fakes.object_name_1,
+            '--file',
+            '-'
+        ]
+
+        verifylist = [
+            ('container', object_fakes.container_name),
+            ('object', object_fakes.object_name_1),
+            ('file', '-'),
+        ]
+
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        with mock.patch('sys.stdout', new=six.BytesIO()) as fake_stdout:
+            self.cmd.take_action(parsed_args)
+
+        self.assertEqual(fake_stdout.getvalue(), object_fakes.object_1_content)
diff --git a/releasenotes/notes/object-stdout-db76cc500948b0e8.yaml b/releasenotes/notes/object-stdout-db76cc500948b0e8.yaml
new file mode 100644
index 0000000000..29b21131bc
--- /dev/null
+++ b/releasenotes/notes/object-stdout-db76cc500948b0e8.yaml
@@ -0,0 +1,5 @@
+---
+features:
+  - |
+    Add support for streaming Swift objects to stdout when using the ``object
+    save`` command, by specifying ``--filename -``.