cmd/copyEnumeratorInit.go (471 lines of code) (raw):
package cmd
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azdatalake/datalakeerror"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azfile/fileerror"
"github.com/Azure/azure-storage-azcopy/v10/jobsAdmin"
"github.com/Azure/azure-storage-azcopy/v10/common"
)
type BucketToContainerNameResolver interface {
ResolveName(bucketName string) (string, error)
}
func (cca *CookedCopyCmdArgs) validateSourceDir(traverser ResourceTraverser) error {
var err error
// Ensure we're only copying a directory under valid conditions
cca.IsSourceDir, err = traverser.IsDirectory(true)
if cca.IsSourceDir &&
!cca.Recursive && // Copies the folder & everything under it
!cca.StripTopDir { // Copies only everything under it
// todo: dir only transfer, also todo: support syncing the root folder's acls on sync.
return errors.New("cannot use directory as source without --recursive or a trailing wildcard (/*)")
}
// check if error is file not found - if it is then we need to make sure it's not a wild card
if err != nil && strings.EqualFold(err.Error(), common.FILE_NOT_FOUND) && !cca.StripTopDir {
return err
}
return nil
}
func (cca *CookedCopyCmdArgs) initEnumerator(jobPartOrder common.CopyJobPartOrderRequest, srcCredInfo common.CredentialInfo, ctx context.Context) (*CopyEnumerator, error) {
var traverser ResourceTraverser
var err error
jobPartOrder.FileAttributes = common.FileTransferAttributes{
TrailingDot: cca.trailingDot,
}
jobPartOrder.CpkOptions = cca.CpkOptions
jobPartOrder.PreserveSMBPermissions = cca.preservePermissions
jobPartOrder.PreserveSMBInfo = cca.preserveSMBInfo
// We set preservePOSIXProperties if the customer has explicitly asked for this in transfer or if it is just a Posix-property only transfer
jobPartOrder.PreservePOSIXProperties = cca.preservePOSIXProperties || (cca.ForceWrite == common.EOverwriteOption.PosixProperties())
// Infer on download so that we get LMT and MD5 on files download
// On S2S transfers the following rules apply:
// If preserve properties is enabled, but get properties in backend is disabled, turn it on
// If source change validation is enabled on files to remote, turn it on (consider a separate flag entirely?)
getRemoteProperties := cca.ForceWrite == common.EOverwriteOption.IfSourceNewer() ||
(cca.FromTo.From() == common.ELocation.File() && !cca.FromTo.To().IsRemote()) || // If it's a download, we still need LMT and MD5 from files.
(cca.FromTo.From() == common.ELocation.File() && cca.FromTo.To().IsRemote() && (cca.s2sSourceChangeValidation || cca.IncludeAfter != nil || cca.IncludeBefore != nil)) || // If S2S from File to *, and sourceChangeValidation is enabled, we get properties so that we have LMTs. Likewise, if we are using includeAfter or includeBefore, which require LMTs.
(cca.FromTo.From().IsRemote() && cca.FromTo.To().IsRemote() && cca.s2sPreserveProperties && !cca.s2sGetPropertiesInBackend) // If S2S and preserve properties AND get properties in backend is on, turn this off, as properties will be obtained in the backend.
jobPartOrder.S2SGetPropertiesInBackend = cca.s2sPreserveProperties && !getRemoteProperties && cca.s2sGetPropertiesInBackend // Infer GetProperties if GetPropertiesInBackend is enabled.
jobPartOrder.S2SSourceChangeValidation = cca.s2sSourceChangeValidation
jobPartOrder.DestLengthValidation = cca.CheckLength
jobPartOrder.S2SInvalidMetadataHandleOption = cca.s2sInvalidMetadataHandleOption
jobPartOrder.S2SPreserveBlobTags = cca.S2sPreserveBlobTags
dest := cca.FromTo.To()
traverser, err = InitResourceTraverser(cca.Source, cca.FromTo.From(), &ctx, &srcCredInfo, cca.SymlinkHandling, cca.ListOfFilesChannel, cca.Recursive, getRemoteProperties, cca.IncludeDirectoryStubs, cca.permanentDeleteOption, func(common.EntityType) {}, cca.ListOfVersionIDs, cca.S2sPreserveBlobTags, common.ESyncHashType.None(), cca.preservePermissions, azcopyLogVerbosity, cca.CpkOptions, nil, cca.StripTopDir, cca.trailingDot, &dest, cca.excludeContainer, false)
if err != nil {
return nil, err
}
err = cca.validateSourceDir(traverser)
if err != nil {
return nil, err
}
// Check if the destination is a directory to correctly decide where our files land
isDestDir := cca.isDestDirectory(cca.Destination, &ctx)
if cca.ListOfVersionIDs != nil && (!(cca.FromTo == common.EFromTo.BlobLocal() || cca.FromTo == common.EFromTo.BlobTrash()) || cca.IsSourceDir || !isDestDir) {
log.Fatalf("Either source is not a blob or destination is not a local folder")
}
srcLevel, err := DetermineLocationLevel(cca.Source.Value, cca.FromTo.From(), true)
if err != nil {
return nil, err
}
dstLevel, err := DetermineLocationLevel(cca.Destination.Value, cca.FromTo.To(), false)
if err != nil {
return nil, err
}
// Disallow list-of-files and include-path on service-level traversal due to a major bug
// TODO: Fix the bug.
// Two primary issues exist with the list-of-files implementation:
// 1) Account name doesn't get trimmed from the path
// 2) List-of-files is not considered an account traverser; therefore containers don't get made.
// Resolve these two issues and service-level list-of-files/include-path will work
if cca.ListOfFilesChannel != nil && srcLevel == ELocationLevel.Service() {
return nil, errors.New("cannot combine list-of-files or include-path with account traversal")
}
if (srcLevel == ELocationLevel.Object() || cca.FromTo.From().IsLocal()) && dstLevel == ELocationLevel.Service() {
return nil, errors.New("cannot transfer individual files/folders to the root of a service. Add a container or directory to the destination URL")
}
if srcLevel == ELocationLevel.Container() && dstLevel == ELocationLevel.Service() && !cca.asSubdir {
return nil, errors.New("cannot use --as-subdir=false with a service level destination")
}
// When copying a container directly to a container, strip the top directory, unless we're attempting to persist permissions.
if srcLevel == ELocationLevel.Container() && dstLevel == ELocationLevel.Container() && cca.FromTo.From().IsRemote() && cca.FromTo.To().IsRemote() {
if cca.preservePermissions.IsTruthy() {
// if we're preserving permissions, we need to keep the top directory, but with container->container, we don't need to add the container name to the path.
// asSubdir is a better option than stripTopDir as stripTopDir disincludes the root.
cca.asSubdir = false
} else {
cca.StripTopDir = true
}
}
// Create a Remote resource resolver
// Giving it nothing to work with as new names will be added as we traverse.
var containerResolver BucketToContainerNameResolver
containerResolver = NewS3BucketNameToAzureResourcesResolver(nil)
if cca.FromTo == common.EFromTo.GCPBlob() {
containerResolver = NewGCPBucketNameToAzureResourcesResolver(nil)
}
existingContainers := make(map[string]bool)
var logDstContainerCreateFailureOnce sync.Once
seenFailedContainers := make(map[string]bool) // Create map of already failed container conversions so we don't log a million items just for one container.
dstContainerName := ""
// Extract the existing destination container name
if cca.FromTo.To().IsRemote() {
dstContainerName, err = GetContainerName(cca.Destination.Value, cca.FromTo.To())
if err != nil {
return nil, err
}
// only create the destination container in S2S scenarios
if cca.FromTo.From().IsRemote() && dstContainerName != "" { // if the destination has a explicit container name
// Attempt to create the container. If we fail, fail silently.
err = cca.createDstContainer(dstContainerName, cca.Destination, ctx, existingContainers, common.ELogLevel.None())
// check against seenFailedContainers so we don't spam the job log with initialization failed errors
if _, ok := seenFailedContainers[dstContainerName]; err != nil && jobsAdmin.JobsAdmin != nil && !ok {
logDstContainerCreateFailureOnce.Do(func() {
glcm.Warn("Failed to create one or more destination container(s). Your transfers may still succeed if the container already exists.")
})
jobsAdmin.JobsAdmin.LogToJobLog(fmt.Sprintf("Failed to create destination container %s. The transfer will continue if the container exists", dstContainerName), common.LogWarning)
jobsAdmin.JobsAdmin.LogToJobLog(fmt.Sprintf("Error %s", err), common.LogDebug)
seenFailedContainers[dstContainerName] = true
}
} else if cca.FromTo.From().IsRemote() { // if the destination has implicit container names
if acctTraverser, ok := traverser.(AccountTraverser); ok && dstLevel == ELocationLevel.Service() {
containers, err := acctTraverser.listContainers()
if err != nil {
return nil, fmt.Errorf("failed to list containers: %w", err)
}
// Resolve all container names up front.
// If we were to resolve on-the-fly, then name order would affect the results inconsistently.
if cca.FromTo == common.EFromTo.S3Blob() {
containerResolver = NewS3BucketNameToAzureResourcesResolver(containers)
} else if cca.FromTo == common.EFromTo.GCPBlob() {
containerResolver = NewGCPBucketNameToAzureResourcesResolver(containers)
}
for _, v := range containers {
bucketName, err := containerResolver.ResolveName(v)
if err != nil {
// Silently ignore the failure; it'll get logged later.
continue
}
err = cca.createDstContainer(bucketName, cca.Destination, ctx, existingContainers, common.ELogLevel.None())
// if JobsAdmin is nil, we're probably in testing mode.
// As a result, container creation failures are expected as we don't give the SAS tokens adequate permissions.
// check against seenFailedContainers so we don't spam the job log with initialization failed errors
if _, ok := seenFailedContainers[bucketName]; err != nil && jobsAdmin.JobsAdmin != nil && !ok {
logDstContainerCreateFailureOnce.Do(func() {
glcm.Warn("Failed to create one or more destination container(s). Your transfers may still succeed if the container already exists.")
})
jobsAdmin.JobsAdmin.LogToJobLog(fmt.Sprintf("failed to initialize destination container %s; the transfer will continue (but be wary it may fail).", bucketName), common.LogWarning)
jobsAdmin.JobsAdmin.LogToJobLog(fmt.Sprintf("Error %s", err), common.LogDebug)
seenFailedContainers[bucketName] = true
}
}
} else {
cName, err := GetContainerName(cca.Source.Value, cca.FromTo.From())
if err != nil || cName == "" {
// this will probably never be reached
return nil, fmt.Errorf("failed to get container name from source (is it formatted correctly?)")
}
resName, err := containerResolver.ResolveName(cName)
if err == nil {
err = cca.createDstContainer(resName, cca.Destination, ctx, existingContainers, common.ELogLevel.None())
if _, ok := seenFailedContainers[dstContainerName]; err != nil && jobsAdmin.JobsAdmin != nil && !ok {
logDstContainerCreateFailureOnce.Do(func() {
glcm.Warn("Failed to create one or more destination container(s). Your transfers may still succeed if the container already exists.")
})
jobsAdmin.JobsAdmin.LogToJobLog(fmt.Sprintf("failed to initialize destination container %s; the transfer will continue (but be wary it may fail).", resName), common.LogWarning)
jobsAdmin.JobsAdmin.LogToJobLog(fmt.Sprintf("Error %s", err), common.LogDebug)
seenFailedContainers[dstContainerName] = true
}
}
}
}
}
filters := cca.InitModularFilters()
// decide our folder transfer strategy
var message string
jobPartOrder.Fpo, message = NewFolderPropertyOption(cca.FromTo, cca.Recursive, cca.StripTopDir, filters, cca.preserveSMBInfo, cca.preservePermissions.IsTruthy(), cca.preservePOSIXProperties, strings.EqualFold(cca.Destination.Value, common.Dev_Null), cca.IncludeDirectoryStubs)
if !cca.dryrunMode {
glcm.Info(message)
}
if jobsAdmin.JobsAdmin != nil {
jobsAdmin.JobsAdmin.LogToJobLog(message, common.LogInfo)
}
processor := func(object StoredObject) error {
// Start by resolving the name and creating the container
if object.ContainerName != "" {
// set up the destination container name.
cName := dstContainerName
// if a destination container name is not specified OR copying service to container/folder, append the src container name.
if cName == "" || (srcLevel == ELocationLevel.Service() && dstLevel > ELocationLevel.Service()) {
cName, err = containerResolver.ResolveName(object.ContainerName)
if err != nil {
if _, ok := seenFailedContainers[object.ContainerName]; !ok {
WarnStdoutAndScanningLog(fmt.Sprintf("failed to add transfers from container %s as it has an invalid name. Please manually transfer from this container to one with a valid name.", object.ContainerName))
seenFailedContainers[object.ContainerName] = true
}
return nil
}
object.DstContainerName = cName
}
}
// If above the service level, we already know the container name and don't need to supply it to makeEscapedRelativePath
if srcLevel != ELocationLevel.Service() {
object.ContainerName = ""
// When copying directly TO a container or object from a container, don't drop under a sub directory
if dstLevel >= ELocationLevel.Container() {
object.DstContainerName = ""
}
}
srcRelPath := cca.MakeEscapedRelativePath(true, isDestDir, cca.asSubdir, object)
dstRelPath := cca.MakeEscapedRelativePath(false, isDestDir, cca.asSubdir, object)
transfer, shouldSendToSte := object.ToNewCopyTransfer(cca.autoDecompress && cca.FromTo.IsDownload(), srcRelPath, dstRelPath, cca.s2sPreserveAccessTier, jobPartOrder.Fpo, cca.SymlinkHandling)
if !cca.S2sPreserveBlobTags {
transfer.BlobTags = cca.blobTags
}
if cca.dryrunMode && shouldSendToSte {
glcm.Dryrun(func(format common.OutputFormat) string {
src := common.GenerateFullPath(cca.Source.Value, srcRelPath)
dst := common.GenerateFullPath(cca.Destination.Value, dstRelPath)
switch format {
case common.EOutputFormat.Json():
tx := DryrunTransfer{
EntityType: transfer.EntityType,
BlobType: common.FromBlobType(transfer.BlobType),
FromTo: cca.FromTo,
Source: src,
Destination: dst,
SourceSize: &transfer.SourceSize,
HttpHeaders: blob.HTTPHeaders{
BlobCacheControl: &transfer.CacheControl,
BlobContentDisposition: &transfer.ContentDisposition,
BlobContentEncoding: &transfer.ContentEncoding,
BlobContentLanguage: &transfer.ContentLanguage,
BlobContentMD5: transfer.ContentMD5,
BlobContentType: &transfer.ContentType,
},
Metadata: transfer.Metadata,
BlobTier: &transfer.BlobTier,
BlobVersion: &transfer.BlobVersionID,
BlobTags: transfer.BlobTags,
BlobSnapshot: &transfer.BlobSnapshotID,
}
buf, _ := json.Marshal(tx)
return string(buf)
default:
return fmt.Sprintf("DRYRUN: copy %v to %v",
src, dst)
}
})
return nil
}
if shouldSendToSte {
return addTransfer(&jobPartOrder, transfer, cca)
}
return nil
}
finalizer := func() error {
return dispatchFinalPart(&jobPartOrder, cca)
}
return NewCopyEnumerator(traverser, filters, processor, finalizer), nil
}
// This is condensed down into an individual function as we don't end up reusing the destination traverser at all.
// This is just for the directory check.
func (cca *CookedCopyCmdArgs) isDestDirectory(dst common.ResourceString, ctx *context.Context) bool {
var err error
dstCredInfo := common.CredentialInfo{}
if ctx == nil {
return false
}
if dstCredInfo, _, err = GetCredentialInfoForLocation(*ctx, cca.FromTo.To(), cca.Destination, false, cca.CpkOptions); err != nil {
return false
}
rt, err := InitResourceTraverser(dst, cca.FromTo.To(), ctx, &dstCredInfo, common.ESymlinkHandlingType.Skip(), nil, false, false, false, common.EPermanentDeleteOption.None(), func(common.EntityType) {}, cca.ListOfVersionIDs, false, common.ESyncHashType.None(), cca.preservePermissions, common.LogNone, cca.CpkOptions, nil, cca.StripTopDir, cca.trailingDot, nil, cca.excludeContainer, false)
if err != nil {
return false
}
isDir, _ := rt.IsDirectory(false)
return isDir
}
// Initialize the modular filters outside of copy to increase readability.
func (cca *CookedCopyCmdArgs) InitModularFilters() []ObjectFilter {
filters := make([]ObjectFilter, 0) // same as []ObjectFilter{} under the hood
if cca.IncludeBefore != nil {
filters = append(filters, &IncludeBeforeDateFilter{Threshold: *cca.IncludeBefore})
}
if cca.IncludeAfter != nil {
filters = append(filters, &IncludeAfterDateFilter{Threshold: *cca.IncludeAfter})
}
if len(cca.IncludePatterns) != 0 {
filters = append(filters, &IncludeFilter{patterns: cca.IncludePatterns}) // TODO should this call buildIncludeFilters?
}
if len(cca.ExcludePatterns) != 0 {
for _, v := range cca.ExcludePatterns {
filters = append(filters, &excludeFilter{pattern: v})
}
}
// include-path is not a filter, therefore it does not get handled here.
// Check up in cook() around the list-of-files implementation as include-path gets included in the same way.
if len(cca.ExcludePathPatterns) != 0 {
for _, v := range cca.ExcludePathPatterns {
filters = append(filters, &excludeFilter{pattern: v, targetsPath: true})
}
}
if len(cca.includeRegex) != 0 {
filters = append(filters, ®exFilter{patterns: cca.includeRegex, isIncluded: true})
}
if len(cca.excludeRegex) != 0 {
filters = append(filters, ®exFilter{patterns: cca.excludeRegex, isIncluded: false})
}
if len(cca.excludeBlobType) != 0 {
excludeSet := map[blob.BlobType]bool{}
for _, v := range cca.excludeBlobType {
excludeSet[v] = true
}
filters = append(filters, &excludeBlobTypeFilter{blobTypes: excludeSet})
}
if len(cca.IncludeFileAttributes) != 0 {
filters = append(filters, buildAttrFilters(cca.IncludeFileAttributes, cca.Source.ValueLocal(), true)...)
}
if len(cca.ExcludeFileAttributes) != 0 {
filters = append(filters, buildAttrFilters(cca.ExcludeFileAttributes, cca.Source.ValueLocal(), false)...)
}
// finally, log any search prefix computed from these
if jobsAdmin.JobsAdmin != nil {
if prefixFilter := FilterSet(filters).GetEnumerationPreFilter(cca.Recursive); prefixFilter != "" {
jobsAdmin.JobsAdmin.LogToJobLog("Search prefix, which may be used to optimize scanning, is: "+prefixFilter, common.LogInfo) // "May be used" because we don't know here which enumerators will use it
}
}
switch cca.permanentDeleteOption {
case common.EPermanentDeleteOption.Snapshots():
filters = append(filters, &permDeleteFilter{deleteSnapshots: true})
case common.EPermanentDeleteOption.Versions():
filters = append(filters, &permDeleteFilter{deleteVersions: true})
case common.EPermanentDeleteOption.SnapshotsAndVersions():
filters = append(filters, &permDeleteFilter{deleteSnapshots: true, deleteVersions: true})
}
return filters
}
func (cca *CookedCopyCmdArgs) createDstContainer(containerName string, dstWithSAS common.ResourceString, parentCtx context.Context, existingContainers map[string]bool, logLevel common.LogLevel) (err error) {
if _, ok := existingContainers[containerName]; ok {
return
}
existingContainers[containerName] = true
var dstCredInfo common.CredentialInfo
// 3minutes is enough time to list properties of a container, and create new if it does not exist.
ctx, cancel := context.WithTimeout(parentCtx, time.Minute*3)
defer cancel()
if dstCredInfo, _, err = GetCredentialInfoForLocation(ctx, cca.FromTo.To(), cca.Destination, false, cca.CpkOptions); err != nil {
return err
}
var reauthTok *common.ScopedAuthenticator
if at, ok := dstCredInfo.OAuthTokenInfo.TokenCredential.(common.AuthenticateToken); ok {
// This will cause a reauth with StorageScope, which is fine, that's the original Authenticate call as it stands.
reauthTok = (*common.ScopedAuthenticator)(common.NewScopedCredential(at, common.ECredentialType.OAuthToken()))
}
options := createClientOptions(common.AzcopyCurrentJobLogger, nil, reauthTok)
sc, err := common.GetServiceClientForLocation(
cca.FromTo.To(),
dstWithSAS,
dstCredInfo.CredentialType,
dstCredInfo.OAuthTokenInfo.TokenCredential,
&options,
nil, // trailingDot is not required when creating a share
)
if err != nil {
return err
}
// Because the only use-cases for createDstContainer will be on service-level S2S and service-level download
// We only need to create "containers" on local and blob.
// TODO: Reduce code dupe somehow
switch cca.FromTo.To() {
case common.ELocation.Local():
err = os.MkdirAll(common.GenerateFullPath(cca.Destination.ValueLocal(), containerName), os.ModeDir|os.ModePerm)
case common.ELocation.Blob():
bsc, _ := sc.BlobServiceClient()
bcc := bsc.NewContainerClient(containerName)
_, err = bcc.GetProperties(ctx, nil)
if err == nil {
return err // Container already exists, return gracefully
}
_, err = bcc.Create(ctx, nil)
if bloberror.HasCode(err, bloberror.ContainerAlreadyExists) {
return nil
}
return err
case common.ELocation.File():
fsc, _ := sc.FileServiceClient()
sc := fsc.NewShareClient(containerName)
_, err = sc.GetProperties(ctx, nil)
if err == nil {
return err
}
// Create a destination share with the default service quota
// TODO: Create a flag for the quota
_, err = sc.Create(ctx, nil)
if fileerror.HasCode(err, fileerror.ShareAlreadyExists) {
return nil
}
return err
case common.ELocation.BlobFS():
dsc, _ := sc.DatalakeServiceClient()
fsc := dsc.NewFileSystemClient(containerName)
_, err = fsc.GetProperties(ctx, nil)
if err == nil {
return err
}
_, err = fsc.Create(ctx, nil)
if datalakeerror.HasCode(err, datalakeerror.FileSystemAlreadyExists) {
return nil
}
return err
default:
panic(fmt.Sprintf("cannot create a destination container at location %s.", cca.FromTo.To()))
}
return
}
// Because some invalid characters weren't being properly encoded by url.PathEscape, we're going to instead manually encode them.
var encodedInvalidCharacters = map[rune]string{
'<': "%3C",
'>': "%3E",
'\\': "%5C",
'/': "%2F",
':': "%3A",
'"': "%22",
'|': "%7C",
'?': "%3F",
'*': "%2A",
}
var reverseEncodedChars = map[string]rune{
"%3C": '<',
"%3E": '>',
"%5C": '\\',
"%2F": '/',
"%3A": ':',
"%22": '"',
"%7C": '|',
"%3F": '?',
"%2A": '*',
}
func pathEncodeRules(path string, fromTo common.FromTo, disableAutoDecoding bool, source bool) string {
var loc common.Location
if source {
loc = fromTo.From()
} else {
loc = fromTo.To()
}
pathParts := strings.Split(path, common.AZCOPY_PATH_SEPARATOR_STRING)
// If downloading on Windows or uploading to files, encode unsafe characters.
if (loc == common.ELocation.Local() && !source && runtime.GOOS == "windows") || (!source && loc == common.ELocation.File()) {
// invalidChars := `<>\/:"|?*` + string(0x00)
for k, c := range encodedInvalidCharacters {
for part, p := range pathParts {
pathParts[part] = strings.ReplaceAll(p, string(k), c)
}
}
// If uploading from Windows or downloading from files, decode unsafe chars if user enables decoding
} else if ((!source && fromTo.From() == common.ELocation.Local() && runtime.GOOS == "windows") || (!source && fromTo.From() == common.ELocation.File())) && !disableAutoDecoding {
for encoded, c := range reverseEncodedChars {
for k, p := range pathParts {
pathParts[k] = strings.ReplaceAll(p, encoded, string(c))
}
}
}
if loc.IsRemote() {
for k, p := range pathParts {
pathParts[k] = url.PathEscape(p)
}
}
path = strings.Join(pathParts, "/")
return path
}
func (cca *CookedCopyCmdArgs) MakeEscapedRelativePath(source bool, dstIsDir bool, asSubdir bool, object StoredObject) (relativePath string) {
// write straight to /dev/null, do not determine a indirect path
if !source && cca.Destination.Value == common.Dev_Null {
return "" // ignore path encode rules
}
if object.relativePath == "\x00" { // Short circuit, our relative path is requesting root/
return "\x00"
}
// source is a EXACT path to the file
if object.isSingleSourceFile() {
// If we're finding an object from the source, it returns "" if it's already got it.
// If we're finding an object on the destination and we get "", we need to hand it the object name (if it's pointing to a folder)
if source {
relativePath = ""
} else {
if dstIsDir {
// Our source points to a specific file (and so has no relative path)
// but our dest does not point to a specific file, it just points to a directory,
// and so relativePath needs the _name_ of the source.
processedVID := ""
if len(object.blobVersionID) > 0 {
processedVID = strings.ReplaceAll(object.blobVersionID, ":", "-") + "-"
}
relativePath += "/" + processedVID + object.name
} else {
relativePath = ""
}
}
return pathEncodeRules(relativePath, cca.FromTo, cca.disableAutoDecoding, source)
}
// If it's out here, the object is contained in a folder, or was found via a wildcard, or object.isSourceRootFolder == true
if object.isSourceRootFolder() {
relativePath = "" // otherwise we get "/" from the line below, and that breaks some clients, e.g. blobFS
} else {
relativePath = "/" + strings.Replace(object.relativePath, common.OS_PATH_SEPARATOR, common.AZCOPY_PATH_SEPARATOR_STRING, -1)
}
if common.Iff(source, object.ContainerName, object.DstContainerName) != "" {
relativePath = `/` + common.Iff(source, object.ContainerName, object.DstContainerName) + relativePath
} else if !source && !cca.StripTopDir && cca.asSubdir { // Avoid doing this where the root is shared or renamed.
// We ONLY need to do this adjustment to the destination.
// The source SAS has already been removed. No need to convert it to a URL or whatever.
// Save to a directory
rootDir := filepath.Base(cca.Source.Value)
/* In windows, when a user tries to copy whole volume (eg. D:\), the upload destination
will contains "//"" in the files/directories names because of rootDir = "\" prefix.
(e.g. D:\file.txt will end up as //file.txt).
Following code will get volume name from source and add volume name as prefix in rootDir
*/
if runtime.GOOS == "windows" && rootDir == `\` {
rootDir = filepath.VolumeName(common.ToShortPath(cca.Source.Value))
}
if cca.FromTo.From().IsRemote() {
ueRootDir, err := url.PathUnescape(rootDir)
// Realistically, err should never not be nil here.
if err == nil {
rootDir = ueRootDir
} else {
panic("unexpected inescapable rootDir name")
}
}
relativePath = "/" + rootDir + relativePath
}
return pathEncodeRules(relativePath, cca.FromTo, cca.disableAutoDecoding, source)
}
// we assume that preserveSmbPermissions and preserveSmbInfo have already been validated, such that they are only true if both resource types support them
func NewFolderPropertyOption(fromTo common.FromTo, recursive, stripTopDir bool, filters []ObjectFilter, preserveSmbInfo, preservePermissions, preservePosixProperties, isDstNull, includeDirectoryStubs bool) (common.FolderPropertyOption, string) {
getSuffix := func(willProcess bool) string {
willProcessString := common.Iff(willProcess, "will be processed", "will not be processed")
template := ". For the same reason, %s defined on folders %s"
switch {
case preservePermissions && preserveSmbInfo:
return fmt.Sprintf(template, "properties and permissions", willProcessString)
case preserveSmbInfo:
return fmt.Sprintf(template, "properties", willProcessString)
case preservePermissions:
return fmt.Sprintf(template, "permissions", willProcessString)
default:
return "" // no preserve flags set, so we have nothing to say about them
}
}
bothFolderAware := (fromTo.AreBothFolderAware() || preservePosixProperties || preservePermissions || includeDirectoryStubs) && !isDstNull
isRemoveFromFolderAware := fromTo == common.EFromTo.FileTrash()
if bothFolderAware || isRemoveFromFolderAware {
if !recursive {
return common.EFolderPropertiesOption.NoFolders(), // doesn't make sense to move folders when not recursive. E.g. if invoked with /* and WITHOUT recursive
"Any empty folders will not be processed, because --recursive was not specified" +
getSuffix(false)
}
// check filters. Otherwise, if filter was say --include-pattern *.txt, we would transfer properties
// (but not contents) for every directory that contained NO text files. Could make heaps of empty directories
// at the destination.
filtersOK := true
for _, f := range filters {
if f.AppliesOnlyToFiles() {
filtersOK = false // we have a least one filter that doesn't apply to folders
}
}
if !filtersOK {
return common.EFolderPropertiesOption.NoFolders(),
"Any empty folders will not be processed, because a file-focused filter is applied" +
getSuffix(false)
}
message := "Any empty folders will be processed, because source and destination both support folders"
if isRemoveFromFolderAware {
message = "Any empty folders will be processed, because deletion is from a folder-aware location"
}
message += getSuffix(true)
if stripTopDir {
return common.EFolderPropertiesOption.AllFoldersExceptRoot(), message
}
return common.EFolderPropertiesOption.AllFolders(), message
}
return common.EFolderPropertiesOption.NoFolders(),
"Any empty folders will not be processed, because source and/or destination doesn't have full folder support" +
getSuffix(false)
}