mirror of https://github.com/docker/docker-py.git
				
				
				
			put/get archive implementation
Signed-off-by: Joffrey F <joffrey@docker.com>
This commit is contained in:
		
							parent
							
								
									eb869c0bd3
								
							
						
					
					
						commit
						4845dae0c0
					
				|  | @ -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} | ||||
|  |  | |||
|  | @ -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)) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
							
								
								
									
										37
									
								
								docs/api.md
								
								
								
								
							
							
						
						
									
										37
									
								
								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. | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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(): | ||||
|  |  | |||
|  | @ -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( | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue