diff --git a/docker/api/container.py b/docker/api/container.py index 94889e97..142bd0f6 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -75,6 +75,12 @@ class ContainerApiMixin(object): @utils.check_resource def copy(self, container, resource): + if utils.version_gte(self._version, '1.20'): + warnings.warn( + 'Client.copy() is deprecated for API version >= 1.20, ' + 'please use get_archive() instead', + DeprecationWarning + ) res = self._post_json( self._url("/containers/{0}/copy".format(container)), data={"Resource": resource}, @@ -145,6 +151,21 @@ class ContainerApiMixin(object): self._raise_for_status(res) return res.raw + @utils.check_resource + @utils.minimum_version('1.20') + def get_archive(self, container, path): + params = { + 'path': path + } + url = self._url('/containers/{0}/archive', container) + res = self._get(url, params=params, stream=True) + self._raise_for_status(res) + encoded_stat = res.headers.get('x-docker-container-path-stat') + return ( + res.raw, + utils.decode_json_header(encoded_stat) if encoded_stat else None + ) + @utils.check_resource def inspect_container(self, container): return self._result( @@ -214,6 +235,15 @@ class ContainerApiMixin(object): return h_ports + @utils.check_resource + @utils.minimum_version('1.20') + def put_archive(self, container, path, data): + params = {'path': path} + url = self._url('/containers/{0}/archive', container) + res = self._put(url, params=params, data=data) + self._raise_for_status(res) + return res.status_code == 200 + @utils.check_resource def remove_container(self, container, v=False, link=False, force=False): params = {'v': v, 'link': link, 'force': force} diff --git a/docker/client.py b/docker/client.py index 79efc9f7..d2194726 100644 --- a/docker/client.py +++ b/docker/client.py @@ -109,6 +109,9 @@ class Client( def _get(self, url, **kwargs): return self.get(url, **self._set_request_timeout(kwargs)) + def _put(self, url, **kwargs): + return self.put(url, **self._set_request_timeout(kwargs)) + def _delete(self, url, **kwargs): return self.delete(url, **self._set_request_timeout(kwargs)) diff --git a/docker/utils/__init__.py b/docker/utils/__init__.py index fd0ef5c0..92e03e9b 100644 --- a/docker/utils/__init__.py +++ b/docker/utils/__init__.py @@ -3,7 +3,7 @@ from .utils import ( mkbuildcontext, tar, exclude_paths, parse_repository_tag, parse_host, kwargs_from_env, convert_filters, create_host_config, create_container_config, parse_bytes, ping_registry, parse_env_file, - version_lt, version_gte + version_lt, version_gte, decode_json_header ) # flake8: noqa from .types import Ulimit, LogConfig # flake8: noqa diff --git a/docker/utils/utils.py b/docker/utils/utils.py index 1fce1377..89837b78 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import base64 import io import os import os.path @@ -66,6 +67,13 @@ def mkbuildcontext(dockerfile): return f +def decode_json_header(header): + data = base64.b64decode(header) + if six.PY3: + data = data.decode('utf-8') + return json.loads(data) + + def tar(path, exclude=None, dockerfile=None): f = tempfile.NamedTemporaryFile() t = tarfile.open(mode='w', fileobj=f) diff --git a/docs/api.md b/docs/api.md index 690fe495..e472927b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -165,6 +165,8 @@ non-running ones ## copy Identical to the `docker cp` command. Get files/folders from the container. +**Deprecated for API version >= 1.20** – Consider using +[`get_archive`](#get_archive) **instead.** **Params**: @@ -376,6 +378,27 @@ Export the contents of a filesystem as a tar archive to STDOUT. **Returns** (str): The filesystem tar archive as a str +## get_archive + +Retrieve a file or folder from a container in the form of a tar archive. + +**Params**: + +* container (str): The container where the file is located +* path (str): Path to the file or folder to retrieve + +**Returns** (tuple): First element is a raw tar data stream. Second element is +a dict containing `stat` information on the specified `path`. + +```python +>>> import docker +>>> c = docker.Client() +>>> ctnr = c.create_container('busybox', 'true') +>>> strm, stat = c.get_archive(ctnr, '/bin/sh') +>>> print(stat) +{u'linkTarget': u'', u'mode': 493, u'mtime': u'2015-09-16T12:34:23-07:00', u'name': u'sh', u'size': 962860} +``` + ## get_image Get an image from the docker daemon. Similar to the `docker save` command. @@ -712,6 +735,20 @@ command. yourname/app/tags/latest}"}\\n'] ``` +## put_archive + +Insert a file or folder in an existing container using a tar archive as source. + +**Params**: + +* container (str): The container where the file(s) will be extracted +* path (str): Path inside the container where the file(s) will be extracted. + Must exist. +* data (bytes): tar data to be extracted + +**Returns** (bool): True if the call succeeds. `docker.errors.APIError` will +be raised if an error occurs. + ## remove_container Remove a container. Similar to the `docker rm` command. diff --git a/tests/helpers.py b/tests/helpers.py index 95692db7..392be3b8 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,5 +1,6 @@ import os import os.path +import tarfile import tempfile @@ -14,3 +15,23 @@ def make_tree(dirs, files): f.write("content") return base + + +def simple_tar(path): + f = tempfile.NamedTemporaryFile() + t = tarfile.open(mode='w', fileobj=f) + + abs_path = os.path.abspath(path) + t.add(abs_path, arcname=os.path.basename(path), recursive=False) + + t.close() + f.seek(0) + return f + + +def untar_file(tardata, filename): + with tarfile.open(mode='r', fileobj=tardata) as t: + f = t.extractfile(filename) + result = f.read() + f.close() + return result diff --git a/tests/integration_test.py b/tests/integration_test.py index da380c1e..8a927084 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -37,6 +37,7 @@ import docker from docker.errors import APIError, NotFound from docker.utils import kwargs_from_env +from . import helpers from .base import requires_api_version from .test import Cleanup @@ -427,6 +428,90 @@ class CreateContainerWithLogConfigTest(BaseTestCase): self.assertEqual(container_log_config['Config'], {}) +@requires_api_version('1.20') +class GetArchiveTest(BaseTestCase): + def test_get_file_archive_from_container(self): + data = 'The Maid and the Pocket Watch of Blood' + ctnr = self.client.create_container( + BUSYBOX, 'sh -c "echo {0} > /vol1/data.txt"'.format(data), + volumes=['/vol1'] + ) + self.tmp_containers.append(ctnr) + self.client.start(ctnr) + self.client.wait(ctnr) + with tempfile.NamedTemporaryFile() as destination: + strm, stat = self.client.get_archive(ctnr, '/vol1/data.txt') + for d in strm: + destination.write(d) + destination.seek(0) + retrieved_data = helpers.untar_file(destination, 'data.txt') + if six.PY3: + retrieved_data = retrieved_data.decode('utf-8') + self.assertEqual(data, retrieved_data.strip()) + + def test_get_file_stat_from_container(self): + data = 'The Maid and the Pocket Watch of Blood' + ctnr = self.client.create_container( + BUSYBOX, 'sh -c "echo -n {0} > /vol1/data.txt"'.format(data), + volumes=['/vol1'] + ) + self.tmp_containers.append(ctnr) + self.client.start(ctnr) + self.client.wait(ctnr) + strm, stat = self.client.get_archive(ctnr, '/vol1/data.txt') + self.assertIn('name', stat) + self.assertEqual(stat['name'], 'data.txt') + self.assertIn('size', stat) + self.assertEqual(stat['size'], len(data)) + + +@requires_api_version('1.20') +class PutArchiveTest(BaseTestCase): + def test_copy_file_to_container(self): + data = b'Deaf To All But The Song' + with tempfile.NamedTemporaryFile() as test_file: + test_file.write(data) + test_file.seek(0) + ctnr = self.client.create_container( + BUSYBOX, + 'cat {0}'.format( + os.path.join('/vol1', os.path.basename(test_file.name)) + ), + volumes=['/vol1'] + ) + self.tmp_containers.append(ctnr) + with helpers.simple_tar(test_file.name) as test_tar: + self.client.put_archive(ctnr, '/vol1', test_tar) + self.client.start(ctnr) + self.client.wait(ctnr) + logs = self.client.logs(ctnr) + if six.PY3: + logs = logs.decode('utf-8') + data = data.decode('utf-8') + self.assertEqual(logs.strip(), data) + + def test_copy_directory_to_container(self): + files = ['a.py', 'b.py', 'foo/b.py'] + dirs = ['foo', 'bar'] + base = helpers.make_tree(dirs, files) + ctnr = self.client.create_container( + BUSYBOX, 'ls -p /vol1', volumes=['/vol1'] + ) + self.tmp_containers.append(ctnr) + with docker.utils.tar(base) as test_tar: + self.client.put_archive(ctnr, '/vol1', test_tar) + self.client.start(ctnr) + self.client.wait(ctnr) + logs = self.client.logs(ctnr) + if six.PY3: + logs = logs.decode('utf-8') + results = logs.strip().split() + self.assertIn('a.py', results) + self.assertIn('b.py', results) + self.assertIn('foo/', results) + self.assertIn('bar/', results) + + class TestCreateContainerReadOnlyFs(BaseTestCase): def runTest(self): if not exec_driver_is_native(): diff --git a/tests/utils_test.py b/tests/utils_test.py index 8ac1dcb9..b1adde26 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import base64 +import json import os import os.path import shutil @@ -14,7 +16,7 @@ from docker.errors import DockerException from docker.utils import ( parse_repository_tag, parse_host, convert_filters, kwargs_from_env, create_host_config, Ulimit, LogConfig, parse_bytes, parse_env_file, - exclude_paths, convert_volume_binds, + exclude_paths, convert_volume_binds, decode_json_header ) from docker.utils.ports import build_port_bindings, split_port from docker.auth import resolve_repository_name, resolve_authconfig @@ -370,6 +372,16 @@ class UtilsTest(base.BaseTestCase): for filters, expected in tests: self.assertEqual(convert_filters(filters), expected) + def test_decode_json_header(self): + obj = {'a': 'b', 'c': 1} + data = None + if six.PY3: + data = base64.b64encode(bytes(json.dumps(obj), 'utf-8')) + else: + data = base64.b64encode(json.dumps(obj)) + decoded_data = decode_json_header(data) + self.assertEqual(obj, decoded_data) + def test_resolve_repository_name(self): # docker hub library image self.assertEqual(