# -*- coding: utf-8 -*- # # Copyright 2023 Google LLC. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """File utils for Artifact Registry commands.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import json import re from apitools.base.protorpclite import protojson from googlecloudsdk.api_lib.artifacts import exceptions from googlecloudsdk.api_lib.artifacts import filter_rewriter from googlecloudsdk.api_lib.util import common_args from googlecloudsdk.command_lib.artifacts import requests from googlecloudsdk.command_lib.artifacts import util from googlecloudsdk.core import properties from googlecloudsdk.core import resources def EscapeFileName(ref): """Escapes slashes, pluses and hats from request names.""" return resources.REGISTRY.Create( "artifactregistry.projects.locations.repositories.files", projectsId=ref.projectsId, locationsId=ref.locationsId, repositoriesId=ref.repositoriesId, filesId=ref.filesId.replace("/", "%2F") .replace("+", "%2B") .replace("^", "%5E"), ) def EscapeFileNameHook(ref, unused_args, req): """Escapes slashes, pluses and hats from request names.""" file = resources.REGISTRY.Create( "artifactregistry.projects.locations.repositories.files", projectsId=ref.projectsId, locationsId=ref.locationsId, repositoriesId=ref.repositoriesId, filesId=ref.filesId.replace("/", "%2F") .replace("+", "%2B") .replace("^", "%5E"), ) req.name = file.RelativeName() return req def EscapeFileNameFromIDs(project_id, location_id, repo_id, file_id): """Escapes slashes and pluses from request names.""" return resources.REGISTRY.Create( "artifactregistry.projects.locations.repositories.files", projectsId=project_id, locationsId=location_id, repositoriesId=repo_id, filesId=file_id.replace("/", "%2F") .replace("+", "%2B") .replace("^", "%5E"), ) def ConvertFilesHashes(files): """Convert hashes of file list to hex strings.""" return [ConvertFileHashes(f, None) for f in files] def ConvertFileHashes(response, unused_args): """Convert file hashes to hex strings.""" # File hashes are "bytes", and if it's returned directly, it will be # automatically encoded with base64. # We want to display them as hex strings instead. # The returned file obj restricts the field type, so we can't simply update # the "bytes" field to a "string" field. # Convert it to a json object and then update the field as a workaround. json_obj = json.loads(protojson.encode_message(response)) hashes = [] for h in response.hashes: hashes.append({ "type": h.type, "value": h.value.hex(), }) if hashes: json_obj["hashes"] = hashes # Proto map fields are converted into type "AnnotationsValue" in the response, # which contains a list of key-value pairs as "additionalProperties". # We want to convert this back to a dict. annotations = {} if response.annotations: for p in response.annotations.additionalProperties: annotations[p.key] = p.value if annotations: json_obj["annotations"] = annotations return json_obj def ListGenericFiles(args): """Lists the Generic Files stored.""" client = requests.GetClient() messages = requests.GetMessages() project = util.GetProject(args) location = util.GetLocation(args) repo = util.GetRepo(args) package = args.package version = args.version version_path = resources.Resource.RelativeName( resources.REGISTRY.Create( "artifactregistry.projects.locations.repositories.packages.versions", projectsId=project, locationsId=location, repositoriesId=repo, packagesId=package, versionsId=version, ) ) arg_filters = 'owner="{}"'.format(version_path) repo_path = resources.Resource.RelativeName( resources.REGISTRY.Create( "artifactregistry.projects.locations.repositories", projectsId=project, locationsId=location, repositoriesId=repo, ) ) files = requests.ListFiles(client, messages, repo_path, arg_filters) return files def ListFiles(args): """Lists files in a given project. Args: args: User input arguments. Returns: List of files. """ client = requests.GetClient() messages = requests.GetMessages() project = util.GetProject(args) location = args.location or properties.VALUES.artifacts.location.Get() repo = util.GetRepo(args) package = args.package version = args.version tag = args.tag page_size = args.page_size order_by = common_args.ParseSortByArg(args.sort_by) _, server_filter = filter_rewriter.Rewriter().Rewrite(args.filter) if order_by is not None: if "," in order_by: # Multi-ordering is not supported yet on backend, fall back to client-side # sort-by. order_by = None if package or version or tag: # Cannot use server-side sort-by with --package, --version or --tag, # fall back to client-side sort-by. order_by = None if args.limit is not None and args.filter is not None: if server_filter is not None: # Apply limit to server-side page_size to improve performance when # server-side filter is used. page_size = args.limit else: # Fall back to client-side paging with client-side filtering. page_size = None if server_filter: if package or version or tag: # Cannot use server-side filter with --package, --version or --tag, # fallback to client-side filter. server_filter = None # Parse fully qualified path in package argument if package: if re.match( r"projects\/.*\/locations\/.*\/repositories\/.*\/packages\/.*", package ): params = ( package.replace("projects/", "", 1) .replace("/locations/", " ", 1) .replace("/repositories/", " ", 1) .replace("/packages/", " ", 1) .split(" ") ) project, location, repo, package = [params[i] for i in range(len(params))] # Escape slashes, pluses and carets in package name if package: package = package.replace("/", "%2F").replace("+", "%2B") package = package.replace("^", "%5E") # Retrieve version from tag name if version and tag: raise exceptions.InvalidInputValueError( "Specify either --version or --tag with --package argument." ) if package and tag: tag_path = resources.Resource.RelativeName( resources.REGISTRY.Create( "artifactregistry.projects.locations.repositories.packages.tags", projectsId=project, locationsId=location, repositoriesId=repo, packagesId=package, tagsId=tag, ) ) version = requests.GetVersionFromTag(client, messages, tag_path) if package and version: version_path = resources.Resource.RelativeName( resources.REGISTRY.Create( "artifactregistry.projects.locations.repositories.packages.versions", projectsId=project, locationsId=location, repositoriesId=repo, packagesId=package, versionsId=version, ) ) server_filter = 'owner="{}"'.format(version_path) elif package: package_path = resources.Resource.RelativeName( resources.REGISTRY.Create( "artifactregistry.projects.locations.repositories.packages", projectsId=project, locationsId=location, repositoriesId=repo, packagesId=package, ) ) server_filter = 'owner="{}"'.format(package_path) elif version or tag: raise exceptions.InvalidInputValueError( "Package name is required when specifying version or tag." ) repo_path = resources.Resource.RelativeName( resources.REGISTRY.Create( "artifactregistry.projects.locations.repositories", projectsId=project, locationsId=location, repositoriesId=repo, ) ) server_args = { "client": client, "messages": messages, "repo": repo_path, "server_filter": server_filter, "page_size": page_size, "order_by": order_by, } server_args_skipped, lfiles = util.RetryOnInvalidArguments( requests.ListFiles, **server_args ) if not server_args_skipped: # If server-side filter or sort-by is parsed correctly and the request # succeeds, remove the client-side filter and sort-by. if server_filter and server_filter == args.filter: args.filter = None if order_by: args.sort_by = None return ConvertFilesHashes(lfiles)