v2: add pagination decorator
This commit is contained in:
parent
5b630ebdb0
commit
3f722f880e
6 changed files with 77 additions and 79 deletions
|
@ -100,3 +100,11 @@ def synthesize_v1_image(repo, storage, image_id, created, comment, command, comp
|
||||||
def save_manifest(namespace_name, repo_name, tag_name, leaf_layer_id, manifest_digest, manifest_bytes):
|
def save_manifest(namespace_name, repo_name, tag_name, leaf_layer_id, manifest_digest, manifest_bytes):
|
||||||
model.tag.store_tag_manifest(namespace_name, repo_name, tag_name, leaf_layer_id, manifest_digest,
|
model.tag.store_tag_manifest(namespace_name, repo_name, tag_name, leaf_layer_id, manifest_digest,
|
||||||
manifest_bytes)
|
manifest_bytes)
|
||||||
|
|
||||||
|
|
||||||
|
def repository_tags(namespace_name, repo_name, limit, offset):
|
||||||
|
return [Tag()]
|
||||||
|
|
||||||
|
|
||||||
|
def get_visible_repositories(username, limit, offset):
|
||||||
|
return [Repository()]
|
||||||
|
|
|
@ -2,13 +2,14 @@ import logging
|
||||||
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
|
from urllib import urlencode
|
||||||
|
|
||||||
from flask import Blueprint, make_response, url_for, request, jsonify
|
from flask import Blueprint, make_response, url_for, request, jsonify
|
||||||
from semantic_version import Spec
|
from semantic_version import Spec
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
|
||||||
from app import app, metric_queue
|
from app import app, metric_queue, get_app_url
|
||||||
from auth.auth_context import get_grant_context
|
from auth.auth_context import get_grant_context
|
||||||
from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission,
|
from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission,
|
||||||
AdministerRepositoryPermission)
|
AdministerRepositoryPermission)
|
||||||
|
@ -19,12 +20,53 @@ from endpoints.v2.errors import V2RegistryException, Unauthorized
|
||||||
from util.http import abort
|
from util.http import abort
|
||||||
from util.registry.dockerver import docker_version
|
from util.registry.dockerver import docker_version
|
||||||
from util.metrics.metricqueue import time_blueprint
|
from util.metrics.metricqueue import time_blueprint
|
||||||
|
from util.pagination import encrypt_page_token, decrypt_page_token
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
v2_bp = Blueprint('v2', __name__)
|
v2_bp = Blueprint('v2', __name__)
|
||||||
|
|
||||||
time_blueprint(v2_bp, metric_queue)
|
time_blueprint(v2_bp, metric_queue)
|
||||||
|
|
||||||
|
|
||||||
|
_MAX_RESULTS_PER_PAGE = 50
|
||||||
|
|
||||||
|
|
||||||
|
def _paginate(limit_kwarg_name='limit', offset_kwarg_name='offset',
|
||||||
|
callback_kwarg_name='pagination_callback'):
|
||||||
|
def wrapper(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
requested_limit = int(request.args.get('n', _MAX_RESULTS_PER_PAGE))
|
||||||
|
except ValueError:
|
||||||
|
requested_limit = 0
|
||||||
|
|
||||||
|
limit = max(min(requested_limit, _MAX_RESULTS_PER_PAGE), 1)
|
||||||
|
next_page_token = request.args.get('next_page', None)
|
||||||
|
|
||||||
|
# Decrypt the next page token, if any.
|
||||||
|
offset = 0
|
||||||
|
page_info = decrypt_page_token(next_page_token)
|
||||||
|
if page_info is not None:
|
||||||
|
# Note: we use offset here instead of ID >= n because one of the V2 queries is a UNION.
|
||||||
|
offset = page_info.get('offset', 0)
|
||||||
|
|
||||||
|
def callback(num_results, response):
|
||||||
|
if num_results <= limit:
|
||||||
|
return
|
||||||
|
next_page_token = encrypt_page_token({'offset': limit+offset})
|
||||||
|
link = get_app_url() + url_for(request.endpoint, **request.view_args)
|
||||||
|
link += '?%s; rel="next"' % urlencode({'n': limit, 'next_page': next_page_token})
|
||||||
|
response.headers['Link'] = link
|
||||||
|
|
||||||
|
kwargs[limit_kwarg_name] = limit
|
||||||
|
kwargs[offset_kwarg_name] = offset
|
||||||
|
kwargs[callback_kwarg_name] = callback
|
||||||
|
func(*args, **kwargs)
|
||||||
|
return wrapped
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
@v2_bp.app_errorhandler(V2RegistryException)
|
@v2_bp.app_errorhandler(V2RegistryException)
|
||||||
def handle_registry_v2_exception(error):
|
def handle_registry_v2_exception(error):
|
||||||
response = jsonify({
|
response = jsonify({
|
||||||
|
@ -104,8 +146,10 @@ def v2_support_enabled():
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
from endpoints.v2 import v2auth
|
from endpoints.v2 import (
|
||||||
from endpoints.v2 import manifest
|
blob,
|
||||||
from endpoints.v2 import blob
|
catalog,
|
||||||
from endpoints.v2 import tag
|
manifest,
|
||||||
from endpoints.v2 import catalog
|
tag,
|
||||||
|
v2auth,
|
||||||
|
)
|
||||||
|
|
|
@ -1,30 +1,24 @@
|
||||||
from flask import jsonify, url_for
|
from flask import jsonify
|
||||||
|
|
||||||
from endpoints.v2 import v2_bp
|
|
||||||
from auth.registry_jwt_auth import process_registry_jwt_auth, get_granted_entity
|
from auth.registry_jwt_auth import process_registry_jwt_auth, get_granted_entity
|
||||||
from endpoints.decorators import anon_protect
|
from endpoints.decorators import anon_protect
|
||||||
from data import model
|
from endpoints.v2 import v2_bp, _paginate
|
||||||
from endpoints.v2.v2util import add_pagination
|
|
||||||
|
|
||||||
@v2_bp.route('/_catalog', methods=['GET'])
|
@v2_bp.route('/_catalog', methods=['GET'])
|
||||||
@process_registry_jwt_auth()
|
@process_registry_jwt_auth()
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def catalog_search():
|
@_paginate()
|
||||||
url = url_for('v2.catalog_search')
|
def catalog_search(limit, offset, pagination_callback):
|
||||||
|
|
||||||
username = None
|
username = None
|
||||||
entity = get_granted_entity()
|
entity = get_granted_entity()
|
||||||
if entity:
|
if entity:
|
||||||
username = entity.user.username
|
username = entity.user.username
|
||||||
|
|
||||||
query = model.repository.get_visible_repositories(username, include_public=(username is None))
|
visible_repositories = v2.get_visible_repositories(username, limit, offset)
|
||||||
link, query = add_pagination(query, url)
|
|
||||||
|
|
||||||
response = jsonify({
|
response = jsonify({
|
||||||
'repositories': ['%s/%s' % (repo.namespace_user.username, repo.name) for repo in query],
|
'repositories': ['%s/%s' % (repo.namespace_name, repo.name)
|
||||||
|
for repo in visible_repositories],
|
||||||
})
|
})
|
||||||
|
|
||||||
if link is not None:
|
pagination_callback(len(visible_repositories), response)
|
||||||
response.headers['Link'] = link
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
|
@ -1,33 +1,27 @@
|
||||||
from flask import jsonify, url_for
|
from flask import jsonify
|
||||||
|
|
||||||
from auth.registry_jwt_auth import process_registry_jwt_auth
|
from auth.registry_jwt_auth import process_registry_jwt_auth
|
||||||
from endpoints.common import parse_repository_name
|
from endpoints.common import parse_repository_name
|
||||||
from endpoints.v2 import v2_bp, require_repo_read
|
from endpoints.v2 import v2_bp, require_repo_read, _paginate
|
||||||
from endpoints.v2.errors import NameUnknown
|
from endpoints.v2.errors import NameUnknown
|
||||||
from endpoints.v2.v2util import add_pagination
|
|
||||||
from endpoints.decorators import anon_protect
|
from endpoints.decorators import anon_protect
|
||||||
from data import model
|
|
||||||
|
|
||||||
@v2_bp.route('/<repopath:repository>/tags/list', methods=['GET'])
|
@v2_bp.route('/<repopath:repository>/tags/list', methods=['GET'])
|
||||||
@parse_repository_name()
|
@parse_repository_name()
|
||||||
@process_registry_jwt_auth(scopes=['pull'])
|
@process_registry_jwt_auth(scopes=['pull'])
|
||||||
@require_repo_read
|
@require_repo_read
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def list_all_tags(namespace_name, repo_name):
|
@_paginate()
|
||||||
repository = model.repository.get_repository(namespace_name, repo_name)
|
def list_all_tags(namespace_name, repo_name, limit, offset, pagination_callback):
|
||||||
if repository is None:
|
repo = v2.get_repository(namespace_name, repo_name)
|
||||||
|
if repo is None:
|
||||||
raise NameUnknown()
|
raise NameUnknown()
|
||||||
|
|
||||||
query = model.tag.list_repository_tags(namespace_name, repo_name)
|
tags = v2.repository_tags(namespace_name, repo_name, limit, offset)
|
||||||
url = url_for('v2.list_all_tags', repository='%s/%s' % (namespace_name, repo_name))
|
|
||||||
link, query = add_pagination(query, url)
|
|
||||||
|
|
||||||
response = jsonify({
|
response = jsonify({
|
||||||
'name': '{0}/{1}'.format(namespace_name, repo_name),
|
'name': '{0}/{1}'.format(namespace_name, repo_name),
|
||||||
'tags': [tag.name for tag in query],
|
'tags': [tag.name for tag in tags],
|
||||||
})
|
})
|
||||||
|
|
||||||
if link is not None:
|
pagination_callback(len(tags), response)
|
||||||
response.headers['Link'] = link
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from cachetools import lru_cache
|
||||||
from flask import request, jsonify, abort
|
from flask import request, jsonify, abort
|
||||||
|
|
||||||
from app import app, userevents, instance_keys
|
from app import app, userevents, instance_keys
|
||||||
|
@ -9,7 +10,6 @@ from auth.auth import process_auth
|
||||||
from auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token
|
from auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token
|
||||||
from auth.permissions import (ModifyRepositoryPermission, ReadRepositoryPermission,
|
from auth.permissions import (ModifyRepositoryPermission, ReadRepositoryPermission,
|
||||||
CreateRepositoryPermission)
|
CreateRepositoryPermission)
|
||||||
from cachetools import lru_cache
|
|
||||||
from endpoints.v2 import v2_bp
|
from endpoints.v2 import v2_bp
|
||||||
from endpoints.decorators import anon_protect
|
from endpoints.decorators import anon_protect
|
||||||
from util.cache import no_cache
|
from util.cache import no_cache
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
from flask import request
|
|
||||||
from app import get_app_url
|
|
||||||
from util.pagination import encrypt_page_token, decrypt_page_token
|
|
||||||
import urllib
|
|
||||||
import logging
|
|
||||||
|
|
||||||
_MAX_RESULTS_PER_PAGE = 50
|
|
||||||
|
|
||||||
def add_pagination(query, url):
|
|
||||||
""" Adds optional pagination to the given query by looking for the Docker V2 pagination request
|
|
||||||
args.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
requested_limit = int(request.args.get('n', _MAX_RESULTS_PER_PAGE))
|
|
||||||
except ValueError:
|
|
||||||
requested_limit = 0
|
|
||||||
|
|
||||||
limit = max(min(requested_limit, _MAX_RESULTS_PER_PAGE), 1)
|
|
||||||
next_page_token = request.args.get('next_page', None)
|
|
||||||
|
|
||||||
# Decrypt the next page token, if any.
|
|
||||||
offset = 0
|
|
||||||
page_info = decrypt_page_token(next_page_token)
|
|
||||||
if page_info is not None:
|
|
||||||
# Note: we use offset here instead of ID >= n because one of the V2 queries is a UNION.
|
|
||||||
offset = page_info.get('offset', 0)
|
|
||||||
query = query.offset(offset)
|
|
||||||
|
|
||||||
query = query.limit(limit + 1)
|
|
||||||
url = get_app_url() + url
|
|
||||||
|
|
||||||
results = list(query)
|
|
||||||
if len(results) <= limit:
|
|
||||||
return None, results
|
|
||||||
|
|
||||||
# Add a link to the next page of results.
|
|
||||||
page_info = dict(offset=limit + offset)
|
|
||||||
next_page_token = encrypt_page_token(page_info)
|
|
||||||
|
|
||||||
link = url + '?' + urllib.urlencode(dict(n=limit, next_page=next_page_token))
|
|
||||||
link = link + '; rel="next"'
|
|
||||||
return link, results[0:-1]
|
|
Reference in a new issue