Skip to content

Commit f3c4c47

Browse files
authored
Merge pull request #95 from Azure/dev
10.0.3
2 parents f967afe + 41e0822 commit f3c4c47

22 files changed

+490
-52
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -325,3 +325,6 @@ testSuite/venv1/*
325325
testSuite/scripts/__pycache__/utility.cpython-36.pyc
326326
testSuite/scripts/__pycache__/utility.cpython-36.pyc
327327
venv*
328+
329+
#json create files
330+
*.json

clean.sh

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77

88
# remove logs and memory mapped plan files
99
rm -rf ~/.azcopy/*
10+
rm ./azcopy*

cmd/copy.go

+58-11
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/Azure/azure-storage-blob-go/2018-03-28/azblob"
3838
"github.com/Azure/azure-storage-file-go/2017-07-29/azfile"
3939
"github.com/spf13/cobra"
40+
"io/ioutil"
4041
)
4142

4243
// upload related
@@ -65,11 +66,12 @@ type rawCopyCmdArgs struct {
6566
//blobUrlForRedirection string
6667

6768
// filters from flags
68-
include string
69-
exclude string
70-
recursive bool
71-
followSymlinks bool
72-
withSnapshots bool
69+
listOfFilesToCopy string
70+
include string
71+
exclude string
72+
recursive bool
73+
followSymlinks bool
74+
withSnapshots bool
7375
// forceWrite flag is used to define the User behavior
7476
// to overwrite the existing blobs or not.
7577
forceWrite bool
@@ -124,6 +126,46 @@ func (raw rawCopyCmdArgs) cook() (cookedCopyCmdArgs, error) {
124126
if err != nil {
125127
return cooked, err
126128
}
129+
// User can provide either listOfFilesToCopy or include since listOFFiles mentions
130+
// file names to include explicitly and include file may mention at pattern.
131+
// This could conflict enumerating the files to queue up for transfer.
132+
if len(raw.listOfFilesToCopy) > 0 && len(raw.include) > 0 {
133+
return cooked, fmt.Errorf("user provided argument with both listOfFilesToCopy and include flag. Only one should be provided")
134+
}
135+
136+
// If the user provided the list of files explicitly to be copied, then parse the argument
137+
// The user passes the location of json file which will have the list of files to be copied.
138+
// The "json file" is chosen as input because there is limit on the number of characters that
139+
// can be supplied with the argument, but Storage Explorer folks requirements was not to impose
140+
// any limit on the number of files that can be copied.
141+
if len(raw.listOfFilesToCopy) > 0 {
142+
//files := strings.Split(raw.listOfFilesToCopy, ";")
143+
jsonFile, err := os.Open(raw.listOfFilesToCopy)
144+
if err != nil {
145+
return cooked, fmt.Errorf("cannot open %s file passed with the list-of-file flag", raw.listOfFilesToCopy)
146+
}
147+
// read our opened xmlFile as a byte array.
148+
jsonBytes, err := ioutil.ReadAll(jsonFile)
149+
if err != nil {
150+
return cooked, fmt.Errorf("error %s read %s file passed with the list-of-file flag", err.Error(), raw.listOfFilesToCopy)
151+
}
152+
var files common.ListOfFiles
153+
err = json.Unmarshal(jsonBytes, &files)
154+
if err != nil {
155+
return cooked, fmt.Errorf("error %s unmarshalling the contents of %s file passed with the list-of-file flag", err.Error(), raw.listOfFilesToCopy)
156+
}
157+
for _, file := range files.Files {
158+
// If split of the include string leads to an empty string
159+
// not include that string
160+
if len(file) == 0 {
161+
continue
162+
}
163+
// replace the OS path separator in includePath string with AZCOPY_PATH_SEPARATOR
164+
// this replacement is done to handle the windows file paths where path separator "\\"
165+
filePath := strings.Replace(file, common.OS_PATH_SEPARATOR, common.AZCOPY_PATH_SEPARATOR_STRING, -1)
166+
cooked.listOfFilesToCopy = append(cooked.listOfFilesToCopy, filePath)
167+
}
168+
}
127169

128170
// initialize the include map which contains the list of files to be included
129171
// parse the string passed in include flag
@@ -260,12 +302,13 @@ type cookedCopyCmdArgs struct {
260302
fromTo common.FromTo
261303

262304
// filters from flags
263-
include map[string]int
264-
exclude map[string]int
265-
recursive bool
266-
followSymlinks bool
267-
withSnapshots bool
268-
forceWrite bool
305+
listOfFilesToCopy []string
306+
include map[string]int
307+
exclude map[string]int
308+
recursive bool
309+
followSymlinks bool
310+
withSnapshots bool
311+
forceWrite bool
269312

270313
// options from flags
271314
blockSize uint32
@@ -883,6 +926,8 @@ Copy an entire account with SAS:
883926
cpCmd.PersistentFlags().BoolVar(&raw.withSnapshots, "with-snapshots", false, "include the snapshots. Only valid when the source is blobs.")
884927
cpCmd.PersistentFlags().StringVar(&raw.include, "include", "", "only include these files when copying. "+
885928
"Support use of *. Files should be separated with ';'.")
929+
// This flag is implemented only for Storage Explorer.
930+
cpCmd.PersistentFlags().StringVar(&raw.listOfFilesToCopy, "list-of-files", "", "defines the location of json which has the list of only files to be copied")
886931
cpCmd.PersistentFlags().StringVar(&raw.exclude, "exclude", "", "exclude these files when copying. Support use of *.")
887932
cpCmd.PersistentFlags().BoolVar(&raw.forceWrite, "overwrite", true, "overwrite the conflicting files/blobs at the destination if this flag is set to true.")
888933
cpCmd.PersistentFlags().BoolVar(&raw.recursive, "recursive", false, "look into sub-directories recursively when uploading from local file system.")
@@ -909,6 +954,8 @@ Copy an entire account with SAS:
909954
cpCmd.PersistentFlags().MarkHidden("acl")
910955

911956
// permanently hidden
957+
// Hide the list-of-files flag since it is implemented only for Storage Explorer.
958+
cpCmd.PersistentFlags().MarkHidden("list-of-files")
912959
cpCmd.PersistentFlags().MarkHidden("include")
913960
cpCmd.PersistentFlags().MarkHidden("output")
914961
cpCmd.PersistentFlags().MarkHidden("stdIn-enable")

cmd/copyDownloadBlobEnumerator.go

+110-1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,115 @@ func (e *copyDownloadBlobEnumerator) enumerate(cca *cookedCopyCmdArgs) error {
112112
}
113113
}
114114

115+
// If the user has provided us with a list of files to be copied explicitly
116+
// then there is no need list using the source and then perform pattern matching.
117+
if len(cca.listOfFilesToCopy) > 0 {
118+
for _, blob := range cca.listOfFilesToCopy {
119+
// copy the blobParts in the temporary blobPart since for each blob mentioned in the listOfFilesToCopy flag
120+
// blobParts will be modified.
121+
tempBlobUrlParts := blobUrlParts
122+
if len(parentSourcePath) > 0 && parentSourcePath[len(parentSourcePath)-1] == common.AZCOPY_PATH_SEPARATOR_CHAR {
123+
parentSourcePath = parentSourcePath[0 : len(parentSourcePath)-1]
124+
}
125+
// Create the blobPath using the given source and blobs mentioned with listOfFile flag.
126+
// For Example:
127+
// 1. source = "https://sdksampleperftest.blob.core.windows.net/bigdata" blob = "file1.txt" blobPath= "file1.txt"
128+
// 2. source = "https://sdksampleperftest.blob.core.windows.net/bigdata/dir-1" blob = "file1.txt" blobPath= "dir-1/file1.txt"
129+
blobPath := fmt.Sprintf("%s%s%s", parentSourcePath, common.AZCOPY_PATH_SEPARATOR_STRING, blob)
130+
if len(blobPath) > 0 && blobPath[0] == common.AZCOPY_PATH_SEPARATOR_CHAR {
131+
blobPath = blobPath[1:]
132+
}
133+
tempBlobUrlParts.BlobName = blobPath
134+
blobURL := azblob.NewBlobURL(tempBlobUrlParts.URL(), p)
135+
blobProperties, err := blobURL.GetProperties(ctx, azblob.BlobAccessConditions{})
136+
if err == nil {
137+
// If the blob represents a folder as per the conditions mentioned in the
138+
// api doesBlobRepresentAFolder, then skip the blob.
139+
if util.doesBlobRepresentAFolder(blobProperties.NewMetadata()) {
140+
continue
141+
}
142+
//blobRelativePath := util.getRelativePath(parentSourcePath, blobPath)
143+
//blobRelativePath := util.getRelativePath(parentSourcePath, blobPath)
144+
blobRelativePath := strings.Replace(blobPath, parentSourcePath, "", 1)
145+
if len(blobRelativePath) > 0 && blobRelativePath[0] == common.AZCOPY_PATH_SEPARATOR_CHAR {
146+
blobRelativePath = blobRelativePath[1:]
147+
}
148+
// check for the special character in blob relative path and get path without special character.
149+
blobRelativePath = util.blobPathWOSpecialCharacters(blobRelativePath)
150+
151+
e.addTransfer(common.CopyTransfer{
152+
Source: util.stripSASFromBlobUrl(util.createBlobUrlFromContainer(blobUrlParts, blobPath)).String(),
153+
Destination: util.generateLocalPath(cca.destination, blobRelativePath),
154+
LastModifiedTime: blobProperties.LastModified(),
155+
SourceSize: blobProperties.ContentLength()}, cca)
156+
continue
157+
}
158+
if !cca.recursive {
159+
glcm.Info(fmt.Sprintf("error fetching properties of %s. Either it is a directory or getting the blob properties failed. For virtual directories try using the recursive flag", blobPath))
160+
continue
161+
}
162+
// Since the given blob in the listOFFiles flag is not a blob, it can be a virtual directory
163+
// If the virtual directory doesn't have a path separator at the end of it, then we should append it.
164+
// This is done to avoid listing blobs which shares the common prefix i.e the virtual directory name.
165+
// For Example:
166+
// 1. source = "https://sdksampleperftest.blob.core.windows.net/bigdata" blob="100k". In this case, it is
167+
// a possibility that we have blobs https://sdksampleperftest.blob.core.windows.net/bigdata/100K and
168+
// https://sdksampleperftest.blob.core.windows.net/bigdata/100K/f1.txt. So we need to list the blob
169+
// https://sdksampleperftest.blob.core.windows.net/bigdata/100K/f1.txt
170+
searchPrefix := tempBlobUrlParts.BlobName
171+
if len(searchPrefix) > 0 && searchPrefix[len(searchPrefix)-1] != common.AZCOPY_PATH_SEPARATOR_CHAR {
172+
searchPrefix += common.AZCOPY_PATH_SEPARATOR_STRING
173+
}
174+
for marker := (azblob.Marker{}); marker.NotDone(); {
175+
// look for all blobs that start with the prefix, so that if a blob is under the virtual directory, it will show up
176+
listBlob, err := containerUrl.ListBlobsFlatSegment(ctx, marker,
177+
azblob.ListBlobsSegmentOptions{Details: azblob.BlobListingDetails{Metadata: true}, Prefix: searchPrefix})
178+
if err != nil {
179+
glcm.Info(fmt.Sprintf("cannot list blobs inside directory %s mentioned.", searchPrefix))
180+
continue
181+
}
182+
// If there was no blob listed inside the directory mentioned in the listOfFilesToCopy flag,
183+
// report to the user and continue to the next blob mentioned.
184+
if !listBlob.NextMarker.NotDone() && len(listBlob.Segment.BlobItems) == 0 {
185+
glcm.Info(fmt.Sprintf("cannot list blobs inside directory %s mentioned.", searchPrefix))
186+
break
187+
}
188+
for _, blobInfo := range listBlob.Segment.BlobItems {
189+
// If the blob represents a folder as per the conditions mentioned in the
190+
// api doesBlobRepresentAFolder, then skip the blob.
191+
if util.doesBlobRepresentAFolder(blobInfo.Metadata) {
192+
continue
193+
}
194+
blobRelativePath := strings.Replace(blobInfo.Name, parentSourcePath, "", 1)
195+
if len(blobRelativePath) > 0 && blobRelativePath[0] == common.AZCOPY_PATH_SEPARATOR_CHAR {
196+
blobRelativePath = blobRelativePath[1:]
197+
}
198+
//blobRelativePath := util.getRelativePath(parentSourcePath, blobInfo.Name)
199+
200+
// check for the special character in blob relative path and get path without special character.
201+
blobRelativePath = util.blobPathWOSpecialCharacters(blobRelativePath)
202+
e.addTransfer(common.CopyTransfer{
203+
Source: util.stripSASFromBlobUrl(util.createBlobUrlFromContainer(blobUrlParts, blobInfo.Name)).String(),
204+
Destination: util.generateLocalPath(cca.destination, blobRelativePath),
205+
LastModifiedTime: blobInfo.Properties.LastModified,
206+
SourceSize: *blobInfo.Properties.ContentLength}, cca)
207+
}
208+
marker = listBlob.NextMarker
209+
}
210+
}
211+
// If there are no transfer to queue up, exit with message
212+
if len(e.Transfers) == 0 {
213+
glcm.Exit(fmt.Sprintf("no transfer queued for copying data from %s to %s", cca.source, cca.destination), 1)
214+
return nil
215+
}
216+
// dispatch the JobPart as Final Part of the Job
217+
err = e.dispatchFinalPart(cca)
218+
if err != nil {
219+
return err
220+
}
221+
return nil
222+
}
223+
115224
// searchPrefix is the used in listing blob inside a container
116225
// all the blob listed should have the searchPrefix as the prefix
117226
// blobNamePattern represents the regular expression which the blobName should Match
@@ -142,7 +251,7 @@ func (e *copyDownloadBlobEnumerator) enumerate(cca *cookedCopyCmdArgs) error {
142251
for _, blobInfo := range listBlob.Segment.BlobItems {
143252
// If the blob represents a folder as per the conditions mentioned in the
144253
// api doesBlobRepresentAFolder, then skip the blob.
145-
if util.doesBlobRepresentAFolder(blobInfo) {
254+
if util.doesBlobRepresentAFolder(blobInfo.Metadata) {
146255
continue
147256
}
148257
// If the blobName doesn't matches the blob name pattern, then blob is not included

cmd/copyEnumeratorHelper.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func enumerateBlobsInContainer(ctx context.Context, containerURL azblob.Containe
8484
for _, blobItem := range listContainerResp.Segment.BlobItems {
8585
// If the blob represents a folder as per the conditions mentioned in the
8686
// api doesBlobRepresentAFolder, then skip the blob.
87-
if gCopyUtil.doesBlobRepresentAFolder(blobItem) {
87+
if gCopyUtil.doesBlobRepresentAFolder(blobItem.Metadata) {
8888
continue
8989
}
9090

cmd/copyUploadEnumerator.go

+79-3
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"github.com/Azure/azure-storage-azcopy/common"
78
"net/url"
89
"os"
910
"path/filepath"
10-
1111
"strings"
12-
13-
"github.com/Azure/azure-storage-azcopy/common"
1412
)
1513

1614
type copyUploadEnumerator common.CopyJobPartOrderRequest
@@ -82,6 +80,84 @@ func (e *copyUploadEnumerator) enumerate(cca *cookedCopyCmdArgs) error {
8280
// temporarily save the path of the container
8381
cleanContainerPath := destinationURL.Path
8482

83+
// If the user has provided the listofFiles explicitly to copy, there is no
84+
// need to glob the source and match the patterns.
85+
// This feature is supported only for Storage Explorer and doesn't follow the symlinks.
86+
if len(cca.listOfFilesToCopy) > 0 {
87+
for _, file := range cca.listOfFilesToCopy {
88+
tempDestinationURl := *destinationURL
89+
parentSourcePath, _ := util.sourceRootPathWithoutWildCards(cca.source)
90+
if len(parentSourcePath) > 0 && parentSourcePath[len(parentSourcePath)-1] == common.AZCOPY_PATH_SEPARATOR_CHAR {
91+
parentSourcePath = parentSourcePath[:len(parentSourcePath)-1]
92+
}
93+
filePath := fmt.Sprintf("%s%s%s", parentSourcePath, common.AZCOPY_PATH_SEPARATOR_STRING, file)
94+
f, err := os.Stat(filePath)
95+
if err != nil {
96+
glcm.Info(fmt.Sprintf("Error getting the fileInfo for file %s. failed with error %s", filePath, err.Error()))
97+
continue
98+
}
99+
if f.Mode().IsRegular() {
100+
// If the file is a regular file, calculate the destination path and queue for transfer.
101+
tempDestinationURl.Path = util.generateObjectPath(tempDestinationURl.Path, f.Name())
102+
err = e.addTransfer(common.CopyTransfer{
103+
Source: filePath,
104+
Destination: tempDestinationURl.String(),
105+
LastModifiedTime: f.ModTime(),
106+
SourceSize: f.Size(),
107+
}, cca)
108+
109+
if err != nil {
110+
glcm.Info(fmt.Sprintf("error %s adding source %s and destination %s as a transfer", err.Error(), filePath, destinationURL))
111+
}
112+
continue
113+
}
114+
// If the last character of the filePath is a path separator, strip the path separator.
115+
if len(filePath) > 0 && filePath[len(filePath)-1] == common.AZCOPY_PATH_SEPARATOR_CHAR {
116+
filePath = filePath[:len(filePath)-1]
117+
}
118+
if f.IsDir() && cca.recursive {
119+
// If the file is a directory, walk through all the elements inside the directory and queue the elements for transfer.
120+
filepath.Walk(filePath, func(pathToFile string, info os.FileInfo, err error) error {
121+
if err != nil {
122+
glcm.Info(fmt.Sprintf("Accessing %s failed with error %s", pathToFile, err.Error()))
123+
return nil
124+
}
125+
if info.IsDir() {
126+
return nil
127+
} else if info.Mode().IsRegular() { // If the resource is file
128+
// replace the OS path separator in pathToFile string with AZCOPY_PATH_SEPARATOR
129+
// this replacement is done to handle the windows file paths where path separator "\\"
130+
pathToFile = strings.Replace(pathToFile, common.OS_PATH_SEPARATOR, common.AZCOPY_PATH_SEPARATOR_STRING, -1)
131+
132+
// replace the OS path separator in fileOrDirectoryPath string with AZCOPY_PATH_SEPARATOR
133+
// this replacement is done to handle the windows file paths where path separator "\\"
134+
filePath = strings.Replace(filePath, common.OS_PATH_SEPARATOR, common.AZCOPY_PATH_SEPARATOR_STRING, -1)
135+
136+
// upload the files
137+
// the path in the blob name started at the given fileOrDirectoryPath
138+
// example: fileOrDirectoryPath = "/dir1/dir2/dir3" pathToFile = "/dir1/dir2/dir3/file1.txt" result = "dir3/file1.txt"
139+
tempDestinationURl.Path = util.generateObjectPath(cleanContainerPath,
140+
util.getRelativePath(filePath, pathToFile))
141+
err = e.addTransfer(common.CopyTransfer{
142+
Source: pathToFile,
143+
Destination: tempDestinationURl.String(),
144+
LastModifiedTime: info.ModTime(),
145+
SourceSize: info.Size(),
146+
}, cca)
147+
if err != nil {
148+
return err
149+
}
150+
}
151+
return nil
152+
})
153+
}
154+
}
155+
if e.PartNum == 0 && len(e.Transfers) == 0 {
156+
return errors.New("nothing can be uploaded, please use --recursive to upload directories")
157+
}
158+
return e.dispatchFinalPart(cca)
159+
}
160+
85161
// Get the source path without the wildcards
86162
// This is defined since the files mentioned with exclude flag
87163
// & include flag are relative to the Source

cmd/copyUtil.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -757,12 +757,12 @@ func (util copyHandlerUtil) blobPathWOSpecialCharacters(blobPath string) string
757757

758758
// doesBlobRepresentAFolder verifies whether blob is valid or not.
759759
// Used to handle special scenarios or conditions.
760-
func (util copyHandlerUtil) doesBlobRepresentAFolder(bInfo azblob.BlobItem) bool {
760+
func (util copyHandlerUtil) doesBlobRepresentAFolder(metadata azblob.Metadata) bool {
761761
// this condition is to handle the WASB V1 directory structure.
762762
// HDFS driver creates a blob for the empty directories (let’s call it ‘myfolder’)
763763
// and names all the blobs under ‘myfolder’ as such: ‘myfolder/myblob’
764764
// The empty directory has meta-data 'hdi_isfolder = true'
765-
return bInfo.Metadata["hdi_isfolder"] == "true"
765+
return metadata["hdi_isfolder"] == "true"
766766
}
767767

768768
func startsWith(s string, t string) bool {

cmd/removeBlobEnumerator.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func (e *removeBlobEnumerator) enumerate(cca *cookedCopyCmdArgs) error {
8888
for _, blobInfo := range listBlob.Segment.BlobItems {
8989
// If the blob represents a folder as per the conditions mentioned in the
9090
// api doesBlobRepresentAFolder, then skip the blob.
91-
if util.doesBlobRepresentAFolder(blobInfo) {
91+
if util.doesBlobRepresentAFolder(blobInfo.Metadata) {
9292
continue
9393
}
9494
// If the blobName doesn't matches the blob name pattern, then blob is not included

cmd/root.go

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ func Execute(azsAppPathFolder string) {
7373
// only help commands reach this point
7474
// execute synchronously before exiting
7575
detectNewVersion()
76+
glcm.Exit("", common.EExitCode.Success())
7677
}
7778
}
7879

0 commit comments

Comments
 (0)