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