Skip to content

Commit 813b61c

Browse files
Merge branch 'main' into dphulkar/syscontainer
2 parents 78e4f37 + bc74a22 commit 813b61c

20 files changed

+201
-81
lines changed

cmd/copy.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -1568,7 +1568,7 @@ func (cca *CookedCopyCmdArgs) processCopyJobPartOrders() (err error) {
15681568
var azureFileSpecificOptions any
15691569
if cca.FromTo.From() == common.ELocation.File() {
15701570
azureFileSpecificOptions = &common.FileClientOptions{
1571-
AllowTrailingDot: cca.trailingDot == common.ETrailingDotOption.Enable(),
1571+
AllowTrailingDot: cca.trailingDot.IsEnabled(),
15721572
}
15731573
}
15741574

@@ -1590,8 +1590,8 @@ func (cca *CookedCopyCmdArgs) processCopyJobPartOrders() (err error) {
15901590

15911591
if cca.FromTo.To() == common.ELocation.File() {
15921592
azureFileSpecificOptions = &common.FileClientOptions{
1593-
AllowTrailingDot: cca.trailingDot == common.ETrailingDotOption.Enable(),
1594-
AllowSourceTrailingDot: cca.trailingDot == common.ETrailingDotOption.Enable() && cca.FromTo.From() == common.ELocation.File(),
1593+
AllowTrailingDot: cca.trailingDot.IsEnabled(),
1594+
AllowSourceTrailingDot: cca.trailingDot.IsEnabled() && cca.FromTo.From() == common.ELocation.File(),
15951595
}
15961596
}
15971597

@@ -2145,9 +2145,11 @@ func init() {
21452145
// so properties can be get in parallel, at same time no additional go routines are created for this specific job.
21462146
// The usage of this hidden flag is to provide fallback to traditional behavior, when service supports returning full properties during list.
21472147
cpCmd.PersistentFlags().BoolVar(&raw.s2sGetPropertiesInBackend, "s2s-get-properties-in-backend", true, "True by default. Gets S3 objects' or Azure files' properties in backend, if properties need to be accessed. Properties need to be accessed if s2s-preserve-properties is true, and in certain other cases where we need the properties for modification time checks or MD5 checks.")
2148-
cpCmd.PersistentFlags().StringVar(&raw.trailingDot, "trailing-dot", "", "'Enable' by default to treat file share related operations in a safe manner. Available options: "+strings.Join(common.ValidTrailingDotOptions(), ", ")+". "+
2149-
"Choose 'Disable' to go back to legacy (potentially unsafe) treatment of trailing dot files where the file service will trim any trailing dots in paths. This can result in potential data corruption if the transfer contains two paths that differ only by a trailing dot (ex: mypath and mypath.). If this flag is set to 'Disable' and AzCopy encounters a trailing dot file, it will warn customers in the scanning log but will not attempt to abort the operation."+
2150-
"If the destination does not support trailing dot files (Windows or Blob Storage), AzCopy will fail if the trailing dot file is the root of the transfer and skip any trailing dot paths encountered during enumeration.")
2148+
cpCmd.PersistentFlags().StringVar(&raw.trailingDot, "trailing-dot", "", "Available options: "+strings.Join(common.ValidTrailingDotOptions(), ", ")+". "+
2149+
"'Enable'(Default) treats trailing dot file operations in a safe manner between systems that support these files. On Windows, the transfers will not occur to stop risk of data corruption. See 'AllowToUnsafeDestination' to bypass this."+
2150+
"'Disable' reverts to the legacy functionality, where trailing dot files are ignored. This can result in potential data corruption if the transfer contains two paths that differ only by a trailing dot (E.g 'path/foo' and 'path/foo.'). If this flag is set to 'Disable' and AzCopy encounters a trailing dot file, it will warn customers in the scanning log but will not attempt to abort the operation."+
2151+
"If the destination does not support trailing dot files (Windows or Blob Storage), AzCopy will fail if the trailing dot file is the root of the transfer and skip any trailing dot paths encountered during enumeration."+
2152+
"'AllowToUnsafeDestination' supports transferring trailing dot files to systems that do not support them e.g Windows. Use with caution acknowledging risk of data corruption, when two files with different contents 'path/bar' and 'path/bar.' (differ only by a trailing dot) are seen as identical.")
21512153

21522154
// Public Documentation: https://docs.microsoft.com/en-us/azure/storage/blobs/encryption-customer-provided-keys
21532155
// Clients making requests against Azure Blob storage have the option to provide an encryption key on a per-request basis.

cmd/jobsResume.go

+32-6
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,28 @@ func (rca resumeCmdArgs) getSourceAndDestinationServiceClients(
295295
}
296296

297297
options := createClientOptions(common.AzcopyCurrentJobLogger, nil)
298+
jobID, err := common.ParseJobID(rca.jobID)
299+
if err != nil {
300+
// Error for invalid JobId format
301+
return nil, nil, fmt.Errorf("error parsing the jobId %s. Failed with error %s", rca.jobID, err.Error())
302+
}
303+
304+
var getJobDetailsResponse common.GetJobDetailsResponse
305+
// Get job details from the STE
306+
Rpc(common.ERpcCmd.GetJobDetails(),
307+
&common.GetJobDetailsRequest{JobID: jobID},
308+
&getJobDetailsResponse)
309+
if getJobDetailsResponse.ErrorMsg != "" {
310+
glcm.Error(getJobDetailsResponse.ErrorMsg)
311+
}
298312

299-
srcServiceClient, err := common.GetServiceClientForLocation(fromTo.From(), source, srcCredType, tc, &options, nil)
313+
var fileSrcClientOptions any
314+
if fromTo.From() == common.ELocation.File() {
315+
fileSrcClientOptions = &common.FileClientOptions{
316+
AllowTrailingDot: getJobDetailsResponse.TrailingDot.IsEnabled(), //Access the trailingDot option of the job
317+
}
318+
}
319+
srcServiceClient, err := common.GetServiceClientForLocation(fromTo.From(), source, srcCredType, tc, &options, fileSrcClientOptions)
300320
if err != nil {
301321
return nil, nil, err
302322
}
@@ -306,11 +326,17 @@ func (rca resumeCmdArgs) getSourceAndDestinationServiceClients(
306326
srcCred = common.NewScopedCredential(tc, srcCredType)
307327
}
308328
options = createClientOptions(common.AzcopyCurrentJobLogger, srcCred)
309-
dstServiceClient, err := common.GetServiceClientForLocation(fromTo.To(), destination, dstCredType, tc, &options, nil)
329+
var fileClientOptions any
330+
if fromTo.To() == common.ELocation.File() {
331+
fileClientOptions = &common.FileClientOptions{
332+
AllowSourceTrailingDot: getJobDetailsResponse.TrailingDot.IsEnabled() && fromTo.From() == common.ELocation.File(),
333+
AllowTrailingDot: getJobDetailsResponse.TrailingDot.IsEnabled(),
334+
}
335+
}
336+
dstServiceClient, err := common.GetServiceClientForLocation(fromTo.To(), destination, dstCredType, tc, &options, fileClientOptions)
310337
if err != nil {
311338
return nil, nil, err
312339
}
313-
314340
return srcServiceClient, dstServiceClient, nil
315341
}
316342

@@ -357,9 +383,9 @@ func (rca resumeCmdArgs) process() error {
357383
}
358384

359385
// Get fromTo info, so we can decide what's the proper credential type to use.
360-
var getJobFromToResponse common.GetJobFromToResponse
361-
Rpc(common.ERpcCmd.GetJobFromTo(),
362-
&common.GetJobFromToRequest{JobID: jobID},
386+
var getJobFromToResponse common.GetJobDetailsResponse
387+
Rpc(common.ERpcCmd.GetJobDetails(),
388+
&common.GetJobDetailsRequest{JobID: jobID},
363389
&getJobFromToResponse)
364390
if getJobFromToResponse.ErrorMsg != "" {
365391
glcm.Error(getJobFromToResponse.ErrorMsg)

cmd/removeEnumerator.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func newRemoveEnumerator(cca *CookedCopyCmdArgs) (enumerator *CopyEnumerator, er
9393
options := createClientOptions(common.AzcopyCurrentJobLogger, nil)
9494
var fileClientOptions any
9595
if cca.FromTo.From() == common.ELocation.File() {
96-
fileClientOptions = &common.FileClientOptions{AllowTrailingDot: cca.trailingDot == common.ETrailingDotOption.Enable()}
96+
fileClientOptions = &common.FileClientOptions{AllowTrailingDot: cca.trailingDot.IsEnabled()}
9797
}
9898
targetServiceClient, err := common.GetServiceClientForLocation(
9999
cca.FromTo.From(),

cmd/rpc.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ func inprocSend(rpcCmd common.RpcCmd, requestData interface{}, responseData inte
5959
case common.ERpcCmd.ResumeJob():
6060
*(responseData.(*common.CancelPauseResumeResponse)) = jobsAdmin.ResumeJobOrder(*requestData.(*common.ResumeJobRequest))
6161

62-
case common.ERpcCmd.GetJobFromTo():
63-
*(responseData.(*common.GetJobFromToResponse)) = jobsAdmin.GetJobFromTo(*requestData.(*common.GetJobFromToRequest))
62+
case common.ERpcCmd.GetJobDetails():
63+
*(responseData.(*common.GetJobDetailsResponse)) = jobsAdmin.GetJobDetails(*requestData.(*common.GetJobDetailsRequest))
6464

6565
default:
6666
panic(fmt.Errorf("Unrecognized RpcCmd: %q", rpcCmd.String()))

cmd/setPropertiesEnumerator.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func setPropertiesEnumerator(cca *CookedCopyCmdArgs) (enumerator *CopyEnumerator
7575
options := createClientOptions(common.AzcopyCurrentJobLogger, nil)
7676
var fileClientOptions any
7777
if cca.FromTo.From() == common.ELocation.File() {
78-
fileClientOptions = &common.FileClientOptions{AllowTrailingDot: cca.trailingDot == common.ETrailingDotOption.Enable()}
78+
fileClientOptions = &common.FileClientOptions{AllowTrailingDot: cca.trailingDot.IsEnabled()}
7979
}
8080

8181
targetServiceClient, err := common.GetServiceClientForLocation(

cmd/zc_enumerator.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ func InitResourceTraverser(resource common.ResourceString, location common.Locat
495495
fileURLParts.ShareSnapshot = ""
496496
fileURLParts.DirectoryOrFilePath = ""
497497
fileOptions := &common.FileClientOptions{
498-
AllowTrailingDot: trailingDot == common.ETrailingDotOption.Enable(),
498+
AllowTrailingDot: trailingDot.IsEnabled(),
499499
}
500500

501501
res, err := SplitResourceString(fileURLParts.String(), common.ELocation.File())

cmd/zc_traverser_file.go

+17-13
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ type fileTraverser struct {
4848

4949
// a generic function to notify that a new stored object has been enumerated
5050
incrementEnumerationCounter enumerationCounterFunc
51-
trailingDot common.TrailingDotOption
52-
destination *common.Location
51+
trailingDot common.TrailingDotOption
52+
destination *common.Location
5353
}
5454

5555
func createShareClientFromServiceClient(fileURLParts file.URLParts, client *service.Client) (*share.Client, error) {
@@ -123,6 +123,10 @@ func (t *fileTraverser) getPropertiesIfSingleFile() (*file.GetPropertiesResponse
123123
func (t *fileTraverser) Traverse(preprocessor objectMorpher, processor objectProcessor, filters []ObjectFilter) (err error) {
124124
invalidBlobOrWindowsName := func(path string) bool {
125125
if t.destination != nil {
126+
if t.trailingDot == common.ETrailingDotOption.AllowToUnsafeDestination() && (*t.destination != common.ELocation.Blob() || *t.destination != common.ELocation.BlobFS()) { // Allow only Local, Trailing dot files not supported in Blob
127+
return false // Please let me shoot myself in the foot!
128+
}
129+
126130
if (t.destination.IsLocal() && runtime.GOOS == "windows") || *t.destination == common.ELocation.Blob() || *t.destination == common.ELocation.BlobFS() {
127131
/* Blob or Windows object name is invalid if it ends with period or
128132
one of (virtual) directories in path ends with period.
@@ -145,7 +149,7 @@ func (t *fileTraverser) Traverse(preprocessor objectMorpher, processor objectPro
145149
WarnStdoutAndScanningLog(fmt.Sprintf(invalidNameErrorMsg, targetURLParts.DirectoryOrFilePath))
146150
return common.EAzError.InvalidBlobOrWindowsName()
147151
}
148-
if t.trailingDot != common.ETrailingDotOption.Enable() && strings.HasSuffix(targetURLParts.DirectoryOrFilePath, ".") {
152+
if !t.trailingDot.IsEnabled() && strings.HasSuffix(targetURLParts.DirectoryOrFilePath, ".") {
149153
azcopyScanningLogger.Log(common.LogWarning, fmt.Sprintf(trailingDotErrMsg, getObjectNameOnly(targetURLParts.DirectoryOrFilePath)))
150154
}
151155
// check if the url points to a single file
@@ -288,7 +292,7 @@ func (t *fileTraverser) Traverse(preprocessor objectMorpher, processor objectPro
288292
WarnStdoutAndScanningLog(fmt.Sprintf(invalidNameErrorMsg, *fileInfo.Name))
289293
continue
290294
} else {
291-
if t.trailingDot != common.ETrailingDotOption.Enable() && strings.HasSuffix(*fileInfo.Name, ".") {
295+
if !t.trailingDot.IsEnabled() && strings.HasSuffix(*fileInfo.Name, ".") {
292296
azcopyScanningLogger.Log(common.LogWarning, fmt.Sprintf(trailingDotErrMsg, *fileInfo.Name))
293297
}
294298
}
@@ -301,13 +305,13 @@ func (t *fileTraverser) Traverse(preprocessor objectMorpher, processor objectPro
301305
WarnStdoutAndScanningLog(fmt.Sprintf(invalidNameErrorMsg, *dirInfo.Name))
302306
continue
303307
} else {
304-
if t.trailingDot != common.ETrailingDotOption.Enable() && strings.HasSuffix(*dirInfo.Name, ".") {
308+
if !t.trailingDot.IsEnabled() && strings.HasSuffix(*dirInfo.Name, ".") {
305309
azcopyScanningLogger.Log(common.LogWarning, fmt.Sprintf(trailingDotErrMsg, *dirInfo.Name))
306310
}
307311
}
308312
enqueueOutput(newAzFileSubdirectoryEntity(currentDirectoryClient, *dirInfo.Name), nil)
309313
if t.recursive {
310-
// If recursive is turned on, add sub directories to be processed
314+
// If recursive is turned on, add sub directories to be processed
311315
enqueueDir(currentDirectoryClient.NewSubdirectoryClient(*dirInfo.Name))
312316
}
313317

@@ -381,14 +385,14 @@ func (t *fileTraverser) Traverse(preprocessor objectMorpher, processor objectPro
381385

382386
func newFileTraverser(rawURL string, serviceClient *service.Client, ctx context.Context, recursive, getProperties bool, incrementEnumerationCounter enumerationCounterFunc, trailingDot common.TrailingDotOption, destination *common.Location) (t *fileTraverser) {
383387
t = &fileTraverser{
384-
rawURL: rawURL,
385-
serviceClient: serviceClient,
386-
ctx: ctx,
387-
recursive: recursive,
388-
getProperties: getProperties,
388+
rawURL: rawURL,
389+
serviceClient: serviceClient,
390+
ctx: ctx,
391+
recursive: recursive,
392+
getProperties: getProperties,
389393
incrementEnumerationCounter: incrementEnumerationCounter,
390-
trailingDot: trailingDot,
391-
destination: destination,
394+
trailingDot: trailingDot,
395+
destination: destination,
392396
}
393397
return
394398
}

cmd/zt_interceptors_for_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func (i *interceptor) intercept(cmd common.RpcCmd, request interface{}, response
5353
case common.ERpcCmd.PauseJob():
5454
case common.ERpcCmd.CancelJob():
5555
case common.ERpcCmd.ResumeJob():
56-
case common.ERpcCmd.GetJobFromTo():
56+
case common.ERpcCmd.GetJobDetails():
5757
fallthrough
5858
default:
5959
panic("RPC mock not implemented")

common/fe-ste-models.go

+10-2
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,15 @@ var ETrailingDotOption = TrailingDotOption(0)
153153

154154
type TrailingDotOption uint8
155155

156-
func (TrailingDotOption) Enable() TrailingDotOption { return TrailingDotOption(0) }
157-
func (TrailingDotOption) Disable() TrailingDotOption { return TrailingDotOption(1) }
156+
func (TrailingDotOption) Enable() TrailingDotOption { return TrailingDotOption(0) }
157+
func (TrailingDotOption) Disable() TrailingDotOption { return TrailingDotOption(1) }
158+
func (TrailingDotOption) AllowToUnsafeDestination() TrailingDotOption { return TrailingDotOption(2) }
159+
160+
// Trailing dots are supported in the Enable and AllowToUnsafeDestination options
161+
func (d TrailingDotOption) IsEnabled() bool {
162+
return d == d.Enable() ||
163+
d == d.AllowToUnsafeDestination()
164+
}
158165

159166
func (d TrailingDotOption) String() string {
160167
return enum.StringInt(d, reflect.TypeOf(d))
@@ -178,6 +185,7 @@ func ValidTrailingDotOptions() []string {
178185
return []string{
179186
ETrailingDotOption.Enable().String(),
180187
ETrailingDotOption.Disable().String(),
188+
ETrailingDotOption.AllowToUnsafeDestination().String(),
181189
}
182190
}
183191

common/rpc-models.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func (RpcCmd) ListJobTransfers() RpcCmd { return RpcCmd("ListJobTransfers") }
2727
func (RpcCmd) CancelJob() RpcCmd { return RpcCmd("Cancel") }
2828
func (RpcCmd) PauseJob() RpcCmd { return RpcCmd("PauseJob") }
2929
func (RpcCmd) ResumeJob() RpcCmd { return RpcCmd("ResumeJob") }
30-
func (RpcCmd) GetJobFromTo() RpcCmd { return RpcCmd("GetJobFromTo") }
30+
func (RpcCmd) GetJobDetails() RpcCmd { return RpcCmd("GetJobDetails") }
3131

3232
func (c RpcCmd) String() string {
3333
return enum.String(c, reflect.TypeOf(c))
@@ -375,15 +375,16 @@ type ListJobTransfersResponse struct {
375375
Details []TransferDetail
376376
}
377377

378-
// GetJobFromToRequest indicates request to get job's FromTo info from job part plan header
379-
type GetJobFromToRequest struct {
378+
// GetJobDetailsRequest indicates request to get job's FromTo and TrailingDot info from job part plan header
379+
type GetJobDetailsRequest struct {
380380
JobID JobID
381381
}
382382

383-
// GetJobFromToResponse indicates response to get job's FromTo info.
384-
type GetJobFromToResponse struct {
383+
// GetJobDetailsResponse indicates response to get job's FromTo and TrailingDot info.
384+
type GetJobDetailsResponse struct {
385385
ErrorMsg string
386386
FromTo FromTo
387387
Source string
388388
Destination string
389+
TrailingDot TrailingDotOption
389390
}

e2etest/newe2e_resource_managers_local.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -320,11 +320,11 @@ func (l *LocalObjectResourceManager) ListChildren(a Asserter, recursive bool) ma
320320
func (l *LocalObjectResourceManager) GetProperties(a Asserter) ObjectProperties {
321321
a.HelperMarker().Helper()
322322
stats, err := os.Stat(l.getWorkingPath())
323-
a.NoError("failed to get stat", err)
324-
var lmt *time.Time
325-
if stats != nil {
326-
lmt = PtrOf(stats.ModTime())
323+
if err != nil { // Prevent nil dereferences
324+
a.NoError("failed to get stat", err)
325+
return ObjectProperties{}
327326
}
327+
lmt := common.Iff(stats == nil, nil, PtrOf(stats.ModTime()))
328328
out := ObjectProperties{
329329
LastModifiedTime: lmt,
330330
}

e2etest/newe2e_task_runazcopy_parameters.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ type GlobalFlags struct {
191191
OutputLevel *common.OutputVerbosity `flag:"output-level,default:DEFAULT"`
192192

193193
// TODO: reconsider/reengineer this flag; WI#26475473
194-
//DebugSkipFiles []string `flag:"debug-skip-files"`
194+
// DebugSkipFiles []string `flag:"debug-skip-files"`
195195

196196
// TODO: handle prompting and input; WI#26475441
197197
//CancelFromStdin *bool `flag:"cancel-from-stdin"`

e2etest/zt_newe2e_basic_functionality_test.go

+52
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package e2etest
22

33
import (
4+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
45
blobsas "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/sas"
56
"github.com/Azure/azure-storage-azcopy/v10/common"
67
"strconv"
@@ -412,6 +413,57 @@ func (s *BasicFunctionalitySuite) Scenario_Copy_EmptySASErrorCodes(svm *Scenario
412413
ValidateContainsError(svm, stdout, []string{"https://aka.ms/AzCopyError/NoAuthenticationInformation", "https://aka.ms/AzCopyError/ResourceNotFound"})
413414
}
414415

416+
// Test of Copy to UnsafeDestinations (Windows Local dest)
417+
func (s *BasicFunctionalitySuite) Scenario_CopyUnSafeDest(svm *ScenarioVariationManager) {
418+
azCopyVerb := AzCopyVerbCopy
419+
420+
fileName := "file."
421+
body := NewRandomObjectContentContainer(SizeFromString("10K"))
422+
423+
// Scale up from service to object
424+
srcObj := CreateResource[ObjectResourceManager](svm,
425+
GetRootResource(svm, common.ELocation.File()), // Only source is File - Windows Local & Blob doesn't support T.D
426+
ResourceDefinitionObject{
427+
ObjectName: pointerTo(fileName),
428+
Body: body,
429+
})
430+
431+
dstObj := CreateResource[ContainerResourceManager](svm,
432+
GetRootResource(svm,
433+
ResolveVariation(svm, []common.Location{common.ELocation.File(),
434+
common.ELocation.Local()})),
435+
ResourceDefinitionContainer{}).GetObject(svm, "file", common.EEntityType.File())
436+
437+
sasOpts := GenericAccountSignatureValues{}
438+
439+
RunAzCopy(
440+
svm,
441+
AzCopyCommand{
442+
Verb: azCopyVerb,
443+
Targets: []ResourceManager{
444+
TryApplySpecificAuthType(srcObj, EExplicitCredentialType.SASToken(), svm,
445+
CreateAzCopyTargetOptions{
446+
SASTokenOptions: sasOpts,
447+
}),
448+
TryApplySpecificAuthType(dstObj, EExplicitCredentialType.SASToken(), svm,
449+
CreateAzCopyTargetOptions{
450+
SASTokenOptions: sasOpts,
451+
}),
452+
},
453+
Flags: CopyFlags{
454+
CopySyncCommonFlags: CopySyncCommonFlags{
455+
Recursive: pointerTo(true),
456+
TrailingDot: to.Ptr(common.ETrailingDotOption.AllowToUnsafeDestination()), // Allow download to unsafe Local destination
457+
},
458+
},
459+
})
460+
461+
ValidateResource[ObjectResourceManager](svm, dstObj,
462+
ResourceDefinitionObject{
463+
Body: body,
464+
}, false)
465+
}
466+
415467
func (s *BasicFunctionalitySuite) Scenario_TagsPermission(svm *ScenarioVariationManager) {
416468
objectType := ResolveVariation(svm, []common.EntityType{common.EEntityType.File(), common.EEntityType.Folder(), common.EEntityType.Symlink()})
417469
srcLoc := ResolveVariation(svm, []common.Location{common.ELocation.Local(), common.ELocation.Blob()})

0 commit comments

Comments
 (0)