File: //snap/google-cloud-cli/394/lib/googlecloudsdk/command_lib/storage/copying.py
# -*- coding: utf-8 -*- #
# Copyright 2016 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.
"""Utilities for computing copy operations from command arguments."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import os
from googlecloudsdk.api_lib.storage import storage_util
from googlecloudsdk.command_lib.storage import expansion
from googlecloudsdk.command_lib.storage import paths
from googlecloudsdk.command_lib.storage import storage_parallel
from googlecloudsdk.core import exceptions
class Error(exceptions.Error):
pass
class WildcardError(Error):
pass
class RecursionError(Error):
pass
class LocationMismatchError(Error):
pass
class DestinationDirectoryExistsError(Error):
pass
class DestinationNotDirectoryError(Error):
pass
class InvalidDestinationError(Error):
def __init__(self, source, dest):
super(InvalidDestinationError, self).__init__(
'Cannot copy [{}] to [{}] because of "." or ".." in the path. '
'gcloud does not support Cloud Storage paths containing these path '
'segments and it is recommended that you do not name objects in '
'this way. Other tooling may convert these paths to incorrect '
'local directories.'.format(source.path, dest.path))
class CopyTaskGenerator(object):
"""A helper to compute and generate the tasks required to perform a copy."""
def __init__(self):
# Create a single instance of each expander so that all expansion uses the
# same cached data.
self._local_expander = expansion.LocalPathExpander()
self._gcs_expander = expansion.GCSPathExpander()
def _GetExpander(self, path):
"""Get the correct expander for this type of path."""
if path.is_remote:
return self._gcs_expander
return self._local_expander
def GetCopyTasks(self, sources, dest, recursive=False):
"""Get all the file copy tasks for the sources given to this copier.
Args:
sources: [paths.Path], The sources (containing optional wildcards) that
you want to copy.
dest: paths.Path, The wildcard-free path you want to copy the sources to.
recursive: bool, True to allow recursive copying of directories.
Raises:
WildcardError: If dest contains a wildcard.
LocationMismatchError: If you are trying to copy local files to local
files.
DestinationNotDirectoryError: If trying to copy multiple files to a single
dest name.
RecursionError: If any of sources are directories, but recursive is
false.
Returns:
[storage_parallel.Task], All the tasks that should be executed to perform
this copy.
"""
# Sources go through the expander where they are converted to absolute
# paths. The dest does not, so convert it manually here.
dest_is_dir = dest.is_dir_like
dest = paths.Path(self._GetExpander(dest).AbsPath(dest.path))
if dest_is_dir:
dest = dest.Join('')
if expansion.PathExpander.HasExpansion(dest.path):
raise WildcardError(
'Destination [{}] cannot contain wildcards.'.format(dest.path))
if not dest.is_remote:
local_sources = [s for s in sources if not s.is_remote]
if local_sources:
raise LocationMismatchError(
'When destination is a local path, all sources must be remote '
'paths.')
files, dirs = self._ExpandFilesToCopy(sources)
if not dest.is_dir_like:
# Destination is a file, we can only perform a single file/dir copy.
if (len(files) + len(dirs)) > 1:
raise DestinationNotDirectoryError(
'When copying multiple sources, destination must be a directory '
'(a path ending with a slash).')
if dirs and not recursive:
raise RecursionError(
'Source path matches directories but --recursive was not specified.')
tasks = []
tasks.extend(self._GetFileCopyTasks(files, dest))
tasks.extend(self._GetDirCopyTasks(dirs, dest))
return tasks
def _ExpandFilesToCopy(self, sources):
"""Do initial expansion of all the wildcard arguments.
Args:
sources: [paths.Path], The sources (containing optional wildcards) that
you want to copy.
Returns:
([paths.Path], [paths.Path]), The file and directory paths that the
initial set of sources expanded to.
"""
files = set()
dirs = set()
for s in sources:
expander = self._GetExpander(s)
(current_files, current_dirs) = expander.ExpandPath(s.path)
files.update(current_files)
dirs.update(current_dirs)
return ([paths.Path(f) for f in sorted(files)],
[paths.Path(d) for d in sorted(dirs)])
def _GetDirCopyTasks(self, dirs, dest):
"""Get the Tasks to be executed to copy the given directories.
If dest is dir-like (ending in a slash), all dirs are copied under the
destination. If it is file-like, at most one directory can be provided and
it is copied directly to the destination name.
File copy tasks are generated recursively for the contents of all
directories.
Args:
dirs: [paths.Path], The directories to copy.
dest: paths.Path, The destination to copy the directories to.
Returns:
[storage_parallel.Task], The file copy tasks to execute.
"""
tasks = []
for d in dirs:
item_dest = self._GetDestinationName(d, dest)
expander = self._GetExpander(d)
(files, sub_dirs) = expander.ExpandPath(d.Join('*').path)
files = [paths.Path(f) for f in sorted(files)]
sub_dirs = [paths.Path(d) for d in sorted(sub_dirs)]
tasks.extend(self._GetFileCopyTasks(files, item_dest))
tasks.extend(self._GetDirCopyTasks(sub_dirs, item_dest))
return tasks
def _GetFileCopyTasks(self, sources, dest):
"""Get the Tasks to be executed to copy the given sources.
If dest is dir-like (ending in a slash), all sources are copied under the
destination. If it is file-like, at most one source can be provided and it
is copied directly to the destination name.
Args:
sources: [paths.Path], The source files to copy. These must all be files
not directories.
dest: paths.Path, The destination to copy the files to.
Returns:
[storage_parallel.Task], The file copy tasks to execute.
"""
if not sources:
return []
tasks = []
for source in sources:
item_dest = self._GetDestinationName(source, dest)
tasks.append(self._MakeTask(source, item_dest))
return tasks
def _GetDestinationName(self, item, dest):
"""Computes the destination name to copy item to.."""
expander = self._GetExpander(dest)
if dest.is_dir_like:
item_dest = dest.Join(
os.path.basename(item.path.rstrip('/').rstrip('\\')))
if item.is_dir_like:
item_dest = item_dest.Join('')
if expander.IsFile(dest.path):
raise DestinationDirectoryExistsError(
'Cannot copy [{}] to [{}]: [{}] exists and is a file.'.format(
item.path, item_dest.path, dest.path))
else:
item_dest = dest
# If copying a directory, then if the target exists at all it's a problem.
# If copying a file we only need to ensure that the target is not a
# directory. If it's just a file it will be overwritten.
check_func = expander.Exists if item.is_dir_like else expander.IsDir
if check_func(item_dest.path):
raise DestinationDirectoryExistsError(
'Cannot copy [{}] to [{}]: The destination already exists. If you '
'meant to copy under this destination, add a slash to the end of its '
'path.'
.format(item.path, item_dest.path))
return item_dest
def _MakeTask(self, source, dest):
"""Make a file copy Task for a single source.
Args:
source: paths.Path, The source file to copy.
dest: path.Path, The destination to copy the file to.
Raises:
InvalidDestinationError: If this would end up copying to a path that has
'.' or '..' as a segment.
LocationMismatchError: If trying to copy a local file to a local file.
Returns:
storage_parallel.Task, The copy task to execute.
"""
if not dest.IsPathSafe():
raise InvalidDestinationError(source, dest)
if source.is_remote:
source_obj = storage_util.ObjectReference.FromUrl(source.path)
if dest.is_remote:
dest_obj = storage_util.ObjectReference.FromUrl(dest.path)
return storage_parallel.FileRemoteCopyTask(source_obj, dest_obj)
return storage_parallel.FileDownloadTask(source_obj, dest.path)
# Local source file.
if dest.is_remote:
dest_obj = storage_util.ObjectReference.FromUrl(dest.path)
return storage_parallel.FileUploadTask(source.path, dest_obj)
# Both local, can't do this.
raise LocationMismatchError(
'Cannot copy local file [{}] to local file [{}]'.format(
source.path, dest.path))