File: //snap/google-cloud-cli/394/lib/googlecloudsdk/command_lib/artifacts/file_util.py
# -*- 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)