diff --git a/endpoints/registry.py b/endpoints/registry.py index 8fd54e19c..cc2342fe4 100644 --- a/endpoints/registry.py +++ b/endpoints/registry.py @@ -88,6 +88,38 @@ def set_cache_headers(f): return wrapper +@registry.route('/images//layer', methods=['HEAD']) +@process_auth +@extract_namespace_repo_from_session +@require_completion +@set_cache_headers +def head_image_layer(namespace, repository, image_id, headers): + permission = ReadRepositoryPermission(namespace, repository) + + profile.debug('Checking repo permissions') + if permission.can() or model.repository_is_public(namespace, repository): + profile.debug('Looking up repo image') + repo_image = model.get_repo_image(namespace, repository, image_id) + if not repo_image: + profile.debug('Image not found') + abort(404, 'Image %(image_id)s not found', issue='unknown-image', + image_id=image_id) + + extra_headers = {} + + # Add the Accept-Ranges header if the storage engine supports resumeable + # downloads. + if store.get_supports_resumeable_downloads(repo_image.storage.locations): + profile.debug('Storage supports resumeable downloads') + extra_headers['Accept-Ranges'] = 'bytes'; + + resp = make_response('') + resp.headers.extend(extra_headers) + return resp + + abort(403) + + @registry.route('/images//layer', methods=['GET']) @process_auth @extract_namespace_repo_from_session @@ -110,10 +142,11 @@ def get_image_layer(namespace, repository, image_id, headers): if direct_download_url: profile.debug('Returning direct download URL') - return redirect(direct_download_url) + resp = redirect(direct_download_url) + return resp profile.debug('Streaming layer data') - return Response(store.stream_read(repo_image.storage.locations, path), headers=headers) + return Response(store.stream_read(repo_image.storage.locations, path), headers=dict(headers, **extra_headers)) except (IOError, AttributeError): profile.debug('Image not found') abort(404, 'Image %(image_id)s not found', issue='unknown-image', @@ -374,6 +407,11 @@ def put_image_json(namespace, repository, image_id): profile.debug('Looking up repo image') repo_image = model.get_repo_image(namespace, repository, image_id) + if not repo_image: + profile.debug('Image not found') + abort(404, 'Image %(image_id)s not found', issue='unknown-image', + image_id=image_id) + uuid = repo_image.storage.uuid if image_id != data['id']: diff --git a/storage/basestorage.py b/storage/basestorage.py index b554845b4..2d3727a5b 100644 --- a/storage/basestorage.py +++ b/storage/basestorage.py @@ -57,6 +57,9 @@ class BaseStorage(StoragePaths): def get_direct_download_url(self, path, expires_in=60): return None + def get_supports_resumeable_downloads(self): + return False + def get_content(self, path): raise NotImplementedError diff --git a/storage/distributedstorage.py b/storage/distributedstorage.py index 796abdc2b..9941f0fa5 100644 --- a/storage/distributedstorage.py +++ b/storage/distributedstorage.py @@ -39,3 +39,4 @@ class DistributedStorage(StoragePaths): list_directory = _location_aware(BaseStorage.list_directory) exists = _location_aware(BaseStorage.exists) remove = _location_aware(BaseStorage.remove) + get_supports_resumeable_downloads = _location_aware(BaseStorage.get_supports_resumeable_downloads) diff --git a/storage/s3.py b/storage/s3.py index 1747d34dc..12c2373de 100644 --- a/storage/s3.py +++ b/storage/s3.py @@ -83,6 +83,9 @@ class S3Storage(BaseStorage): key.set_contents_from_string(content, encrypt_key=True) return path + def get_supports_resumeable_downloads(self): + return True + def get_direct_download_url(self, path, expires_in=60): self._initialize_s3() path = self._init_path(path)