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
|
@utils.check_resource
|
||||||
def copy(self, container, 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(
|
res = self._post_json(
|
||||||
self._url("/containers/{0}/copy".format(container)),
|
self._url("/containers/{0}/copy".format(container)),
|
||||||
data={"Resource": resource},
|
data={"Resource": resource},
|
||||||
|
@ -145,6 +151,21 @@ class ContainerApiMixin(object):
|
||||||
self._raise_for_status(res)
|
self._raise_for_status(res)
|
||||||
return res.raw
|
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
|
@utils.check_resource
|
||||||
def inspect_container(self, container):
|
def inspect_container(self, container):
|
||||||
return self._result(
|
return self._result(
|
||||||
|
@ -214,6 +235,15 @@ class ContainerApiMixin(object):
|
||||||
|
|
||||||
return h_ports
|
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
|
@utils.check_resource
|
||||||
def remove_container(self, container, v=False, link=False, force=False):
|
def remove_container(self, container, v=False, link=False, force=False):
|
||||||
params = {'v': v, 'link': link, 'force': force}
|
params = {'v': v, 'link': link, 'force': force}
|
||||||
|
|
|
@ -109,6 +109,9 @@ class Client(
|
||||||
def _get(self, url, **kwargs):
|
def _get(self, url, **kwargs):
|
||||||
return self.get(url, **self._set_request_timeout(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):
|
def _delete(self, url, **kwargs):
|
||||||
return self.delete(url, **self._set_request_timeout(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,
|
mkbuildcontext, tar, exclude_paths, parse_repository_tag, parse_host,
|
||||||
kwargs_from_env, convert_filters, create_host_config,
|
kwargs_from_env, convert_filters, create_host_config,
|
||||||
create_container_config, parse_bytes, ping_registry, parse_env_file,
|
create_container_config, parse_bytes, ping_registry, parse_env_file,
|
||||||
version_lt, version_gte
|
version_lt, version_gte, decode_json_header
|
||||||
) # flake8: noqa
|
) # flake8: noqa
|
||||||
|
|
||||||
from .types import Ulimit, LogConfig # flake8: noqa
|
from .types import Ulimit, LogConfig # flake8: noqa
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import base64
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
|
@ -66,6 +67,13 @@ def mkbuildcontext(dockerfile):
|
||||||
return f
|
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):
|
def tar(path, exclude=None, dockerfile=None):
|
||||||
f = tempfile.NamedTemporaryFile()
|
f = tempfile.NamedTemporaryFile()
|
||||||
t = tarfile.open(mode='w', fileobj=f)
|
t = tarfile.open(mode='w', fileobj=f)
|
||||||
|
|
37
docs/api.md
37
docs/api.md
|
@ -165,6 +165,8 @@ non-running ones
|
||||||
|
|
||||||
## copy
|
## copy
|
||||||
Identical to the `docker cp` command. Get files/folders from the container.
|
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**:
|
**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
|
**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_image
|
||||||
|
|
||||||
Get an image from the docker daemon. Similar to the `docker save` command.
|
Get an image from the docker daemon. Similar to the `docker save` command.
|
||||||
|
@ -712,6 +735,20 @@ command.
|
||||||
yourname/app/tags/latest}"}\\n']
|
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_container
|
||||||
|
|
||||||
Remove a container. Similar to the `docker rm` command.
|
Remove a container. Similar to the `docker rm` command.
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
|
import tarfile
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,3 +15,23 @@ def make_tree(dirs, files):
|
||||||
f.write("content")
|
f.write("content")
|
||||||
|
|
||||||
return base
|
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.errors import APIError, NotFound
|
||||||
from docker.utils import kwargs_from_env
|
from docker.utils import kwargs_from_env
|
||||||
|
|
||||||
|
from . import helpers
|
||||||
from .base import requires_api_version
|
from .base import requires_api_version
|
||||||
from .test import Cleanup
|
from .test import Cleanup
|
||||||
|
|
||||||
|
@ -427,6 +428,90 @@ class CreateContainerWithLogConfigTest(BaseTestCase):
|
||||||
self.assertEqual(container_log_config['Config'], {})
|
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):
|
class TestCreateContainerReadOnlyFs(BaseTestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
if not exec_driver_is_native():
|
if not exec_driver_is_native():
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import shutil
|
import shutil
|
||||||
|
@ -14,7 +16,7 @@ from docker.errors import DockerException
|
||||||
from docker.utils import (
|
from docker.utils import (
|
||||||
parse_repository_tag, parse_host, convert_filters, kwargs_from_env,
|
parse_repository_tag, parse_host, convert_filters, kwargs_from_env,
|
||||||
create_host_config, Ulimit, LogConfig, parse_bytes, parse_env_file,
|
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.utils.ports import build_port_bindings, split_port
|
||||||
from docker.auth import resolve_repository_name, resolve_authconfig
|
from docker.auth import resolve_repository_name, resolve_authconfig
|
||||||
|
@ -370,6 +372,16 @@ class UtilsTest(base.BaseTestCase):
|
||||||
for filters, expected in tests:
|
for filters, expected in tests:
|
||||||
self.assertEqual(convert_filters(filters), expected)
|
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):
|
def test_resolve_repository_name(self):
|
||||||
# docker hub library image
|
# docker hub library image
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|
Loading…
Reference in New Issue