From d39038cd8ac73200158886211c4193049a423302 Mon Sep 17 00:00:00 2001 From: "Vincent Jiang(Azure Dev)" Date: Wed, 16 Sep 2015 12:50:57 +0800 Subject: [PATCH] Microsoft Azure Storage Data Movement Library v0.1.0 --- .gitattributes | 63 + .nuget/NuGet.Config | 6 + .nuget/NuGet.exe | Bin 0 -> 1664512 bytes .nuget/NuGet.targets | 144 + DataMovement.sln | 82 + LICENSE | 222 +- README.md | 154 + changelog.txt | 2 + lib/AssemblyInfo.cs | 15 + lib/Constants.cs | 140 + lib/DataMovement.csproj | 170 + lib/Exceptions/TransferErrorCode.cs | 104 + lib/Exceptions/TransferException.cs | 152 + lib/Extensions/CloudBlobExtensions.cs | 158 + lib/Extensions/CloudFileExtensions.cs | 100 + lib/Extensions/StorageExtensions.cs | 189 + lib/GlobalMemoryStatusNativeMethods.cs | 52 + lib/MD5HashStream.cs | 462 +++ lib/MemoryManager.cs | 139 + lib/OverwriteCallback.cs | 17 + lib/Resources.Designer.cs | 569 +++ lib/Resources.resx | 322 ++ .../SerializableAccessCondition.cs | 161 + .../SerializableBlobRequestOptions.cs | 105 + .../SerializableCloudBlob.cs | 134 + .../SerializableCloudFile.cs | 127 + .../SerializableFileRequestOptions.cs | 111 + .../SerializableRequestOptions.cs | 111 + lib/TransferCheckpoint.cs | 139 + lib/TransferConfigurations.cs | 156 + lib/TransferContext.cs | 124 + .../AsyncCopyController.cs | 678 +++ .../BlobAsyncCopyController.cs | 173 + .../FileAsyncCopyController.cs | 125 + .../ITransferController.cs | 27 + .../SyncTransferController.cs | 201 + .../TransferControllerBase.cs | 336 ++ .../TransferReaderWriterBase.cs | 111 + .../TransferReaders/BlockBasedBlobReader.cs | 357 ++ .../TransferReaders/CloudFileReader.cs | 100 + .../TransferReaders/PageBlobReader.cs | 113 + .../TransferReaders/RangeBasedReader.cs | 890 ++++ .../TransferReaders/StreamedReader.cs | 426 ++ .../TransferWriters/AppendBlobWriter.cs | 424 ++ .../TransferWriters/BlockBlobWriter.cs | 377 ++ .../TransferWriters/CloudFileWriter.cs | 141 + .../TransferWriters/PageBlobWriter.cs | 169 + .../TransferWriters/RangeBasedWriter.cs | 412 ++ .../TransferWriters/StreamedWriter.cs | 356 ++ lib/TransferJobs/SingleObjectCheckpoint.cs | 81 + lib/TransferJobs/SingleObjectTransfer.cs | 160 + lib/TransferJobs/Transfer.cs | 199 + lib/TransferJobs/TransferJob.cs | 178 + lib/TransferJobs/TransferJobStatus.cs | 46 + lib/TransferJobs/TransferLocation.cs | 458 ++ lib/TransferJobs/TransferLocationType.cs | 17 + lib/TransferJobs/TransferMethod.cs | 22 + lib/TransferManager.cs | 845 ++++ lib/TransferOptions/CopyOptions.cs | 23 + lib/TransferOptions/DownloadOptions.cs | 25 + lib/TransferOptions/UploadOptions.cs | 23 + lib/TransferProgress.cs | 49 + lib/TransferScheduler.cs | 424 ++ lib/TransferStatusHelpers/Attributes.cs | 58 + lib/TransferStatusHelpers/ReadDataState.cs | 61 + .../SharedTransferData.cs | 45 + lib/TransferStatusHelpers/TransferData.cs | 44 + .../TransferDataState.cs | 69 + .../TransferDownloadBuffer.cs | 67 + .../TransferDownloadStream.cs | 262 ++ .../TransferProgressTracker.cs | 277 ++ lib/Transfer_RequestOptions.cs | 305 ++ lib/Utils.cs | 393 ++ lib/packages.config | 10 + .../DataMovementSamples.sln | 22 + .../DataMovementSamples/App.config | 7 + .../DataMovementSamples.csproj | 97 + .../Properties/AssemblyInfo.cs | 42 + .../DataMovementSamples/Samples.cs | 258 ++ .../DataMovementSamples/Util.cs | 131 + .../DataMovementSamples/azure.png | Bin 0 -> 6534 bytes .../DataMovementSamples/packages.config | 11 + test/DMLibTest/Cases/AccessConditionTest.cs | 132 + .../Cases/AllTransferDirectionTest.cs | 418 ++ test/DMLibTest/Cases/BVT.cs | 179 + test/DMLibTest/Cases/BigFileTest.cs | 57 + test/DMLibTest/Cases/CheckContentMD5Test.cs | 93 + test/DMLibTest/Cases/MetadataTest.cs | 71 + test/DMLibTest/Cases/OverwriteTest.cs | 115 + test/DMLibTest/Cases/ProgressHandlerTest.cs | 66 + test/DMLibTest/Cases/ResumeTest.cs | 146 + test/DMLibTest/Cases/SetContentTypeTest.cs | 68 + .../Cases/UnsupportedDirectionTest.cs | 91 + test/DMLibTest/DMLibTest.csproj | 209 + .../Framework/AssemblyInitCleanup.cs | 31 + .../Framework/BlobDataAdaptorBase.cs | 174 + .../Framework/CloudBlobDataAdaptor.cs | 243 ++ .../Framework/CloudFileDataAdaptor.cs | 352 ++ .../Framework/CloudObjectExtensions.cs | 79 + test/DMLibTest/Framework/CopyWrapper.cs | 43 + test/DMLibTest/Framework/DMLibDataHelper.cs | 472 +++ test/DMLibTest/Framework/DMLibDataInfo.cs | 441 ++ test/DMLibTest/Framework/DMLibInputHelper.cs | 31 + test/DMLibTest/Framework/DMLibTestBase.cs | 357 ++ test/DMLibTest/Framework/DMLibWrapper.cs | 21 + test/DMLibTest/Framework/DataAdaptor.cs | 65 + test/DMLibTest/Framework/DownloadWrapper.cs | 68 + test/DMLibTest/Framework/IDataInfo.cs | 14 + test/DMLibTest/Framework/LocalDataAdaptor.cs | 163 + .../Framework/LocalDataAdaptorBase.cs | 93 + .../Framework/MultiDirectionTestBase.cs | 297 ++ .../Framework/MultiDirectionTestHelper.cs | 107 + .../Framework/MultiDirectionTestInfo.cs | 37 + test/DMLibTest/Framework/ProgressChecker.cs | 101 + .../Framework/SharedAccessPermissions.cs | 53 + .../Framework/TestExecutionOptions.cs | 52 + test/DMLibTest/Framework/TestResult.cs | 41 + test/DMLibTest/Framework/TransferItem.cs | 168 + .../DMLibTest/Framework/URIBlobDataAdaptor.cs | 55 + test/DMLibTest/Framework/UploadWrapper.cs | 69 + .../DMLibTest/Framework/VerificationHelper.cs | 67 + test/DMLibTest/Properties/AssemblyInfo.cs | 14 + test/DMLibTest/TestData.xml | 22 + test/DMLibTest/Util/DMLibTestConstants.cs | 111 + test/DMLibTest/Util/DMLibTestHelper.cs | 411 ++ test/DMLibTest/Util/Helpers.cs | 3668 +++++++++++++++++ test/DMLibTest/Util/TestAccounts.cs | 163 + test/DMLibTest/packages.config | 10 + test/DMLibTestCodeGen/App.config | 6 + test/DMLibTestCodeGen/DMLibDataType.cs | 52 + test/DMLibTestCodeGen/DMLibDirectionFilter.cs | 85 + test/DMLibTestCodeGen/DMLibTestCodeGen.csproj | 79 + test/DMLibTestCodeGen/DMLibTestContext.cs | 16 + test/DMLibTestCodeGen/DMLibTestMethodSet.cs | 163 + .../DMLibTestMetholdAttribute.cs | 89 + .../DMLibTransferDirection.cs | 113 + test/DMLibTestCodeGen/DirectionFilter.cs | 72 + test/DMLibTestCodeGen/ITestDirection.cs | 20 + test/DMLibTestCodeGen/MultiDirectionTag.cs | 13 + .../MultiDirectionTestClass.cs | 104 + .../MultiDirectionTestClassAttribute.cs | 17 + .../MultiDirectionTestContext.cs | 22 + .../MultiDirectionTestMethod.cs | 56 + .../MultiDirectionTestMethodAttribute.cs | 26 + .../MultiDirectionTestMethodSetAttribute.cs | 60 + test/DMLibTestCodeGen/Program.cs | 48 + .../Properties/AssemblyInfo.cs | 14 + test/DMLibTestCodeGen/SourceCodeGenerator.cs | 263 ++ test/DMLibTestCodeGen/TestMethodDirection.cs | 50 + test/MsTestLib/ClassConfig.cs | 55 + test/MsTestLib/ConsoleLogger.cs | 163 + test/MsTestLib/Exceptions.cs | 30 + test/MsTestLib/FileLogger.cs | 192 + test/MsTestLib/ILogger.cs | 52 + test/MsTestLib/MessageBuilder.cs | 85 + test/MsTestLib/MethodConfig.cs | 44 + test/MsTestLib/MsTestLib.csproj | 73 + test/MsTestLib/Test.cs | 139 + test/MsTestLib/TestConfig.cs | 181 + test/MsTestLib/TestHelper.cs | 258 ++ test/MsTestLib/TestLogger.cs | 148 + tools/AssemblyInfo/SharedAssemblyInfo.cs | 32 + tools/analysis/fxcop/azure-storage-dm.ruleset | 363 ++ tools/apidoc/.gitignore | 1 + tools/apidoc/dmlib.shfbproj | 62 + ...icrosoft.Azure.Storage.DataMovement.nuspec | 38 + tools/nupkg/buildNupkg.cmd | 10 + tools/scripts/InjectBuildNumber.ps1 | 30 + tools/strongnamekeys/fake/windows.snk | Bin 0 -> 160 bytes 169 files changed, 27416 insertions(+), 201 deletions(-) create mode 100644 .gitattributes create mode 100644 .nuget/NuGet.Config create mode 100644 .nuget/NuGet.exe create mode 100644 .nuget/NuGet.targets create mode 100644 DataMovement.sln create mode 100644 README.md create mode 100644 changelog.txt create mode 100644 lib/AssemblyInfo.cs create mode 100644 lib/Constants.cs create mode 100644 lib/DataMovement.csproj create mode 100644 lib/Exceptions/TransferErrorCode.cs create mode 100644 lib/Exceptions/TransferException.cs create mode 100644 lib/Extensions/CloudBlobExtensions.cs create mode 100644 lib/Extensions/CloudFileExtensions.cs create mode 100644 lib/Extensions/StorageExtensions.cs create mode 100644 lib/GlobalMemoryStatusNativeMethods.cs create mode 100644 lib/MD5HashStream.cs create mode 100644 lib/MemoryManager.cs create mode 100644 lib/OverwriteCallback.cs create mode 100644 lib/Resources.Designer.cs create mode 100644 lib/Resources.resx create mode 100644 lib/SerializationHelper/SerializableAccessCondition.cs create mode 100644 lib/SerializationHelper/SerializableBlobRequestOptions.cs create mode 100644 lib/SerializationHelper/SerializableCloudBlob.cs create mode 100644 lib/SerializationHelper/SerializableCloudFile.cs create mode 100644 lib/SerializationHelper/SerializableFileRequestOptions.cs create mode 100644 lib/SerializationHelper/SerializableRequestOptions.cs create mode 100644 lib/TransferCheckpoint.cs create mode 100644 lib/TransferConfigurations.cs create mode 100644 lib/TransferContext.cs create mode 100644 lib/TransferControllers/AsyncCopyControllers/AsyncCopyController.cs create mode 100644 lib/TransferControllers/AsyncCopyControllers/BlobAsyncCopyController.cs create mode 100644 lib/TransferControllers/AsyncCopyControllers/FileAsyncCopyController.cs create mode 100644 lib/TransferControllers/ITransferController.cs create mode 100644 lib/TransferControllers/SyncTransferController.cs create mode 100644 lib/TransferControllers/TransferControllerBase.cs create mode 100644 lib/TransferControllers/TransferReaderWriterBase.cs create mode 100644 lib/TransferControllers/TransferReaders/BlockBasedBlobReader.cs create mode 100644 lib/TransferControllers/TransferReaders/CloudFileReader.cs create mode 100644 lib/TransferControllers/TransferReaders/PageBlobReader.cs create mode 100644 lib/TransferControllers/TransferReaders/RangeBasedReader.cs create mode 100644 lib/TransferControllers/TransferReaders/StreamedReader.cs create mode 100644 lib/TransferControllers/TransferWriters/AppendBlobWriter.cs create mode 100644 lib/TransferControllers/TransferWriters/BlockBlobWriter.cs create mode 100644 lib/TransferControllers/TransferWriters/CloudFileWriter.cs create mode 100644 lib/TransferControllers/TransferWriters/PageBlobWriter.cs create mode 100644 lib/TransferControllers/TransferWriters/RangeBasedWriter.cs create mode 100644 lib/TransferControllers/TransferWriters/StreamedWriter.cs create mode 100644 lib/TransferJobs/SingleObjectCheckpoint.cs create mode 100644 lib/TransferJobs/SingleObjectTransfer.cs create mode 100644 lib/TransferJobs/Transfer.cs create mode 100644 lib/TransferJobs/TransferJob.cs create mode 100644 lib/TransferJobs/TransferJobStatus.cs create mode 100644 lib/TransferJobs/TransferLocation.cs create mode 100644 lib/TransferJobs/TransferLocationType.cs create mode 100644 lib/TransferJobs/TransferMethod.cs create mode 100644 lib/TransferManager.cs create mode 100644 lib/TransferOptions/CopyOptions.cs create mode 100644 lib/TransferOptions/DownloadOptions.cs create mode 100644 lib/TransferOptions/UploadOptions.cs create mode 100644 lib/TransferProgress.cs create mode 100644 lib/TransferScheduler.cs create mode 100644 lib/TransferStatusHelpers/Attributes.cs create mode 100644 lib/TransferStatusHelpers/ReadDataState.cs create mode 100644 lib/TransferStatusHelpers/SharedTransferData.cs create mode 100644 lib/TransferStatusHelpers/TransferData.cs create mode 100644 lib/TransferStatusHelpers/TransferDataState.cs create mode 100644 lib/TransferStatusHelpers/TransferDownloadBuffer.cs create mode 100644 lib/TransferStatusHelpers/TransferDownloadStream.cs create mode 100644 lib/TransferStatusHelpers/TransferProgressTracker.cs create mode 100644 lib/Transfer_RequestOptions.cs create mode 100644 lib/Utils.cs create mode 100644 lib/packages.config create mode 100644 samples/DataMovementSamples/DataMovementSamples.sln create mode 100644 samples/DataMovementSamples/DataMovementSamples/App.config create mode 100644 samples/DataMovementSamples/DataMovementSamples/DataMovementSamples.csproj create mode 100644 samples/DataMovementSamples/DataMovementSamples/Properties/AssemblyInfo.cs create mode 100644 samples/DataMovementSamples/DataMovementSamples/Samples.cs create mode 100644 samples/DataMovementSamples/DataMovementSamples/Util.cs create mode 100644 samples/DataMovementSamples/DataMovementSamples/azure.png create mode 100644 samples/DataMovementSamples/DataMovementSamples/packages.config create mode 100644 test/DMLibTest/Cases/AccessConditionTest.cs create mode 100644 test/DMLibTest/Cases/AllTransferDirectionTest.cs create mode 100644 test/DMLibTest/Cases/BVT.cs create mode 100644 test/DMLibTest/Cases/BigFileTest.cs create mode 100644 test/DMLibTest/Cases/CheckContentMD5Test.cs create mode 100644 test/DMLibTest/Cases/MetadataTest.cs create mode 100644 test/DMLibTest/Cases/OverwriteTest.cs create mode 100644 test/DMLibTest/Cases/ProgressHandlerTest.cs create mode 100644 test/DMLibTest/Cases/ResumeTest.cs create mode 100644 test/DMLibTest/Cases/SetContentTypeTest.cs create mode 100644 test/DMLibTest/Cases/UnsupportedDirectionTest.cs create mode 100644 test/DMLibTest/DMLibTest.csproj create mode 100644 test/DMLibTest/Framework/AssemblyInitCleanup.cs create mode 100644 test/DMLibTest/Framework/BlobDataAdaptorBase.cs create mode 100644 test/DMLibTest/Framework/CloudBlobDataAdaptor.cs create mode 100644 test/DMLibTest/Framework/CloudFileDataAdaptor.cs create mode 100644 test/DMLibTest/Framework/CloudObjectExtensions.cs create mode 100644 test/DMLibTest/Framework/CopyWrapper.cs create mode 100644 test/DMLibTest/Framework/DMLibDataHelper.cs create mode 100644 test/DMLibTest/Framework/DMLibDataInfo.cs create mode 100644 test/DMLibTest/Framework/DMLibInputHelper.cs create mode 100644 test/DMLibTest/Framework/DMLibTestBase.cs create mode 100644 test/DMLibTest/Framework/DMLibWrapper.cs create mode 100644 test/DMLibTest/Framework/DataAdaptor.cs create mode 100644 test/DMLibTest/Framework/DownloadWrapper.cs create mode 100644 test/DMLibTest/Framework/IDataInfo.cs create mode 100644 test/DMLibTest/Framework/LocalDataAdaptor.cs create mode 100644 test/DMLibTest/Framework/LocalDataAdaptorBase.cs create mode 100644 test/DMLibTest/Framework/MultiDirectionTestBase.cs create mode 100644 test/DMLibTest/Framework/MultiDirectionTestHelper.cs create mode 100644 test/DMLibTest/Framework/MultiDirectionTestInfo.cs create mode 100644 test/DMLibTest/Framework/ProgressChecker.cs create mode 100644 test/DMLibTest/Framework/SharedAccessPermissions.cs create mode 100644 test/DMLibTest/Framework/TestExecutionOptions.cs create mode 100644 test/DMLibTest/Framework/TestResult.cs create mode 100644 test/DMLibTest/Framework/TransferItem.cs create mode 100644 test/DMLibTest/Framework/URIBlobDataAdaptor.cs create mode 100644 test/DMLibTest/Framework/UploadWrapper.cs create mode 100644 test/DMLibTest/Framework/VerificationHelper.cs create mode 100644 test/DMLibTest/Properties/AssemblyInfo.cs create mode 100644 test/DMLibTest/TestData.xml create mode 100644 test/DMLibTest/Util/DMLibTestConstants.cs create mode 100644 test/DMLibTest/Util/DMLibTestHelper.cs create mode 100644 test/DMLibTest/Util/Helpers.cs create mode 100644 test/DMLibTest/Util/TestAccounts.cs create mode 100644 test/DMLibTest/packages.config create mode 100644 test/DMLibTestCodeGen/App.config create mode 100644 test/DMLibTestCodeGen/DMLibDataType.cs create mode 100644 test/DMLibTestCodeGen/DMLibDirectionFilter.cs create mode 100644 test/DMLibTestCodeGen/DMLibTestCodeGen.csproj create mode 100644 test/DMLibTestCodeGen/DMLibTestContext.cs create mode 100644 test/DMLibTestCodeGen/DMLibTestMethodSet.cs create mode 100644 test/DMLibTestCodeGen/DMLibTestMetholdAttribute.cs create mode 100644 test/DMLibTestCodeGen/DMLibTransferDirection.cs create mode 100644 test/DMLibTestCodeGen/DirectionFilter.cs create mode 100644 test/DMLibTestCodeGen/ITestDirection.cs create mode 100644 test/DMLibTestCodeGen/MultiDirectionTag.cs create mode 100644 test/DMLibTestCodeGen/MultiDirectionTestClass.cs create mode 100644 test/DMLibTestCodeGen/MultiDirectionTestClassAttribute.cs create mode 100644 test/DMLibTestCodeGen/MultiDirectionTestContext.cs create mode 100644 test/DMLibTestCodeGen/MultiDirectionTestMethod.cs create mode 100644 test/DMLibTestCodeGen/MultiDirectionTestMethodAttribute.cs create mode 100644 test/DMLibTestCodeGen/MultiDirectionTestMethodSetAttribute.cs create mode 100644 test/DMLibTestCodeGen/Program.cs create mode 100644 test/DMLibTestCodeGen/Properties/AssemblyInfo.cs create mode 100644 test/DMLibTestCodeGen/SourceCodeGenerator.cs create mode 100644 test/DMLibTestCodeGen/TestMethodDirection.cs create mode 100644 test/MsTestLib/ClassConfig.cs create mode 100644 test/MsTestLib/ConsoleLogger.cs create mode 100644 test/MsTestLib/Exceptions.cs create mode 100644 test/MsTestLib/FileLogger.cs create mode 100644 test/MsTestLib/ILogger.cs create mode 100644 test/MsTestLib/MessageBuilder.cs create mode 100644 test/MsTestLib/MethodConfig.cs create mode 100644 test/MsTestLib/MsTestLib.csproj create mode 100644 test/MsTestLib/Test.cs create mode 100644 test/MsTestLib/TestConfig.cs create mode 100644 test/MsTestLib/TestHelper.cs create mode 100644 test/MsTestLib/TestLogger.cs create mode 100644 tools/AssemblyInfo/SharedAssemblyInfo.cs create mode 100644 tools/analysis/fxcop/azure-storage-dm.ruleset create mode 100644 tools/apidoc/.gitignore create mode 100644 tools/apidoc/dmlib.shfbproj create mode 100644 tools/nupkg/Microsoft.Azure.Storage.DataMovement.nuspec create mode 100644 tools/nupkg/buildNupkg.cmd create mode 100644 tools/scripts/InjectBuildNumber.ps1 create mode 100644 tools/strongnamekeys/fake/windows.snk diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..1ff0c423 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.nuget/NuGet.Config b/.nuget/NuGet.Config new file mode 100644 index 00000000..67f8ea04 --- /dev/null +++ b/.nuget/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.nuget/NuGet.exe b/.nuget/NuGet.exe new file mode 100644 index 0000000000000000000000000000000000000000..9ca66594f912a1fe7aec510819006fb1a80bc1a9 GIT binary patch literal 1664512 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

  • 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

    w}k|O3;Ng3Ak`gW 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 z zpXaYP$LY5z1M|9koI^Wb&i8|`rw{U5jJ=4k&lj`{6Xuug{g?JUF!$rIxDof|kO^sz z2XI~=VCSi9e)YP8A?A1lx>Qks^vVAHc{XmhoPQ33`mM+5TNIOTS_6vu+q#bHW839? z0(R_s5*}A~pZD3iAGRO#=f}Ana{J+s>w)jzb8x=rn=mY1F=0eBA%8s`e-Fpm4)<@# zoPJXjcH5x;#H_>WUe07Yw!hTvH8H=zxNf;?$gXa`Y&U3!$AR3>|7h|-zo3Tv{BOtE z5Bs(6S15<)=<7TM?^|eFoPOUXKwLN1FD??;%sY@dE4iF~@t> zaX-h`0jIO=EiC#4{02Fv4|3ao(JvgD^BnfZRfiaRA#dHz5fs<&ACG|_9v=;dt{9x_ zg|;t&WH6!E3EOJ*j{)3IJg=0 zYr~Le+iIM~qyLgeM4xl*^}i3o?4*!>R|oMC2z$y8=kH4Bf+3FswiWX8DnEbQ_sL;# z73a&0&uJ~bdCwfi{f~Zc51iYRjr;HIIe+Z`PS~@d9lz^-kkfNG)#yT4w@2#=kdBWB z!#nW)+pag@>oy|(VtA^UZ^9IB+!0LA=Y8uT{hAEY2ahx7FnFBfTz($^@w_*WgILFQ zIBZ4#!N-%s?&C5p7vFDYZ=>E=&;9fwGY`z`DMKRLjQePwEQ#}RKeX$4LgE$p<8f

    >2--I}aFCqV3hWqn}_Ps+y^e(pjbMW|1 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 RGHFl)S5g03{{M}@-vCWeV3Gg; literal 0 HcmV?d00001 diff --git a/.nuget/NuGet.targets b/.nuget/NuGet.targets new file mode 100644 index 00000000..3f8c37b2 --- /dev/null +++ b/.nuget/NuGet.targets @@ -0,0 +1,144 @@ + + + + $(MSBuildProjectDirectory)\..\ + + + false + + + false + + + true + + + false + + + + + + + + + + + $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) + + + + + $(SolutionDir).nuget + + + + $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config + $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config + + + + $(MSBuildProjectDirectory)\packages.config + $(PackagesProjectConfig) + + + + + $(NuGetToolsPath)\NuGet.exe + @(PackageSource) + + "$(NuGetExePath)" + mono --runtime=v4.0.30319 "$(NuGetExePath)" + + $(TargetDir.Trim('\\')) + + -RequireConsent + -NonInteractive + + "$(SolutionDir) " + "$(SolutionDir)" + + + $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir) + $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols + + + + RestorePackages; + $(BuildDependsOn); + + + + + $(BuildDependsOn); + BuildPackage; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DataMovement.sln b/DataMovement.sln new file mode 100644 index 00000000..0edc60f6 --- /dev/null +++ b/DataMovement.sln @@ -0,0 +1,82 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.21005.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataMovement", "lib\DataMovement.csproj", "{B821E031-09CC-48F0-BDC6-2793228D4027}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{E2E6D76F-6339-4E02-96EB-94CC8E6D62B2}" + ProjectSection(SolutionItems) = preProject + .nuget\NuGet.Config = .nuget\NuGet.Config + .nuget\NuGet.exe = .nuget\NuGet.exe + .nuget\NuGet.targets = .nuget\NuGet.targets + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{4353D299-C4E9-41FF-BB35-6769BACA424A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DMLibTest", "test\DMLibTest\DMLibTest.csproj", "{2A4656A4-F744-4653-A9D6-15112E9AB352}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DMLibTestCodeGen", "test\DMLibTestCodeGen\DMLibTestCodeGen.csproj", "{7018EE4E-D389-424E-A8DD-F9B4FFDA5194}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MsTestLib", "test\MsTestLib\MsTestLib.csproj", "{AC39B50F-DC27-4411-9ED4-A4A137190ACB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|Mixed Platforms = Debug|Mixed Platforms + Debug|Win32 = Debug|Win32 + Release|Any CPU = Release|Any CPU + Release|Mixed Platforms = Release|Mixed Platforms + Release|Win32 = Release|Win32 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B821E031-09CC-48F0-BDC6-2793228D4027}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B821E031-09CC-48F0-BDC6-2793228D4027}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B821E031-09CC-48F0-BDC6-2793228D4027}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B821E031-09CC-48F0-BDC6-2793228D4027}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {B821E031-09CC-48F0-BDC6-2793228D4027}.Debug|Win32.ActiveCfg = Debug|Any CPU + {B821E031-09CC-48F0-BDC6-2793228D4027}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B821E031-09CC-48F0-BDC6-2793228D4027}.Release|Any CPU.Build.0 = Release|Any CPU + {B821E031-09CC-48F0-BDC6-2793228D4027}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {B821E031-09CC-48F0-BDC6-2793228D4027}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {B821E031-09CC-48F0-BDC6-2793228D4027}.Release|Win32.ActiveCfg = Release|Any CPU + {2A4656A4-F744-4653-A9D6-15112E9AB352}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A4656A4-F744-4653-A9D6-15112E9AB352}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A4656A4-F744-4653-A9D6-15112E9AB352}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {2A4656A4-F744-4653-A9D6-15112E9AB352}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {2A4656A4-F744-4653-A9D6-15112E9AB352}.Debug|Win32.ActiveCfg = Debug|Any CPU + {2A4656A4-F744-4653-A9D6-15112E9AB352}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A4656A4-F744-4653-A9D6-15112E9AB352}.Release|Any CPU.Build.0 = Release|Any CPU + {2A4656A4-F744-4653-A9D6-15112E9AB352}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {2A4656A4-F744-4653-A9D6-15112E9AB352}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {2A4656A4-F744-4653-A9D6-15112E9AB352}.Release|Win32.ActiveCfg = Release|Any CPU + {7018EE4E-D389-424E-A8DD-F9B4FFDA5194}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7018EE4E-D389-424E-A8DD-F9B4FFDA5194}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7018EE4E-D389-424E-A8DD-F9B4FFDA5194}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {7018EE4E-D389-424E-A8DD-F9B4FFDA5194}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {7018EE4E-D389-424E-A8DD-F9B4FFDA5194}.Debug|Win32.ActiveCfg = Debug|Any CPU + {7018EE4E-D389-424E-A8DD-F9B4FFDA5194}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7018EE4E-D389-424E-A8DD-F9B4FFDA5194}.Release|Any CPU.Build.0 = Release|Any CPU + {7018EE4E-D389-424E-A8DD-F9B4FFDA5194}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {7018EE4E-D389-424E-A8DD-F9B4FFDA5194}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {7018EE4E-D389-424E-A8DD-F9B4FFDA5194}.Release|Win32.ActiveCfg = Release|Any CPU + {AC39B50F-DC27-4411-9ED4-A4A137190ACB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AC39B50F-DC27-4411-9ED4-A4A137190ACB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AC39B50F-DC27-4411-9ED4-A4A137190ACB}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {AC39B50F-DC27-4411-9ED4-A4A137190ACB}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {AC39B50F-DC27-4411-9ED4-A4A137190ACB}.Debug|Win32.ActiveCfg = Debug|Any CPU + {AC39B50F-DC27-4411-9ED4-A4A137190ACB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AC39B50F-DC27-4411-9ED4-A4A137190ACB}.Release|Any CPU.Build.0 = Release|Any CPU + {AC39B50F-DC27-4411-9ED4-A4A137190ACB}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {AC39B50F-DC27-4411-9ED4-A4A137190ACB}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {AC39B50F-DC27-4411-9ED4-A4A137190ACB}.Release|Win32.ActiveCfg = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {2A4656A4-F744-4653-A9D6-15112E9AB352} = {4353D299-C4E9-41FF-BB35-6769BACA424A} + {7018EE4E-D389-424E-A8DD-F9B4FFDA5194} = {4353D299-C4E9-41FF-BB35-6769BACA424A} + {AC39B50F-DC27-4411-9ED4-A4A137190ACB} = {4353D299-C4E9-41FF-BB35-6769BACA424A} + EndGlobalSection +EndGlobal diff --git a/LICENSE b/LICENSE index ad410e11..5761bc66 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,21 @@ -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - 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. \ No newline at end of file +The MIT License (MIT) + +Copyright (c) 2015 Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..fb536c37 --- /dev/null +++ b/README.md @@ -0,0 +1,154 @@ +# Microsoft Azure Storage Data Movement Library (0.1.0) + +The Microsoft Azure Storage Data Movement Library designed for high-performance uploading, downloading and copying Azure Storage Blob and File. + +[AzCopy](https://azure.microsoft.com/documentation/articles/storage-use-azcopy/), the Azure Storage data management command line utility, is refering to this library. + +For more information about the Azure Storage, please visit [Microsoft Azure Storage Documentation](https://azure.microsoft.com/documentation/services/storage/). + +# Features + +- Blobs + - Download/Upload/Copy Blobs. + - Synchronous and asynchronous copy Blobs + - Concurrently transfer Blobs and Blob chunks, define number of concurrents + - Download Specific Blob Snapshot + +- Files + - Download/Upload/Copy Files. + - Synchronous and asynchronous copy Files + - Concurrently transfer Files and File ranges, define number of concurrents + +- General + - Track data transfer progress + - Recover the data transfer + - Set Access Condition + - Set User Agent Suffix + +# Getting started + +For the best development experience, we recommend that developers use the official Microsoft NuGet packages for libraries. NuGet packages are regularly updated with new functionality and hotfixes. + + +## Requirements + +To call Azure services, you must first have an Azure subscription. Sign up for a [free trial](/en-us/pricing/free-trial/) or use your [MSDN subscriber benefits](/en-us/pricing/member-offers/msdn-benefits-details/). + + +## Download & Install + + +### Via Git + +To get the source code of the SDK via git just type: + +```bash +git clone https://github.com/Azure/azure-storage-net-data-movement.git +cd azure-storage-net-data-movement +``` + +### Via NuGet + +To get the binaries of this library as distributed by Microsoft, ready for use +within your project you can also have them installed by the .NET package manager [NuGet](http://www.nuget.org/). + +`Install-Package WindowsAzure.Storage.DataMovment` + + +## Dependencies + +### Azure Storage Client Library for .NET + +This version depends on Azure Storage Client Library for .NET. + +- [WindowsAzure.Storage](https://www.nuget.org/packages/WindowsAzure.Storage/) + + + +## Code Samples + +Find more samples at [getting started with Storage Data Movement Library (TBC)]() and the [sample folder (TBC)](). + +### Upload a blob + +First, include the classes you need, here we include Storage client library, the Storage data movement library and the .NET threading because data movement libary provides Task Asynchronous interfaces to transfer storage objects: + +```csharp +using System; +using System.Threading; +using Microsoft.WindowsAzure.Storage; +using Microsoft.WindowsAzure.Storage.Auth; +using Microsoft.WindowsAzure.Storage.Blob; +using Microsoft.WindowsAzure.Storage.DataMovement; +``` + +Now use the interfaces provided by Storage client lib to setup the storage context (find more details at [how to use Blob Storage from .NET](https://azure.microsoft.com/documentation/articles/storage-dotnet-how-to-use-blobs/)): + +```csharp +CloudStorageAccount account = CloudStorageAccount.Parse( + configurationManager.ConnectionStrings["StorageConnectionString"]); +CloudBlobClient blobClient = account.CreateCloudBlobClient(); +CloudBlobContainer blobContainer = blobClient.GetContainerReference("mycontainer"); +blobContainer.CreateIfNotExists(); +string sourcePath = "path\\to\\test.txt"; +CloudBlockBlob destBlob = blobContainer.GetBlockBlobReference("myblob"); +``` + +Once you setup the storage blob context, you can start to use `WindowsAzure.Storage.DataMovement.TransferManager` to upload the blob and track the upload progress, + +```csharp +// Setup the number of the concurrent operations +TransferManager.Configurations.ParallelOperations = 64; +// Setup the transfer context and track the upoload progress +TransferContext context = new TransferContext(); +context.ProgressHandler = new Progress((progress) => +{ + Console.WriteLine("Bytes uploaded: {0}/{1}", + progress.BytesTransferred, progress.TotalSize); +}); +// Upload a local blob +var task = TransferManager.UploadAsync( + sourcePath, destBlob, null, context, CancellationToken.None); +task.Wait(); +``` +# Best Practice + +### Increase .NET HTTP connections limit +By default, the .Net HTTP connection limit is 2. This implies that only two concurrent connections can be maintained. It prevents more parallel connections accessing Azure blob storage from your application. + +AzCopy will set ServicePointManager.DefaultConnectionLimit to the number of eight multiple the core number by default. To have a comparable performance when using Data Movement Library alone, we recommend you set this value as well. + +```csharp +ServicePoint myServicePoint = ServicePointManager.FindServicePoint(myServiceUri); +myServicePoint.ConnectionLimit = 48 +``` + +### Turn off 100-continue +When the property "Expect100Continue" is set to true, client requests that use the PUT and POST methods will add an Expect: 100-continue header to the request and it will expect to receive a 100-Continue response from the server to indicate that the client should send the data to be posted. This mechanism allows clients to avoid sending large amounts of data over the network when the server, based on the request headers, intends to reject the request. + +However, once the entire payload is received on the server end, other errors may still occur. And if Windows Azure clients have tested the client well enough to ensure that it is not sending any bad requests, clients could turn off 100-continue so that the entire request is sent in one roundtrip. This is especially true when clients send small size storage objects. + +```csharp +ServicePointManager.Expect100Continue = false; +``` + +# Need Help? +Be sure to check out the Microsoft Azure [Developer Forums on MSDN](http://go.microsoft.com/fwlink/?LinkId=234489) if you have trouble with the provided code or use StackOverflow. + + +# Collaborate & Contribute + +We gladly accept community contributions. + +- Issues: Please report bugs using the Issues section of GitHub +- Forums: Interact with the development teams on StackOverflow or the Microsoft Azure Forums +- Source Code Contributions: Please follow the [contribution guidelines for Microsoft Azure open source](http://azure.github.io/guidelines.html) that details information on onboarding as a contributor + +For general suggestions about Microsoft Azure please use our [UserVoice forum](http://feedback.azure.com/forums/34192--general-feedback). + + +# Learn More + +- [Storage Data Movement Library API reference (TBC)]() +- [Storage Client Library Reference for .NET - MSDN](http://msdn.microsoft.com/library/azure/dn495001(v=azure.10).aspx) +- [Azure Storage Team Blog](http://blogs.msdn.com/b/windowsazurestorage/) diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 00000000..3d65d23f --- /dev/null +++ b/changelog.txt @@ -0,0 +1,2 @@ +2015.07.17 Version 0.1.0 + * Initial Release diff --git a/lib/AssemblyInfo.cs b/lib/AssemblyInfo.cs new file mode 100644 index 00000000..513b08a9 --- /dev/null +++ b/lib/AssemblyInfo.cs @@ -0,0 +1,15 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +using System; +using System.Reflection; +using System.Resources; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Microsoft.WindowsAzure.Storage.DataMovement.dll")] +[assembly: AssemblyDescription("")] diff --git a/lib/Constants.cs b/lib/Constants.cs new file mode 100644 index 00000000..0dc9f3d6 --- /dev/null +++ b/lib/Constants.cs @@ -0,0 +1,140 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.Reflection; + + ///

    + /// Constants for use with the transfer classes. + /// + public static class Constants + { + /// + /// Stores the max block size, 4MB. + /// + public const int MaxBlockSize = 4 * 1024 * 1024; + + /// + /// Default block size, 4MB. + /// + public const int DefaultBlockSize = 4 * 1024 * 1024; + + /// + /// Define cache size for one parallel operation. + /// + internal const long CacheSizeMultiplierInByte = 12 * 1024 * 1024; + + /// + /// Default to root container name if none is specified. + /// + internal const string DefaultContainerName = "$root"; + + /// + /// Minimum block size, 256KB. + /// + internal const int MinBlockSize = 256 * 1024; + + /// + /// Stores the max page blob file size, 1TB. + /// + internal const long MaxPageBlobFileSize = (long)1024 * 1024 * 1024 * 1024; + + /// + /// Stores the max block blob file size, 50000 * 4M. + /// + internal const long MaxBlockBlobFileSize = (long)50000 * 4 * 1024 * 1024; + + /// + /// Stores the max cloud file size, 1TB. + /// + internal const long MaxCloudFileSize = (long)1024 * 1024 * 1024 * 1024; + + /// + /// Max transfer window size. + /// There can be multiple threads to transfer a file, + /// and we need to record transfer window + /// and have constant length for a transfer entry record in restart journal, + /// so set a limitation for transfer window here. + /// + internal const int MaxCountInTransferWindow = 128; + + /// + /// Length to get page ranges in one request. + /// In blog http://blogs.msdn.com/b/windowsazurestorage/archive/2012/03/26/getting-the-page-ranges-of-a-large-page-blob-in-segments.aspx, + /// it says that it's safe to get page ranges of 150M in one request. + /// We use 148MB which is multiples of 4MB. + /// + internal const long PageRangesSpanSize = 148 * 1024 * 1024; + + /// + /// Length to get file ranges in one request. + /// Use the same number as page blob for now because cloud file leverages page blob in implementation. + /// TODO: update this number when doc for cloud file is available. + /// + internal const long FileRangeSpanSize = 148 * 1024 * 1024; + + /// + /// Percentage of available we'll try to use for our memory cache. + /// + internal const double MemoryCacheMultiplier = 0.5; + + /// + /// Maximum amount of memory to use for our memory cache. + /// + internal static readonly long MemoryCacheMaximum = GetMemoryCacheMaximum(); + + /// + /// Maximum amount of cells in memory manager. + /// + internal const int MemoryManagerCellsMaximum = 8 * 1024; + + /// + /// The life time in minutes of SAS auto generated for blob to blob copy. + /// + internal const int CopySASLifeTimeInMinutes = 7 * 24 * 60; + + /// + /// The time in milliseconds to wait to refresh copy status for asynchronous copy. + /// + internal const long AsyncCopyStatusRefreshWaitTimeInMilliseconds = 100; + + internal const string BlobTypeMismatch = "Blob type of the blob reference doesn't match blob type of the blob."; + + /// + /// The product name used in UserAgent header. + /// + internal const string UserAgentProductName = "DataMovement"; + + /// + /// UserAgent header. + /// + internal static readonly string UserAgent = GetUserAgent(); + + internal static readonly string FormatVersion = GetFormatVersion(); + + /// + /// Gets the UserAgent string. + /// + /// UserAgent string. + private static string GetUserAgent() + { + AssemblyName assemblyName = Assembly.GetExecutingAssembly().GetName(); + return UserAgentProductName + "/" + assemblyName.Version.ToString(); + } + + private static string GetFormatVersion() + { + AssemblyName assemblyName = Assembly.GetExecutingAssembly().GetName(); + return assemblyName.Name + "/" + assemblyName.Version.ToString(); + } + + private static long GetMemoryCacheMaximum() + { + return Environment.Is64BitProcess ? (long)2 * 1024 * 1024 * 1024 : (long)512 * 1024 * 1024; + } + } +} diff --git a/lib/DataMovement.csproj b/lib/DataMovement.csproj new file mode 100644 index 00000000..bdd9ec6d --- /dev/null +++ b/lib/DataMovement.csproj @@ -0,0 +1,170 @@ + + + + + Debug + AnyCPU + {B821E031-09CC-48F0-BDC6-2793228D4027} + Library + Properties + Microsoft.WindowsAzure.Storage.DataMovement + Microsoft.WindowsAzure.Storage.DataMovement + v4.5 + 512 + ..\ + + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + ..\tools\analysis\fxcop\azure-storage-dm.ruleset + false + true + bin\Debug\Microsoft.WindowsAzure.Storage.DataMovement.XML + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + ..\tools\analysis\fxcop\azure-storage-dm.ruleset + true + true + true + + + true + true + ..\tools\strongnamekeys\fake\windows.snk + + + + False + ..\packages\Microsoft.Data.Edm.5.6.4\lib\net40\Microsoft.Data.Edm.dll + + + False + ..\packages\Microsoft.Data.OData.5.6.4\lib\net40\Microsoft.Data.OData.dll + + + False + ..\packages\Microsoft.Data.Services.Client.5.6.4\lib\net40\Microsoft.Data.Services.Client.dll + + + False + ..\packages\Microsoft.WindowsAzure.ConfigurationManager.1.8.0.0\lib\net35-full\Microsoft.WindowsAzure.Configuration.dll + + + False + ..\packages\WindowsAzure.Storage.5.0.0\lib\net40\Microsoft.WindowsAzure.Storage.dll + + + False + ..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll + + + + + + + False + ..\packages\System.Spatial.5.6.4\lib\net40\System.Spatial.dll + + + + + SharedAssemblyInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/lib/Exceptions/TransferErrorCode.cs b/lib/Exceptions/TransferErrorCode.cs new file mode 100644 index 00000000..562b30f1 --- /dev/null +++ b/lib/Exceptions/TransferErrorCode.cs @@ -0,0 +1,104 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + + /// + /// Error codes for TransferException. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1027:MarkEnumsWithFlags")] + public enum TransferErrorCode + { + /// + /// No error. + /// + None = 0, + + /// + /// Invalid source location specified. + /// + InvalidSourceLocation = 1, + + /// + /// Invalid destination location specified. + /// + InvalidDestinationLocation = 2, + + /// + /// Failed to open file for upload or download. + /// + OpenFileFailed = 3, + + /// + /// The file to transfer is too large for the destination. + /// + UploadSourceFileSizeTooLarge = 4, + + /// + /// The file size is invalid for the specified blob type. + /// + UploadBlobSourceFileSizeInvalid = 5, + + /// + /// User canceled. + /// + OperationCanceled = 6, + + /// + /// Both Source and Destination are locally accessible locations. + /// At least one of source and destination should be an Azure Storage location. + /// + LocalToLocalTransfersUnsupported = 7, + + /// + /// Failed to do asynchronous copy. + /// + AsyncCopyFailed = 8, + + /// + /// Source and destination are the same. + /// + SameSourceAndDestination = 9, + + /// + /// AsyncCopyController detects mismatch between copy id stored in transfer entry and + /// that retrieved from server. + /// + MismatchCopyId = 10, + + /// + /// AsyncCopyControler fails to retrieve CopyState for the object which we are to monitor. + /// + FailToRetrieveCopyStateForObject = 11, + + /// + /// Fails to allocate memory in MemoryManager. + /// + FailToAllocateMemory = 12, + + /// + /// Fails to get source's last write time. + /// + FailToGetSourceLastWriteTime = 13, + + /// + /// User choose not to overwrite existing destination. + /// + NotOverwriteExistingDestination = 14, + + /// + /// Transfer with the same source and destination already exists. + /// + TransferAlreadyExists = 15, + + /// + /// Uncategorized transfer error. + /// + Unknown = 32, + } +} diff --git a/lib/Exceptions/TransferException.cs b/lib/Exceptions/TransferException.cs new file mode 100644 index 00000000..07cfa787 --- /dev/null +++ b/lib/Exceptions/TransferException.cs @@ -0,0 +1,152 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.Runtime.Serialization; + + /// + /// Base exception class for exceptions thrown by Blob/FileTransferJobs. + /// + [Serializable] + public sealed class TransferException : Exception + { + /// + /// Version of current TransferException serialization format. + /// + private const int ExceptionVersion = 1; + + /// + /// Serialization field name for Version. + /// + private const string VersionFieldName = "Version"; + + /// + /// Serialization field name for ErrorCode. + /// + private const string ErrorCodeFieldName = "ErrorCode"; + + /// + /// Transfer error code. + /// + private TransferErrorCode errorCode; + + /// + /// Initializes a new instance of the class. + /// + public TransferException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public TransferException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference + /// if no inner exception is specified. + public TransferException(string message, Exception ex) + : base(message, ex) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Transfer error code. + public TransferException(TransferErrorCode errorCode) + { + this.errorCode = errorCode; + } + + /// + /// Initializes a new instance of the class. + /// + /// Transfer error code. + /// Exception message. + public TransferException( + TransferErrorCode errorCode, + string message) + : base(message) + { + this.errorCode = errorCode; + } + + /// + /// Initializes a new instance of the class. + /// + /// Transfer error code. + /// Exception message. + /// Inner exception. + public TransferException( + TransferErrorCode errorCode, + string message, + Exception innerException) + : base(message, innerException) + { + this.errorCode = errorCode; + } + + /// + /// Initializes a new instance of the class. + /// + /// Serialization information. + /// Streaming context. + private TransferException( + SerializationInfo info, + StreamingContext context) + : base(info, context) + { + int exceptionVersion = info.GetInt32(VersionFieldName); + + if (exceptionVersion >= 1) + { + this.errorCode = (TransferErrorCode)info.GetInt32(ErrorCodeFieldName); + } + } + + /// + /// Gets the detailed error code. + /// + /// The error code of the exception. + public TransferErrorCode ErrorCode + { + get + { + return this.errorCode; + } + } + + /// + /// Serializes the exception. + /// + /// Serialization info object. + /// Streaming context. + public override void GetObjectData( + SerializationInfo info, + StreamingContext context) + { + if (null == info) + { + throw new ArgumentNullException("info"); + } + + info.AddValue(VersionFieldName, ExceptionVersion); + info.AddValue(ErrorCodeFieldName, this.errorCode); + + base.GetObjectData(info, context); + } + } +} diff --git a/lib/Extensions/CloudBlobExtensions.cs b/lib/Extensions/CloudBlobExtensions.cs new file mode 100644 index 00000000..0f65f070 --- /dev/null +++ b/lib/Extensions/CloudBlobExtensions.cs @@ -0,0 +1,158 @@ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.IO; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.DataMovement.TransferJobs; + + /// + /// Defines extensions methods for ICloudBlob for use with BlobTransfer. + /// + public static class CloudBlobExtensions + { + /// + /// Creates a job to start copying from a blob. + /// + /// Destination blob to copy to. + /// User should call the method on this object. + /// Source blob to copy from. + /// Job object to start copying. + public static BlobStartCopyJob CreateStartCopyJob( + this ICloudBlob destBlob, + ICloudBlob sourceBlob) + { + return new BlobStartCopyJob() + { + SourceBlob = sourceBlob, + DestBlob = destBlob + }; + } + + /// + /// Creates a job to start copying from a URI source. + /// + /// Destination blob to copy to. + /// User should call the method on this object. + /// Source to copy from. + /// Job object to start copying. + public static BlobStartCopyJob CreateStartCopyJob( + this ICloudBlob destBlob, + Uri sourceUri) + { + return new BlobStartCopyJob() + { + SourceUri = sourceUri, + DestBlob = destBlob + }; + } + + /// + /// Creates a job to copy from a blob. + /// + /// Destination blob to copy to. + /// User should call the method on this object. + /// Source blob to copy from. + /// Job object to do copying. + public static BlobCopyJob CreateCopyJob( + this ICloudBlob destBlob, + ICloudBlob sourceBlob) + { + return new BlobCopyJob() + { + SourceBlob = sourceBlob, + DestBlob = destBlob + }; + } + + /// + /// Creates a job to copy from a URI source. + /// + /// Destination blob to copy to. + /// User should call the method on this object. + /// Source to copy from. + /// Job object to do copying. + public static BlobCopyJob CreateCopyJob( + this ICloudBlob destBlob, + Uri sourceUri) + { + return new BlobCopyJob() + { + SourceUri = sourceUri, + DestBlob = destBlob + }; + } + + /// + /// Creates a job to download a blob. + /// + /// Source blob that to be downloaded. + /// Path of destination to download to. + /// Job instance to download blob. + public static BlobDownloadJob CreateDownloadJob( + this ICloudBlob sourceBlob, + string destPath) + { + return new BlobDownloadJob() + { + SourceBlob = sourceBlob, + DestPath = destPath + }; + } + + /// + /// Creates a job to download a blob. + /// + /// Source blob that to be downloaded. + /// Destination stream to download to. + /// Job instance to download blob. + public static BlobDownloadJob CreateDownloadJob( + this ICloudBlob sourceBlob, + Stream destStream) + { + return new BlobDownloadJob() + { + SourceBlob = sourceBlob, + DestStream = destStream + }; + } + + /// + /// Creates a job to upload a blob. + /// + /// Destination blob to upload to. + /// Path of source file to upload from. + /// Job instance to upload blob. + public static BlobUploadJob CreateUploadJob( + this ICloudBlob destBlob, + string sourcePath) + { + return new BlobUploadJob() + { + SourcePath = sourcePath, + DestBlob = destBlob + }; + } + + /// + /// Creates a job to upload a blob. + /// + /// Destination blob to upload to. + /// Path of source file to upload from. + /// Job instance to upload blob. + public static BlobUploadJob CreateUploadJob( + this ICloudBlob destBlob, + Stream sourceStream) + { + return new BlobUploadJob() + { + SourceStream = sourceStream, + DestBlob = destBlob + }; + } + } +} diff --git a/lib/Extensions/CloudFileExtensions.cs b/lib/Extensions/CloudFileExtensions.cs new file mode 100644 index 00000000..e4a701c5 --- /dev/null +++ b/lib/Extensions/CloudFileExtensions.cs @@ -0,0 +1,100 @@ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.IO; + using Microsoft.WindowsAzure.Storage.DataMovement.TransferJobs; + using Microsoft.WindowsAzure.Storage.File; + + /// + /// Defines extensions methods for CloudFile to create FileTransferJobs. + /// + public static class CloudFileExtensions + { + /// + /// Creates a job to download a cloud file. + /// + /// Source file that to be downloaded. + /// Path of destination to download to. + /// Job instance to download file. + public static FileDownloadJob CreateDownloadJob( + this CloudFile sourceFile, + string destPath) + { + return new FileDownloadJob() + { + SourceFile = sourceFile, + DestPath = destPath + }; + } + + /// + /// Creates a job to download a cloud file. + /// + /// Source file that to be downloaded. + /// Destination stream to download to. + /// Job instance to download file. + public static FileDownloadJob CreateDownloadJob( + this CloudFile sourceFile, + Stream destStream) + { + return new FileDownloadJob() + { + SourceFile = sourceFile, + DestStream = destStream + }; + } + + /// + /// Creates a job to upload a cloud file. + /// + /// Destination file to upload to. + /// Path of source file to upload from. + /// Job instance to upload file. + public static FileUploadJob CreateUploadJob( + this CloudFile destFile, + string sourcePath) + { + return new FileUploadJob() + { + DestFile = destFile, + SourcePath = sourcePath + }; + } + + /// + /// Creates a job to upload a cloud file. + /// + /// Destination file to upload to. + /// Path of source file to upload from. + /// Job instance to upload file. + public static FileUploadJob CreateUploadJob( + this CloudFile destFile, + Stream sourceStream) + { + return new FileUploadJob() + { + DestFile = destFile, + SourceStream = sourceStream + }; + } + + /// + /// Creates a job to delete a cloud file. + /// + /// File to delete. + /// Job instance to delete file. + public static FileDeleteJob CreateDeleteJob( + this CloudFile fileToDelete) + { + return new FileDeleteJob() + { + File = fileToDelete + }; + } + } +} diff --git a/lib/Extensions/StorageExtensions.cs b/lib/Extensions/StorageExtensions.cs new file mode 100644 index 00000000..d177cf3e --- /dev/null +++ b/lib/Extensions/StorageExtensions.cs @@ -0,0 +1,189 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.Globalization; + using Microsoft.WindowsAzure.Storage.Auth; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.File; + + /// + /// Extension methods for CloudBlobs for use with BlobTransfer. + /// + internal static class StorageExtensions + { + /// + /// Determines whether two blobs have the same Uri and SnapshotTime. + /// + /// Blob to compare. + /// Comparand object. + /// True if the two blobs have the same Uri and SnapshotTime; otherwise, false. + internal static bool Equals( + CloudBlob blob, + CloudBlob comparand) + { + if (blob == comparand) + { + return true; + } + + if (null == blob || null == comparand) + { + return false; + } + + return blob.Uri.Equals(comparand.Uri) && + blob.SnapshotTime.Equals(comparand.SnapshotTime); + } + + internal static CloudFile GenerateCopySourceFile( + this CloudFile file) + { + if (null == file) + { + throw new ArgumentNullException("file"); + } + + string sasToken = GetFileSASToken(file); + + if (string.IsNullOrEmpty(sasToken)) + { + return file; + } + + return new CloudFile(file.Uri, new StorageCredentials(sasToken)); + } + + private static string GetFileSASToken(CloudFile file) + { + if (null == file.ServiceClient.Credentials + || file.ServiceClient.Credentials.IsAnonymous) + { + return string.Empty; + } + else if (file.ServiceClient.Credentials.IsSAS) + { + return file.ServiceClient.Credentials.SASToken; + } + + // SAS life time is at least 10 minutes. + TimeSpan sasLifeTime = TimeSpan.FromMinutes(Constants.CopySASLifeTimeInMinutes); + + SharedAccessFilePolicy policy = new SharedAccessFilePolicy() + { + SharedAccessExpiryTime = DateTime.Now.Add(sasLifeTime), + Permissions = SharedAccessFilePermissions.Read, + }; + + return file.GetSharedAccessSignature(policy); + } + + /// + /// Append an auto generated SAS to a blob uri. + /// + /// Blob to append SAS. + /// Blob Uri with SAS appended. + internal static CloudBlob GenerateCopySourceBlob( + this CloudBlob blob) + { + if (null == blob) + { + throw new ArgumentNullException("blob"); + } + + string sasToken = GetBlobSasToken(blob); + + if (string.IsNullOrEmpty(sasToken)) + { + return blob; + } + + Uri blobUri = null; + + if (blob.IsSnapshot) + { + blobUri = blob.SnapshotQualifiedUri; + } + else + { + blobUri = blob.Uri; + } + + return Utils.GetBlobReference(blobUri, new StorageCredentials(sasToken), blob.BlobType); + } + + /// + /// Append an auto generated SAS to a blob uri. + /// + /// Blob to append SAS. + /// Blob Uri with SAS appended. + internal static Uri GenerateUriWithCredentials( + this CloudBlob blob) + { + if (null == blob) + { + throw new ArgumentNullException("blob"); + } + + string sasToken = GetBlobSasToken(blob); + + if (string.IsNullOrEmpty(sasToken)) + { + return blob.SnapshotQualifiedUri; + } + + string uriStr = null; + + if (blob.IsSnapshot) + { + uriStr = string.Format(CultureInfo.InvariantCulture, "{0}&{1}", blob.SnapshotQualifiedUri.AbsoluteUri, sasToken.Substring(1)); + } + else + { + uriStr = string.Format(CultureInfo.InvariantCulture, "{0}{1}", blob.Uri.AbsoluteUri, sasToken); + } + + return new Uri(uriStr); + } + + private static string GetBlobSasToken(CloudBlob blob) + { + if (null == blob.ServiceClient.Credentials + || blob.ServiceClient.Credentials.IsAnonymous) + { + return string.Empty; + } + else if (blob.ServiceClient.Credentials.IsSAS) + { + return blob.ServiceClient.Credentials.SASToken; + } + + // SAS life time is at least 10 minutes. + TimeSpan sasLifeTime = TimeSpan.FromMinutes(Constants.CopySASLifeTimeInMinutes); + + SharedAccessBlobPolicy policy = new SharedAccessBlobPolicy() + { + SharedAccessExpiryTime = DateTime.Now.Add(sasLifeTime), + Permissions = SharedAccessBlobPermissions.Read, + }; + + CloudBlob rootBlob = null; + + if (!blob.IsSnapshot) + { + rootBlob = blob; + } + else + { + rootBlob = Utils.GetBlobReference(blob.Uri, blob.ServiceClient.Credentials, blob.BlobType); + } + + return rootBlob.GetSharedAccessSignature(policy); + } + } +} diff --git a/lib/GlobalMemoryStatusNativeMethods.cs b/lib/GlobalMemoryStatusNativeMethods.cs new file mode 100644 index 00000000..50d35491 --- /dev/null +++ b/lib/GlobalMemoryStatusNativeMethods.cs @@ -0,0 +1,52 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System.Runtime.InteropServices; + + internal class GlobalMemoryStatusNativeMethods + { + private MEMORYSTATUSEX memStatus; + + public GlobalMemoryStatusNativeMethods() + { + this.memStatus = new MEMORYSTATUSEX(); + if (GlobalMemoryStatusEx(this.memStatus)) + { + this.AvailablePhysicalMemory = this.memStatus.ullAvailPhys; + } + } + + public ulong AvailablePhysicalMemory + { + get; + private set; + } + + [return: MarshalAs(UnmanagedType.Bool)] + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern bool GlobalMemoryStatusEx([In, Out] MEMORYSTATUSEX lpBuffer); + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + private class MEMORYSTATUSEX + { + public uint dwLength; + public uint dwMemoryLoad; + public ulong ullTotalPhys; + public ulong ullAvailPhys; + public ulong ullTotalPageFile; + public ulong ullAvailPageFile; + public ulong ullTotalVirtual; + public ulong ullAvailVirtual; + public ulong ullAvailExtendedVirtual; + + public MEMORYSTATUSEX() + { + this.dwLength = (uint)Marshal.SizeOf(typeof(MEMORYSTATUSEX)); + } + } + } +} diff --git a/lib/MD5HashStream.cs b/lib/MD5HashStream.cs new file mode 100644 index 00000000..c0784b40 --- /dev/null +++ b/lib/MD5HashStream.cs @@ -0,0 +1,462 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Security.Cryptography; + using System.Threading; + using System.Threading.Tasks; + + /// + /// Class to make thread safe stream access and calculate MD5 hash. + /// + internal class MD5HashStream : IDisposable + { + /// + /// Stream object. + /// + private Stream stream; + + /// + /// Semaphore object. In our case, we can only have one operation at the same time. + /// + private SemaphoreSlim semaphore; + + /// + /// In restart mode, we start a separate thread to calculate MD5hash of transferred part. + /// This variable indicates whether finished to calculate this part of MD5hash. + /// + private volatile bool finishedSeparateMd5Calculator = false; + + /// + /// Indicates whether succeeded in calculating MD5hash of the transferred bytes. + /// + private bool succeededSeparateMd5Calculator = false; + + /// + /// Running md5 hash of the blob being downloaded. + /// + private MD5CryptoServiceProvider md5hash; + + /// + /// Offset of the transferred bytes. We should calculate MD5hash on all bytes before this offset. + /// + private long md5hashOffset; + + /// + /// Initializes a new instance of the class. + /// + /// Stream object. + /// Offset of the transferred bytes. + /// Whether need to calculate MD5Hash. + public MD5HashStream( + Stream stream, + long lastTransferOffset, + bool md5hashCheck) + { + this.stream = stream; + this.md5hashOffset = lastTransferOffset; + + if ((0 == this.md5hashOffset) + || (!md5hashCheck)) + { + this.finishedSeparateMd5Calculator = true; + this.succeededSeparateMd5Calculator = true; + } + else + { + this.semaphore = new SemaphoreSlim(1, 1); + } + + if (md5hashCheck) + { + this.md5hash = new MD5CryptoServiceProvider(); + } + + if ((!this.finishedSeparateMd5Calculator) + && (!this.stream.CanRead)) + { + throw new NotSupportedException(string.Format( + CultureInfo.CurrentCulture, + Resources.StreamMustSupportReadException, + "Stream")); + } + + if (!this.stream.CanSeek) + { + throw new NotSupportedException(string.Format( + CultureInfo.CurrentCulture, + Resources.StreamMustSupportSeekException, + "Stream")); + } + } + + /// + /// Gets a value indicating whether need to calculate MD5 hash. + /// + public bool CheckMd5Hash + { + get + { + return null != this.md5hash; + } + } + + /// + /// Gets MD5 hash bytes. + /// + public byte[] Hash + { + get + { + return null == this.md5hash ? null : this.md5hash.Hash; + } + } + + /// + /// Gets a value indicating whether already finished to calculate MD5 hash of transferred bytes. + /// + public bool FinishedSeparateMd5Calculator + { + get + { + return this.finishedSeparateMd5Calculator; + } + } + + /// + /// Gets a value indicating whether already succeeded in calculating MD5 hash of transferred bytes. + /// + public bool SucceededSeparateMd5Calculator + { + get + { + this.WaitMD5CalculationToFinish(); + return this.succeededSeparateMd5Calculator; + } + } + + /// + /// Calculate MD5 hash of transferred bytes. + /// + /// Reference to MemoryManager object to require buffer from. + /// Action to check whether to cancel this calculation. + public void CalculateMd5(MemoryManager memoryManager, Action checkCancellation) + { + if (null == this.md5hash) + { + return; + } + + byte[] buffer = null; + + try + { + buffer = Utils.RequireBuffer(memoryManager, checkCancellation); + } + catch (Exception) + { + lock (this.md5hash) + { + this.finishedSeparateMd5Calculator = true; + } + + throw; + } + + long offset = 0; + int readLength = 0; + + while (true) + { + lock (this.md5hash) + { + if (offset >= this.md5hashOffset) + { + Debug.Assert( + offset == this.md5hashOffset, + "We should stop the separate calculator thread just at the transferred offset"); + + this.succeededSeparateMd5Calculator = true; + this.finishedSeparateMd5Calculator = true; + break; + } + + readLength = (int)Math.Min(this.md5hashOffset - offset, buffer.Length); + } + + try + { + checkCancellation(); + readLength = this.Read(offset, buffer, 0, readLength); + + lock (this.md5hash) + { + this.md5hash.TransformBlock(buffer, 0, readLength, null, 0); + } + } + catch (Exception) + { + lock (this.md5hash) + { + this.finishedSeparateMd5Calculator = true; + } + + memoryManager.ReleaseBuffer(buffer); + + throw; + } + + offset += readLength; + } + + memoryManager.ReleaseBuffer(buffer); + } + + /// + /// Begin async read from stream. + /// + /// Offset in stream to read from. + /// The buffer to read the data into. + /// The byte offset in buffer at which to begin writing data read from the stream. + /// The maximum number of bytes to read. + /// Token used to cancel the asynchronous reading. + /// A task that represents the asynchronous read operation. The value of the + /// TResult parameter contains the total number of bytes read into the buffer. + public async Task ReadAsync(long readOffset, byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + await this.WaitOnSemaphoreAsync(cancellationToken); + + try + { + this.stream.Position = readOffset; + + return await this.stream.ReadAsync( + buffer, + offset, + count, + cancellationToken); + } + finally + { + this.ReleaseSemaphore(); + } + } + + /// + /// Begin async write to stream. + /// + /// Offset in stream to write to. + /// The buffer to write the data from. + /// The byte offset in buffer from which to begin writing. + /// The maximum number of bytes to write. + /// Token used to cancel the asynchronous writing. + /// A task that represents the asynchronous write operation. + public async Task WriteAsync(long writeOffset, byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + await this.WaitOnSemaphoreAsync(cancellationToken); + + try + { + this.stream.Position = writeOffset; + await this.stream.WriteAsync( + buffer, + offset, + count, + cancellationToken); + } + finally + { + this.ReleaseSemaphore(); + } + } + + /// + /// Computes the hash value for the specified region of the input byte array + /// and copies the specified region of the input byte array to the specified + /// region of the output byte array. + /// + /// Offset in stream of the block on which to calculate MD5 hash. + /// The input to compute the hash code for. + /// The offset into the input byte array from which to begin using data. + /// The number of bytes in the input byte array to use as data. + /// A copy of the part of the input array used to compute the hash code. + /// The offset into the output byte array from which to begin writing data. + /// Whether succeeded in calculating MD5 hash + /// or not finished the separate thread to calculate MD5 hash at the time. + public bool MD5HashTransformBlock(long streamOffset, byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) + { + if (null == this.md5hash) + { + return true; + } + + if (!this.finishedSeparateMd5Calculator) + { + lock (this.md5hash) + { + if (!this.finishedSeparateMd5Calculator) + { + if (streamOffset == this.md5hashOffset) + { + this.md5hashOffset += inputCount; + } + + return true; + } + else + { + if (!this.succeededSeparateMd5Calculator) + { + return false; + } + } + } + } + + if (streamOffset >= this.md5hashOffset) + { + Debug.Assert( + this.finishedSeparateMd5Calculator, + "The separate thread to calculate MD5 hash should have finished or md5hashOffset should get updated."); + + this.md5hash.TransformBlock(inputBuffer, inputOffset, inputCount, outputBuffer, outputOffset); + } + + return true; + } + + /// + /// Computes the hash value for the specified region of the specified byte array. + /// + /// The input to compute the hash code for. + /// The offset into the byte array from which to begin using data. + /// The number of bytes in the byte array to use as data. + /// An array that is a copy of the part of the input that is hashed. + public byte[] MD5HashTransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount) + { + this.WaitMD5CalculationToFinish(); + + if (!this.succeededSeparateMd5Calculator) + { + return null; + } + + return null == this.md5hash ? null : this.md5hash.TransformFinalBlock(inputBuffer, inputOffset, inputCount); + } + + /// + /// Releases or resets unmanaged resources. + /// + public virtual void Dispose() + { + this.Dispose(true); + } + + /// + /// Private dispose method to release managed/unmanaged objects. + /// If disposing = true clean up managed resources as well as unmanaged resources. + /// If disposing = false only clean up unmanaged resources. + /// + /// Indicates whether or not to dispose managed resources. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (null != this.md5hash) + { + this.md5hash.Clear(); + this.md5hash = null; + } + + if (null != this.semaphore) + { + this.semaphore.Dispose(); + this.semaphore = null; + } + } + } + + /// + /// Read from stream. + /// + /// Offset in stream to read from. + /// An array of bytes. When this method returns, the buffer contains the specified + /// byte array with the values between offset and (offset + count - 1) replaced + /// by the bytes read from the current source. + /// The zero-based byte offset in buffer at which to begin storing the data read from the current stream. + /// The maximum number of bytes to be read from the current stream. + /// The total number of bytes read into the buffer. + private int Read(long readOffset, byte[] buffer, int offset, int count) + { + if (!this.finishedSeparateMd5Calculator) + { + this.semaphore.Wait(); + } + + try + { + this.stream.Position = readOffset; + int readBytes = this.stream.Read(buffer, offset, count); + + return readBytes; + } + finally + { + this.ReleaseSemaphore(); + } + } + + /// + /// Wait for one semaphore. + /// + /// Token used to cancel waiting on the semaphore. + private async Task WaitOnSemaphoreAsync(CancellationToken cancellationToken) + { + if (!this.finishedSeparateMd5Calculator) + { + await this.semaphore.WaitAsync(cancellationToken); + } + } + + /// + /// Release semaphore. + /// + private void ReleaseSemaphore() + { + if (!this.finishedSeparateMd5Calculator) + { + this.semaphore.Release(); + } + } + + /// + /// Wait for MD5 calculation to be finished. + /// In our test, MD5 calculation is really fast, + /// and SpinOnce has sleep mechanism, so use Spin instead of sleep here. + /// + private void WaitMD5CalculationToFinish() + { + if (this.finishedSeparateMd5Calculator) + { + return; + } + + SpinWait sw = new SpinWait(); + + while (!this.finishedSeparateMd5Calculator) + { + sw.SpinOnce(); + } + + sw.Reset(); + } + } +} diff --git a/lib/MemoryManager.cs b/lib/MemoryManager.cs new file mode 100644 index 00000000..04ec40f3 --- /dev/null +++ b/lib/MemoryManager.cs @@ -0,0 +1,139 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.Collections.Concurrent; + + /// + /// Class for maintaining a pool of memory buffer objects. + /// + internal class MemoryManager + { + private MemoryPool memoryPool; + + public MemoryManager( + long capacity, int bufferSize) + { + long availableCells = capacity / bufferSize; + + int cellNumber = (int)Math.Min((long)Constants.MemoryManagerCellsMaximum, availableCells); + + this.memoryPool = new MemoryPool(cellNumber, bufferSize); + } + + public byte[] RequireBuffer() + { + return this.memoryPool.GetBuffer(); + } + + public void ReleaseBuffer(byte[] buffer) + { + this.memoryPool.AddBuffer(buffer); + } + + private class MemoryPool + { + public readonly int BufferSize; + + private int availableCells; + private int allocatedCells; + private object cellsListLock; + private MemoryCell cellsListHeadCell; + private ConcurrentDictionary cellsInUse; + + public MemoryPool(int cellsCount, int bufferSize) + { + this.BufferSize = bufferSize; + + this.availableCells = cellsCount; + this.allocatedCells = 0; + this.cellsListLock = new object(); + this.cellsListHeadCell = null; + this.cellsInUse = new ConcurrentDictionary(); + } + + public byte[] GetBuffer() + { + if (this.availableCells > 0) + { + MemoryCell retCell = null; + + lock (this.cellsListLock) + { + if (this.availableCells > 0) + { + if (null != this.cellsListHeadCell) + { + retCell = this.cellsListHeadCell; + this.cellsListHeadCell = retCell.NextCell; + retCell.NextCell = null; + } + else + { + retCell = new MemoryCell(this.BufferSize); + ++this.allocatedCells; + } + + --this.availableCells; + } + } + + if (null != retCell) + { + this.cellsInUse.TryAdd(retCell.Buffer, retCell); + return retCell.Buffer; + } + } + + return null; + } + + public void AddBuffer(byte[] buffer) + { + if (null == buffer) + { + throw new ArgumentNullException("buffer"); + } + + MemoryCell cell; + if (this.cellsInUse.TryRemove(buffer, out cell)) + { + lock (this.cellsListLock) + { + cell.NextCell = this.cellsListHeadCell; + this.cellsListHeadCell = cell; + ++this.availableCells; + } + } + } + } + + private class MemoryCell + { + private byte[] buffer; + + public MemoryCell(int size) + { + this.buffer = new byte[size]; + } + + public MemoryCell NextCell + { + get; + set; + } + + public byte[] Buffer + { + get + { + return this.buffer; + } + } + } + } +} diff --git a/lib/OverwriteCallback.cs b/lib/OverwriteCallback.cs new file mode 100644 index 00000000..a3f70e65 --- /dev/null +++ b/lib/OverwriteCallback.cs @@ -0,0 +1,17 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + /// + /// Callback invoked to tell whether to overwrite an existing destination + /// + /// Path of the source file used to overwrite the destination. + /// Path of the file to be overwritten. + /// True if the file should be overwritten; otherwise false. + public delegate bool OverwriteCallback( + string sourcePath, + string destinationPath); +} diff --git a/lib/Resources.Designer.cs b/lib/Resources.Designer.cs new file mode 100644 index 00000000..e7e704fa --- /dev/null +++ b/lib/Resources.Designer.cs @@ -0,0 +1,569 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.WindowsAzure.Storage.DataMovement.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to AppendBlob. + /// + internal static string AppendBlob { + get { + return ResourceManager.GetString("AppendBlob", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copying from File Storage to append Blob Storage asynchronously is not supported.. + /// + internal static string AsyncCopyFromFileToAppendBlobNotSupportException { + get { + return ResourceManager.GetString("AsyncCopyFromFileToAppendBlobNotSupportException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copying from File Storage to page Blob Storage asynchronously is not supported.. + /// + internal static string AsyncCopyFromFileToPageBlobNotSupportException { + get { + return ResourceManager.GetString("AsyncCopyFromFileToPageBlobNotSupportException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to File size {0} is invalid for {1}, must be a multiple of {2}.. + /// + internal static string BlobFileSizeInvalidException { + get { + return ResourceManager.GetString("BlobFileSizeInvalidException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to File size {0} is larger than {1} maximum size {2}.. + /// + internal static string BlobFileSizeTooLargeException { + get { + return ResourceManager.GetString("BlobFileSizeTooLargeException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The blob transfer has been cancelled.. + /// + internal static string BlobTransferCancelledException { + get { + return ResourceManager.GetString("BlobTransferCancelledException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BlockBlob. + /// + internal static string BlockBlob { + get { + return ResourceManager.GetString("BlockBlob", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BlockSize must be between {0} and {1}.. + /// + internal static string BlockSizeOutOfRangeException { + get { + return ResourceManager.GetString("BlockSizeOutOfRangeException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot deserialize to TransferLocation when its TransferLocationType is {0}.. + /// + internal static string CannotDeserializeLocationType { + get { + return ResourceManager.GetString("CannotDeserializeLocationType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The TransferLocation cannot be serialized when it represents a stream location.. + /// + internal static string CannotSerializeStreamLocation { + get { + return ResourceManager.GetString("CannotSerializeStreamLocation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Destination of asynchronous copying must be File Storage or Blob Storage.. + /// + internal static string CanOnlyCopyToFileOrBlobException { + get { + return ResourceManager.GetString("CanOnlyCopyToFileOrBlobException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to File size {0} is larger than cloud file maximum size {1} bytes.. + /// + internal static string CloudFileSizeTooLargeException { + get { + return ResourceManager.GetString("CloudFileSizeTooLargeException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} Deserialization failed: Version number doesn't match. Version number:{1}, expect:{2}.. + /// + internal static string DeserializationVersionNotMatchException { + get { + return ResourceManager.GetString("DeserializationVersionNotMatchException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User specified blob type does not match the blob type of the existing destination blob.. + /// + internal static string DestinationBlobTypeNotMatch { + get { + return ResourceManager.GetString("DestinationBlobTypeNotMatch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Destination might be changed by other process or application.. + /// + internal static string DestinationChangedException { + get { + return ResourceManager.GetString("DestinationChangedException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Destination must be a base blob.. + /// + internal static string DestinationMustBeBaseBlob { + get { + return ResourceManager.GetString("DestinationMustBeBaseBlob", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The MD5 hash calculated from the downloaded data does not match the MD5 hash stored in the property of source: {0}. Please refer to help or documentation for detail. + ///MD5 calculated: {1} + ///MD5 in property: {2}. + /// + internal static string DownloadedMd5MismatchException { + get { + return ResourceManager.GetString("DownloadedMd5MismatchException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to allocate required memory.. + /// + internal static string FailedToAllocateMemoryException { + get { + return ResourceManager.GetString("FailedToAllocateMemoryException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to copy from "{0}" to "{1}". Copy status: {2}; Description: {3}.. + /// + internal static string FailedToAsyncCopyObjectException { + get { + return ResourceManager.GetString("FailedToAsyncCopyObjectException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to retrieve the original BlobType.. + /// + internal static string FailedToGetBlobTypeException { + get { + return ResourceManager.GetString("FailedToGetBlobTypeException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to open file {0}: {1}.. + /// + internal static string FailedToOpenFileException { + get { + return ResourceManager.GetString("FailedToOpenFileException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to retrieve CopyState for object "{0}".. + /// + internal static string FailedToRetrieveCopyStateForObjectException { + get { + return ResourceManager.GetString("FailedToRetrieveCopyStateForObjectException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The initial entry status {0} is invalid for {1}.. + /// + internal static string InvalidInitialEntryStatusForControllerException { + get { + return ResourceManager.GetString("InvalidInitialEntryStatusForControllerException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Both Source and Destination are locally accessible locations. At least one of source and destination should be an Azure Storage location.. + /// + internal static string LocalToLocalTransferUnsupportedException { + get { + return ResourceManager.GetString("LocalToLocalTransferUnsupportedException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The local copy id is different from the one returned from the server.. + /// + internal static string MismatchFoundBetweenLocalAndServerCopyIdsException { + get { + return ResourceManager.GetString("MismatchFoundBetweenLocalAndServerCopyIdsException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Blob type '{0}' is not supported.. + /// + internal static string NotSupportedBlobType { + get { + return ResourceManager.GetString("NotSupportedBlobType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Skiped file "{0}" because target "{1}" already exists.. + /// + internal static string OverwriteCallbackCancelTransferException { + get { + return ResourceManager.GetString("OverwriteCallbackCancelTransferException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PageBlob. + /// + internal static string PageBlob { + get { + return ResourceManager.GetString("PageBlob", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parallel operations count must be positive.. + /// + internal static string ParallelCountNotPositiveException { + get { + return ResourceManager.GetString("ParallelCountNotPositiveException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} cannot be null.. + /// + internal static string ParameterCannotBeNullException { + get { + return ResourceManager.GetString("ParameterCannotBeNullException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Exactly one of these parameters must be provided: {0}, {1}, {2}.. + /// + internal static string ProvideExactlyOneOfThreeParameters { + get { + return ResourceManager.GetString("ProvideExactlyOneOfThreeParameters", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0:0.##} bytes. + /// + internal static string ReadableSizeFormatBytes { + get { + return ResourceManager.GetString("ReadableSizeFormatBytes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0:0.##}EB. + /// + internal static string ReadableSizeFormatExaBytes { + get { + return ResourceManager.GetString("ReadableSizeFormatExaBytes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0:0.##}GB. + /// + internal static string ReadableSizeFormatGigaBytes { + get { + return ResourceManager.GetString("ReadableSizeFormatGigaBytes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0:0.##}KB. + /// + internal static string ReadableSizeFormatKiloBytes { + get { + return ResourceManager.GetString("ReadableSizeFormatKiloBytes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0:0.##}MB. + /// + internal static string ReadableSizeFormatMegaBytes { + get { + return ResourceManager.GetString("ReadableSizeFormatMegaBytes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0:0.##}PB. + /// + internal static string ReadableSizeFormatPetaBytes { + get { + return ResourceManager.GetString("ReadableSizeFormatPetaBytes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0:0.##}TB. + /// + internal static string ReadableSizeFormatTeraBytes { + get { + return ResourceManager.GetString("ReadableSizeFormatTeraBytes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to read restartable info from file.. + /// + internal static string RestartableInfoCorruptedException { + get { + return ResourceManager.GetString("RestartableInfoCorruptedException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MaximumCacheSize cannot be less than {0}.. + /// + internal static string SmallMemoryCacheSizeLimitationException { + get { + return ResourceManager.GetString("SmallMemoryCacheSizeLimitationException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Blob type of source and destination must be the same.. + /// + internal static string SourceAndDestinationBlobTypeDifferent { + get { + return ResourceManager.GetString("SourceAndDestinationBlobTypeDifferent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Source and destination cannot be the same.. + /// + internal static string SourceAndDestinationLocationCannotBeEqualException { + get { + return ResourceManager.GetString("SourceAndDestinationLocationCannotBeEqualException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Source blob does not exist.. + /// + internal static string SourceBlobDoesNotExistException { + get { + return ResourceManager.GetString("SourceBlobDoesNotExistException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User specified blob type does not match the blob type of the existing source blob.. + /// + internal static string SourceBlobTypeNotMatch { + get { + return ResourceManager.GetString("SourceBlobTypeNotMatch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Source does not exist.. + /// + internal static string SourceDoesNotExistException { + get { + return ResourceManager.GetString("SourceDoesNotExistException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} must support Read.. + /// + internal static string StreamMustSupportReadException { + get { + return ResourceManager.GetString("StreamMustSupportReadException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} must support Seek.. + /// + internal static string StreamMustSupportSeekException { + get { + return ResourceManager.GetString("StreamMustSupportSeekException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} must support Write.. + /// + internal static string StreamMustSupportWriteException { + get { + return ResourceManager.GetString("StreamMustSupportWriteException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The stream is not expandable.. + /// + internal static string StreamNotExpandable { + get { + return ResourceManager.GetString("StreamNotExpandable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copying from uri to Azure Blob Storage synchronously is not supported.. + /// + internal static string SyncCopyFromUriToAzureBlobNotSupportedException { + get { + return ResourceManager.GetString("SyncCopyFromUriToAzureBlobNotSupportedException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copying from uri to Azure File Storage synchronously is not supported.. + /// + internal static string SyncCopyFromUriToAzureFileNotSupportedException { + get { + return ResourceManager.GetString("SyncCopyFromUriToAzureFileNotSupportedException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A transfer operation with the same source and destination already exists.. + /// + internal static string TransferAlreadyExists { + get { + return ResourceManager.GetString("TransferAlreadyExists", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TransferEntry.CopyId cannot be null or empty because we need it to verify we are monitoring the right blob copying process.. + /// + internal static string TransferEntryCopyIdCannotBeNullOrEmptyException { + get { + return ResourceManager.GetString("TransferEntryCopyIdCannotBeNullOrEmptyException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The transfer failed.. + /// + internal static string UncategorizedException { + get { + return ResourceManager.GetString("UncategorizedException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The given blob type {0} is not supported.. + /// + internal static string UnsupportedBlobTypeException { + get { + return ResourceManager.GetString("UnsupportedBlobTypeException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The given transfer location type {0} is not supported.. + /// + internal static string UnsupportedTransferLocationException { + get { + return ResourceManager.GetString("UnsupportedTransferLocationException", resourceCulture); + } + } + } +} diff --git a/lib/Resources.resx b/lib/Resources.resx new file mode 100644 index 00000000..9e7ab94d --- /dev/null +++ b/lib/Resources.resx @@ -0,0 +1,322 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + File size {0} is invalid for {1}, must be a multiple of {2}. + {0} is file size. {1} is the destination blob type. {2} should be 512 bytes for pageblob. + + + File size {0} is larger than {1} maximum size {2}. + {0} is file size. {1} is the destination blob type. {2} is the size limit of the destination. + + + The blob transfer has been cancelled. + + + BlockSize must be between {0} and {1}. + + + Cannot deserialize to TransferLocation when its TransferLocationType is {0}. + + + The TransferLocation cannot be serialized when it represents a stream location. + + + File size {0} is larger than cloud file maximum size {1} bytes. + {0} is file size. {1} is the size limit of the destination. + + + Destination of asynchronous copying must be File Storage or Blob Storage. + + + Copying from File Storage to page Blob Storage asynchronously is not supported. + + + {0} Deserialization failed: Version number doesn't match. Version number:{1}, expect:{2}. + {0} is the class name. + {1} is the version number in serialization binary. + {2} is the expect version number. + + + Destination might be changed by other process or application. + + + The MD5 hash calculated from the downloaded data does not match the MD5 hash stored in the property of source: {0}. Please refer to help or documentation for detail. +MD5 calculated: {1} +MD5 in property: {2} + {0} is the uri of source, {1} is the calculated MD5, {2} is the MD5 stored in the source property + + + User specified blob type does not match the blob type of the existing destination blob. + + + Destination must be a base blob. + + + Failed to allocate required memory. + + + Failed to copy from "{0}" to "{1}". Copy status: {2}; Description: {3}. + {0} is uri of source, {1} is uri of destination. {2} is the copy status, {3} is the copy status description. + + + Failed to retrieve CopyState for object "{0}". + {0} is uri of target object. + + + Failed to retrieve the original BlobType. + + + Failed to open file {0}: {1}. + {0} is file name, {1} is detailed error message. + + + The initial entry status {0} is invalid for {1}. + {0} is the initial entry status, {1} is the controller. + + + Both Source and Destination are locally accessible locations. At least one of source and destination should be an Azure Storage location. + + + The local copy id is different from the one returned from the server. + + + Blob type '{0}' is not supported. + {0} is the blob type name. + + + Skiped file "{0}" because target "{1}" already exists. + {0} is source file name, {1} is destination file name. + + + Parallel operations count must be positive. + + + {0} cannot be null. + {0} is the property or parameter name. + + + Exactly one of these parameters must be provided: {0}, {1}, {2}. + {0} is the first parameter, {1} is the second one, {2} is the third one. + + + {0:0.##} bytes + {0: -> take value from the first parameter. +0.##} ->Display at least 1 digit before the decimal point and up to 2 digits after the decimal point. + + + {0:0.##}EB + {0: -> take value from the first parameter. +0.##} ->Display at least 1 digit before the decimal point and up to 2 digits after the decimal point. + + + {0:0.##}GB + {0: -> take value from the first parameter. +0.##} ->Display at least 1 digit before the decimal point and up to 2 digits after the decimal point. + + + {0:0.##}KB + {0: -> take value from the first parameter. +0.##} ->Display at least 1 digit before the decimal point and up to 2 digits after the decimal point. + + + {0:0.##}MB + {0: -> take value from the first parameter. +0.##} ->Display at least 1 digit before the decimal point and up to 2 digits after the decimal point. + + + {0:0.##}PB + {0: -> take value from the first parameter. +0.##} ->Display at least 1 digit before the decimal point and up to 2 digits after the decimal point. + + + {0:0.##}TB + {0: -> take value from the first parameter. +0.##} ->Display at least 1 digit before the decimal point and up to 2 digits after the decimal point. + + + Failed to read restartable info from file. + + + MaximumCacheSize cannot be less than {0}. + {0} is minimum memory cache size limitation + + + Blob type of source and destination must be the same. + + + Source and destination cannot be the same. + + + Source does not exist. + + + Source blob does not exist. + + + User specified blob type does not match the blob type of the existing source blob. + + + {0} must support Read. + + + {0} must support Seek. + + + {0} must support Write. + + + The stream is not expandable. + + + TransferEntry.CopyId cannot be null or empty because we need it to verify we are monitoring the right blob copying process. + + + The given blob type {0} is not supported. + {0} is the given blob type. + + + The given transfer location type {0} is not supported. + {0} is the given transfer location type. + + + Copying from File Storage to append Blob Storage asynchronously is not supported. + + + AppendBlob + + + BlockBlob + + + PageBlob + + + Copying from uri to Azure Blob Storage synchronously is not supported. + + + Copying from uri to Azure File Storage synchronously is not supported. + + + A transfer operation with the same source and destination already exists. + + + The transfer failed. + + \ No newline at end of file diff --git a/lib/SerializationHelper/SerializableAccessCondition.cs b/lib/SerializationHelper/SerializableAccessCondition.cs new file mode 100644 index 00000000..88650747 --- /dev/null +++ b/lib/SerializationHelper/SerializableAccessCondition.cs @@ -0,0 +1,161 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement.SerializationHelper +{ + using System; + using System.Runtime.Serialization; + + [Serializable] + internal sealed class SerializableAccessCondition : ISerializable + { + private const string IfMatchETagName = "IfMatchETag"; + private const string IfModifiedSinceTimeName = "IfModifiedSinceTime"; + private const string IfNoneMatchETagName = "IfNoneMatchETag"; + private const string IfNotModifiedSinceTimeName = "IfNotModifiedSinceTime"; + private const string IfSequenceNumberEqualName = "IfSequenceNumberEqual"; + private const string IfSequenceNumberLessThanName = "IfSequenceNumberLessThan"; + private const string IfSequenceNumberLessThanOrEqualName = "IfSequenceNumberLessThanOrEqual"; + private const string LeaseIdName = "LeaseId"; + + private AccessCondition accessCondition; + + public SerializableAccessCondition() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Serialization information. + /// Streaming context. + private SerializableAccessCondition(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new ArgumentNullException("info"); + } + + string ifMatchETag = info.GetString(IfMatchETagName); + DateTimeOffset? ifModifiedSinceTime = (DateTimeOffset?)info.GetValue(IfModifiedSinceTimeName, typeof(DateTimeOffset?)); + string ifNoneMatchETag = info.GetString(IfNoneMatchETagName); + DateTimeOffset? ifNotModifiedSinceTime = (DateTimeOffset?)info.GetValue(IfNotModifiedSinceTimeName, typeof(DateTimeOffset?)); + long? ifSequenceNumberEqual = (long?)info.GetValue(IfSequenceNumberEqualName, typeof(long?)); + long? ifSequenceNumberLessThan = (long?)info.GetValue(IfSequenceNumberLessThanName, typeof(long?)); + long? ifSequenceNumberLessThanOrEqual = (long?)info.GetValue(IfSequenceNumberLessThanOrEqualName, typeof(long?)); + string leaseId = info.GetString(LeaseIdName); + + if (!string.IsNullOrEmpty(ifMatchETag) + || null != ifModifiedSinceTime + || !string.IsNullOrEmpty(ifNoneMatchETag) + || null != ifNotModifiedSinceTime + || null != ifSequenceNumberEqual + || null != ifSequenceNumberLessThan + || null != ifSequenceNumberLessThanOrEqual + || !string.IsNullOrEmpty(leaseId)) + { + this.accessCondition = new AccessCondition() + { + IfMatchETag = ifMatchETag, + IfModifiedSinceTime = ifModifiedSinceTime, + IfNoneMatchETag = ifNoneMatchETag, + IfNotModifiedSinceTime = ifNotModifiedSinceTime, + IfSequenceNumberEqual = ifSequenceNumberEqual, + IfSequenceNumberLessThan = ifSequenceNumberLessThan, + IfSequenceNumberLessThanOrEqual = ifSequenceNumberLessThanOrEqual, + LeaseId = leaseId + }; + } + else + { + this.accessCondition = null; + } + } + + internal AccessCondition AccessCondition + { + get + { + return this.accessCondition; + } + + set + { + this.accessCondition = value; + } + } + + /// + /// Serializes the object. + /// + /// Serialization info object. + /// Streaming context. + public void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new ArgumentNullException("info"); + } + + if (null == this.accessCondition) + { + info.AddValue(IfMatchETagName, null); + info.AddValue(IfModifiedSinceTimeName, null); + info.AddValue(IfNoneMatchETagName, null); + info.AddValue(IfNotModifiedSinceTimeName, null); + info.AddValue(IfSequenceNumberEqualName, null); + info.AddValue(IfSequenceNumberLessThanName, null); + info.AddValue(IfSequenceNumberLessThanOrEqualName, null); + info.AddValue(LeaseIdName, null); + } + else + { + + info.AddValue(IfMatchETagName, this.accessCondition.IfMatchETag); + info.AddValue(IfModifiedSinceTimeName, this.accessCondition.IfModifiedSinceTime); + info.AddValue(IfNoneMatchETagName, this.accessCondition.IfNoneMatchETag); + info.AddValue(IfNotModifiedSinceTimeName, this.accessCondition.IfNotModifiedSinceTime); + info.AddValue(IfSequenceNumberEqualName, this.accessCondition.IfSequenceNumberEqual); + info.AddValue(IfSequenceNumberLessThanName, this.accessCondition.IfSequenceNumberLessThan); + info.AddValue(IfSequenceNumberLessThanOrEqualName, this.accessCondition.IfSequenceNumberLessThanOrEqual); + info.AddValue(LeaseIdName, this.accessCondition.LeaseId); + } + } + + internal static AccessCondition GetAccessCondition(SerializableAccessCondition serialization) + { + if (null == serialization) + { + return null; + } + + return serialization.AccessCondition; + } + + internal static void SetAccessCondition( + ref SerializableAccessCondition serialization, + AccessCondition value) + { + if ((null == serialization) + && (null == value)) + { + return; + } + + if (null != serialization) + { + serialization.AccessCondition = value; + } + else + { + serialization = new SerializableAccessCondition() + { + AccessCondition = value + }; + } + } + } +} diff --git a/lib/SerializationHelper/SerializableBlobRequestOptions.cs b/lib/SerializationHelper/SerializableBlobRequestOptions.cs new file mode 100644 index 00000000..c886d84e --- /dev/null +++ b/lib/SerializationHelper/SerializableBlobRequestOptions.cs @@ -0,0 +1,105 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement.SerializationHelper +{ + using System; + using System.Diagnostics; + using System.Runtime.Serialization; + using Microsoft.WindowsAzure.Storage.Blob; + + [Serializable] + internal sealed class SerializableBlobRequestOptions : SerializableRequestOptions + { + private const string DisableContentMD5ValidationName = "DisableContentMD5Validation"; + private const string MaximumExecutionTimeName = "MaximumExecutionTime"; + private const string ServerTimeoutName = "ServerTimeout"; + private const string StoreBlobContentMD5Name = "StoreBlobContentMD5"; + private const string UseTransactionalMD5Name = "UseTransactionalMD5"; + + private BlobRequestOptions blobRequestOptions; + + public SerializableBlobRequestOptions() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Serialization information. + /// Streaming context. + private SerializableBlobRequestOptions(SerializationInfo info, StreamingContext context) + : base(info, context) + { + bool? disableContentMD5Validation = (bool?)info.GetValue(DisableContentMD5ValidationName, typeof(bool?)); + TimeSpan? maximumExecutionTime = (TimeSpan?)info.GetValue(MaximumExecutionTimeName, typeof(TimeSpan?)); + TimeSpan? serverTimeout = (TimeSpan?)info.GetValue(ServerTimeoutName, typeof(TimeSpan?)); + bool? storeBlobContentMD5 = (bool?)info.GetValue(StoreBlobContentMD5Name, typeof(bool?)); + bool? useTransactionalMD5 = (bool?)info.GetValue(UseTransactionalMD5Name, typeof(bool?)); + + if (null != disableContentMD5Validation + || null != maximumExecutionTime + || null != serverTimeout + || null != storeBlobContentMD5 + || null != useTransactionalMD5) + { + this.blobRequestOptions = Transfer_RequestOptions.DefaultBlobRequestOptions; + + this.blobRequestOptions.DisableContentMD5Validation = disableContentMD5Validation; + this.blobRequestOptions.MaximumExecutionTime = maximumExecutionTime; + this.blobRequestOptions.ServerTimeout = serverTimeout; + this.blobRequestOptions.StoreBlobContentMD5 = storeBlobContentMD5; + this.blobRequestOptions.UseTransactionalMD5 = useTransactionalMD5; + } + else + { + this.blobRequestOptions = null; + } + } + + protected override IRequestOptions RequestOptions + { + get + { + return this.blobRequestOptions; + } + + set + { + BlobRequestOptions requestOptions = value as BlobRequestOptions; + Debug.Assert(null != requestOptions, "Setting RequestOptions in BlobRequestOptionsSerializer, but the value is not a BlobRequestOptions instance."); + this.blobRequestOptions = requestOptions; + } + } + + /// + /// Serializes the object. + /// + /// Serialization info object. + /// Streaming context. + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + + if (null == this.blobRequestOptions) + { + info.AddValue(DisableContentMD5ValidationName, null); + info.AddValue(MaximumExecutionTimeName, null, typeof(TimeSpan?)); + info.AddValue(ServerTimeoutName, null, typeof(TimeSpan?)); + info.AddValue(StoreBlobContentMD5Name, null); + info.AddValue(UseTransactionalMD5Name, null); + } + else + { + info.AddValue(DisableContentMD5ValidationName, this.blobRequestOptions.DisableContentMD5Validation); + info.AddValue(MaximumExecutionTimeName, this.blobRequestOptions.MaximumExecutionTime, typeof(TimeSpan?)); + info.AddValue(ServerTimeoutName, this.blobRequestOptions.ServerTimeout, typeof(TimeSpan?)); + info.AddValue(StoreBlobContentMD5Name, this.blobRequestOptions.StoreBlobContentMD5); + info.AddValue(UseTransactionalMD5Name, this.blobRequestOptions.UseTransactionalMD5); + } + } + } +} diff --git a/lib/SerializationHelper/SerializableCloudBlob.cs b/lib/SerializationHelper/SerializableCloudBlob.cs new file mode 100644 index 00000000..45ece249 --- /dev/null +++ b/lib/SerializationHelper/SerializableCloudBlob.cs @@ -0,0 +1,134 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement.SerializationHelper +{ + using System; + using System.Globalization; + using System.Runtime.Serialization; + using Microsoft.WindowsAzure.Storage.Auth; + using Microsoft.WindowsAzure.Storage.Blob; + + [Serializable] + internal class SerializableCloudBlob : ISerializable + { + private const string BlobUriName = "BlobUri"; + private const string BlobTypeName = "BlobType"; + + private Uri blobUri; + + private BlobType blobType; + + private CloudBlob blob; + + public SerializableCloudBlob() + { + } + + private SerializableCloudBlob(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new ArgumentNullException("info"); + } + + this.blobUri = (Uri)info.GetValue(BlobUriName, typeof(Uri)); + this.blobType = (BlobType)info.GetValue(BlobTypeName, typeof(BlobType)); + this.CreateCloudBlobInstance(null); + } + + internal CloudBlob Blob + { + get + { + return this.blob; + } + + set + { + this.blob = value; + + if (null == this.blob) + { + this.blobUri = null; + this.blobType = BlobType.Unspecified; + } + else + { + this.blobUri = this.blob.SnapshotQualifiedUri; + this.blobType = this.blob.BlobType; + } + } + } + + public void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new ArgumentNullException("info"); + } + + info.AddValue(BlobUriName, this.blobUri, typeof(Uri)); + info.AddValue(BlobTypeName, this.blobType); + } + + internal static CloudBlob GetBlob(SerializableCloudBlob blobSerialization) + { + if (null == blobSerialization) + { + return null; + } + + return blobSerialization.Blob; + } + + internal static void SetBlob(ref SerializableCloudBlob blobSerialization, CloudBlob value) + { + if ((null == blobSerialization) + && (null == value)) + { + return; + } + + if (null != blobSerialization) + { + blobSerialization.Blob = value; + } + else + { + blobSerialization = new SerializableCloudBlob() + { + Blob = value + }; + } + } + + internal void UpdateStorageCredentials(StorageCredentials credentials) + { + this.CreateCloudBlobInstance(credentials); + } + + private void CreateCloudBlobInstance(StorageCredentials credentials) + { + if ((null != this.blob) + && this.blob.ServiceClient.Credentials == credentials) + { + return; + } + + if (null == this.blobUri) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + Resources.ParameterCannotBeNullException, + "blobUri")); + } + + this.blob = Utils.GetBlobReference(this.blobUri, credentials, this.blobType); + } + } +} diff --git a/lib/SerializationHelper/SerializableCloudFile.cs b/lib/SerializationHelper/SerializableCloudFile.cs new file mode 100644 index 00000000..cbc2773f --- /dev/null +++ b/lib/SerializationHelper/SerializableCloudFile.cs @@ -0,0 +1,127 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement.SerializationHelper +{ + using System; + using System.Diagnostics; + using System.Globalization; + using System.Runtime.Serialization; + using Microsoft.WindowsAzure.Storage.Auth; + using Microsoft.WindowsAzure.Storage.File; + + [Serializable] + internal class SerializableCloudFile : ISerializable + { + private const string FileUriName = "FileUri"; + + private Uri fileUri; + + private CloudFile file; + + public SerializableCloudFile() + { + } + + private SerializableCloudFile(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new ArgumentNullException("info"); + } + + this.fileUri = (Uri)info.GetValue(FileUriName, typeof(Uri)); + this.CreateCloudFileInstance(null); + } + + internal CloudFile File + { + get + { + return this.file; + } + + set + { + this.file = value; + + if (null == this.file) + { + this.fileUri = null; + } + else + { + this.fileUri = this.file.Uri; + } + } + } + + public void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new ArgumentNullException("info"); + } + + info.AddValue(FileUriName, this.fileUri, typeof(Uri)); + } + + internal static CloudFile GetFile(SerializableCloudFile fileSerialization) + { + if (null == fileSerialization) + { + return null; + } + + return fileSerialization.File; + } + + internal static void SetFile(ref SerializableCloudFile fileSerialization, CloudFile value) + { + if (null == fileSerialization + && null == value) + { + return; + } + + if (null != fileSerialization) + { + fileSerialization.File = value; + } + else + { + fileSerialization = new SerializableCloudFile() + { + File = value + }; + } + } + + internal void UpdateStorageCredentials(StorageCredentials credentials) + { + this.CreateCloudFileInstance(credentials); + } + + private void CreateCloudFileInstance(StorageCredentials credentials) + { + if (null != this.file + && this.file.ServiceClient.Credentials == credentials) + { + return; + } + + if (null == this.fileUri) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + Resources.ParameterCannotBeNullException, + "fileUri")); + } + + this.file = new CloudFile(this.fileUri, credentials); + } + } +} diff --git a/lib/SerializationHelper/SerializableFileRequestOptions.cs b/lib/SerializationHelper/SerializableFileRequestOptions.cs new file mode 100644 index 00000000..03014954 --- /dev/null +++ b/lib/SerializationHelper/SerializableFileRequestOptions.cs @@ -0,0 +1,111 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement.SerializationHelper +{ + using System; + using System.Diagnostics; + using System.Runtime.Serialization; + using Microsoft.WindowsAzure.Storage.File; + + /// + /// Define class to serialize FileRequestOptions instance. + /// + [Serializable] + internal sealed class SerializableFileRequestOptions : SerializableRequestOptions, ISerializable + { + private const string DisableContentMD5ValidationName = "DisableContentMD5Validation"; + private const string MaximumExecutionTimeName = "MaximumExecutionTime"; + private const string ServerTimeoutName = "ServerTimeout"; + private const string StoreFileContentMD5Name = "StoreFileContentMD5"; + private const string UseTransactionalMD5Name = "UseTransactionalMD5"; + + private FileRequestOptions fileRequestOptions; + + /// + /// Initializes a new instance of the class. + /// + public SerializableFileRequestOptions() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Serialization information. + /// Streaming context. + private SerializableFileRequestOptions(SerializationInfo info, StreamingContext context) + : base (info, context) + { + bool? disableContentMD5Validation = (bool?)info.GetValue(DisableContentMD5ValidationName, typeof(bool?)); + TimeSpan? maximumExecutionTime = (TimeSpan?)info.GetValue(MaximumExecutionTimeName, typeof(TimeSpan?)); + TimeSpan? serverTimeout = (TimeSpan?)info.GetValue(ServerTimeoutName, typeof(TimeSpan?)); + bool? storeFileContentMD5 = (bool?)info.GetValue(StoreFileContentMD5Name, typeof(bool?)); + bool? useTransactionalMD5 = (bool?)info.GetValue(UseTransactionalMD5Name, typeof(bool?)); + + if (null != disableContentMD5Validation + || null != maximumExecutionTime + || null != serverTimeout + || null != storeFileContentMD5 + || null != useTransactionalMD5) + { + this.fileRequestOptions = Transfer_RequestOptions.DefaultFileRequestOptions; + + this.fileRequestOptions.DisableContentMD5Validation = disableContentMD5Validation; + this.fileRequestOptions.MaximumExecutionTime = maximumExecutionTime; + this.fileRequestOptions.ServerTimeout = serverTimeout; + this.fileRequestOptions.StoreFileContentMD5 = storeFileContentMD5; + this.fileRequestOptions.UseTransactionalMD5 = useTransactionalMD5; + } + else + { + this.fileRequestOptions = null; + } + } + + protected override IRequestOptions RequestOptions + { + get + { + return this.fileRequestOptions; + } + + set + { + FileRequestOptions requestOptions = value as FileRequestOptions; + Debug.Assert(null != requestOptions, "Setting RequestOptions in FlobRequestOptionsSerializer, but the value is not a FileRequestOptions instance."); + + this.fileRequestOptions = requestOptions; + } + } + + /// + /// Serializes the object. + /// + /// Serialization info object. + /// Streaming context. + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + + if (null == this.fileRequestOptions) + { + info.AddValue(DisableContentMD5ValidationName, null); + info.AddValue(MaximumExecutionTimeName, null, typeof(TimeSpan?)); + info.AddValue(ServerTimeoutName, null, typeof(TimeSpan?)); + info.AddValue(StoreFileContentMD5Name, null); + info.AddValue(UseTransactionalMD5Name, null); + } + else + { + info.AddValue(DisableContentMD5ValidationName, this.fileRequestOptions.DisableContentMD5Validation); + info.AddValue(MaximumExecutionTimeName, this.fileRequestOptions.MaximumExecutionTime, typeof(TimeSpan?)); + info.AddValue(ServerTimeoutName, this.fileRequestOptions.ServerTimeout, typeof(TimeSpan?)); + info.AddValue(StoreFileContentMD5Name, this.fileRequestOptions.StoreFileContentMD5); + info.AddValue(UseTransactionalMD5Name, this.fileRequestOptions.UseTransactionalMD5); + } + } + } +} diff --git a/lib/SerializationHelper/SerializableRequestOptions.cs b/lib/SerializationHelper/SerializableRequestOptions.cs new file mode 100644 index 00000000..5424c771 --- /dev/null +++ b/lib/SerializationHelper/SerializableRequestOptions.cs @@ -0,0 +1,111 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement.SerializationHelper +{ + using System; + using System.Diagnostics; + using System.Runtime.Serialization; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.File; + + [Serializable] + internal abstract class SerializableRequestOptions : ISerializable + { + protected SerializableRequestOptions() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Serialization information. + /// Streaming context. + protected SerializableRequestOptions(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new System.ArgumentNullException("info"); + } + } + + abstract protected IRequestOptions RequestOptions + { + get; + set; + } + + /// + /// Serializes the object. + /// + /// Serialization info object. + /// Streaming context. + public virtual void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new System.ArgumentNullException("info"); + } + } + + internal static IRequestOptions GetRequestOptions(SerializableRequestOptions serializer) + { + if (null == serializer) + { + return null; + } + + return serializer.RequestOptions; + } + + internal static void SetRequestOptions(ref SerializableRequestOptions serializer, IRequestOptions requestOptions) + { + if (null == serializer && null == requestOptions) + { + return; + } + + if (null == serializer) + { + serializer = CreateSerializableRequestOptions(requestOptions); + } + else + { + if ((requestOptions is FileRequestOptions) + && (serializer is SerializableBlobRequestOptions)) + { + serializer = new SerializableFileRequestOptions(); + } + else if ((requestOptions is BlobRequestOptions) + && (serializer is SerializableFileRequestOptions)) + { + serializer = new SerializableBlobRequestOptions(); + } + + serializer.RequestOptions = requestOptions; + } + } + + private static SerializableRequestOptions CreateSerializableRequestOptions(IRequestOptions requestOptions) + { + if (requestOptions is FileRequestOptions) + { + return new SerializableFileRequestOptions() + { + RequestOptions = requestOptions + }; + } + else + { + Debug.Assert(requestOptions is BlobRequestOptions, "Request options should be an instance of BlobRequestOptions when code reach here."); + return new SerializableBlobRequestOptions() + { + RequestOptions = requestOptions + }; + } + } + } +} diff --git a/lib/TransferCheckpoint.cs b/lib/TransferCheckpoint.cs new file mode 100644 index 00000000..faf9e7c9 --- /dev/null +++ b/lib/TransferCheckpoint.cs @@ -0,0 +1,139 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Runtime.Serialization; + using TransferKey = System.Tuple; + + /// + /// Represents a checkpoint from which a transfer may be resumed and continue. + /// + [Serializable] + public class TransferCheckpoint : ISerializable + { + private const string SingleObjectTransfersName = "SingleObjectTransfers"; + + /// + /// Transfers associated with this transfer checkpoint. + /// + private ConcurrentDictionary transfers = new ConcurrentDictionary(); + + /// + /// Initializes a new instance of the class. + /// + internal TransferCheckpoint() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Serialization information. + /// Streaming context. + protected TransferCheckpoint(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new System.ArgumentNullException("info"); + } + + var singleObjectTransfers = (List)info.GetValue(SingleObjectTransfersName, typeof(List)); + foreach(var transfer in singleObjectTransfers) + { + this.AddTransfer(transfer); + } + } + + + /// + /// Gets a list of all transfers + /// + internal ICollection AllTransfers + { + get + { + return this.transfers.Values; + } + } + + /// + /// Serializes the checkpoint. + /// + /// Serialization info object. + /// Streaming context. + public virtual void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new ArgumentNullException("info"); + } + + List singleObjectTransfers = new List(); + foreach(var kvPair in this.transfers) + { + SingleObjectTransfer transfer = kvPair.Value as SingleObjectTransfer; + if (transfer != null) + { + singleObjectTransfers.Add(transfer); + } + } + + info.AddValue(SingleObjectTransfersName, singleObjectTransfers, typeof(List)); + } + + /// + /// Adds a transfer to the transfer checkpoint. + /// + /// The transfer to be kept track of. + internal void AddTransfer(Transfer transfer) + { + this.transfers.TryAdd(new TransferKey(transfer.Source, transfer.Destination), transfer); + } + + /// + /// Gets a transfer with the specified source location, destination location and transfer method. + /// + /// Source location of the transfer. + /// Destination location of the transfer. + /// Transfer method. + /// A transfer that matches the specified source location, destination location and transfer method; Or null if no matches. + internal Transfer GetTransfer(TransferLocation sourceLocation, TransferLocation destLocation, TransferMethod transferMethod) + { + Transfer transfer = null; + if (this.transfers.TryGetValue(new TransferKey(sourceLocation, destLocation), out transfer)) + { + if (transfer.TransferMethod == transferMethod) + { + return transfer; + } + } + + return null; + } + + /// + /// Gets a static snapshot of this transfer checkpoint + /// + /// A snapshot of current transfer checkpoint + internal TransferCheckpoint Copy() + { + TransferCheckpoint copyObj = new TransferCheckpoint(); + foreach (var kvPair in this.transfers) + { + SingleObjectTransfer transfer = kvPair.Value as SingleObjectTransfer; + if (transfer != null) + { + copyObj.AddTransfer(transfer.Copy()); + } + } + + return copyObj; + } + } +} diff --git a/lib/TransferConfigurations.cs b/lib/TransferConfigurations.cs new file mode 100644 index 00000000..ce3284e4 --- /dev/null +++ b/lib/TransferConfigurations.cs @@ -0,0 +1,156 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.Globalization; + using System.Reflection; + using ClientLibraryConstants = Microsoft.WindowsAzure.Storage.Shared.Protocol.Constants; + + /// + /// TransferConfigurations class. + /// + public class TransferConfigurations + { + /// + /// Stores the BlockSize to use for Windows Azure Storage transfers. + /// + private int blockSize; + + /// + /// How many work items to process in parallel. + /// + private int parallelOperations; + + /// + /// Maximum amount of cache memory to use in bytes. + /// + private long maximumCacheSize; + + /// + /// Instance to call native methods to get current memory status. + /// + private GlobalMemoryStatusNativeMethods memStatus = new GlobalMemoryStatusNativeMethods(); + + /// + /// Initializes a new instance of the + /// class. + /// + public TransferConfigurations() + { + // setup default values. + this.ParallelOperations = Environment.ProcessorCount * 8; + this.BlockSize = Constants.DefaultBlockSize; + } + + /// + /// Gets or sets a value indicating how many work items to process + /// concurrently. Downloading or uploading a single blob can consist + /// of a large number of work items. + /// + /// How many work items to process concurrently. + public int ParallelOperations + { + get + { + return this.parallelOperations; + } + + set + { + if (value <= 0) + { + throw new ArgumentException(string.Format( + CultureInfo.CurrentCulture, + Resources.ParallelCountNotPositiveException)); + } + + this.parallelOperations = value; + this.SetMaxMemoryCacheSize(); + } + } + + /// + /// Gets or sets the user agent suffix + /// + public string UserAgentSuffix + { + get; + set; + } + + /// + /// Gets or sets a value indicating how much memory we can cache + /// during upload/download. + /// + /// Maximum amount of cache memory to use in bytes. + internal long MaximumCacheSize + { + get + { + return this.maximumCacheSize; + } + + set + { + if (value < Constants.MaxBlockSize) + { + throw new ArgumentException(string.Format( + CultureInfo.CurrentCulture, + Resources.SmallMemoryCacheSizeLimitationException, + Utils.BytesToHumanReadableSize(Constants.MaxBlockSize))); + } + + this.maximumCacheSize = value; + } + } + + /// + /// Gets or sets the BlockSize to use for Windows Azure Storage transfers. + /// + /// BlockSize to use for Windows Azure Storage transfers. + internal int BlockSize + { + get + { + return this.blockSize; + } + + set + { + if (Constants.MinBlockSize > value || value > Constants.MaxBlockSize) + { + string errorMessage = string.Format( + CultureInfo.CurrentCulture, + Resources.BlockSizeOutOfRangeException, + Utils.BytesToHumanReadableSize(Constants.MinBlockSize), + Utils.BytesToHumanReadableSize(Constants.MaxBlockSize)); + + throw new ArgumentOutOfRangeException("value", value, errorMessage); + } + + this.blockSize = value; + } + } + + private void SetMaxMemoryCacheSize() + { + if (0 == this.memStatus.AvailablePhysicalMemory) + { + this.MaximumCacheSize = Constants.CacheSizeMultiplierInByte * this.ParallelOperations; + } + else + { + this.MaximumCacheSize = + Math.Min( + Constants.CacheSizeMultiplierInByte * this.ParallelOperations, + Math.Min( + (long)(this.memStatus.AvailablePhysicalMemory * Constants.MemoryCacheMultiplier), + Constants.MemoryCacheMaximum)); + } + } + } +} diff --git a/lib/TransferContext.cs b/lib/TransferContext.cs new file mode 100644 index 00000000..ec35df8c --- /dev/null +++ b/lib/TransferContext.cs @@ -0,0 +1,124 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + + /// + /// Represents the context for a transfer, and provides additional runtime information about its execution. + /// + public class TransferContext + { + /// + /// Initializes a new instance of the class. + /// + public TransferContext() + : this(null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// An object representing the last checkpoint from which the transfer continues on. + public TransferContext(TransferCheckpoint checkpoint) + { + if (checkpoint == null) + { + this.Checkpoint = new TransferCheckpoint(); + } + else + { + this.Checkpoint = checkpoint.Copy(); + } + + this.OverallProgressTracker = new TransferProgressTracker(); + foreach(Transfer transfer in this.Checkpoint.AllTransfers) + { + this.OverallProgressTracker.AddBytesTransferred(transfer.ProgressTracker.BytesTransferred); + this.OverallProgressTracker.AddNumberOfFilesTransferred(transfer.ProgressTracker.NumberOfFilesTransferred); + this.OverallProgressTracker.AddNumberOfFilesSkipped(transfer.ProgressTracker.NumberOfFilesSkipped); + this.OverallProgressTracker.AddNumberOfFilesFailed(transfer.ProgressTracker.NumberOfFilesFailed); + } + } + + /// + /// Gets or sets the client request id. + /// + /// A string containing the client request id. + /// + /// Setting this property modifies all the requests involved in the related transfer operation to include the the HTTP x-ms-client-request-id header. + /// + public string ClientRequestId + { + get; + set; + } + + /// + /// Gets or sets the logging level to be used for the related tranfer operation. + /// + /// A value of type that specifies which events are logged for the related transfer operation. + public LogLevel LogLevel + { + get; + set; + } + + /// + /// Gets the last checkpoint of the transfer. + /// + public TransferCheckpoint LastCheckpoint + { + get + { + return this.Checkpoint.Copy(); + } + } + + /// + /// Callback invoked to tell whether to overwrite an existing destination. + /// + public OverwriteCallback OverwriteCallback + { + get; + set; + } + + /// + /// Gets or sets the progress update handler. + /// + public IProgress ProgressHandler + { + get + { + return this.OverallProgressTracker.ProgressHandler; + } + set + { + this.OverallProgressTracker.ProgressHandler = value; + } + } + + /// + /// Gets the overall transfer progress. + /// + internal TransferProgressTracker OverallProgressTracker + { + get; + set; + } + + /// + /// Gets the transfer checkpoint that tracks all transfers related to this transfer context. + /// + internal TransferCheckpoint Checkpoint + { + get; + private set; + } + } +} diff --git a/lib/TransferControllers/AsyncCopyControllers/AsyncCopyController.cs b/lib/TransferControllers/AsyncCopyControllers/AsyncCopyController.cs new file mode 100644 index 00000000..ed5a80a0 --- /dev/null +++ b/lib/TransferControllers/AsyncCopyControllers/AsyncCopyController.cs @@ -0,0 +1,678 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement.TransferControllers +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.Net; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.Blob.Protocol; + using Microsoft.WindowsAzure.Storage.File; + + internal abstract class AsyncCopyController : TransferControllerBase + { + /// + /// Timer to signal refresh status. + /// + private Timer statusRefreshTimer; + + /// + /// Lock to protect statusRefreshTimer. + /// + private object statusRefreshTimerLock = new object(); + + /// + /// Keeps track of the internal state-machine state. + /// + private volatile State state; + + /// + /// Indicates whether the controller has work available + /// or not for the calling code. + /// + private bool hasWork; + + /// + /// Indicates the BytesCopied value of last CopyState + /// + private long lastBytesCopied; + + /// + /// Initializes a new instance of the class. + /// + /// Scheduler object which creates this object. + /// Instance of job to start async copy. + /// Token user input to notify about cancellation. + internal AsyncCopyController( + TransferScheduler scheduler, + TransferJob transferJob, + CancellationToken userCancellationToken) + : base(scheduler, transferJob, userCancellationToken) + { + if (null == transferJob.Destination) + { + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + Resources.ParameterCannotBeNullException, + "Dest"), + "transferJob"); + } + + if ((null == transferJob.Source.SourceUri && null == transferJob.Source.Blob && null == transferJob.Source.AzureFile) + || (null != transferJob.Source.SourceUri && null != transferJob.Source.Blob) + || (null != transferJob.Source.Blob && null != transferJob.Source.AzureFile) + || (null != transferJob.Source.SourceUri && null != transferJob.Source.AzureFile)) + { + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + Resources.ProvideExactlyOneOfThreeParameters, + "Source.SourceUri", + "Source.Blob", + "Source.AzureFile"), + "transferJob"); + } + + this.SourceUri = this.TransferJob.Source.SourceUri; + this.SourceBlob = this.TransferJob.Source.Blob; + this.SourceFile = this.TransferJob.Source.AzureFile; + + // initialize the status refresh timer + this.statusRefreshTimer = new Timer( + new TimerCallback( + delegate(object timerState) + { + this.hasWork = true; + })); + + this.SetInitialStatus(); + } + + /// + /// Internal state values. + /// + private enum State + { + FetchSourceAttributes, + GetDestination, + StartCopy, + GetCopyState, + Finished, + Error, + } + + public override bool HasWork + { + get + { + return this.hasWork; + } + } + + protected CloudBlob SourceBlob + { + get; + private set; + } + + protected CloudFile SourceFile + { + get; + private set; + } + + protected Uri SourceUri + { + get; + private set; + } + + protected abstract Uri DestUri + { + get; + } + + public static AsyncCopyController CreateAsyncCopyController(TransferScheduler transferScheduler, TransferJob transferJob, CancellationToken cancellationToken) + { + if (transferJob.Destination.TransferLocationType == TransferLocationType.AzureFile) + { + return new FileAsyncCopyController(transferScheduler, transferJob, cancellationToken); + } + + if (transferJob.Destination.TransferLocationType == TransferLocationType.AzureBlob) + { + return new BlobAsyncCopyController(transferScheduler, transferJob, cancellationToken); + } + + throw new InvalidOperationException(Resources.CanOnlyCopyToFileOrBlobException); + } + + /// + /// Do work in the controller. + /// A controller controls the whole transfer from source to destination, + /// which could be split into several work items. This method is to let controller to do one of those work items. + /// There could be several work items to do at the same time in the controller. + /// + /// Whether the controller has completed. This is to tell TransferScheduler + /// whether the controller can be disposed. + protected override async Task DoWorkInternalAsync() + { + switch (this.state) + { + case State.FetchSourceAttributes: + await this.FetchSourceAttributesAsync(); + break; + case State.GetDestination: + await this.GetDestinationAsync(); + break; + case State.StartCopy: + await this.StartCopyAsync(); + break; + case State.GetCopyState: + await this.GetCopyStateAsync(); + break; + case State.Finished: + case State.Error: + default: + break; + } + + return (State.Error == this.state || State.Finished == this.state); + } + + /// + /// Sets the state of the controller to Error, while recording + /// the last occurred exception and setting the HasWork and + /// IsFinished fields. + /// + /// Exception to record. + protected override void SetErrorState(Exception ex) + { + Debug.Assert( + this.state != State.Finished, + "SetErrorState called, while controller already in Finished state"); + + this.state = State.Error; + this.hasWork = false; + } + + /// + /// Taken from Microsoft.WindowsAzure.Storage.Core.Util.HttpUtility: Parse the http query string. + /// + /// Http query string. + /// A dictionary of query pairs. + protected static Dictionary ParseQueryString(string query) + { + Dictionary retVal = new Dictionary(); + if (query == null || query.Length == 0) + { + return retVal; + } + + // remove ? if present + if (query.StartsWith("?", StringComparison.OrdinalIgnoreCase)) + { + query = query.Substring(1); + } + + string[] valuePairs = query.Split(new string[] { "&" }, StringSplitOptions.RemoveEmptyEntries); + + foreach (string vp in valuePairs) + { + int equalDex = vp.IndexOf("=", StringComparison.OrdinalIgnoreCase); + if (equalDex < 0) + { + retVal.Add(Uri.UnescapeDataString(vp), null); + continue; + } + + string key = vp.Substring(0, equalDex); + string value = vp.Substring(equalDex + 1); + + retVal.Add(Uri.UnescapeDataString(key), Uri.UnescapeDataString(value)); + } + + return retVal; + } + + private void SetInitialStatus() + { + switch (this.TransferJob.Status) + { + case TransferJobStatus.NotStarted: + this.TransferJob.Status = TransferJobStatus.Transfer; + break; + case TransferJobStatus.Transfer: + break; + case TransferJobStatus.Monitor: + break; + case TransferJobStatus.Finished: + default: + throw new ArgumentException(string.Format( + CultureInfo.CurrentCulture, + Resources.InvalidInitialEntryStatusForControllerException, + this.TransferJob.Status, + this.GetType().Name)); + } + + this.SetHasWorkAfterStatusChanged(); + } + + private void SetHasWorkAfterStatusChanged() + { + if (TransferJobStatus.Transfer == this.TransferJob.Status) + { + if (null != this.SourceUri) + { + this.state = State.GetDestination; + } + else + { + this.state = State.FetchSourceAttributes; + } + } + else if(TransferJobStatus.Monitor == this.TransferJob.Status) + { + this.state = State.GetCopyState; + } + else + { + Debug.Fail("We should never be here"); + } + + this.hasWork = true; + } + + private async Task FetchSourceAttributesAsync() + { + Debug.Assert( + this.state == State.FetchSourceAttributes, + "FetchSourceAttributesAsync called, but state isn't FetchSourceAttributes"); + + this.hasWork = false; + this.StartCallbackHandler(); + + try + { + await this.DoFetchSourceAttributesAsync(); + } + catch (StorageException e) + { + HandleFetchSourceAttributesException(e); + throw; + } + + this.TransferJob.Source.CheckedAccessCondition = true; + + this.state = State.GetDestination; + this.hasWork = true; + } + + private static void HandleFetchSourceAttributesException(StorageException e) + { + // Getting a storage exception is expected if the source doesn't + // exist. For those cases that indicate the source doesn't exist + // we will set a specific error state. + if (null != e.RequestInformation && + e.RequestInformation.HttpStatusCode == (int)HttpStatusCode.NotFound) + { + throw new InvalidOperationException(Resources.SourceDoesNotExistException); + } + } + + private async Task GetDestinationAsync() + { + Debug.Assert( + this.state == State.GetDestination, + "GetDestinationAsync called, but state isn't GetDestination"); + + this.hasWork = false; + this.StartCallbackHandler(); + + try + { + await this.DoFetchDestAttributesAsync(); + } + catch (StorageException se) + { + if (!this.HandleGetDestinationResult(se)) + { + throw se; + } + return; + } + + this.HandleGetDestinationResult(null); + } + + private bool HandleGetDestinationResult(Exception e) + { + bool destExist = true; + + if (null != e) + { + StorageException se = e as StorageException; + + // Getting a storage exception is expected if the destination doesn't + // exist. In this case we won't error out, but set the + // destExist flag to false to indicate we will copy to + // a new blob/file instead of overwriting an existing one. + if (null != se && + null != se.RequestInformation && + se.RequestInformation.HttpStatusCode == (int)HttpStatusCode.NotFound) + { + destExist = false; + } + else + { + this.DoHandleGetDestinationException(se); + return false; + } + } + + this.TransferJob.Destination.CheckedAccessCondition = true; + + if ((TransferJobStatus.Monitor == this.TransferJob.Status) + && string.IsNullOrEmpty(this.TransferJob.CopyId)) + { + throw new InvalidOperationException(Resources.RestartableInfoCorruptedException); + } + + // If destination file exists, query user whether to overwrite it. + + Uri sourceUri = this.GetSourceUri(); + this.CheckOverwrite( + destExist, + sourceUri.ToString(), + this.DestUri.ToString()); + + this.UpdateProgressAddBytesTransferred(0); + + this.state = State.StartCopy; + + this.hasWork = true; + return true; + } + + private async Task StartCopyAsync() + { + Debug.Assert( + this.state == State.StartCopy, + "StartCopyAsync called, but state isn't StartCopy"); + + this.hasWork = false; + + try + { + this.TransferJob.CopyId = await this.DoStartCopyAsync(); + } + catch (StorageException se) + { + if (!this.HandleStartCopyResult(se)) + { + throw; + } + + return; + } + + this.HandleStartCopyResult(null); + } + + private bool HandleStartCopyResult(StorageException se) + { + if (null != se) + { + if (null != se.RequestInformation + && null != se.RequestInformation.ExtendedErrorInformation + && BlobErrorCodeStrings.PendingCopyOperation == se.RequestInformation.ExtendedErrorInformation.ErrorCode) + { + CopyState copyState = this.FetchCopyStateAsync().Result; + + if (null == copyState) + { + return false; + } + + string baseUriString = copyState.Source.GetComponents( + UriComponents.Host | UriComponents.Port | UriComponents.Path, UriFormat.UriEscaped); + + Uri sourceUri = this.GetSourceUri(); + + string ourBaseUriString = sourceUri.GetComponents(UriComponents.Host | UriComponents.Port | UriComponents.Path, UriFormat.UriEscaped); + + DateTimeOffset? baseSnapshot = null; + DateTimeOffset? ourSnapshot = null == this.SourceBlob ? null : this.SourceBlob.SnapshotTime; + + string snapshotString; + if (ParseQueryString(copyState.Source.Query).TryGetValue("snapshot", out snapshotString)) + { + if (!string.IsNullOrEmpty(snapshotString)) + { + DateTimeOffset snapshotTime; + if (DateTimeOffset.TryParse( + snapshotString, + CultureInfo.CurrentCulture, + DateTimeStyles.AdjustToUniversal, + out snapshotTime)) + { + baseSnapshot = snapshotTime; + } + } + } + + if (!baseUriString.Equals(ourBaseUriString) || + !baseSnapshot.Equals(ourSnapshot)) + { + return false; + } + + if (string.IsNullOrEmpty(this.TransferJob.CopyId)) + { + this.TransferJob.CopyId = copyState.CopyId; + } + } + else + { + return false; + } + } + + this.state = State.GetCopyState; + this.hasWork = true; + return true; + } + + private async Task GetCopyStateAsync() + { + Debug.Assert( + this.state == State.GetCopyState, + "GetCopyStateAsync called, but state isn't GetCopyState"); + + this.hasWork = false; + this.StartCallbackHandler(); + + CopyState copyState = null; + + try + { + copyState = await this.FetchCopyStateAsync(); + } + catch (StorageException se) + { + if (null != se.RequestInformation && + se.RequestInformation.HttpStatusCode == (int)HttpStatusCode.NotFound) + { + // The reason of 404 (Not Found) may be that the destination blob has not been created yet. + this.RestartTimer(); + } + else + { + throw; + } + } + + this.HandleFetchCopyStateResult(copyState); + } + + private void HandleFetchCopyStateResult(CopyState copyState) + { + if (null == copyState) + { + // Reach here, the destination should already exist. + string exceptionMessage = string.Format( + CultureInfo.CurrentCulture, + Resources.FailedToRetrieveCopyStateForObjectException, + this.DestUri.ToString()); + + throw new TransferException( + TransferErrorCode.FailToRetrieveCopyStateForObject, + exceptionMessage); + } + else + { + // Verify we are monitoring the right blob copying process. + if (!this.TransferJob.CopyId.Equals(copyState.CopyId)) + { + throw new TransferException( + TransferErrorCode.MismatchCopyId, + Resources.MismatchFoundBetweenLocalAndServerCopyIdsException); + } + + if (CopyStatus.Success == copyState.Status) + { + this.UpdateTransferProgress(copyState); + + this.DisposeStatusRefreshTimer(); + + this.SetFinished(); + } + else if (CopyStatus.Pending == copyState.Status) + { + this.UpdateTransferProgress(copyState); + + // Wait a period to restart refresh the status. + this.RestartTimer(); + } + else + { + string exceptionMessage = string.Format( + CultureInfo.CurrentCulture, + Resources.FailedToAsyncCopyObjectException, + this.GetSourceUri().ToString(), + this.DestUri.ToString(), + copyState.Status.ToString(), + copyState.StatusDescription); + + // CopyStatus.Invalid | Failed | Aborted + throw new TransferException( + TransferErrorCode.AsyncCopyFailed, + exceptionMessage); + } + } + } + + private void UpdateTransferProgress(CopyState copyState) + { + if (null != copyState && + copyState.TotalBytes.HasValue) + { + Debug.Assert( + copyState.BytesCopied.HasValue, + "BytesCopied cannot be null as TotalBytes is not null."); + + if (this.TransferContext != null) + { + long bytesTransferred = copyState.BytesCopied.Value; + this.UpdateProgressAddBytesTransferred(bytesTransferred - this.lastBytesCopied); + + this.lastBytesCopied = bytesTransferred; + } + } + } + + private void SetFinished() + { + this.state = State.Finished; + this.hasWork = false; + + this.FinishCallbackHandler(null); + } + + private void RestartTimer() + { + // Wait a period to restart refresh the status. + this.statusRefreshTimer.Change( + TimeSpan.FromMilliseconds(Constants.AsyncCopyStatusRefreshWaitTimeInMilliseconds), + new TimeSpan(-1)); + } + + private void DisposeStatusRefreshTimer() + { + if (null != this.statusRefreshTimer) + { + lock (this.statusRefreshTimerLock) + { + if (null != this.statusRefreshTimer) + { + this.statusRefreshTimer.Dispose(); + this.statusRefreshTimer = null; + } + } + } + } + + private Uri GetSourceUri() + { + if (null != this.SourceUri) + { + return this.SourceUri; + } + + if (null != this.SourceBlob) + { + return this.SourceBlob.SnapshotQualifiedUri; + } + + return this.SourceFile.Uri; + } + + protected async Task DoFetchSourceAttributesAsync() + { + AccessCondition accessCondition = Utils.GenerateConditionWithCustomerCondition( + this.TransferJob.Source.AccessCondition, + this.TransferJob.Source.CheckedAccessCondition); + OperationContext operationContext = Utils.GenerateOperationContext(this.TransferContext); + + if (this.SourceBlob != null) + { + await this.SourceBlob.FetchAttributesAsync( + accessCondition, + Utils.GenerateBlobRequestOptions(this.TransferJob.Source.BlobRequestOptions), + operationContext, + this.CancellationToken); + } + else if (this.SourceFile != null) + { + await this.SourceFile.FetchAttributesAsync( + accessCondition, + Utils.GenerateFileRequestOptions(this.TransferJob.Source.FileRequestOptions), + operationContext, + this.CancellationToken); + } + } + + protected abstract Task DoFetchDestAttributesAsync(); + protected abstract Task DoStartCopyAsync(); + protected abstract void DoHandleGetDestinationException(StorageException se); + protected abstract Task FetchCopyStateAsync(); + } +} diff --git a/lib/TransferControllers/AsyncCopyControllers/BlobAsyncCopyController.cs b/lib/TransferControllers/AsyncCopyControllers/BlobAsyncCopyController.cs new file mode 100644 index 00000000..848d7598 --- /dev/null +++ b/lib/TransferControllers/AsyncCopyControllers/BlobAsyncCopyController.cs @@ -0,0 +1,173 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement.TransferControllers +{ + using System; + using System.Globalization; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.WindowsAzure.Storage; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.DataMovement; + + /// + /// Blob asynchronous copy. + /// + internal class BlobAsyncCopyController : AsyncCopyController + { + private CloudBlob destBlob; + + public BlobAsyncCopyController( + TransferScheduler transferScheduler, + TransferJob transferJob, + CancellationToken cancellationToken) + : base(transferScheduler, transferJob, cancellationToken) + { + CloudBlob transferDestBlob = transferJob.Destination.Blob; + if (null == transferDestBlob) + { + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + Resources.ParameterCannotBeNullException, + "Dest.Blob"), + "transferJob"); + } + + if (transferDestBlob.IsSnapshot) + { + throw new ArgumentException(Resources.DestinationMustBeBaseBlob, "transferJob"); + } + + CloudBlob transferSourceBlob = transferJob.Source.Blob; + + if (null != transferSourceBlob && transferDestBlob.BlobType != transferSourceBlob.BlobType) + { + throw new ArgumentException(Resources.SourceAndDestinationBlobTypeDifferent, "transferJob"); + } + + if ((null != transferSourceBlob) + && (StorageExtensions.Equals(transferSourceBlob, transferDestBlob))) + { + throw new InvalidOperationException(Resources.SourceAndDestinationLocationCannotBeEqualException); + } + + this.destBlob = transferDestBlob; + } + + protected override Uri DestUri + { + get + { + return this.destBlob.Uri; + } + } + + protected override Task DoFetchDestAttributesAsync() + { + AccessCondition accessCondition = Utils.GenerateConditionWithCustomerCondition( + this.TransferJob.Destination.AccessCondition, + this.TransferJob.Destination.CheckedAccessCondition); + + return this.destBlob.FetchAttributesAsync( + accessCondition, + Utils.GenerateBlobRequestOptions(this.TransferJob.Destination.BlobRequestOptions), + Utils.GenerateOperationContext(this.TransferContext), + this.CancellationToken); + } + + protected override Task DoStartCopyAsync() + { + AccessCondition destAccessCondition = Utils.GenerateConditionWithCustomerCondition(this.TransferJob.Destination.AccessCondition); + + if (null != this.SourceUri) + { + return this.destBlob.StartCopyAsync( + this.SourceUri, + null, + destAccessCondition, + Utils.GenerateBlobRequestOptions(this.TransferJob.Destination.BlobRequestOptions), + Utils.GenerateOperationContext(this.TransferContext), + this.CancellationToken); + } + else if (null != this.SourceBlob) + { + AccessCondition sourceAccessCondition = + AccessCondition.GenerateIfMatchCondition(this.SourceBlob.Properties.ETag); + + return this.destBlob.StartCopyAsync( + this.SourceBlob.GenerateUriWithCredentials(), + sourceAccessCondition, + destAccessCondition, + Utils.GenerateBlobRequestOptions(this.TransferJob.Destination.BlobRequestOptions), + Utils.GenerateOperationContext(this.TransferContext), + this.CancellationToken); + } + else + { + if (BlobType.BlockBlob == this.destBlob.BlobType) + { + return (this.destBlob as CloudBlockBlob).StartCopyAsync( + this.SourceFile.GenerateCopySourceFile(), + null, + destAccessCondition, + Utils.GenerateBlobRequestOptions(this.TransferJob.Destination.BlobRequestOptions), + Utils.GenerateOperationContext(this.TransferContext), + this.CancellationToken); + } + else if (BlobType.PageBlob == this.destBlob.BlobType) + { + throw new InvalidOperationException(Resources.AsyncCopyFromFileToPageBlobNotSupportException); + } + else if (BlobType.AppendBlob == this.destBlob.BlobType) + { + throw new InvalidOperationException(Resources.AsyncCopyFromFileToAppendBlobNotSupportException); + } + else + { + throw new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + Resources.NotSupportedBlobType, + this.destBlob.BlobType)); + } + } + } + + protected override void DoHandleGetDestinationException(StorageException se) + { + if (null != se) + { + if (0 == string.Compare(se.Message, Constants.BlobTypeMismatch, StringComparison.OrdinalIgnoreCase)) + { + // Current use error message to decide whether it caused by blob type mismatch, + // We should ask xscl to expose an error code for this.. + // Opened workitem 1487579 to track this. + throw new InvalidOperationException(Resources.DestinationBlobTypeNotMatch); + } + } + else + { + if (null != this.SourceBlob && this.SourceBlob.Properties.BlobType != this.destBlob.Properties.BlobType) + { + throw new InvalidOperationException(Resources.SourceAndDestinationBlobTypeDifferent); + } + } + } + + protected override async Task FetchCopyStateAsync() + { + await this.destBlob.FetchAttributesAsync( + Utils.GenerateConditionWithCustomerCondition(this.TransferJob.Destination.AccessCondition), + Utils.GenerateBlobRequestOptions(this.TransferJob.Destination.BlobRequestOptions), + Utils.GenerateOperationContext(this.TransferContext), + this.CancellationToken); + + return this.destBlob.CopyState; + } + } +} diff --git a/lib/TransferControllers/AsyncCopyControllers/FileAsyncCopyController.cs b/lib/TransferControllers/AsyncCopyControllers/FileAsyncCopyController.cs new file mode 100644 index 00000000..e4f31628 --- /dev/null +++ b/lib/TransferControllers/AsyncCopyControllers/FileAsyncCopyController.cs @@ -0,0 +1,125 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement.TransferControllers +{ + using System; + using System.Globalization; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.WindowsAzure.Storage; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.DataMovement; + using Microsoft.WindowsAzure.Storage.File; + + /// + /// Azure file asynchronous copy. + /// + internal class FileAsyncCopyController : AsyncCopyController + { + private CloudFile destFile; + + public FileAsyncCopyController( + TransferScheduler transferScheduler, + TransferJob transferJob, + CancellationToken cancellationToken) + : base(transferScheduler, transferJob, cancellationToken) + { + if (null == transferJob.Destination.AzureFile) + { + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + Resources.ParameterCannotBeNullException, + "Dest.AzureFile"), + "transferJob"); + } + + if ((null == transferJob.Source.SourceUri && null == transferJob.Source.Blob && null == transferJob.Source.AzureFile) + || (null != transferJob.Source.SourceUri && null != transferJob.Source.Blob) + || (null != transferJob.Source.Blob && null != transferJob.Source.AzureFile) + || (null != transferJob.Source.SourceUri && null != transferJob.Source.AzureFile)) + { + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + Resources.ProvideExactlyOneOfThreeParameters, + "Source.SourceUri", + "Source.Blob", + "Source.AzureFile"), + "transferJob"); + } + + this.destFile = this.TransferJob.Destination.AzureFile; + } + + protected override Uri DestUri + { + get + { + return this.destFile.Uri; + } + } + + protected override Task DoFetchDestAttributesAsync() + { + return this.destFile.FetchAttributesAsync( + null, + Utils.GenerateFileRequestOptions(this.TransferJob.Destination.FileRequestOptions), + null, + this.CancellationToken); + } + + protected override Task DoStartCopyAsync() + { + OperationContext operationContext = Utils.GenerateOperationContext(this.TransferContext); + if (null != this.SourceUri) + { + return this.destFile.StartCopyAsync( + this.SourceUri, + null, + null, + Utils.GenerateFileRequestOptions(this.TransferJob.Destination.FileRequestOptions), + operationContext, + this.CancellationToken); + } + else if (null != this.SourceBlob) + { + return this.destFile.StartCopyAsync( + this.SourceBlob.GenerateCopySourceBlob(), + null, + null, + Utils.GenerateFileRequestOptions(this.TransferJob.Destination.FileRequestOptions), + operationContext, + this.CancellationToken); + } + else + { + return this.destFile.StartCopyAsync( + this.SourceFile.GenerateCopySourceFile(), + null, + null, + Utils.GenerateFileRequestOptions(this.TransferJob.Destination.FileRequestOptions), + operationContext, + this.CancellationToken); + } + } + + protected override void DoHandleGetDestinationException(StorageException se) + { + } + + protected override async Task FetchCopyStateAsync() + { + await this.destFile.FetchAttributesAsync( + Utils.GenerateConditionWithCustomerCondition(this.TransferJob.Destination.AccessCondition), + Utils.GenerateFileRequestOptions(this.TransferJob.Destination.FileRequestOptions), + Utils.GenerateOperationContext(this.TransferContext), + this.CancellationToken); + + return this.destFile.CopyState; + } + } +} diff --git a/lib/TransferControllers/ITransferController.cs b/lib/TransferControllers/ITransferController.cs new file mode 100644 index 00000000..12f94fbf --- /dev/null +++ b/lib/TransferControllers/ITransferController.cs @@ -0,0 +1,27 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement.TransferControllers +{ + using System; + using System.Threading.Tasks; + + internal interface ITransferController + { + bool HasWork + { + get; + } + + bool IsFinished + { + get; + } + + Task DoWorkAsync(); + + void CancelWork(); + } +} diff --git a/lib/TransferControllers/SyncTransferController.cs b/lib/TransferControllers/SyncTransferController.cs new file mode 100644 index 00000000..a6c7bfdf --- /dev/null +++ b/lib/TransferControllers/SyncTransferController.cs @@ -0,0 +1,201 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement.TransferControllers +{ + using System; + using System.Collections.Concurrent; + using System.Globalization; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.WindowsAzure.Storage.Blob; + + internal class SyncTransferController : TransferControllerBase + { + private TransferReaderWriterBase reader; + private TransferReaderWriterBase writer; + + public SyncTransferController( + TransferScheduler transferScheduler, + TransferJob transferJob, + CancellationToken userCancellationToken) + : base(transferScheduler, transferJob, userCancellationToken) + { + if (null == transferScheduler) + { + throw new ArgumentNullException("transferScheduler"); + } + + if (null == transferJob) + { + throw new ArgumentNullException("transferJob"); + } + + this.SharedTransferData = new SharedTransferData() + { + TransferJob = this.TransferJob, + AvailableData = new ConcurrentDictionary(), + }; + + if (null == transferJob.CheckPoint) + { + transferJob.CheckPoint = new SingleObjectCheckpoint(); + } + + reader = this.GetReader(transferJob.Source); + writer = this.GetWriter(transferJob.Destination); + } + + public SharedTransferData SharedTransferData + { + get; + private set; + } + + public bool ErrorOccurred + { + get; + private set; + } + + public override bool HasWork + { + get + { + var hasWork = (!this.reader.PreProcessed && this.reader.HasWork) || (this.reader.PreProcessed && this.writer.HasWork) || (this.writer.PreProcessed && this.reader.HasWork); + return !this.ErrorOccurred && hasWork; + } + } + + protected override async Task DoWorkInternalAsync() + { + if (!this.reader.PreProcessed && this.reader.HasWork) + { + await this.reader.DoWorkInternalAsync(); + } + else if (this.reader.PreProcessed && this.writer.HasWork) + { + await this.writer.DoWorkInternalAsync(); + } + else if (this.writer.PreProcessed && this.reader.HasWork) + { + await this.reader.DoWorkInternalAsync(); + } + + return this.ErrorOccurred || this.writer.IsFinished; + } + + protected override void SetErrorState(Exception ex) + { + this.ErrorOccurred = true; + } + + private TransferReaderWriterBase GetReader(TransferLocation sourceLocation) + { + switch (sourceLocation.TransferLocationType) + { + case TransferLocationType.Stream: + return new StreamedReader(this.Scheduler, this, this.CancellationToken); + case TransferLocationType.FilePath: + return new StreamedReader(this.Scheduler, this, this.CancellationToken); + case TransferLocationType.AzureBlob: + if (sourceLocation.Blob is CloudPageBlob) + { + return new PageBlobReader(this.Scheduler, this, this.CancellationToken); + } + else if (sourceLocation.Blob is CloudBlockBlob) + { + return new BlockBasedBlobReader(this.Scheduler, this, this.CancellationToken); + } + else if (sourceLocation.Blob is CloudAppendBlob) + { + return new BlockBasedBlobReader(this.Scheduler, this, this.CancellationToken); + } + else + { + throw new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + Resources.UnsupportedBlobTypeException, + sourceLocation.Blob.BlobType)); + } + case TransferLocationType.AzureFile: + return new CloudFileReader(this.Scheduler, this, this.CancellationToken); + default: + throw new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + Resources.UnsupportedTransferLocationException, + sourceLocation.TransferLocationType)); + } + } + + private TransferReaderWriterBase GetWriter(TransferLocation destLocation) + { + switch (destLocation.TransferLocationType) + { + case TransferLocationType.Stream: + return new StreamedWriter(this.Scheduler, this, this.CancellationToken); + case TransferLocationType.FilePath: + return new StreamedWriter(this.Scheduler, this, this.CancellationToken); + case TransferLocationType.AzureBlob: + if (destLocation.Blob is CloudPageBlob) + { + return new PageBlobWriter(this.Scheduler, this, this.CancellationToken); + } + else if (destLocation.Blob is CloudBlockBlob) + { + return new BlockBlobWriter(this.Scheduler, this, this.CancellationToken); + } + else if (destLocation.Blob is CloudAppendBlob) + { + return new AppendBlobWriter(this.Scheduler, this, this.CancellationToken); + } + else + { + throw new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + Resources.UnsupportedBlobTypeException, + destLocation.Blob.BlobType)); + } + case TransferLocationType.AzureFile: + return new CloudFileWriter(this.Scheduler, this, this.CancellationToken); + default: + throw new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + Resources.UnsupportedTransferLocationException, + destLocation.TransferLocationType)); + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + if (null != this.reader) + { + this.reader.Dispose(); + } + + if (null != this.writer) + { + this.writer.Dispose(); + } + + foreach(var transferData in this.SharedTransferData.AvailableData.Values) + { + transferData.Dispose(); + } + + this.SharedTransferData.AvailableData.Clear(); + } + } + } +} diff --git a/lib/TransferControllers/TransferControllerBase.cs b/lib/TransferControllers/TransferControllerBase.cs new file mode 100644 index 00000000..fe8ee5f0 --- /dev/null +++ b/lib/TransferControllers/TransferControllerBase.cs @@ -0,0 +1,336 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement.TransferControllers +{ + using System; + using System.Globalization; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.WindowsAzure.Storage.DataMovement; + + internal abstract class TransferControllerBase : ITransferController, IDisposable + { + /// + /// Count of active tasks in this controller. + /// + private int activeTasks; + + private volatile bool isFinished = false; + + private object lockOnFinished = new object(); + + private int notifiedFinish; + + private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + + private CancellationTokenRegistration transferSchedulerCancellationTokenRegistration; + + private CancellationTokenRegistration userCancellationTokenRegistration; + + protected TransferControllerBase(TransferScheduler transferScheduler, TransferJob transferJob, CancellationToken userCancellationToken) + { + if (null == transferScheduler) + { + throw new ArgumentNullException("transferScheduler"); + } + + if (null == transferJob) + { + throw new ArgumentNullException("transferJob"); + } + + this.Scheduler = transferScheduler; + this.TransferJob = transferJob; + + this.transferSchedulerCancellationTokenRegistration = + this.Scheduler.CancellationTokenSource.Token.Register(this.CancelWork); + + this.userCancellationTokenRegistration = userCancellationToken.Register(this.CancelWork); + this.TaskCompletionSource = new TaskCompletionSource(); + } + + ~TransferControllerBase() + { + this.Dispose(false); + } + + /// + /// Gets or sets the transfer context for the controller. + /// + public TransferContext TransferContext + { + get + { + return this.TransferJob.Transfer.Context; + } + } + + /// + /// Gets or sets a value indicating whether the controller has work available + /// or not for the calling code. If HasWork is false, while IsFinished + /// is also false this indicates that there are currently still active + /// async tasks running. The caller should continue checking if this + /// controller HasWork available later; once the currently active + /// async tasks are done HasWork will change to True, or IsFinished + /// will be set to True. + /// + public abstract bool HasWork + { + get; + } + + /// + /// Gets a value indicating whether this controller is finished with + /// its transferring task. + /// + public bool IsFinished + { + get + { + return this.isFinished; + } + } + + public TaskCompletionSource TaskCompletionSource + { + get; + set; + } + + /// + /// Gets scheduler object which creates this object. + /// + protected TransferScheduler Scheduler + { + get; + private set; + } + + /// + /// Gets TransferJob related to this controller. + /// + protected TransferJob TransferJob + { + get; + private set; + } + + protected CancellationToken CancellationToken + { + get + { + return cancellationTokenSource.Token; + } + } + + /// + /// Do work in the controller. + /// A controller controls the whole transfer from source to destination, + /// which could be split into several work items. This method is to let controller to do one of those work items. + /// There could be several work items to do at the same time in the controller. + /// + /// Whether the controller has completed. This is to tell TransferScheduler + /// whether the controller can be disposed. + public async Task DoWorkAsync() + { + if (!this.HasWork) + { + return false; + } + + bool setFinish = false; + Exception exception = null; + this.PreWork(); + + try + { + setFinish = await this.DoWorkInternalAsync(); + } + catch (Exception ex) + { + this.SetErrorState(ex); + setFinish = true; + exception = ex; + } + + if (setFinish) + { + var postWork = this.SetFinishedAndPostWork(); + if (exception != null) + { + // There might be still some active tasks running while error occurs, and + // those tasks shouldn't take long time to complete, so just spin until they are done. + var spin = new SpinWait(); + while (this.activeTasks != 0) + { + spin.SpinOnce(); + } + + this.FinishCallbackHandler(exception); + return true; + } + else + { + return postWork; + } + } + else + { + return this.PostWork(); + } + } + + /// + /// Cancels all work in the controller. + /// + public void CancelWork() + { + this.cancellationTokenSource.Cancel(); + } + + /// + /// Public dispose method to release all resources owned. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + public void CheckCancellation() + { + Utils.CheckCancellation(this.cancellationTokenSource); + } + + public void UpdateProgressAddBytesTransferred(long bytesTransferredToAdd) + { + this.TransferJob.Transfer.ProgressTracker.AddBytesTransferred(bytesTransferredToAdd); + } + + public void StartCallbackHandler() + { + if (this.TransferJob.Status == TransferJobStatus.NotStarted) + { + this.TransferJob.Status = TransferJobStatus.Transfer; + } + } + + public void FinishCallbackHandler(Exception exception) + { + if (Interlocked.CompareExchange(ref this.notifiedFinish, 1, 0) == 0) + { + if (null != exception) + { + this.TaskCompletionSource.SetException(exception); + } + else + { + this.TaskCompletionSource.SetResult(null); + } + } + } + + protected abstract Task DoWorkInternalAsync(); + + /// + /// Pre work action. + /// + protected void PreWork() + { + Interlocked.Increment(ref this.activeTasks); + } + + /// + /// Post work action. + /// + /// + /// Count of current active task in the controller. + /// A Controller can only be destroyed after this count of active tasks is 0. + /// + protected bool PostWork() + { + lock (this.lockOnFinished) + { + return 0 == Interlocked.Decrement(ref this.activeTasks) && this.isFinished; + } + } + + protected bool SetFinishedAndPostWork() + { + lock (this.lockOnFinished) + { + this.isFinished = true; + return 0 == Interlocked.Decrement(ref this.activeTasks) && this.isFinished; + } + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + try + { + this.transferSchedulerCancellationTokenRegistration.Dispose(); + } + catch (ObjectDisposedException) + { + // Object has been disposed before, just catch this exception, do nothing else. + } + + try + { + this.userCancellationTokenRegistration.Dispose(); + } + catch (ObjectDisposedException) + { + // Object has been disposed before, just catch this exception, do nothing else. + } + + try + { + this.cancellationTokenSource.Dispose(); + } + catch (ObjectDisposedException) + { + // Object has been disposed before, just catch this exception, do nothing else. + } + } + } + + /// + /// Sets the state of the controller to Error, while recording + /// the last occurred exception and setting the HasWork and + /// IsFinished fields. + /// + /// Exception to record. + protected abstract void SetErrorState(Exception ex); + + public void CheckOverwrite( + bool exist, + string sourceFileName, + string destFileName) + { + if (null == this.TransferJob.Overwrite) + { + this.TransferJob.Overwrite = true; + if (exist) + { + if (null == this.TransferContext || null == this.TransferContext.OverwriteCallback || !this.TransferContext.OverwriteCallback(sourceFileName, destFileName)) + { + this.TransferJob.Overwrite = false; + } + } + } + + if (exist && !this.TransferJob.Overwrite.Value) + { + string exceptionMessage = string.Format(CultureInfo.InvariantCulture, Resources.OverwriteCallbackCancelTransferException, sourceFileName, destFileName); + throw new TransferException(TransferErrorCode.NotOverwriteExistingDestination, exceptionMessage); + } + } + } +} diff --git a/lib/TransferControllers/TransferReaderWriterBase.cs b/lib/TransferControllers/TransferReaderWriterBase.cs new file mode 100644 index 00000000..820a2900 --- /dev/null +++ b/lib/TransferControllers/TransferReaderWriterBase.cs @@ -0,0 +1,111 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement.TransferControllers +{ + using System; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + + internal abstract class TransferReaderWriterBase : IDisposable + { + protected TransferReaderWriterBase( + TransferScheduler scheduler, + SyncTransferController controller, + CancellationToken cancellationToken) + { + this.Scheduler = scheduler; + this.Controller = controller; + this.CancellationToken = cancellationToken; + } + + /// + /// Gets a value indicating whether it finished preprocess. + /// For producer, preprocess is to validate source and fetch block list/page ranges; + /// For consumer, preprocess is to open or create destination. + /// + public virtual bool PreProcessed + { + get; + protected set; + } + + public abstract bool HasWork + { + get; + } + + public abstract bool IsFinished + { + get; + } + + protected TransferScheduler Scheduler + { + get; + private set; + } + + protected SyncTransferController Controller + { + get; + private set; + } + + protected SharedTransferData SharedTransferData + { + get + { + return this.Controller.SharedTransferData; + } + } + + protected CancellationToken CancellationToken + { + get; + private set; + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + } + + protected void NotifyStarting() + { + this.Controller.StartCallbackHandler(); + } + + protected void NotifyFinished(Exception ex) + { + this.Controller.FinishCallbackHandler(ex); + } + + public abstract Task DoWorkInternalAsync(); + + public TransferData GetFirstAvailable() + { + TransferData transferData = null; + var transferDatas = this.SharedTransferData.AvailableData.Values; + + if (transferDatas.Any()) + { + transferData = transferDatas.First(); + TransferData tempData; + this.SharedTransferData.AvailableData.TryRemove(transferData.StartOffset, out tempData); + return transferData; + } + + return null; + } + } +} diff --git a/lib/TransferControllers/TransferReaders/BlockBasedBlobReader.cs b/lib/TransferControllers/TransferReaders/BlockBasedBlobReader.cs new file mode 100644 index 00000000..ca4a07cc --- /dev/null +++ b/lib/TransferControllers/TransferReaders/BlockBasedBlobReader.cs @@ -0,0 +1,357 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement.TransferControllers +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Net; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.WindowsAzure.Storage.Blob; + + internal sealed class BlockBasedBlobReader : TransferReaderWriterBase + { + /// + /// Block/append blob instance to be downloaded from. + /// + private CloudBlob blob; + + /// + /// Window to record unfinished chunks to be retransferred again. + /// + private Queue lastTransferWindow; + + /// + /// Instance to represent source location. + /// + private TransferLocation transferLocation; + + private TransferJob transferJob; + + /// + /// Value to indicate whether the transfer is finished. + /// This is to tell the caller that the reader can be disposed, + /// Both error happened or completed will be treated to be finished. + /// + private volatile bool isFinished = false; + + private volatile bool hasWork; + + private CountdownEvent downloadCountdownEvent; + + public BlockBasedBlobReader( + TransferScheduler scheduler, + SyncTransferController controller, + CancellationToken cancellationToken) + : base(scheduler, controller, cancellationToken) + { + this.transferLocation = this.SharedTransferData.TransferJob.Source; + this.transferJob = this.SharedTransferData.TransferJob; + this.blob = this.transferLocation.Blob; + + Debug.Assert( + (this.blob is CloudBlockBlob) ||(this.blob is CloudAppendBlob), + "Initializing BlockBlobReader while source location is not a block blob or an append blob."); + + this.hasWork = true; + } + + public override bool IsFinished + { + get + { + return this.isFinished; + } + } + + public override bool HasWork + { + get + { + return this.hasWork; + } + } + + public override async Task DoWorkInternalAsync() + { + try + { + if (!this.PreProcessed) + { + await this.FetchAttributeAsync(); + } + else + { + await this.DownloadBlockBlobAsync(); + } + } + catch (Exception) + { + this.isFinished = true; + throw; + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + if (null != this.downloadCountdownEvent) + { + this.downloadCountdownEvent.Dispose(); + this.downloadCountdownEvent = null; + } + } + } + + private async Task FetchAttributeAsync() + { + this.hasWork = false; + this.NotifyStarting(); + + AccessCondition accessCondition = Utils.GenerateIfMatchConditionWithCustomerCondition( + this.transferLocation.ETag, + this.transferLocation.AccessCondition, + this.transferLocation.CheckedAccessCondition); + + try + { + await this.blob.FetchAttributesAsync( + accessCondition, + Utils.GenerateBlobRequestOptions(this.transferLocation.BlobRequestOptions), + Utils.GenerateOperationContext(this.Controller.TransferContext), + this.CancellationToken); + } + catch (StorageException e) + { + if (null != e.RequestInformation && + e.RequestInformation.HttpStatusCode == (int)HttpStatusCode.NotFound) + { + throw new InvalidOperationException(Resources.SourceBlobDoesNotExistException); + } + else + { + throw; + } + } + + this.transferLocation.CheckedAccessCondition = true; + + if (this.blob.Properties.BlobType == BlobType.Unspecified) + { + throw new InvalidOperationException(Resources.FailedToGetBlobTypeException); + } + + if (string.IsNullOrEmpty(this.transferLocation.ETag)) + { + if (0 != this.SharedTransferData.TransferJob.CheckPoint.EntryTransferOffset) + { + throw new InvalidOperationException(Resources.RestartableInfoCorruptedException); + } + + this.transferLocation.ETag = this.blob.Properties.ETag; + } + else if ((this.SharedTransferData.TransferJob.CheckPoint.EntryTransferOffset > this.blob.Properties.Length) + || (this.SharedTransferData.TransferJob.CheckPoint.EntryTransferOffset < 0)) + { + throw new InvalidOperationException(Resources.RestartableInfoCorruptedException); + } + + this.SharedTransferData.SourceLocation = this.blob.Uri.ToString(); + + this.SharedTransferData.DisableContentMD5Validation = + null != this.transferLocation.BlobRequestOptions ? + this.transferLocation.BlobRequestOptions.DisableContentMD5Validation.HasValue ? + this.transferLocation.BlobRequestOptions.DisableContentMD5Validation.Value : false : false; + + this.SharedTransferData.TotalLength = this.blob.Properties.Length; + this.SharedTransferData.Attributes = Utils.GenerateAttributes(this.blob); + + if ((0 == this.SharedTransferData.TransferJob.CheckPoint.EntryTransferOffset) + && (null != this.SharedTransferData.TransferJob.CheckPoint.TransferWindow) + && (0 != this.SharedTransferData.TransferJob.CheckPoint.TransferWindow.Count)) + { + throw new InvalidOperationException(Resources.RestartableInfoCorruptedException); + } + + this.lastTransferWindow = new Queue(this.SharedTransferData.TransferJob.CheckPoint.TransferWindow); + + int downloadCount = this.lastTransferWindow.Count + + (int)Math.Ceiling((double)(this.blob.Properties.Length - this.SharedTransferData.TransferJob.CheckPoint.EntryTransferOffset) / this.Scheduler.TransferOptions.BlockSize); + + if (0 == downloadCount) + { + this.isFinished = true; + this.PreProcessed = true; + this.hasWork = true; + } + else + { + this.downloadCountdownEvent = new CountdownEvent(downloadCount); + + this.PreProcessed = true; + this.hasWork = true; + } + } + + private async Task DownloadBlockBlobAsync() + { + this.hasWork = false; + + byte[] memoryBuffer = this.Scheduler.MemoryManager.RequireBuffer(); + + if (null != memoryBuffer) + { + long startOffset = 0; + + if (!this.IsTransferWindowEmpty()) + { + startOffset = this.lastTransferWindow.Dequeue(); + } + else + { + bool canUpload = false; + + lock (this.transferJob.CheckPoint.TransferWindowLock) + { + if (this.transferJob.CheckPoint.TransferWindow.Count < Constants.MaxCountInTransferWindow) + { + startOffset = this.transferJob.CheckPoint.EntryTransferOffset; + + if (this.transferJob.CheckPoint.EntryTransferOffset < this.SharedTransferData.TotalLength) + { + this.transferJob.CheckPoint.TransferWindow.Add(startOffset); + this.transferJob.CheckPoint.EntryTransferOffset = Math.Min( + this.transferJob.CheckPoint.EntryTransferOffset + this.Scheduler.TransferOptions.BlockSize, + this.SharedTransferData.TotalLength); + + canUpload = true; + } + } + } + + if (!canUpload) + { + this.hasWork = true; + this.Scheduler.MemoryManager.ReleaseBuffer(memoryBuffer); + return; + } + } + + if ((startOffset > this.SharedTransferData.TotalLength) + || (startOffset < 0)) + { + this.Scheduler.MemoryManager.ReleaseBuffer(memoryBuffer); + throw new InvalidOperationException(Resources.RestartableInfoCorruptedException); + } + + this.SetBlockDownloadHasWork(); + + ReadDataState asyncState = new ReadDataState + { + MemoryBuffer = memoryBuffer, + BytesRead = 0, + StartOffset = startOffset, + Length = (int)Math.Min(this.Scheduler.TransferOptions.BlockSize, this.SharedTransferData.TotalLength - startOffset), + MemoryManager = this.Scheduler.MemoryManager, + }; + + using (asyncState) + { + await this.DownloadChunkAsync(asyncState); + } + + return; + } + + this.SetBlockDownloadHasWork(); + } + + private async Task DownloadChunkAsync(ReadDataState asyncState) + { + Debug.Assert(null != asyncState, "asyncState object expected"); + + // If a parallel operation caused the controller to be placed in + // error state exit early to avoid unnecessary I/O. + if (this.Controller.ErrorOccurred) + { + return; + } + + AccessCondition accessCondition = Utils.GenerateIfMatchConditionWithCustomerCondition( + this.blob.Properties.ETag, + this.transferLocation.AccessCondition); + + // We're to download this block. + asyncState.MemoryStream = + new MemoryStream( + asyncState.MemoryBuffer, + 0, + asyncState.Length); + + await this.blob.DownloadRangeToStreamAsync( + asyncState.MemoryStream, + asyncState.StartOffset, + asyncState.Length, + accessCondition, + Utils.GenerateBlobRequestOptions(this.transferLocation.BlobRequestOptions), + Utils.GenerateOperationContext(this.Controller.TransferContext), + this.CancellationToken); + + TransferData transferData = new TransferData(this.Scheduler.MemoryManager) + { + StartOffset = asyncState.StartOffset, + Length = asyncState.Length, + MemoryBuffer = asyncState.MemoryBuffer + }; + + this.SharedTransferData.AvailableData.TryAdd(transferData.StartOffset, transferData); + + // Set memory buffer to null. We don't want its dispose method to + // be called once our asyncState is disposed. The memory should + // not be reused yet, we still need to write it to disk. + asyncState.MemoryBuffer = null; + + this.SetFinish(); + this.SetBlockDownloadHasWork(); + } + + private void SetFinish() + { + if (this.downloadCountdownEvent.Signal()) + { + this.isFinished = true; + } + } + + private void SetBlockDownloadHasWork() + { + if (this.HasWork) + { + return; + } + + // Check if we have blocks available to download. + if (!this.IsTransferWindowEmpty() + || this.transferJob.CheckPoint.EntryTransferOffset < this.SharedTransferData.TotalLength) + { + this.hasWork = true; + return; + } + } + + private bool IsTransferWindowEmpty() + { + return null == this.lastTransferWindow || this.lastTransferWindow.Count == 0; + } + } +} diff --git a/lib/TransferControllers/TransferReaders/CloudFileReader.cs b/lib/TransferControllers/TransferReaders/CloudFileReader.cs new file mode 100644 index 00000000..5948a809 --- /dev/null +++ b/lib/TransferControllers/TransferReaders/CloudFileReader.cs @@ -0,0 +1,100 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement.TransferControllers +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.WindowsAzure.Storage.File; + + class CloudFileReader : RangeBasedReader + { + private CloudFile file; + + public CloudFileReader( + TransferScheduler scheduler, + SyncTransferController controller, + CancellationToken cancellationToken) + :base(scheduler, controller, cancellationToken) + { + this.file = this.SharedTransferData.TransferJob.Source.AzureFile; + Debug.Assert(null != this.file, "Initializing a CloudFileReader, the source location should be a CloudFile instance."); + } + + protected override async Task DoFetchAttributesAsync() + { + await this.file.FetchAttributesAsync( + null, + Utils.GenerateFileRequestOptions(this.Location.FileRequestOptions), + Utils.GenerateOperationContext(this.Controller.TransferContext), + this.CancellationToken); + + if (string.IsNullOrEmpty(this.Location.ETag)) + { + if ((0 != this.SharedTransferData.TransferJob.CheckPoint.EntryTransferOffset) + || (this.SharedTransferData.TransferJob.CheckPoint.TransferWindow.Any())) + { + throw new InvalidOperationException(Resources.RestartableInfoCorruptedException); + } + + this.Location.ETag = this.Location.AzureFile.Properties.ETag; + } + else if ((this.SharedTransferData.TransferJob.CheckPoint.EntryTransferOffset > this.Location.AzureFile.Properties.Length) + || (this.SharedTransferData.TransferJob.CheckPoint.EntryTransferOffset < 0)) + { + throw new InvalidOperationException(Resources.RestartableInfoCorruptedException); + } + + this.SharedTransferData.DisableContentMD5Validation = + null != this.Location.FileRequestOptions ? + this.Location.FileRequestOptions.DisableContentMD5Validation.HasValue ? + this.Location.FileRequestOptions.DisableContentMD5Validation.Value : false : false; + + this.SharedTransferData.Attributes = Utils.GenerateAttributes(this.file); + this.SharedTransferData.TotalLength = this.file.Properties.Length; + this.SharedTransferData.SourceLocation = this.file.Uri.ToString(); + } + + protected override async Task> DoGetRangesAsync(RangesSpan rangesSpan) + { + List rangeList = new List(); + + foreach (var fileRange in await this.file.ListRangesAsync( + rangesSpan.StartOffset, + rangesSpan.EndOffset - rangesSpan.StartOffset + 1, + null, + Utils.GenerateFileRequestOptions(this.Location.FileRequestOptions), + Utils.GenerateOperationContext(this.Controller.TransferContext), + this.CancellationToken)) + { + rangeList.Add(new Range() + { + StartOffset = fileRange.StartOffset, + EndOffset = fileRange.EndOffset, + HasData = true + }); + } + + return rangeList; + } + + protected override async Task DoDownloadRangeToStreamAsync(RangeBasedDownloadState asyncState) + { + await this.Location.AzureFile.DownloadRangeToStreamAsync( + asyncState.DownloadStream, + asyncState.StartOffset, + asyncState.Length, + null, + Utils.GenerateFileRequestOptions(this.Location.FileRequestOptions), + Utils.GenerateOperationContext(this.Controller.TransferContext), + this.CancellationToken); + } + } +} diff --git a/lib/TransferControllers/TransferReaders/PageBlobReader.cs b/lib/TransferControllers/TransferReaders/PageBlobReader.cs new file mode 100644 index 00000000..9d2d7142 --- /dev/null +++ b/lib/TransferControllers/TransferReaders/PageBlobReader.cs @@ -0,0 +1,113 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement.TransferControllers +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.WindowsAzure.Storage.Blob; + + internal sealed class PageBlobReader : RangeBasedReader + { + private CloudPageBlob pageBlob; + + public PageBlobReader( + TransferScheduler scheduler, + SyncTransferController controller, + CancellationToken cancellationToken) + :base(scheduler, controller, cancellationToken) + { + pageBlob = this.SharedTransferData.TransferJob.Source.Blob as CloudPageBlob; + Debug.Assert(null != this.pageBlob, "Initializing a PageBlobReader, the source location should be a CloudPageBlob instance."); + } + + protected override async Task DoFetchAttributesAsync() + { + AccessCondition accessCondition = Utils.GenerateIfMatchConditionWithCustomerCondition( + this.Location.ETag, + this.Location.AccessCondition, + this.Location.CheckedAccessCondition); + + await this.pageBlob.FetchAttributesAsync( + accessCondition, + Utils.GenerateBlobRequestOptions(this.Location.BlobRequestOptions), + Utils.GenerateOperationContext(this.Controller.TransferContext), + this.CancellationToken); + + if (string.IsNullOrEmpty(this.Location.ETag)) + { + if ((0 != this.SharedTransferData.TransferJob.CheckPoint.EntryTransferOffset) + || (this.SharedTransferData.TransferJob.CheckPoint.TransferWindow.Any())) + { + throw new InvalidOperationException(Resources.RestartableInfoCorruptedException); + } + + this.Location.ETag = this.Location.Blob.Properties.ETag; + } + else if ((this.SharedTransferData.TransferJob.CheckPoint.EntryTransferOffset > this.Location.Blob.Properties.Length) + || (this.SharedTransferData.TransferJob.CheckPoint.EntryTransferOffset < 0)) + { + throw new InvalidOperationException(Resources.RestartableInfoCorruptedException); + } + + this.SharedTransferData.DisableContentMD5Validation = + null != this.Location.BlobRequestOptions ? + this.Location.BlobRequestOptions.DisableContentMD5Validation.HasValue ? + this.Location.BlobRequestOptions.DisableContentMD5Validation.Value : false : false; + + this.SharedTransferData.Attributes = Utils.GenerateAttributes(this.pageBlob); + this.SharedTransferData.TotalLength = this.pageBlob.Properties.Length; + this.SharedTransferData.SourceLocation = this.pageBlob.Uri.ToString(); + } + + protected override async Task> DoGetRangesAsync(RangesSpan rangesSpan) + { + AccessCondition accessCondition = Utils.GenerateIfMatchConditionWithCustomerCondition( + this.Location.Blob.Properties.ETag, + this.Location.AccessCondition); + + List rangeList = new List(); + + foreach (var pageRange in await this.pageBlob.GetPageRangesAsync( + rangesSpan.StartOffset, + rangesSpan.EndOffset - rangesSpan.StartOffset + 1, + accessCondition, + Utils.GenerateBlobRequestOptions(this.Location.BlobRequestOptions), + Utils.GenerateOperationContext(this.Controller.TransferContext), + this.CancellationToken)) + { + rangeList.Add(new Range() + { + StartOffset = pageRange.StartOffset, + EndOffset = pageRange.EndOffset, + HasData = true + }); + } + + return rangeList; + } + + protected override async Task DoDownloadRangeToStreamAsync(RangeBasedDownloadState asyncState) + { + AccessCondition accessCondition = Utils.GenerateIfMatchConditionWithCustomerCondition( + this.Location.Blob.Properties.ETag, + this.Location.AccessCondition); + + await this.Location.Blob.DownloadRangeToStreamAsync( + asyncState.DownloadStream, + asyncState.StartOffset, + asyncState.Length, + accessCondition, + Utils.GenerateBlobRequestOptions(this.Location.BlobRequestOptions), + Utils.GenerateOperationContext(this.Controller.TransferContext), + this.CancellationToken); + } + } +} diff --git a/lib/TransferControllers/TransferReaders/RangeBasedReader.cs b/lib/TransferControllers/TransferReaders/RangeBasedReader.cs new file mode 100644 index 00000000..10e0be00 --- /dev/null +++ b/lib/TransferControllers/TransferReaders/RangeBasedReader.cs @@ -0,0 +1,890 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement.TransferControllers +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Net; + using System.Threading; + using System.Threading.Tasks; + + internal abstract class RangeBasedReader : TransferReaderWriterBase + { + /// + /// Minimum size of empty range, the empty ranges which is smaller than this size will be merged to the adjacent range with data. + /// + const int MinimumNoDataRangeSize = 8 * 1024; + + private volatile State state; + private TransferJob transferJob; + private CountdownEvent getRangesCountDownEvent; + private CountdownEvent toDownloadItemsCountdownEvent; + private int getRangesSpanIndex = 0; + private List rangesSpanList; + private List rangeList; + private int nextDownloadIndex = 0; + private long lastTransferOffset; + private TransferDownloadBuffer currentDownloadBuffer = null; + + private volatile bool hasWork; + + public RangeBasedReader( + TransferScheduler scheduler, + SyncTransferController controller, + CancellationToken cancellationToken) + : base(scheduler, controller, cancellationToken) + { + this.transferJob = this.SharedTransferData.TransferJob; + this.Location = this.transferJob.Source; + this.hasWork = true; + } + + private enum State + { + FetchAttributes, + GetRanges, + Download, + Error, + Finished + }; + + public override async Task DoWorkInternalAsync() + { + try + { + switch (this.state) + { + case State.FetchAttributes: + await this.FetchAttributesAsync(); + break; + case State.GetRanges: + await this.GetRangesAsync(); + break; + case State.Download: + await this.DownloadRangeAsync(); + break; + default: + break; + } + } + catch + { + this.state = State.Error; + throw; + } + } + + public override bool HasWork + { + get + { + return this.hasWork; + } + } + + public override bool IsFinished + { + get + { + return State.Error == this.state || State.Finished == this.state; + } + } + + protected TransferLocation Location + { + get; + private set; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + if (null != this.getRangesCountDownEvent) + { + this.getRangesCountDownEvent.Dispose(); + this.getRangesCountDownEvent = null; + } + + if (null != this.toDownloadItemsCountdownEvent) + { + this.toDownloadItemsCountdownEvent.Dispose(); + this.toDownloadItemsCountdownEvent = null; + } + } + } + + private async Task FetchAttributesAsync() + { + Debug.Assert( + this.state == State.FetchAttributes, + "FetchAttributesAsync called, but state isn't FetchAttributes"); + + this.hasWork = false; + this.NotifyStarting(); + + try + { + await this.DoFetchAttributesAsync(); + } + catch (StorageException e) + { + // Getting a storage exception is expected if the blob doesn't + // exist. For those cases that indicate the blob doesn't exist + // we will set a specific error state. + if (null != e.RequestInformation && + e.RequestInformation.HttpStatusCode == (int)HttpStatusCode.NotFound) + { + throw new InvalidOperationException(Resources.SourceBlobDoesNotExistException); + } + else + { + throw; + } + } + + this.Location.CheckedAccessCondition = true; + + this.Controller.CheckCancellation(); + + this.state = State.GetRanges; + this.PrepareToGetRanges(); + + if (!this.rangesSpanList.Any()) + { + // InitDownloadInfo will set hasWork. + this.InitDownloadInfo(); + this.PreProcessed = true; + return; + } + + this.PreProcessed = true; + this.hasWork = true; + } + + private async Task GetRangesAsync() + { + Debug.Assert( + (this.state == State.GetRanges) || (this.state == State.Error), + "GetRangesAsync called, but state isn't GetRanges or Error"); + + this.hasWork = false; + + this.lastTransferOffset = this.SharedTransferData.TransferJob.CheckPoint.EntryTransferOffset; + + int spanIndex = Interlocked.Increment(ref this.getRangesSpanIndex); + + this.hasWork = spanIndex < (this.rangesSpanList.Count - 1); + + RangesSpan rangesSpan = this.rangesSpanList[spanIndex]; + + rangesSpan.Ranges = await this.DoGetRangesAsync(rangesSpan); + + List ranges = new List(); + Range currentRange = null; + long currentStartOffset = rangesSpan.StartOffset; + + foreach (var range in rangesSpan.Ranges) + { + long emptySize = range.StartOffset - currentStartOffset; + if (emptySize > 0 && emptySize < MinimumNoDataRangeSize) + { + // There is empty range which size is smaller than MinimumNoDataRangeSize + // merge it to the adjacent data range. + if (null == currentRange) + { + currentRange = new Range() + { + StartOffset = currentStartOffset, + EndOffset = range.EndOffset, + HasData = range.HasData + }; + } + else + { + currentRange.EndOffset = range.EndOffset; + } + } + else + { + // Empty range size is larger than MinimumNoDataRangeSize + // put current data range in list and start to deal with the next data range. + if (null != currentRange) + { + ranges.Add(currentRange); + } + + currentRange = new Range + { + StartOffset = range.StartOffset, + EndOffset = range.EndOffset, + HasData = range.HasData + }; + } + + currentStartOffset = range.EndOffset + 1; + } + + if (null != currentRange) + { + ranges.Add(currentRange); + } + + rangesSpan.Ranges = ranges; + + if (this.getRangesCountDownEvent.Signal()) + { + this.ArrangeRanges(); + + // Don't call CallFinish here, InitDownloadInfo will call it. + this.InitDownloadInfo(); + } + } + + private async Task DownloadRangeAsync() + { + Debug.Assert( + this.state == State.Error || this.state == State.Download, + "DownloadRangeAsync called, but state isn't Download or Error"); + + this.hasWork = false; + + if (State.Error == this.state) + { + // Some thread has set error message, just return here. + return; + } + + if (this.nextDownloadIndex < this.rangeList.Count) + { + Range rangeData = this.rangeList[this.nextDownloadIndex]; + + int blockSize = this.Scheduler.TransferOptions.BlockSize; + long blockStartOffset = (rangeData.StartOffset / blockSize) * blockSize; + long nextBlockStartOffset = Math.Min(blockStartOffset + blockSize, this.SharedTransferData.TotalLength); + + TransferDownloadStream downloadStream = null; + + if ((rangeData.StartOffset > blockStartOffset) && (rangeData.EndOffset < nextBlockStartOffset)) + { + Debug.Assert(null != this.currentDownloadBuffer, "Download buffer should have been allocated when range start offset is not block size aligned"); + downloadStream = new TransferDownloadStream(this.Scheduler.MemoryManager, this.currentDownloadBuffer, (int)(rangeData.StartOffset - blockStartOffset), (int)(rangeData.EndOffset + 1 - rangeData.StartOffset)); + } + else + { + // Attempt to reserve memory. If none available we'll + // retry some time later. + byte[] memoryBuffer = this.Scheduler.MemoryManager.RequireBuffer(); + + if (null == memoryBuffer) + { + this.SetRangeDownloadHasWork(); + return; + } + + if (rangeData.EndOffset >= this.lastTransferOffset) + { + bool canRead = true; + lock (this.transferJob.CheckPoint.TransferWindowLock) + { + if (this.transferJob.CheckPoint.TransferWindow.Count >= Constants.MaxCountInTransferWindow) + { + canRead = false; + } + else + { + if (this.transferJob.CheckPoint.EntryTransferOffset < this.SharedTransferData.TotalLength) + { + this.transferJob.CheckPoint.TransferWindow.Add(this.transferJob.CheckPoint.EntryTransferOffset); + this.transferJob.CheckPoint.EntryTransferOffset = Math.Min(this.transferJob.CheckPoint.EntryTransferOffset + this.Scheduler.TransferOptions.BlockSize, this.SharedTransferData.TotalLength); + } + } + } + + if (!canRead) + { + this.Scheduler.MemoryManager.ReleaseBuffer(memoryBuffer); + this.SetRangeDownloadHasWork(); + return; + } + } + + if (rangeData.StartOffset == blockStartOffset) + { + this.currentDownloadBuffer = new TransferDownloadBuffer(blockStartOffset, (int)Math.Min(blockSize, this.SharedTransferData.TotalLength - blockStartOffset), memoryBuffer); + downloadStream = new TransferDownloadStream(this.Scheduler.MemoryManager, this.currentDownloadBuffer, 0, (int)(rangeData.EndOffset + 1 - rangeData.StartOffset)); + } + else + { + Debug.Assert(null != this.currentDownloadBuffer, "Download buffer should have been allocated when range start offset is not block size aligned"); + + TransferDownloadBuffer nextBuffer = new TransferDownloadBuffer(nextBlockStartOffset, (int)Math.Min(blockSize, this.SharedTransferData.TotalLength - nextBlockStartOffset), memoryBuffer); + + downloadStream = new TransferDownloadStream( + this.Scheduler.MemoryManager, + this.currentDownloadBuffer, + (int)(rangeData.StartOffset - blockStartOffset), + (int)(nextBlockStartOffset - rangeData.StartOffset), + nextBuffer, + 0, + (int)(rangeData.EndOffset + 1 - nextBlockStartOffset)); + + this.currentDownloadBuffer = nextBuffer; + } + } + + using (downloadStream) + { + this.nextDownloadIndex++; + this.SetRangeDownloadHasWork(); + + RangeBasedDownloadState rangeBasedDownloadState = new RangeBasedDownloadState + { + Range = rangeData, + DownloadStream = downloadStream + }; + + await this.DownloadRangeAsync(rangeBasedDownloadState); + } + + this.SetChunkFinish(); + return; + } + + this.SetRangeDownloadHasWork(); + } + + private void SetRangeDownloadHasWork() + { + if (this.HasWork) + { + return; + } + + // Check if we have ranges available to download. + if (this.nextDownloadIndex < this.rangeList.Count) + { + this.hasWork = true; + return; + } + } + + private async Task DownloadRangeAsync(RangeBasedDownloadState asyncState) + { + Debug.Assert(null != asyncState, "asyncState object expected"); + Debug.Assert( + this.state == State.Download || this.state == State.Error, + "DownloadRangeAsync called, but state isn't Download or Error"); + + // If a parallel operation caused the controller to be placed in + // error state exit early to avoid unnecessary I/O. + if (this.state == State.Error) + { + return; + } + + if (asyncState.Range.HasData) + { + await this.DoDownloadRangeToStreamAsync(asyncState); + } + else + { + // Zero memory buffer. + asyncState.DownloadStream.SetAllZero(); + } + + asyncState.DownloadStream.FinishWrite(); + asyncState.DownloadStream.ReserveBuffer = true; + + foreach (var buffer in asyncState.DownloadStream.GetBuffers()) + { + // Two download streams can refer to the same download buffer instance. It may cause the download + // buffer be added into shared transfer data twice if only buffer.Finished is checked here: + // Thread A: FinishedWrite() + // Thread B: FinishedWrite(), buffer.Finished is true now + // Thread A: Check buffer.Finished + // Thread B: Check buffer.Finished + // Thread A: Add buffer into sharedTransferData + // Thread C: Writer remove buffer from sharedTransferData + // Thread B: Add buffer into sharedTransferData again + // So call MarkAsProcessed to make sure buffer is added exactly once. + if (buffer.Finished && buffer.MarkAsProcessed()) + { + TransferData transferData = new TransferData(this.Scheduler.MemoryManager) + { + StartOffset = buffer.StartOffset, + Length = buffer.Length, + MemoryBuffer = buffer.MemoryBuffer + }; + + this.SharedTransferData.AvailableData.TryAdd(buffer.StartOffset, transferData); + } + } + } + + /// + /// It might fail to get large ranges list from storage. This method is to split the whole file to spans of 148MB to get ranges. + /// In restartable, we only need to get ranges for chunks in TransferWindow and after TransferEntryOffset in check point. + /// In TransferWindow, there might be some chunks adjacent to TransferEntryOffset, so this method will first merge these chunks into TransferEntryOffset; + /// Then in remained chunks in the TransferWindow, it's very possible that ranges of several chunks can be got in one 148MB span. + /// To avoid sending too many get ranges requests, this method will merge the chunks to 148MB spans. + /// + private void PrepareToGetRanges() + { + this.getRangesSpanIndex = -1; + this.rangesSpanList = new List(); + this.rangeList = new List(); + + this.nextDownloadIndex = 0; + + SingleObjectCheckpoint checkpoint = this.transferJob.CheckPoint; + int blockSize = this.Scheduler.TransferOptions.BlockSize; + + RangesSpan rangesSpan = null; + + if ((null != checkpoint.TransferWindow) + && (checkpoint.TransferWindow.Any())) + { + checkpoint.TransferWindow.Sort(); + + long lastOffset = 0; + if (checkpoint.EntryTransferOffset == this.SharedTransferData.TotalLength) + { + long lengthBeforeLastChunk = checkpoint.EntryTransferOffset % blockSize; + lastOffset = 0 == lengthBeforeLastChunk ? + checkpoint.EntryTransferOffset - blockSize : + checkpoint.EntryTransferOffset - lengthBeforeLastChunk; + } + else + { + lastOffset = checkpoint.EntryTransferOffset - blockSize; + } + + for (int i = checkpoint.TransferWindow.Count - 1; i >= 0; i--) + { + if (lastOffset == checkpoint.TransferWindow[i]) + { + checkpoint.TransferWindow.RemoveAt(i); + checkpoint.EntryTransferOffset = lastOffset; + } + else if (lastOffset < checkpoint.TransferWindow[i]) + { + throw new FormatException(Resources.RestartableInfoCorruptedException); + } + else + { + break; + } + + lastOffset = checkpoint.EntryTransferOffset - blockSize; + } + + if (this.transferJob.CheckPoint.TransferWindow.Any()) + { + rangesSpan = new RangesSpan(); + rangesSpan.StartOffset = checkpoint.TransferWindow[0]; + rangesSpan.EndOffset = Math.Min(rangesSpan.StartOffset + Constants.PageRangesSpanSize, this.SharedTransferData.TotalLength) - 1; + + for (int i = 1; i < checkpoint.TransferWindow.Count; ++i ) + { + if (checkpoint.TransferWindow[i] + blockSize > rangesSpan.EndOffset) + { + long lastEndOffset = rangesSpan.EndOffset; + this.rangesSpanList.Add(rangesSpan); + rangesSpan = new RangesSpan(); + rangesSpan.StartOffset = checkpoint.TransferWindow[i] > lastEndOffset ? checkpoint.TransferWindow[i] : lastEndOffset + 1; + rangesSpan.EndOffset = Math.Min(rangesSpan.StartOffset + Constants.PageRangesSpanSize, this.SharedTransferData.TotalLength) - 1; + } + } + + this.rangesSpanList.Add(rangesSpan); + } + } + + long offset = null != rangesSpan ? + rangesSpan.EndOffset > checkpoint.EntryTransferOffset ? + rangesSpan.EndOffset + 1 : + checkpoint.EntryTransferOffset : + checkpoint.EntryTransferOffset; + + while (offset < this.SharedTransferData.TotalLength) + { + rangesSpan = new RangesSpan() + { + StartOffset = offset, + EndOffset = Math.Min(offset + Constants.PageRangesSpanSize, this.SharedTransferData.TotalLength) - 1 + }; + + this.rangesSpanList.Add(rangesSpan); + offset = rangesSpan.EndOffset + 1; + } + + if (this.rangesSpanList.Any()) + { + this.getRangesCountDownEvent = new CountdownEvent(this.rangesSpanList.Count); + } + } + + private void ClearForGetRanges() + { + this.rangesSpanList = null; + + if (null != this.getRangesCountDownEvent) + { + this.getRangesCountDownEvent.Dispose(); + this.getRangesCountDownEvent = null; + } + } + + /// + /// Turn raw ranges get from Azure Storage in rangesSpanList + /// into list of Range. + /// + private void ArrangeRanges() + { + long currentEndOffset = -1; + + IEnumerator enumerator = this.rangesSpanList.GetEnumerator(); + bool hasValue = enumerator.MoveNext(); + bool reachLastTransferOffset = false; + int lastTransferWindowIndex = 0; + + RangesSpan current; + RangesSpan next; + + if (hasValue) + { + current = enumerator.Current; + + while (hasValue) + { + hasValue = enumerator.MoveNext(); + + if (!current.Ranges.Any()) + { + current = enumerator.Current; + continue; + } + + if (hasValue) + { + next = enumerator.Current; + + Debug.Assert( + current.EndOffset < this.transferJob.CheckPoint.EntryTransferOffset + || ((current.EndOffset + 1) == next.StartOffset), + "Something wrong with ranges list."); + + if (next.Ranges.Any()) + { + if ((current.Ranges.Last().EndOffset + 1) == next.Ranges.First().StartOffset) + { + Range mergedRange = new Range() + { + StartOffset = current.Ranges.Last().StartOffset, + EndOffset = next.Ranges.First().EndOffset, + HasData = true + }; + + current.Ranges.RemoveAt(current.Ranges.Count - 1); + next.Ranges.RemoveAt(0); + current.Ranges.Add(mergedRange); + current.EndOffset = mergedRange.EndOffset; + next.StartOffset = mergedRange.EndOffset + 1; + + if (next.EndOffset == mergedRange.EndOffset) + { + continue; + } + } + } + } + + foreach (Range range in current.Ranges) + { + // Check if we have a gap before the current range. + // If so we'll generate a range with HasData = false. + if (currentEndOffset != range.StartOffset - 1) + { + this.AddRangesByCheckPoint( + currentEndOffset + 1, + range.StartOffset - 1, + false, + ref reachLastTransferOffset, + ref lastTransferWindowIndex); + } + + this.AddRangesByCheckPoint( + range.StartOffset, + range.EndOffset, + true, + ref reachLastTransferOffset, + ref lastTransferWindowIndex); + + currentEndOffset = range.EndOffset; + } + + current = enumerator.Current; + } + } + + if (currentEndOffset < this.SharedTransferData.TotalLength - 1) + { + this.AddRangesByCheckPoint( + currentEndOffset + 1, + this.SharedTransferData.TotalLength - 1, + false, + ref reachLastTransferOffset, + ref lastTransferWindowIndex); + } + } + + private void AddRangesByCheckPoint(long startOffset, long endOffset, bool hasData, ref bool reachLastTransferOffset, ref int lastTransferWindowIndex) + { + SingleObjectCheckpoint checkpoint = this.transferJob.CheckPoint; + if (reachLastTransferOffset) + { + this.rangeList.AddRange( + new Range + { + StartOffset = startOffset, + EndOffset = endOffset, + HasData = hasData, + }.SplitRanges(this.Scheduler.TransferOptions.BlockSize)); + } + else + { + Range range = new Range() + { + StartOffset = -1, + HasData = hasData + }; + + while (lastTransferWindowIndex < checkpoint.TransferWindow.Count) + { + long lastTransferWindowStart = checkpoint.TransferWindow[lastTransferWindowIndex]; + long lastTransferWindowEnd = Math.Min(checkpoint.TransferWindow[lastTransferWindowIndex] + this.Scheduler.TransferOptions.BlockSize - 1, this.SharedTransferData.TotalLength); + + if (lastTransferWindowStart <= endOffset) + { + if (-1 == range.StartOffset) + { + // New range + range.StartOffset = Math.Max(lastTransferWindowStart, startOffset); + range.EndOffset = Math.Min(lastTransferWindowEnd, endOffset); + } + else + { + if (range.EndOffset != lastTransferWindowStart - 1) + { + // Store the previous range and create a new one + this.rangeList.AddRange(range.SplitRanges(this.Scheduler.TransferOptions.BlockSize)); + range = new Range() + { + StartOffset = Math.Max(lastTransferWindowStart, startOffset), + HasData = hasData + }; + } + + range.EndOffset = Math.Min(lastTransferWindowEnd, endOffset); + } + + if (range.EndOffset == lastTransferWindowEnd) + { + // Reach the end of transfer window, move to next + ++lastTransferWindowIndex; + continue; + } + } + + break; + } + + if (-1 != range.StartOffset) + { + this.rangeList.AddRange(range.SplitRanges(this.Scheduler.TransferOptions.BlockSize)); + } + + if (checkpoint.EntryTransferOffset <= endOffset + 1) + { + reachLastTransferOffset = true; + + if (checkpoint.EntryTransferOffset <= endOffset) + { + this.rangeList.AddRange(new Range() + { + StartOffset = checkpoint.EntryTransferOffset, + EndOffset = endOffset, + HasData = hasData, + }.SplitRanges(this.Scheduler.TransferOptions.BlockSize)); + } + } + } + } + + /// + /// To initialize range based object download related information in the controller. + /// This method will call CallFinish. + /// + private void InitDownloadInfo() + { + this.ClearForGetRanges(); + + this.state = State.Download; + + if (this.rangeList.Count == this.nextDownloadIndex) + { + this.toDownloadItemsCountdownEvent = new CountdownEvent(1); + this.SetChunkFinish(); + } + else + { + this.toDownloadItemsCountdownEvent = new CountdownEvent(this.rangeList.Count); + this.hasWork = true; + } + } + + private void SetChunkFinish() + { + if (this.toDownloadItemsCountdownEvent.Signal()) + { + this.state = State.Finished; + this.hasWork = false; + } + } + + protected class RangesSpan + { + public long StartOffset + { + get; + set; + } + + public long EndOffset + { + get; + set; + } + + public List Ranges + { + get; + set; + } + } + + protected class Range + { + public long StartOffset + { + get; + set; + } + + public long EndOffset + { + get; + set; + } + + public bool HasData + { + get; + set; + } + + /// + /// Split a Range into multiple Range objects, each at most maxRangeSize long. + /// + /// Maximum length for each piece. + /// List of Range objects. + public IEnumerable SplitRanges(long maxRangeSize) + { + long startOffset = this.StartOffset; + long rangeSize = this.EndOffset - this.StartOffset + 1; + + do + { + long singleRangeSize = Math.Min(rangeSize, maxRangeSize); + Range subRange = new Range + { + StartOffset = startOffset, + EndOffset = startOffset + singleRangeSize - 1, + HasData = this.HasData, + }; + + startOffset += singleRangeSize; + rangeSize -= singleRangeSize; + + yield return subRange; + } + while (rangeSize > 0); + } + } + + protected class RangeBasedDownloadState + { + private Range range; + + public Range Range + { + get + { + return this.range; + } + + set + { + this.range = value; + + this.StartOffset = value.StartOffset; + this.Length = (int)(value.EndOffset - value.StartOffset + 1); + } + } + + /// + /// Gets or sets a handle to the memory buffer to ensure the + /// memory buffer remains in memory during the entire operation. + /// + public TransferDownloadStream DownloadStream + { + get; + set; + } + + /// + /// Gets or sets the starting offset of this part of data. + /// + public long StartOffset + { + get; + set; + } + + /// + /// Gets or sets the length of this part of data. + /// + public int Length + { + get; + set; + } + } + + protected abstract Task DoFetchAttributesAsync(); + + protected abstract Task DoDownloadRangeToStreamAsync(RangeBasedDownloadState asyncState); + + protected abstract Task> DoGetRangesAsync(RangesSpan rangesSpan); + } +} diff --git a/lib/TransferControllers/TransferReaders/StreamedReader.cs b/lib/TransferControllers/TransferReaders/StreamedReader.cs new file mode 100644 index 00000000..f84f5704 --- /dev/null +++ b/lib/TransferControllers/TransferReaders/StreamedReader.cs @@ -0,0 +1,426 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement.TransferControllers +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Security; + using System.Threading; + using System.Threading.Tasks; + + internal sealed class StreamedReader : TransferReaderWriterBase + { + /// + /// Source stream to be read from. + /// It's a user input stream or a FileStream with the user input FilePath in source location. + /// + private Stream inputStream; + + /// + /// Value to indicate whether the input stream is a file stream owned by this reader or input by user. + /// If it's a file stream owned by this reader, we should close it when reading is finished. + /// + private bool ownsStream; + + /// + /// Transfer job instance. + /// + private TransferJob transferJob; + + /// + /// Countdown event to track the download status. + /// Its count should be the same with count of chunks to be read. + /// + private CountdownEvent countdownEvent; + + /// + /// Transfer window in check point. + /// + private Queue lastTransferWindow; + + private volatile State state; + + private volatile bool hasWork; + + /// + /// Stream to read from source and calculate md5 hash of source. + /// + private MD5HashStream md5HashStream; + + public StreamedReader( + TransferScheduler scheduler, + SyncTransferController controller, + CancellationToken cancellationToken) + : base(scheduler, controller, cancellationToken) + { + this.transferJob = this.SharedTransferData.TransferJob; + this.hasWork = true; + } + + private enum State + { + OpenInputStream, + ReadStream, + Error, + Finished + } + + public override bool IsFinished + { + get + { + return this.state == State.Error || this.state == State.Finished; + } + } + + public override bool HasWork + { + get + { + return this.hasWork; + } + } + + public override async Task DoWorkInternalAsync() + { + switch (this.state) + { + case State.OpenInputStream: + await this.OpenInputStreamAsync(); + break; + case State.ReadStream: + await this.ReadStreamAsync(); + break; + case State.Error: + case State.Finished: + break; + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + this.CloseOwnStream(); + + if (null != this.md5HashStream) + { + this.md5HashStream.Dispose(); + this.md5HashStream = null; + } + + if (null != this.countdownEvent) + { + this.countdownEvent.Dispose(); + } + } + } + + private async Task OpenInputStreamAsync() + { + Debug.Assert( + State.OpenInputStream == this.state, + "OpenInputStreamAsync called, but state is not OpenInputStream."); + + this.hasWork = false; + + await Task.Run(() => + { + this.NotifyStarting(); + this.Controller.CheckCancellation(); + + if (this.transferJob.Source.Stream != null) + { + this.inputStream = this.transferJob.Source.Stream; + this.ownsStream = false; + + if (!this.inputStream.CanRead) + { + throw new NotSupportedException(string.Format( + CultureInfo.CurrentCulture, + Resources.StreamMustSupportReadException, + "inputStream")); + } + + if (!this.inputStream.CanSeek) + { + throw new NotSupportedException(string.Format( + CultureInfo.CurrentCulture, + Resources.StreamMustSupportSeekException, + "inputStream")); + } + } + else + { + Debug.Assert( + !string.IsNullOrEmpty(this.transferJob.Source.FilePath), + "Initializing StreamedReader instance, but source is neither a stream nor a file"); + this.SharedTransferData.SourceLocation = this.transferJob.Source.FilePath; + + try + { + // Attempt to open the file first so that we throw an exception before getting into the async work + this.inputStream = new FileStream( + this.transferJob.Source.FilePath, + FileMode.Open, + FileAccess.Read, + FileShare.Read); + + this.ownsStream = true; + } + catch (Exception ex) + { + if ((ex is NotSupportedException) || + (ex is IOException) || + (ex is UnauthorizedAccessException) || + (ex is SecurityException) || + (ex is ArgumentException && !(ex is ArgumentNullException))) + { + string exceptionMessage = string.Format( + CultureInfo.CurrentCulture, + Resources.FailedToOpenFileException, + this.transferJob.Source.FilePath, + ex.Message); + + throw new TransferException( + TransferErrorCode.OpenFileFailed, + exceptionMessage, + ex); + } + else + { + throw; + } + } + } + }); + + this.SharedTransferData.TotalLength = this.inputStream.Length; + + int count = (int)Math.Ceiling((double)(this.SharedTransferData.TotalLength - this.transferJob.CheckPoint.EntryTransferOffset) / this.Scheduler.TransferOptions.BlockSize); + + if (null != this.transferJob.CheckPoint.TransferWindow) + { + count += this.transferJob.CheckPoint.TransferWindow.Count; + } + + this.lastTransferWindow = new Queue(this.transferJob.CheckPoint.TransferWindow); + + this.md5HashStream = new MD5HashStream( + this.inputStream, + this.transferJob.CheckPoint.EntryTransferOffset, + true); + + this.PreProcessed = true; + + if (!this.md5HashStream.FinishedSeparateMd5Calculator) + { + await Task.Run(() => + { + this.md5HashStream.CalculateMd5(this.Scheduler.MemoryManager, this.Controller.CheckCancellation); + }); + } + + if (0 == count) + { + this.countdownEvent = new CountdownEvent(1); + this.SetChunkFinish(); + } + else + { + this.countdownEvent = new CountdownEvent(count); + + this.state = State.ReadStream; + this.hasWork = true; + } + } + + private async Task ReadStreamAsync() + { + Debug.Assert( + this.state == State.ReadStream || this.state == State.Error, + "ReadChunks called, but state isn't ReadStream or Error"); + + this.hasWork = false; + + byte[] memoryBuffer = this.Scheduler.MemoryManager.RequireBuffer(); + + if (null != memoryBuffer) + { + long startOffset = 0; + + if (0 != this.lastTransferWindow.Count) + { + startOffset = this.lastTransferWindow.Dequeue(); + } + else + { + bool canRead = false; + + lock (this.transferJob.CheckPoint.TransferWindowLock) + { + if (this.transferJob.CheckPoint.TransferWindow.Count < Constants.MaxCountInTransferWindow) + { + startOffset = this.transferJob.CheckPoint.EntryTransferOffset; + + if (this.transferJob.CheckPoint.EntryTransferOffset < this.SharedTransferData.TotalLength) + { + this.transferJob.CheckPoint.TransferWindow.Add(startOffset); + this.transferJob.CheckPoint.EntryTransferOffset = Math.Min( + this.transferJob.CheckPoint.EntryTransferOffset + this.Scheduler.TransferOptions.BlockSize, + this.SharedTransferData.TotalLength); + + canRead = true; + } + } + } + + if (!canRead) + { + this.Scheduler.MemoryManager.ReleaseBuffer(memoryBuffer); + this.hasWork = true; + return; + } + } + + if ((startOffset > this.SharedTransferData.TotalLength) + || (startOffset < 0)) + { + this.Scheduler.MemoryManager.ReleaseBuffer(memoryBuffer); + throw new InvalidOperationException(Resources.RestartableInfoCorruptedException); + } + + ReadDataState asyncState = new ReadDataState + { + MemoryBuffer = memoryBuffer, + BytesRead = 0, + StartOffset = startOffset, + Length = (int)Math.Min(this.Scheduler.TransferOptions.BlockSize, this.SharedTransferData.TotalLength - startOffset), + MemoryManager = this.Scheduler.MemoryManager, + }; + + using (asyncState) + { + await this.ReadChunkAsync(asyncState); + } + } + + this.SetHasWork(); + } + + private async Task ReadChunkAsync(ReadDataState asyncState) + { + Debug.Assert(null != asyncState, "asyncState object expected"); + Debug.Assert( + this.state == State.ReadStream || this.state == State.Error, + "ReadChunkAsync called, but state isn't Upload or Error"); + + int readBytes = await this.md5HashStream.ReadAsync( + asyncState.StartOffset + asyncState.BytesRead, + asyncState.MemoryBuffer, + asyncState.BytesRead, + asyncState.Length - asyncState.BytesRead, + this.CancellationToken); + + // If a parallel operation caused the controller to be placed in + // error state exit early to avoid unnecessary I/O. + // Note that this check needs to be after the EndRead operation + // above to avoid leaking resources. + if (this.state == State.Error) + { + return; + } + + asyncState.BytesRead += readBytes; + + if (asyncState.BytesRead < asyncState.Length) + { + await this.ReadChunkAsync(asyncState); + } + else + { + this.Controller.CheckCancellation(); + + if (!this.md5HashStream.MD5HashTransformBlock(asyncState.StartOffset, asyncState.MemoryBuffer, 0, asyncState.Length, null, 0)) + { + // Error info has been set in Calculate MD5 action, just return + return; + } + + TransferData transferData = new TransferData(this.Scheduler.MemoryManager) + { + StartOffset = asyncState.StartOffset, + Length = asyncState.Length, + MemoryBuffer = asyncState.MemoryBuffer + }; + + asyncState.MemoryBuffer = null; + + this.SharedTransferData.AvailableData.TryAdd(transferData.StartOffset, transferData); + + this.SetChunkFinish(); + } + } + + private void SetHasWork() + { + if (this.HasWork) + { + return; + } + + // Check if we have blocks available to download. + if ((null != this.lastTransferWindow && this.lastTransferWindow.Any()) + || this.transferJob.CheckPoint.EntryTransferOffset < this.SharedTransferData.TotalLength) + { + this.hasWork = true; + return; + } + } + + private void SetChunkFinish() + { + if (this.countdownEvent.Signal()) + { + this.state = State.Finished; + this.CloseOwnStream(); + + if (!this.md5HashStream.SucceededSeparateMd5Calculator) + { + return; + } + + this.md5HashStream.MD5HashTransformFinalBlock(new byte[0], 0, 0); + this.SharedTransferData.Attributes = new Attributes() + { + ContentMD5 = Convert.ToBase64String(this.md5HashStream.Hash), + OverWriteAll = false + }; + + this.SharedTransferData.Attributes.ContentType = this.transferJob.ContentType; + } + } + + private void CloseOwnStream() + { + if (this.ownsStream) + { + if (null != this.inputStream) + { + this.inputStream.Close(); + this.inputStream = null; + } + } + } + } +} diff --git a/lib/TransferControllers/TransferWriters/AppendBlobWriter.cs b/lib/TransferControllers/TransferWriters/AppendBlobWriter.cs new file mode 100644 index 00000000..c56a6d25 --- /dev/null +++ b/lib/TransferControllers/TransferWriters/AppendBlobWriter.cs @@ -0,0 +1,424 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement.TransferControllers +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Net; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.Blob.Protocol; + + internal sealed class AppendBlobWriter : TransferReaderWriterBase + { + private volatile State state; + private volatile bool hasWork; + private TransferLocation location; + private CloudAppendBlob appendBlob; + private long expectedOffset = 0; + + /// + /// To indicate whether the destination already exist before this writing. + /// If no, when try to set destination's attribute, should get its attributes first. + /// + private bool destExist = false; + + public AppendBlobWriter( + TransferScheduler scheduler, + SyncTransferController controller, + CancellationToken cancellationToken) + : base(scheduler, controller, cancellationToken) + { + this.location = this.SharedTransferData.TransferJob.Destination; + this.appendBlob = this.location.Blob as CloudAppendBlob; + + Debug.Assert(null != this.appendBlob, "The destination is not an append blob while initializing a AppendBlobWriter instance."); + + this.state = State.FetchAttributes; + this.hasWork = true; + } + + public override bool HasWork + { + get + { + return this.hasWork && + ((State.FetchAttributes == this.state) || + (State.Create == this.state) || + (State.UploadBlob == this.state && this.SharedTransferData.AvailableData.ContainsKey(this.expectedOffset)) || + (State.Commit == this.state && null != this.SharedTransferData.Attributes)); + } + } + + public override bool IsFinished + { + get + { + return State.Error == this.state || State.Finished == this.state; + } + } + + private enum State + { + FetchAttributes, + Create, + UploadBlob, + Commit, + Error, + Finished + }; + + public override async Task DoWorkInternalAsync() + { + switch (this.state) + { + case State.FetchAttributes: + await this.FetchAttributesAsync(); + break; + case State.Create: + await this.CreateAsync(); + break; + case State.UploadBlob: + await this.UploadBlobAsync(); + break; + case State.Commit: + await this.CommitAsync(); + break; + case State.Error: + default: + break; + } + } + + private async Task FetchAttributesAsync() + { + Debug.Assert( + this.state == State.FetchAttributes, + "FetchAttributesAsync called, but state isn't FetchAttributes", + "Current state is {0}", + this.state); + + this.hasWork = false; + + if (this.SharedTransferData.TotalLength > Constants.MaxBlockBlobFileSize) + { + string exceptionMessage = string.Format( + CultureInfo.CurrentCulture, + Resources.BlobFileSizeTooLargeException, + Utils.BytesToHumanReadableSize(this.SharedTransferData.TotalLength), + Resources.AppendBlob, + Utils.BytesToHumanReadableSize(Constants.MaxBlockBlobFileSize)); + + throw new TransferException( + TransferErrorCode.UploadSourceFileSizeTooLarge, + exceptionMessage); + } + + bool existingBlob = true; + + AccessCondition accessCondition = Utils.GenerateConditionWithCustomerCondition( + this.location.AccessCondition, + this.location.CheckedAccessCondition); + + try + { + await this.appendBlob.FetchAttributesAsync( + accessCondition, + Utils.GenerateBlobRequestOptions(this.location.BlobRequestOptions), + Utils.GenerateOperationContext(this.Controller.TransferContext), + this.CancellationToken); + + this.destExist = true; + } + catch (StorageException se) + { + // Getting a storage exception is expected if the blob doesn't + // exist. In this case we won't error out, but set the + // existingBlob flag to false to indicate we're uploading + // a new blob instead of overwriting an existing blob. + if (null != se.RequestInformation && + se.RequestInformation.HttpStatusCode == (int)HttpStatusCode.NotFound) + { + existingBlob = false; + } + else if (null != se && + (0 == string.Compare(se.Message, Constants.BlobTypeMismatch, StringComparison.OrdinalIgnoreCase))) + { + throw new InvalidOperationException(Resources.DestinationBlobTypeNotMatch); + } + else + { + throw; + } + } + + this.HandleFetchAttributesResult(existingBlob); + } + + private void HandleFetchAttributesResult(bool existingBlob) + { + this.location.CheckedAccessCondition = true; + + // If destination file exists, query user whether to overwrite it. + this.Controller.CheckOverwrite( + existingBlob, + this.SharedTransferData.SourceLocation, + this.appendBlob.Uri.ToString()); + + this.Controller.UpdateProgressAddBytesTransferred(0); + + if (existingBlob) + { + if (this.appendBlob.Properties.BlobType == BlobType.Unspecified) + { + throw new InvalidOperationException(Resources.FailedToGetBlobTypeException); + } + + if (this.appendBlob.Properties.BlobType != BlobType.AppendBlob) + { + throw new InvalidOperationException(Resources.DestinationBlobTypeNotMatch); + } + } + + // We do check point consistency validation in reader, so directly use it here. + SingleObjectCheckpoint checkpoint = this.SharedTransferData.TransferJob.CheckPoint; + + if ((null != checkpoint.TransferWindow) + && (checkpoint.TransferWindow.Any())) + { + checkpoint.TransferWindow.Sort(); + this.expectedOffset = checkpoint.TransferWindow[0]; + } + else + { + this.expectedOffset = checkpoint.EntryTransferOffset; + } + + if (0 == this.expectedOffset) + { + this.state = State.Create; + } + else + { + if (!existingBlob) + { + throw new TransferException(Resources.DestinationChangedException); + } + + this.PreProcessed = true; + + if (this.expectedOffset == this.SharedTransferData.TotalLength) + { + this.state = State.Commit; + } + else + { + this.state = State.UploadBlob; + } + } + + this.hasWork = true; + } + + private async Task CreateAsync() + { + Debug.Assert(State.Create == this.state, "Calling CreateAsync, state should be Create"); + + this.hasWork = false; + + AccessCondition accessCondition = Utils.GenerateConditionWithCustomerCondition( + this.location.AccessCondition, + true); + + await this.appendBlob.CreateOrReplaceAsync( + accessCondition, + Utils.GenerateBlobRequestOptions(this.location.BlobRequestOptions), + Utils.GenerateOperationContext(this.Controller.TransferContext), + this.CancellationToken); + + this.PreProcessed = true; + + if (this.expectedOffset == this.SharedTransferData.TotalLength) + { + this.state = State.Commit; + } + else + { + this.state = State.UploadBlob; + } + + this.hasWork = true; + } + + private async Task UploadBlobAsync() + { + Debug.Assert(State.UploadBlob == this.state, "Calling UploadBlobAsync, state should be UploadBlob"); + + this.hasWork = false; + + TransferData transferData = null; + if (!this.SharedTransferData.AvailableData.TryRemove(this.expectedOffset, out transferData)) + { + this.hasWork = true; + return; + } + + if (null != transferData) + { + using (transferData) + { + long currentOffset = this.expectedOffset; + this.expectedOffset += transferData.Length; + + transferData.Stream = new MemoryStream(transferData.MemoryBuffer, 0, transferData.Length); + + AccessCondition accessCondition = Utils.GenerateConditionWithCustomerCondition(this.location.AccessCondition, true) ?? new AccessCondition(); + accessCondition.IfAppendPositionEqual = currentOffset; + + bool needToCheckContent = false; + + try + { + await this.appendBlob.AppendBlockAsync(transferData.Stream, + null, + accessCondition, + Utils.GenerateBlobRequestOptions(this.location.BlobRequestOptions), + Utils.GenerateOperationContext(this.Controller.TransferContext), + this.CancellationToken); + } + catch (StorageException se) + { + if ((null != se.RequestInformation) && + ((int)HttpStatusCode.PreconditionFailed == se.RequestInformation.HttpStatusCode) && + (null != se.RequestInformation.ExtendedErrorInformation) && + (se.RequestInformation.ExtendedErrorInformation.ErrorCode == BlobErrorCodeStrings.InvalidAppendCondition)) + { + needToCheckContent = true; + } + else + { + throw; + } + } + + if (needToCheckContent && + (!await this.ValidateUploadedChunkAsync(transferData.MemoryBuffer, currentOffset, (long)transferData.Length))) + { + throw new InvalidOperationException(Resources.DestinationChangedException); + } + + lock(this.SharedTransferData.TransferJob.CheckPoint.TransferWindowLock) + { + this.SharedTransferData.TransferJob.CheckPoint.TransferWindow.Remove(currentOffset); + } + + // update progress + this.Controller.UpdateProgressAddBytesTransferred(transferData.Length); + + if (this.expectedOffset == this.SharedTransferData.TotalLength) + { + this.state = State.Commit; + } + + this.hasWork = true; + } + } + } + + private async Task CommitAsync() + { + Debug.Assert(State.Commit == this.state, "Calling CommitAsync, state should be Commit"); + + this.hasWork = false; + + BlobRequestOptions blobRequestOptions = Utils.GenerateBlobRequestOptions(this.location.BlobRequestOptions); + OperationContext operationContext = Utils.GenerateOperationContext(this.Controller.TransferContext); + + if (!this.destExist) + { + await this.appendBlob.FetchAttributesAsync( + Utils.GenerateConditionWithCustomerCondition(this.location.AccessCondition), + blobRequestOptions, + operationContext, + this.CancellationToken); + } + + var originalMetadata = new Dictionary(this.appendBlob.Metadata); + Utils.SetAttributes(this.appendBlob, this.SharedTransferData.Attributes); + + await this.appendBlob.SetPropertiesAsync( + Utils.GenerateConditionWithCustomerCondition(this.location.AccessCondition), + blobRequestOptions, + operationContext, + this.CancellationToken); + + if (!originalMetadata.DictionaryEquals(this.appendBlob.Metadata)) + { + await this.appendBlob.SetMetadataAsync( + Utils.GenerateConditionWithCustomerCondition(this.location.AccessCondition), + blobRequestOptions, + operationContext, + this.CancellationToken); + } + + this.SetFinish(); + } + + private async Task ValidateUploadedChunkAsync(byte[] currentData, long startOffset, long length) + { + AccessCondition accessCondition = Utils.GenerateConditionWithCustomerCondition(this.location.AccessCondition, true); + OperationContext operationContext = Utils.GenerateOperationContext(this.Controller.TransferContext); + await this.appendBlob.FetchAttributesAsync( + accessCondition, + Utils.GenerateBlobRequestOptions(this.location.BlobRequestOptions), + operationContext, + this.CancellationToken); + + this.destExist = true; + + if (this.appendBlob.Properties.Length != (startOffset + length)) + { + return false; + } + + byte[] buffer = new byte[length]; + + // Do not expect any exception here. + await this.appendBlob.DownloadRangeToByteArrayAsync( + buffer, + 0, + startOffset, + length, + accessCondition, + Utils.GenerateBlobRequestOptions(this.location.BlobRequestOptions), + operationContext, + this.CancellationToken); + + for (int i = 0; i < length; ++i) + { + if (currentData[i] != buffer[i]) + { + return false; + } + } + + return true; + } + + private void SetFinish() + { + this.state = State.Finished; + this.NotifyFinished(null); + this.hasWork = false; + } + } +} diff --git a/lib/TransferControllers/TransferWriters/BlockBlobWriter.cs b/lib/TransferControllers/TransferWriters/BlockBlobWriter.cs new file mode 100644 index 00000000..f59d8579 --- /dev/null +++ b/lib/TransferControllers/TransferWriters/BlockBlobWriter.cs @@ -0,0 +1,377 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement.TransferControllers +{ + using System; + using System.Collections.Concurrent; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Net; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.WindowsAzure.Storage.Blob; + + internal sealed class BlockBlobWriter : TransferReaderWriterBase + { + private volatile bool hasWork; + private volatile State state; + private CountdownEvent countdownEvent; + private TransferLocation location; + private string[] blockIdSequence; + private CloudBlockBlob blockBlob; + + public BlockBlobWriter( + TransferScheduler scheduler, + SyncTransferController controller, + CancellationToken cancellationToken) + : base(scheduler, controller, cancellationToken) + { + this.location = this.SharedTransferData.TransferJob.Destination; + this.blockBlob = this.location.Blob as CloudBlockBlob; + + Debug.Assert(null != this.blockBlob, "The destination is not a block blob while initializing a BlockBlobWriter instance."); + + this.state = State.FetchAttributes; + this.hasWork = true; + } + + private enum State + { + FetchAttributes, + UploadBlob, + Commit, + Error, + Finished + }; + + public override bool PreProcessed + { + get; + protected set; + } + + public override bool HasWork + { + get + { + return this.hasWork && + (!this.PreProcessed + || ((this.state == State.UploadBlob) && this.SharedTransferData.AvailableData.Any()) + || ((this.state == State.Commit) && (null != this.SharedTransferData.Attributes))); + } + } + + public override bool IsFinished + { + get + { + return State.Error == this.state || State.Finished == this.state; + } + } + + public override async Task DoWorkInternalAsync() + { + switch (this.state) + { + case State.FetchAttributes: + await this.FetchAttributesAsync(); + break; + case State.UploadBlob: + await this.UploadBlobAsync(); + break; + case State.Commit: + await this.CommitAsync(); + break; + case State.Error: + default: + break; + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + if (null != this.countdownEvent) + { + this.countdownEvent.Dispose(); + this.countdownEvent = null; + } + } + } + + private async Task FetchAttributesAsync() + { + Debug.Assert( + this.state == State.FetchAttributes, + "FetchAttributesAsync called, but state isn't FetchAttributes", + "Current state is {0}", + this.state); + + this.hasWork = false; + + if (this.SharedTransferData.TotalLength > Constants.MaxBlockBlobFileSize) + { + string exceptionMessage = string.Format( + CultureInfo.CurrentCulture, + Resources.BlobFileSizeTooLargeException, + Utils.BytesToHumanReadableSize(this.SharedTransferData.TotalLength), + Resources.BlockBlob, + Utils.BytesToHumanReadableSize(Constants.MaxBlockBlobFileSize)); + + throw new TransferException( + TransferErrorCode.UploadSourceFileSizeTooLarge, + exceptionMessage); + } + + AccessCondition accessCondition = Utils.GenerateConditionWithCustomerCondition( + this.location.AccessCondition, + this.location.CheckedAccessCondition); + + try + { + await this.location.Blob.FetchAttributesAsync( + accessCondition, + Utils.GenerateBlobRequestOptions(this.location.BlobRequestOptions), + Utils.GenerateOperationContext(this.Controller.TransferContext), + this.CancellationToken); + } + catch (Exception e) + { + this.HandleFetchAttributesResult(e); + return; + } + + this.HandleFetchAttributesResult(null); + } + + private void HandleFetchAttributesResult(Exception e) + { + bool existingBlob = true; + + if (null != e) + { + StorageException se = e as StorageException; + + if (null != se) + { + // Getting a storage exception is expected if the blob doesn't + // exist. In this case we won't error out, but set the + // existingBlob flag to false to indicate we're uploading + // a new blob instead of overwriting an existing blob. + if (null != se.RequestInformation && + se.RequestInformation.HttpStatusCode == (int)HttpStatusCode.NotFound) + { + existingBlob = false; + } + else if (null != se && + (0 == string.Compare(se.Message, Constants.BlobTypeMismatch, StringComparison.OrdinalIgnoreCase))) + { + throw new InvalidOperationException(Resources.DestinationBlobTypeNotMatch); + } + else + { + throw se; + } + } + } + + this.location.CheckedAccessCondition = true; + + if (string.IsNullOrEmpty(this.location.BlockIdPrefix)) + { + // BlockIdPrefix is never set before that this is the first time to transfer this file. + // In block blob upload, it stores uploaded but not committed blocks on Azure Storage. + // In DM, we use block id to identify the blocks uploaded so we only need to upload it once. + // Keep BlockIdPrefix in upload job object for restarting the transfer if anything happens. + this.location.BlockIdPrefix = Guid.NewGuid().ToString("N") + "-"; + } + + // If destination file exists, query user whether to overwrite it. + this.Controller.CheckOverwrite( + existingBlob, + this.SharedTransferData.SourceLocation, + this.location.Blob.Uri.ToString()); + + this.Controller.UpdateProgressAddBytesTransferred(0); + + if (existingBlob) + { + if (this.location.Blob.Properties.BlobType == BlobType.Unspecified) + { + throw new InvalidOperationException(Resources.FailedToGetBlobTypeException); + } + if (this.location.Blob.Properties.BlobType != BlobType.BlockBlob) + { + throw new InvalidOperationException(Resources.DestinationBlobTypeNotMatch); + } + + Debug.Assert( + this.location.Blob.Properties.BlobType == BlobType.BlockBlob, + "BlobType should be BlockBlob if we reach here."); + } + + // Calculate number of blocks. + int numBlocks = (int)Math.Ceiling( + this.SharedTransferData.TotalLength / (double)this.Scheduler.TransferOptions.BlockSize); + + // Create sequence array. + this.blockIdSequence = new string[numBlocks]; + + for (int i = 0; i < numBlocks; ++i) + { + string blockIdSuffix = i.ToString("D6", CultureInfo.InvariantCulture); + byte[] blockIdInBytes = System.Text.Encoding.UTF8.GetBytes(this.location.BlockIdPrefix + blockIdSuffix); + string blockId = Convert.ToBase64String(blockIdInBytes); + this.blockIdSequence[i] = blockId; + } + + SingleObjectCheckpoint checkpoint = this.SharedTransferData.TransferJob.CheckPoint; + + int leftBlockCount = (int)Math.Ceiling( + (this.SharedTransferData.TotalLength - checkpoint.EntryTransferOffset) / (double)this.Scheduler.TransferOptions.BlockSize) + checkpoint.TransferWindow.Count; + + if (0 == leftBlockCount) + { + this.state = State.Commit; + } + else + { + this.countdownEvent = new CountdownEvent(leftBlockCount); + + this.state = State.UploadBlob; + } + + this.PreProcessed = true; + this.hasWork = true; + } + + private async Task UploadBlobAsync() + { + Debug.Assert( + State.UploadBlob == this.state || State.Error == this.state, + "UploadBlobAsync called but state is not UploadBlob nor Error.", + "Current state is {0}", + this.state); + + TransferData transferData = this.GetFirstAvailable(); + + if (null != transferData) + { + using (transferData) + { + transferData.Stream = new MemoryStream(transferData.MemoryBuffer, 0, transferData.Length); + + await this.blockBlob.PutBlockAsync( + this.GetBlockId(transferData.StartOffset), + transferData.Stream, + null, + Utils.GenerateConditionWithCustomerCondition(this.location.AccessCondition, true), + Utils.GenerateBlobRequestOptions(this.location.BlobRequestOptions), + Utils.GenerateOperationContext(this.Controller.TransferContext), + this.CancellationToken); + } + + lock (this.SharedTransferData.TransferJob.CheckPoint.TransferWindowLock) + { + this.SharedTransferData.TransferJob.CheckPoint.TransferWindow.Remove(transferData.StartOffset); + } + + this.FinishBlock(transferData.Length); + } + + // Do not set hasWork to true because it's always true in State.UploadBlob + // Otherwise it may cause CommitAsync be called multiple times: + // 1. UploadBlobAsync downloads all content, but doesn't set hasWork to true yet + // 2. Call CommitAysnc, set hasWork to false + // 3. UploadBlobAsync set hasWork to true. + // 4. Call CommitAsync again since hasWork is true. + } + + private async Task CommitAsync() + { + Debug.Assert( + this.state == State.Commit, + "CommitAsync called, but state isn't Commit", + "Current state is {0}", + this.state); + + this.hasWork = false; + + Utils.SetAttributes(this.blockBlob, this.SharedTransferData.Attributes); + + BlobRequestOptions blobRequestOptions = Utils.GenerateBlobRequestOptions(this.location.BlobRequestOptions); + OperationContext operationContext = Utils.GenerateOperationContext(this.Controller.TransferContext); + + await this.blockBlob.PutBlockListAsync( + this.blockIdSequence, + Utils.GenerateConditionWithCustomerCondition(this.location.AccessCondition), + blobRequestOptions, + operationContext, + this.CancellationToken); + + // REST API PutBlockList cannot clear existing Content-Type of block blob, so if it's needed to clear existing + // Content-Type, REST API SetBlobProperties must be called explicitly: + // 1. The attributes are inherited from others and Content-Type is null or empty. + // 2. User specifies Content-Type to string.Empty while uploading. + if (this.SharedTransferData.Attributes.OverWriteAll && string.IsNullOrEmpty(this.SharedTransferData.Attributes.ContentType) + || (!this.SharedTransferData.Attributes.OverWriteAll && this.SharedTransferData.Attributes.ContentType == string.Empty)) + { + await this.blockBlob.SetPropertiesAsync( + Utils.GenerateConditionWithCustomerCondition(this.location.AccessCondition), + blobRequestOptions, + operationContext, + this.CancellationToken); + } + + this.SetFinish(); + } + + private void SetFinish() + { + this.state = State.Finished; + this.NotifyFinished(null); + this.hasWork = false; + } + + private void FinishBlock(long length) + { + Debug.Assert( + this.state == State.UploadBlob || this.state == State.Error, + "FinishBlock called, but state isn't Upload or Error", + "Current state is {0}", + this.state); + + // If a parallel operation caused the controller to be placed in + // error state exit, make sure not to accidentally change it to + // the Commit state. + if (this.state == State.Error) + { + return; + } + + this.Controller.UpdateProgressAddBytesTransferred(length); + + if (this.countdownEvent.Signal()) + { + this.state = State.Commit; + } + } + + private string GetBlockId(long startOffset) + { + Debug.Assert(startOffset % this.Scheduler.TransferOptions.BlockSize == 0, "Block startOffset should be multiples of block size."); + + int count = (int)(startOffset / this.Scheduler.TransferOptions.BlockSize); + return this.blockIdSequence[count]; + } + } +} diff --git a/lib/TransferControllers/TransferWriters/CloudFileWriter.cs b/lib/TransferControllers/TransferWriters/CloudFileWriter.cs new file mode 100644 index 00000000..c9fd93d4 --- /dev/null +++ b/lib/TransferControllers/TransferWriters/CloudFileWriter.cs @@ -0,0 +1,141 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement.TransferControllers +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.WindowsAzure.Storage.File; + + internal sealed class CloudFileWriter : RangeBasedWriter + { + private CloudFile cloudFile; + + /// + /// To indicate whether the destination already exist before this writing. + /// If no, when try to set destination's attribute, should get its attributes first. + /// + private bool destExist = false; + + internal CloudFileWriter( + TransferScheduler scheduler, + SyncTransferController controller, + CancellationToken cancellationToken) + : base(scheduler, controller, cancellationToken) + { + this.cloudFile = this.TransferJob.Destination.AzureFile; + } + + protected override Uri DestUri + { + get + { + return this.cloudFile.Uri; + } + } + + protected override void CheckInputStreamLength(long inputStreamLength) + { + if (inputStreamLength > Constants.MaxCloudFileSize) + { + string exceptionMessage = string.Format( + CultureInfo.CurrentCulture, + Resources.CloudFileSizeTooLargeException, + Utils.BytesToHumanReadableSize(inputStreamLength), + Utils.BytesToHumanReadableSize(Constants.MaxCloudFileSize)); + + throw new TransferException( + TransferErrorCode.UploadSourceFileSizeTooLarge, + exceptionMessage); + } + + return; + } + + protected override async Task DoFetchAttributesAsync() + { + await this.cloudFile.FetchAttributesAsync( + null, + Utils.GenerateFileRequestOptions(this.TransferJob.Destination.FileRequestOptions), + Utils.GenerateOperationContext(this.Controller.TransferContext), + this.CancellationToken); + this.destExist = true; + } + + protected override void HandleFetchAttributesResult(Exception e) + { + // Do nothing here. + } + + protected override async Task DoCreateAsync(long size) + { + await this.cloudFile.CreateAsync( + size, + null, + Utils.GenerateFileRequestOptions(this.TransferJob.Destination.FileRequestOptions), + Utils.GenerateOperationContext(this.Controller.TransferContext), + this.CancellationToken); + } + + protected override async Task DoResizeAsync(long size) + { + await this.cloudFile.ResizeAsync( + size, + null, + Utils.GenerateFileRequestOptions(this.TransferJob.Destination.FileRequestOptions), + Utils.GenerateOperationContext(this.Controller.TransferContext), + this.CancellationToken); + } + + protected override async Task WriteRangeAsync(TransferData transferData) + { + await this.cloudFile.WriteRangeAsync( + transferData.Stream, + transferData.StartOffset, + null, + null, + Utils.GenerateFileRequestOptions(this.TransferJob.Destination.FileRequestOptions), + Utils.GenerateOperationContext(this.Controller.TransferContext), + this.CancellationToken); + } + + protected override async Task DoCommitAsync() + { + FileRequestOptions fileRequestOptions = Utils.GenerateFileRequestOptions(this.TransferJob.Destination.FileRequestOptions); + OperationContext operationContext = Utils.GenerateOperationContext(this.Controller.TransferContext); + + if (!this.destExist) + { + await this.cloudFile.FetchAttributesAsync( + null, + fileRequestOptions, + operationContext, + this.CancellationToken); + } + + var originalMetadata = new Dictionary(this.cloudFile.Metadata); + Utils.SetAttributes(this.cloudFile, this.SharedTransferData.Attributes); + + await this.cloudFile.SetPropertiesAsync( + null, + fileRequestOptions, + operationContext, + this.CancellationToken); + + if (!originalMetadata.DictionaryEquals(this.cloudFile.Metadata)) + { + await this.cloudFile.SetMetadataAsync( + null, + fileRequestOptions, + operationContext, + this.CancellationToken); + } + } + } +} diff --git a/lib/TransferControllers/TransferWriters/PageBlobWriter.cs b/lib/TransferControllers/TransferWriters/PageBlobWriter.cs new file mode 100644 index 00000000..4e668af9 --- /dev/null +++ b/lib/TransferControllers/TransferWriters/PageBlobWriter.cs @@ -0,0 +1,169 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement.TransferControllers +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.WindowsAzure.Storage.Blob; + + internal sealed class PageBlobWriter : RangeBasedWriter + { + private CloudPageBlob pageBlob; + + /// + /// Size of all files transferred to page blob must be exactly + /// divided by this constant. + /// + private const long PageBlobPageSize = (long)512; + + /// + /// To indicate whether the destination already exist before this writing. + /// If no, when try to set destination's attribute, should get its attributes first. + /// + private bool destExist = false; + + internal PageBlobWriter( + TransferScheduler scheduler, + SyncTransferController controller, + CancellationToken cancellationToken) + : base(scheduler, controller, cancellationToken) + { + this.pageBlob = this.TransferJob.Destination.Blob as CloudPageBlob; + } + + protected override Uri DestUri + { + get + { + return this.pageBlob.Uri; + } + } + + protected override void CheckInputStreamLength(long inputStreamLength) + { + if (inputStreamLength > Constants.MaxPageBlobFileSize) + { + string exceptionMessage = string.Format( + CultureInfo.CurrentCulture, + Resources.BlobFileSizeTooLargeException, + Utils.BytesToHumanReadableSize(inputStreamLength), + Resources.PageBlob, + Utils.BytesToHumanReadableSize(Constants.MaxPageBlobFileSize)); + + throw new TransferException( + TransferErrorCode.UploadSourceFileSizeTooLarge, + exceptionMessage); + } + + if (0 != inputStreamLength % PageBlobPageSize) + { + string exceptionMessage = string.Format( + CultureInfo.CurrentCulture, + Resources.BlobFileSizeInvalidException, + Utils.BytesToHumanReadableSize(inputStreamLength), + Resources.PageBlob, + Utils.BytesToHumanReadableSize(PageBlobPageSize)); + + throw new TransferException( + TransferErrorCode.UploadBlobSourceFileSizeInvalid, + exceptionMessage); + } + + return; + } + + protected override async Task DoFetchAttributesAsync() + { + await this.pageBlob.FetchAttributesAsync( + this.TransferJob.Destination.AccessCondition, + Utils.GenerateBlobRequestOptions(this.TransferJob.Destination.BlobRequestOptions), + Utils.GenerateOperationContext(this.Controller.TransferContext), + this.CancellationToken); + this.destExist = true; + } + + protected override void HandleFetchAttributesResult(Exception e) + { + StorageException se = e as StorageException; + if (null != se && + (0 == string.Compare(se.Message, Constants.BlobTypeMismatch, StringComparison.OrdinalIgnoreCase))) + { + throw new InvalidOperationException(Resources.DestinationBlobTypeNotMatch); + } + } + + protected override async Task DoCreateAsync(long size) + { + await this.pageBlob.CreateAsync( + size, + Utils.GenerateConditionWithCustomerCondition(this.TransferJob.Destination.AccessCondition), + Utils.GenerateBlobRequestOptions(this.TransferJob.Destination.BlobRequestOptions), + Utils.GenerateOperationContext(this.Controller.TransferContext), + this.CancellationToken); + } + + protected override async Task DoResizeAsync(long size) + { + await this.pageBlob.ResizeAsync( + size, + Utils.GenerateConditionWithCustomerCondition(this.TransferJob.Destination.AccessCondition), + Utils.GenerateBlobRequestOptions(this.TransferJob.Destination.BlobRequestOptions), + Utils.GenerateOperationContext(this.Controller.TransferContext), + this.CancellationToken); + } + + protected override async Task WriteRangeAsync(TransferData transferData) + { + await this.pageBlob.WritePagesAsync( + transferData.Stream, + transferData.StartOffset, + null, + Utils.GenerateConditionWithCustomerCondition(this.TransferJob.Destination.AccessCondition), + Utils.GenerateBlobRequestOptions(this.TransferJob.Destination.BlobRequestOptions), + Utils.GenerateOperationContext(this.Controller.TransferContext), + this.CancellationToken); + } + + protected override async Task DoCommitAsync() + { + BlobRequestOptions blobRequestOptions = Utils.GenerateBlobRequestOptions(this.TransferJob.Destination.BlobRequestOptions); + OperationContext operationContext = Utils.GenerateOperationContext(this.Controller.TransferContext); + + if (!this.destExist) + { + await this.pageBlob.FetchAttributesAsync( + Utils.GenerateConditionWithCustomerCondition(this.TransferJob.Destination.AccessCondition), + blobRequestOptions, + operationContext, + this.CancellationToken); + } + + var originalMetadata = new Dictionary(this.pageBlob.Metadata); + Utils.SetAttributes(this.pageBlob, this.SharedTransferData.Attributes); + + await this.pageBlob.SetPropertiesAsync( + Utils.GenerateConditionWithCustomerCondition(this.TransferJob.Destination.AccessCondition), + blobRequestOptions, + null, + this.CancellationToken); + + if (!originalMetadata.DictionaryEquals(this.pageBlob.Metadata)) + { + await this.pageBlob.SetMetadataAsync( + Utils.GenerateConditionWithCustomerCondition(this.TransferJob.Destination.AccessCondition), + blobRequestOptions, + operationContext, + this.CancellationToken); + } + } + } +} diff --git a/lib/TransferControllers/TransferWriters/RangeBasedWriter.cs b/lib/TransferControllers/TransferWriters/RangeBasedWriter.cs new file mode 100644 index 00000000..9375f9df --- /dev/null +++ b/lib/TransferControllers/TransferWriters/RangeBasedWriter.cs @@ -0,0 +1,412 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement.TransferControllers +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Net; + using System.Threading; + using System.Threading.Tasks; + + abstract class RangeBasedWriter : TransferReaderWriterBase + { + /// + /// Keeps track of the internal state-machine state. + /// + private volatile State state; + + /// + /// Countdown event to track number of chunks that still need to be + /// uploaded/are in progress of being uploaded. Used to detect when + /// all blocks have finished uploading and change state to Commit + /// state. + /// + private CountdownEvent toUploadChunksCountdownEvent; + + private volatile bool hasWork; + + protected RangeBasedWriter( + TransferScheduler scheduler, + SyncTransferController controller, + CancellationToken cancellationToken) + : base(scheduler, controller, cancellationToken) + { + this.hasWork = true; + } + + private enum State + { + FetchAttributes, + Create, + Resize, + Upload, + Commit, + Error, + Finished + }; + + public override bool IsFinished + { + get + { + return State.Error == this.state || State.Finished == this.state; + } + } + + public override bool HasWork + { + get + { + return this.hasWork && + (!this.PreProcessed + || ((State.Upload == this.state) && this.SharedTransferData.AvailableData.Any()) + || ((State.Commit == this.state) && (null != this.SharedTransferData.Attributes))); + } + } + + protected TransferJob TransferJob + { + get + { + return this.SharedTransferData.TransferJob; + } + } + + protected abstract Uri DestUri + { + get; + } + + public override async Task DoWorkInternalAsync() + { + switch (this.state) + { + case State.FetchAttributes: + await this.FetchAttributesAsync(); + break; + case State.Create: + await this.CreateAsync(); + break; + case State.Resize: + await this.ResizeAsync(); + break; + case State.Upload: + await this.UploadAsync(); + break; + case State.Commit: + await this.CommitAsync(); + break; + case State.Error: + case State.Finished: + break; + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + if (null != this.toUploadChunksCountdownEvent) + { + this.toUploadChunksCountdownEvent.Dispose(); + this.toUploadChunksCountdownEvent = null; + } + } + } + + private async Task FetchAttributesAsync() + { + Debug.Assert( + this.state == State.FetchAttributes, + "FetchAttributesAsync called, but state isn't FetchAttributes", + "Current state is {0}", + this.state); + + this.hasWork = false; + + this.CheckInputStreamLength(this.SharedTransferData.TotalLength); + + bool exist = true; + + try + { + await this.DoFetchAttributesAsync(); + } + catch (StorageException se) + { + // Getting a storage exception is expected if the file doesn't + // exist. In this case we won't error out, but set the + // exist flag to false to indicate we're uploading + // a new file instead of overwriting an existing one. + if (null != se.RequestInformation && + se.RequestInformation.HttpStatusCode == (int)HttpStatusCode.NotFound) + { + exist = false; + } + else + { + this.HandleFetchAttributesResult(se); + throw; + } + } + catch (Exception e) + { + this.HandleFetchAttributesResult(e); + throw; + } + + this.TransferJob.Destination.CheckedAccessCondition = true; + + this.Controller.CheckOverwrite( + exist, + this.SharedTransferData.SourceLocation, + this.DestUri.ToString()); + + this.Controller.UpdateProgressAddBytesTransferred(0); + + if (exist) + { + // If the destination has already existed, + // and if we haven't uploaded anything to it, try to resize it to the expected length. + // Or if we have uploaded something, the destination should be created by the last transferring, + // don't do resize again. + SingleObjectCheckpoint checkpoint = this.TransferJob.CheckPoint; + bool shouldResize = (checkpoint.EntryTransferOffset == 0) && (!checkpoint.TransferWindow.Any()); + + if (shouldResize) + { + this.state = State.Resize; + } + else + { + this.InitUpload(); + } + } + else + { + this.state = State.Create; + } + + this.hasWork = true; + } + + private async Task CreateAsync() + { + Debug.Assert( + this.state == State.Create, + "CreateAsync called, but state isn't Create", + "Current state is {0}", + this.state); + + this.hasWork = false; + + await this.DoCreateAsync(this.SharedTransferData.TotalLength); + + this.InitUpload(); + } + + private async Task ResizeAsync() + { + Debug.Assert( + this.state == State.Resize, + "ResizeAsync called, but state isn't Resize", + "Current state is {0}", + this.state); + + this.hasWork = false; + + // Resize destination to 0 to clear all exist page ranges, + // then in uploading, we don't need to clear them if source data is all zero.. + await this.DoResizeAsync(0); + + await this.DoResizeAsync(this.SharedTransferData.TotalLength); + + this.InitUpload(); + } + + private void InitUpload() + { + Debug.Assert( + null == this.toUploadChunksCountdownEvent, + "toUploadChunksCountdownEvent expected to be null"); + + if ((this.TransferJob.CheckPoint.EntryTransferOffset != this.SharedTransferData.TotalLength) + && (0 != this.TransferJob.CheckPoint.EntryTransferOffset % this.Scheduler.TransferOptions.BlockSize)) + { + throw new FormatException(Resources.RestartableInfoCorruptedException); + } + + // Calculate number of chunks. + int numChunks = (int)Math.Ceiling( + (this.SharedTransferData.TotalLength - this.TransferJob.CheckPoint.EntryTransferOffset) / (double)this.Scheduler.TransferOptions.BlockSize) + + this.TransferJob.CheckPoint.TransferWindow.Count; + + if (0 == numChunks) + { + this.PreProcessed = true; + this.SetCommit(); + } + else + { + this.toUploadChunksCountdownEvent = new CountdownEvent(numChunks); + + this.state = State.Upload; + this.PreProcessed = true; + this.hasWork = true; + } + } + + private async Task UploadAsync() + { + Debug.Assert( + State.Upload == this.state || State.Error == this.state, + "UploadAsync called, but state isn't Upload", + "Current state is {0}", + this.state); + + this.hasWork = false; + + Debug.Assert( + null != this.toUploadChunksCountdownEvent, + "toUploadChunksCountdownEvent not expected to be null"); + + if (State.Error == this.state) + { + // Some thread has set the error message, just return here. + return; + } + + TransferData transferData = this.GetFirstAvailable(); + + this.hasWork = true; + + if (null != transferData) + { + using (transferData) + { + await this.UploadChunkAsync(transferData); + } + } + } + + private async Task UploadChunkAsync(TransferData transferData) + { + Debug.Assert(null != transferData, "transferData object expected"); + Debug.Assert( + this.state == State.Upload || this.state == State.Error, + "UploadChunkAsync called, but state isn't Upload or Error", + "Current state is {0}", + this.state); + + // If a parallel operation caused the controller to be placed in + // error state exit early to avoid unnecessary I/O. + if (this.state == State.Error) + { + return; + } + + bool allZero = true; + + for (int i = 0; i < transferData.MemoryBuffer.Length; ++i) + { + if (0 != transferData.MemoryBuffer[i]) + { + allZero = false; + break; + } + } + + this.Controller.CheckCancellation(); + + if (!allZero) + { + transferData.Stream = new MemoryStream(transferData.MemoryBuffer, 0, transferData.Length); + await this.WriteRangeAsync(transferData); + } + + this.FinishChunk(transferData); + } + + private void FinishChunk(TransferData transferData) + { + Debug.Assert(null != transferData, "transferData object expected"); + Debug.Assert( + this.state == State.Upload || this.state == State.Error, + "FinishChunk called, but state isn't Upload or Error", + "Current state is {0}", + this.state); + + // If a parallel operation caused the controller to be placed in + // error state exit, make sure not to accidentally change it to + // the Commit state. + if (this.state == State.Error) + { + return; + } + + lock (this.TransferJob.CheckPoint.TransferWindowLock) + { + this.TransferJob.CheckPoint.TransferWindow.Remove(transferData.StartOffset); + } + + this.Controller.UpdateProgressAddBytesTransferred(transferData.Length); + + if (this.toUploadChunksCountdownEvent.Signal()) + { + this.SetCommit(); + } + } + + private void SetCommit() + { + this.state = State.Commit; + this.hasWork = true; + } + + private async Task CommitAsync() + { + Debug.Assert( + this.state == State.Commit, + "CommitAsync called, but state isn't Commit", + "Current state is {0}", + this.state); + + this.hasWork = false; + + await this.DoCommitAsync(); + + this.SetFinished(); + } + + private void SetFinished() + { + this.state = State.Finished; + this.hasWork = false; + + this.NotifyFinished(null); + } + + protected abstract void CheckInputStreamLength(long streamLength); + + protected abstract Task DoFetchAttributesAsync(); + + protected abstract void HandleFetchAttributesResult(Exception e); + + protected abstract Task DoCreateAsync(long size); + + protected abstract Task DoResizeAsync(long size); + + protected abstract Task WriteRangeAsync(TransferData transferData); + + protected abstract Task DoCommitAsync(); + } +} diff --git a/lib/TransferControllers/TransferWriters/StreamedWriter.cs b/lib/TransferControllers/TransferWriters/StreamedWriter.cs new file mode 100644 index 00000000..f3182f85 --- /dev/null +++ b/lib/TransferControllers/TransferWriters/StreamedWriter.cs @@ -0,0 +1,356 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement.TransferControllers +{ + using System; + using System.Diagnostics; + using System.Globalization; + using System.Linq; + using System.IO; + using System.Threading; + using System.Threading.Tasks; + + internal sealed class StreamedWriter : TransferReaderWriterBase, IDisposable + { + /// + /// Streamed destination is written sequentially. + /// This variable records offset of next chunk to be written. + /// + private long expectOffset = 0; + + /// + /// Value to indicate whether there's work to do in the writer. + /// + private volatile bool hasWork; + + /// + /// Stream to calculation destination's content MD5. + /// + private MD5HashStream md5HashStream; + + private Stream outputStream; + + /// + /// Value to indicate whether the stream is a file stream opened by the writer or input by user. + /// If it's a file stream opened by the writer, we should closed it after transferring finished. + /// + private bool ownsStream; + + private volatile State state; + + public StreamedWriter( + TransferScheduler scheduler, + SyncTransferController controller, + CancellationToken cancellationToken) + : base(scheduler, controller, cancellationToken) + { + this.hasWork = true; + this.state = State.OpenOutputStream; + } + + private enum State + { + OpenOutputStream, + CalculateMD5, + Write, + Error, + Finished + }; + + private TransferJob TransferJob + { + get + { + return this.SharedTransferData.TransferJob; + } + } + + public override bool HasWork + { + get + { + return this.hasWork && + ((State.OpenOutputStream == this.state) + || (State.CalculateMD5 == this.state) + || ((State.Write == this.state) + && ((this.SharedTransferData.TotalLength == this.expectOffset) || this.SharedTransferData.AvailableData.ContainsKey(this.expectOffset)))); + } + } + + public override bool IsFinished + { + get + { + return State.Error == this.state || State.Finished == this.state; + } + } + + public override async Task DoWorkInternalAsync() + { + switch (this.state) + { + case State.OpenOutputStream: + await HandleOutputStreamAsync(); + break; + case State.CalculateMD5: + await CalculateMD5Async(); + break; + case State.Write: + await this.WriteChunkDataAsync(); + break; + default: + break; + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (disposing) + { + this.CloseOwnedOutputStream(); + } + } + + private async Task HandleOutputStreamAsync() + { + this.hasWork = false; + + await Task.Run(() => + { + if (TransferLocationType.Stream == this.TransferJob.Destination.TransferLocationType) + { + Stream streamInDestination = this.TransferJob.Destination.Stream; + if (!streamInDestination.CanWrite) + { + throw new NotSupportedException(string.Format( + CultureInfo.CurrentCulture, + Resources.StreamMustSupportWriteException, + "outputStream")); + } + + if (!streamInDestination.CanSeek) + { + throw new NotSupportedException(string.Format( + CultureInfo.CurrentCulture, + Resources.StreamMustSupportSeekException, + "outputStream")); + } + + this.outputStream = this.TransferJob.Destination.Stream; + } + else + { + this.Controller.CheckOverwrite( + File.Exists(this.TransferJob.Destination.FilePath), + this.SharedTransferData.SourceLocation, + this.TransferJob.Destination.FilePath); + + this.Controller.UpdateProgressAddBytesTransferred(0); + + this.Controller.CheckCancellation(); + + // We do check point consistancy validation in reader, and directly use it in writer. + if ((null != this.TransferJob.CheckPoint.TransferWindow) + && (this.TransferJob.CheckPoint.TransferWindow.Any())) + { + this.TransferJob.CheckPoint.TransferWindow.Sort(); + this.expectOffset = this.TransferJob.CheckPoint.TransferWindow[0]; + } + else + { + this.expectOffset = this.TransferJob.CheckPoint.EntryTransferOffset; + } + + try + { + FileMode fileMode = 0 == this.expectOffset ? FileMode.OpenOrCreate : FileMode.Open; + + // Attempt to open the file first so that we throw an exception before getting into the async work + this.outputStream = new FileStream( + this.TransferJob.Destination.FilePath, + fileMode, + FileAccess.ReadWrite, + FileShare.None); + + this.ownsStream = true; + } + catch (Exception ex) + { + string exceptionMessage = string.Format( + CultureInfo.CurrentCulture, + Resources.FailedToOpenFileException, + this.TransferJob.Destination.FilePath, + ex.Message); + + throw new TransferException( + TransferErrorCode.OpenFileFailed, + exceptionMessage, + ex); + } + } + + this.outputStream.SetLength(this.SharedTransferData.TotalLength); + + this.Controller.UpdateProgressAddBytesTransferred(0); + + this.md5HashStream = new MD5HashStream( + this.outputStream, + this.expectOffset, + !this.SharedTransferData.DisableContentMD5Validation); + + if (this.md5HashStream.FinishedSeparateMd5Calculator) + { + this.state = State.Write; + } + else + { + this.state = State.CalculateMD5; + } + + this.PreProcessed = true; + this.hasWork = true; + }); + } + + private Task CalculateMD5Async() + { + Debug.Assert( + this.state == State.CalculateMD5, + "GetCalculateMD5Action called, but state isn't CalculateMD5", + "Current state is {0}", + this.state); + + this.state = State.Write; + this.hasWork = true; + + return Task.Run( + delegate + { + this.md5HashStream.CalculateMd5(this.Scheduler.MemoryManager, this.Controller.CheckCancellation); + }); + } + + private async Task WriteChunkDataAsync() + { + Debug.Assert( + this.state == State.Write || this.state == State.Error, + "WriteChunkDataAsync called, but state isn't Write or Error", + "Current state is {0}", + this.state); + + this.hasWork = false; + long currentWriteOffset = this.expectOffset; + TransferData transferData; + if (this.SharedTransferData.AvailableData.TryRemove(this.expectOffset, out transferData)) + { + this.expectOffset = Math.Min(this.expectOffset + transferData.Length, this.SharedTransferData.TotalLength); + } + else + { + this.SetHasWorkOrFinished(); + return; + } + + Debug.Assert(null != transferData, "TransferData in available data should not be null"); + Debug.Assert(currentWriteOffset == transferData.StartOffset, "StartOffset of TransferData in available data should be the same with the key."); + + try + { + await this.md5HashStream.WriteAsync( + currentWriteOffset, + transferData.MemoryBuffer, + 0, + transferData.Length, + this.CancellationToken); + + // If MD5HashTransformBlock returns false, it means some error happened in md5HashStream to calculate MD5. + // then exception was already thrown out there, don't do anything more here. + if (!this.md5HashStream.MD5HashTransformBlock( + transferData.StartOffset, + transferData.MemoryBuffer, + 0, + transferData.Length, + null, + 0)) + { + return; + } + } + finally + { + this.Scheduler.MemoryManager.ReleaseBuffer(transferData.MemoryBuffer); + } + + int blockSize = this.Scheduler.TransferOptions.BlockSize; + long chunkStartOffset = (currentWriteOffset / blockSize) * blockSize; + + if ((currentWriteOffset + transferData.Length) >= Math.Min(chunkStartOffset + blockSize, this.SharedTransferData.TotalLength)) + { + lock (this.TransferJob.CheckPoint.TransferWindowLock) + { + this.TransferJob.CheckPoint.TransferWindow.Remove(chunkStartOffset); + } + } + + this.Controller.UpdateProgressAddBytesTransferred(transferData.Length); + this.SetHasWorkOrFinished(); + } + + private void SetHasWorkOrFinished() + { + if (this.expectOffset == this.SharedTransferData.TotalLength) + { + Exception ex = null; + if (this.md5HashStream.CheckMd5Hash && this.md5HashStream.SucceededSeparateMd5Calculator) + { + this.md5HashStream.MD5HashTransformFinalBlock(new byte[0], 0, 0); + + string calculatedMd5 = Convert.ToBase64String(this.md5HashStream.Hash); + string storedMd5 = this.SharedTransferData.Attributes.ContentMD5; + + if (!calculatedMd5.Equals(storedMd5)) + { + ex = new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + Resources.DownloadedMd5MismatchException, + this.SharedTransferData.SourceLocation, + calculatedMd5, + storedMd5)); + } + } + + this.CloseOwnedOutputStream(); + this.NotifyFinished(ex); + this.state = State.Finished; + } + else + { + this.hasWork = true; + } + } + + private void CloseOwnedOutputStream() + { + if (null != this.md5HashStream) + { + this.md5HashStream.Dispose(); + this.md5HashStream = null; + } + + if (this.ownsStream) + { + if (null != this.outputStream) + { + this.outputStream.Close(); + this.outputStream = null; + } + } + } + } +} diff --git a/lib/TransferJobs/SingleObjectCheckpoint.cs b/lib/TransferJobs/SingleObjectCheckpoint.cs new file mode 100644 index 00000000..0b5274ba --- /dev/null +++ b/lib/TransferJobs/SingleObjectCheckpoint.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.Collections.Generic; + + /// + /// Represents checkpoint of a single transfer job, + /// includes position of transferred bytes and transfer window. + /// + [Serializable] + internal sealed class SingleObjectCheckpoint + { + /// + /// Initializes a new instance of the class. + /// + /// Transferred offset of this transfer entry. + /// Transfer window of this transfer entry. + public SingleObjectCheckpoint(long entryTransferOffset, IEnumerable transferWindow) + { + this.EntryTransferOffset = entryTransferOffset; + if (null != transferWindow) + { + this.TransferWindow = new List(transferWindow); + } + else + { + this.TransferWindow = new List(Constants.MaxCountInTransferWindow); + } + + this.TransferWindowLock = new object(); + } + + public SingleObjectCheckpoint() + : this(0, null) + { + } + + /// + /// Gets or sets transferred offset of this transfer entry. + /// + /// Transferred offset of this transfer entry. + public long EntryTransferOffset + { + get; + set; + } + + /// + /// Gets or sets transfer window of this transfer entry. + /// + /// Transfer window of this transfer entry. + public List TransferWindow + { + get; + set; + } + + public object TransferWindowLock + { + get; + private set; + } + + public SingleObjectCheckpoint Copy() + { + SingleObjectCheckpoint copyObj = new SingleObjectCheckpoint(); + lock (this.TransferWindowLock) + { + copyObj.EntryTransferOffset = this.EntryTransferOffset; + copyObj.TransferWindow = new List(this.TransferWindow); + } + + return copyObj; + } + } +} diff --git a/lib/TransferJobs/SingleObjectTransfer.cs b/lib/TransferJobs/SingleObjectTransfer.cs new file mode 100644 index 00000000..d13461d8 --- /dev/null +++ b/lib/TransferJobs/SingleObjectTransfer.cs @@ -0,0 +1,160 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.Runtime.Serialization; + using System.Threading; + using System.Threading.Tasks; + + /// + /// Represents a single object transfer operation. + /// + [Serializable] + internal class SingleObjectTransfer : Transfer + { + private const string TransferJobName = "TransferJob"; + + /// + /// Internal transfer jobs. + /// + private TransferJob transferJob; + + /// + /// Initializes a new instance of the class. + /// This constructor will check whether source and destination is valid for the operation: + /// Uri is only valid for non-staging copy. + /// cannot copy from local file/stream to local file/stream + /// + /// Transfer source. + /// Transfer destination. + /// Transfer method, see for detail available methods. + public SingleObjectTransfer(TransferLocation source, TransferLocation dest, TransferMethod transferMethod) + : base(source, dest, transferMethod) + { + if (null == source) + { + throw new ArgumentNullException("source"); + } + + if (null == dest) + { + throw new ArgumentNullException("dest"); + } + + if ((null != source.FilePath || null != source.Stream) + && (null != dest.FilePath || null != dest.Stream)) + { + throw new InvalidOperationException(Resources.LocalToLocalTransferUnsupportedException); + } + + if ((null != source.Blob) + && (null != dest.Blob)) + { + if (source.Blob.BlobType != dest.Blob.BlobType) + { + throw new InvalidOperationException(Resources.SourceAndDestinationBlobTypeDifferent); + } + + if (StorageExtensions.Equals(source.Blob, dest.Blob)) + { + throw new InvalidOperationException(Resources.SourceAndDestinationLocationCannotBeEqualException); + } + } + + if ((null != source.AzureFile) + && (null != dest.AzureFile) + && string.Equals(source.AzureFile.Uri.Host, dest.AzureFile.Uri.Host, StringComparison.OrdinalIgnoreCase) + && string.Equals(source.AzureFile.Uri.AbsolutePath, dest.AzureFile.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException(Resources.SourceAndDestinationLocationCannotBeEqualException); + } + + this.transferJob = new TransferJob(this.Source, this.Destination); + this.transferJob.Transfer = this; + } + + protected SingleObjectTransfer(SerializationInfo info, StreamingContext context) + : base(info, context) + { + this.transferJob = (TransferJob)info.GetValue(TransferJobName, typeof(TransferJob)); + this.transferJob.Transfer = this; + } + + private SingleObjectTransfer(SingleObjectTransfer other) + : base(other) + { + this.transferJob = other.transferJob.Copy(); + this.transferJob.Transfer = this; + } + + /// + /// Serializes the object. + /// + /// Serialization info object. + /// Streaming context. + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue(TransferJobName, this.transferJob, typeof(TransferJob)); + } + + /// + /// Gets a copy of this transfer object. + /// + /// A copy of current transfer object + public SingleObjectTransfer Copy() + { + lock (this.ProgressTracker) + { + return new SingleObjectTransfer(this); + } + } + + /// + /// Execute the transfer asynchronously. + /// + /// Transfer scheduler + /// Token that can be used to cancel the transfer. + /// A task representing the transfer operation. + public override async Task ExecuteAsync(TransferScheduler scheduler, CancellationToken cancellationToken) + { + if (this.transferJob.Status == TransferJobStatus.Finished || + this.transferJob.Status == TransferJobStatus.Skipped) + { + return; + } + + if (transferJob.Status == TransferJobStatus.Failed) + { + // Resuming a failed transfer job + this.UpdateTransferJobStatus(transferJob, TransferJobStatus.Transfer); + } + + try + { + await scheduler.ExecuteJobAsync(transferJob, cancellationToken); + this.UpdateTransferJobStatus(transferJob, TransferJobStatus.Finished); + } + catch (TransferException exception) + { + if (exception.ErrorCode == TransferErrorCode.NotOverwriteExistingDestination) + { + // transfer skipped + this.UpdateTransferJobStatus(transferJob, TransferJobStatus.Skipped); + } + else + { + // transfer failed + this.UpdateTransferJobStatus(transferJob, TransferJobStatus.Failed); + } + + throw; + } + } + } +} diff --git a/lib/TransferJobs/Transfer.cs b/lib/TransferJobs/Transfer.cs new file mode 100644 index 00000000..e80abf10 --- /dev/null +++ b/lib/TransferJobs/Transfer.cs @@ -0,0 +1,199 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.Globalization; + using System.Runtime.Serialization; + using System.Threading; + using System.Threading.Tasks; + + /// + /// Base class for transfer operation. + /// + internal abstract class Transfer : ISerializable + { + private const string FormatVersionName = "Version"; + private const string SourceName = "Source"; + private const string DestName = "Dest"; + private const string TransferMethodName = "TransferMethod"; + private const string TransferProgressName = "Progress"; + + /// + /// Initializes a new instance of the class. + /// + /// Transfer source. + /// Transfer destination. + /// Transfer method, see for detail available methods. + public Transfer(TransferLocation source, TransferLocation dest, TransferMethod transferMethod) + { + this.Source = source; + this.Destination = dest; + this.TransferMethod = transferMethod; + this.ProgressTracker = new TransferProgressTracker(); + } + + /// + /// Initializes a new instance of the class. + /// + /// Serialization information. + /// Streaming context. + protected Transfer(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new System.ArgumentNullException("info"); + } + + string version = info.GetString(FormatVersionName); + if (!string.Equals(Constants.FormatVersion, version, StringComparison.Ordinal)) + { + throw new System.InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + Resources.DeserializationVersionNotMatchException, + "TransferJob", + version, + Constants.FormatVersion)); + } + + this.Source = (TransferLocation)info.GetValue(SourceName, typeof(TransferLocation)); + this.Destination = (TransferLocation)info.GetValue(DestName, typeof(TransferLocation)); + this.TransferMethod = (TransferMethod)info.GetValue(TransferMethodName, typeof(TransferMethod)); + this.ProgressTracker = (TransferProgressTracker)info.GetValue(TransferProgressName, typeof(TransferProgressTracker)); + } + + /// + /// Initializes a new instance of the class. + /// + protected Transfer(Transfer other) + { + this.Source = other.Source; + this.Destination = other.Destination; + this.TransferMethod = other.TransferMethod; + this.ContentType = other.ContentType; + this.ProgressTracker = other.ProgressTracker.Copy(); + } + + /// + /// Gets source location for this transfer. + /// + public TransferLocation Source + { + get; + private set; + } + + /// + /// Gets destination location for this transfer. + /// + public TransferLocation Destination + { + get; + private set; + } + + /// + /// Gets the transfer method used in this transfer. + /// + public TransferMethod TransferMethod + { + get; + private set; + } + + /// + /// Gets or sets the transfer context of this transfer. + /// + public TransferContext Context + { + get; + set; + } + + /// + /// Gets or sets content type to set to destination in uploading. + /// + public string ContentType + { + get; + set; + } + + /// + /// Gets the progress tracker for this transfer. + /// + public TransferProgressTracker ProgressTracker + { + get; + private set; + } + + /// + /// Serializes the object. + /// + /// Serialization info object. + /// Streaming context. + public virtual void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new ArgumentNullException("info"); + } + + info.AddValue(FormatVersionName, Constants.FormatVersion, typeof(string)); + info.AddValue(SourceName, this.Source, typeof(TransferLocation)); + info.AddValue(DestName, this.Destination, typeof(TransferLocation)); + info.AddValue(TransferMethodName, this.TransferMethod); + info.AddValue(TransferProgressName, this.ProgressTracker); + } + + /// + /// Execute the transfer asynchronously. + /// + /// Transfer scheduler + /// Token that can be used to cancel the transfer. + /// A task representing the transfer operation. + public abstract Task ExecuteAsync(TransferScheduler scheduler, CancellationToken cancellationToken); + + public void UpdateTransferJobStatus(TransferJob transferJob, TransferJobStatus targetStatus) + { + lock (this.ProgressTracker) + { + switch (targetStatus) + { + case TransferJobStatus.Transfer: + if (transferJob.Status == TransferJobStatus.Failed) + { + this.ProgressTracker.AddNumberOfFilesFailed(-1); + } + + break; + + case TransferJobStatus.Skipped: + this.ProgressTracker.AddNumberOfFilesSkipped(1); + break; + + case TransferJobStatus.Finished: + this.ProgressTracker.AddNumberOfFilesTransferred(1); + break; + + case TransferJobStatus.Failed: + this.ProgressTracker.AddNumberOfFilesFailed(1); + break; + + case TransferJobStatus.NotStarted: + case TransferJobStatus.Monitor: + default: + break; + } + + transferJob.Status = targetStatus; + } + } + } +} diff --git a/lib/TransferJobs/TransferJob.cs b/lib/TransferJobs/TransferJob.cs new file mode 100644 index 00000000..85db8c1e --- /dev/null +++ b/lib/TransferJobs/TransferJob.cs @@ -0,0 +1,178 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.Globalization; + using System.Runtime.Serialization; + + /// + /// Represents transfer of a single file/blob. + /// + [Serializable] + internal class TransferJob : ISerializable + { + private const string SourceName = "Source"; + private const string DestName = "Dest"; + private const string CheckedOverwriteName = "CheckedOverwrite"; + private const string OverwriteName = "Overwrite"; + private const string CopyIdName = "CopyId"; + private const string CheckpointName = "Checkpoint"; + + /// + /// Initializes a new instance of the class. + /// + /// Source location. + /// Destination location. + public TransferJob(TransferLocation source, TransferLocation dest) + { + this.Source = source; + this.Destination = dest; + + this.CheckPoint = new SingleObjectCheckpoint(); + } + + /// + /// Initializes a new instance of the class. + /// + /// Serialization information. + /// Streaming context. + protected TransferJob(SerializationInfo info, StreamingContext context) + { + this.Source = (TransferLocation)info.GetValue(SourceName, typeof(TransferLocation)); + this.Destination = (TransferLocation)info.GetValue(DestName, typeof(TransferLocation)); + + if (info.GetBoolean(CheckedOverwriteName)) + { + this.Overwrite = info.GetBoolean(OverwriteName); + } + else + { + this.Overwrite = null; + } + + this.CopyId = info.GetString(CopyIdName); + this.CheckPoint = (SingleObjectCheckpoint)info.GetValue(CheckpointName, typeof(SingleObjectCheckpoint)); + } + + /// + /// Initializes a new instance of the class. + /// + private TransferJob(TransferJob other) + { + this.Source = other.Source; + this.Destination = other.Destination; + this.Overwrite = other.Overwrite; + this.CopyId = other.CopyId; + this.CheckPoint = other.CheckPoint.Copy(); + this.Status = other.Status; + } + + /// + /// Gets source location for this transfer job. + /// + public TransferLocation Source + { + get; + private set; + } + + /// + /// Gets destination location for this transfer job. + /// + public TransferLocation Destination + { + get; + private set; + } + + /// + /// Gets or sets the overwrite flag. + /// + public bool? Overwrite + { + get; + set; + } + + /// + /// Gets ID for the asynchronous copy operation. + /// + /// ID for the asynchronous copy operation. + public string CopyId + { + get; + set; + } + + public TransferJobStatus Status + { + get; + set; + } + + public SingleObjectCheckpoint CheckPoint + { + get; + set; + } + + /// + /// Gets or sets the parent transfer of this transfer job + /// + public Transfer Transfer + { + get; + set; + } + + /// + /// Gets or sets content type to set to destination in uploading. + /// + public string ContentType + { + get + { + return this.Transfer.ContentType; + } + } + + /// + /// Serializes the object. + /// + /// Serialization info object. + /// Streaming context. + public virtual void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new ArgumentNullException("info"); + } + + info.AddValue(SourceName, this.Source, typeof(TransferLocation)); + info.AddValue(DestName, this.Destination, typeof(TransferLocation)); + + info.AddValue(CheckedOverwriteName, this.Overwrite.HasValue); + if (this.Overwrite.HasValue) + { + info.AddValue(OverwriteName, this.Overwrite.Value); + } + + info.AddValue(CopyIdName, this.CopyId, typeof(string)); + info.AddValue(CheckpointName, this.CheckPoint, typeof(SingleObjectCheckpoint)); + } + + /// + /// Gets a copy of this transfer job. + /// + /// A copy of current transfer job + public TransferJob Copy() + { + return new TransferJob(this); + } + } +} diff --git a/lib/TransferJobs/TransferJobStatus.cs b/lib/TransferJobs/TransferJobStatus.cs new file mode 100644 index 00000000..4c1bc937 --- /dev/null +++ b/lib/TransferJobs/TransferJobStatus.cs @@ -0,0 +1,46 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + /// + /// Status for TransferEntry. + /// NotStarted -> Skipped + /// -> Transfer -> [Monitor ->] Finished. + /// Failed. + /// + internal enum TransferJobStatus + { + /// + /// Transfer is not started. + /// + NotStarted, + + /// + /// Transfer is skipped + /// + Skipped, + + /// + /// Transfer file. + /// + Transfer, + + /// + /// Monitor transfer process. + /// + Monitor, + + /// + /// Transfer is finished successfully. + /// + Finished, + + /// + /// Transfer is failed. + /// + Failed, + } +} diff --git a/lib/TransferJobs/TransferLocation.cs b/lib/TransferJobs/TransferLocation.cs new file mode 100644 index 00000000..14822d12 --- /dev/null +++ b/lib/TransferJobs/TransferLocation.cs @@ -0,0 +1,458 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.Globalization; + using System.IO; + using System.Runtime.Serialization; + using Microsoft.WindowsAzure.Storage.Auth; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.DataMovement.SerializationHelper; + using Microsoft.WindowsAzure.Storage.File; + + [Serializable] + internal sealed class TransferLocation : ISerializable + { + private const string TransferLocationTypeName = "LocationType"; + private const string FilePathName = "FilePath"; + private const string SourceUriName = "SourceUri"; + private const string BlobName = "Blob"; + private const string AzureFileName = "AzureFile"; + private const string AccessConditionName = "AccessCondition"; + private const string CheckedAccessConditionName = "CheckedAccessCondition"; + private const string RequestOptionsName = "RequestOptions"; + private const string ETagName = "ETag"; + private const string BlockIDPrefixName = "BlockIDPrefix"; + + private SerializableAccessCondition accessCondition; + private SerializableRequestOptions requestOptions; + private SerializableCloudBlob blobSerializer; + private SerializableCloudFile fileSerializer; + + private TransferLocation(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new System.ArgumentNullException("info"); + } + + this.TransferLocationType = (TransferLocationType)info.GetValue(TransferLocationTypeName, typeof(TransferLocationType)); + + switch (this.TransferLocationType) + { + case TransferLocationType.FilePath: + this.FilePath = info.GetString(FilePathName); + break; + case TransferLocationType.Stream: + throw new InvalidOperationException(Resources.CannotSerializeStreamLocation); + case TransferLocationType.SourceUri: + this.SourceUri = (Uri)info.GetValue(SourceUriName, typeof(Uri)); + break; + case TransferLocationType.AzureBlob: + this.blobSerializer = (SerializableCloudBlob)info.GetValue(BlobName, typeof(SerializableCloudBlob)); + break; + case TransferLocationType.AzureFile: + this.fileSerializer = (SerializableCloudFile)info.GetValue(AzureFileName, typeof(SerializableCloudFile)); + break; + default: + break; + } + + this.accessCondition = (SerializableAccessCondition)info.GetValue(AccessConditionName, typeof(SerializableAccessCondition)); + this.CheckedAccessCondition = info.GetBoolean(CheckedAccessConditionName); + this.requestOptions = (SerializableRequestOptions)info.GetValue(RequestOptionsName, typeof(SerializableRequestOptions)); + this.ETag = info.GetString(ETagName); + this.BlockIdPrefix = info.GetString(BlockIDPrefixName); + } + + /// + /// Initializes a new instance of the class. + /// + /// Path to the local file as a source/destination to be read from/written to in a transfer. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads", Justification="We need to distinct from local file with URI")] + public TransferLocation(string filePath) + { + if (null == filePath) + { + throw new ArgumentNullException("filePath"); + } + + if (string.IsNullOrWhiteSpace(filePath)) + { + throw new ArgumentException("message, should not be an empty string", "filePath"); + } + this.FilePath = filePath; + this.TransferLocationType = TransferLocationType.FilePath; + } + + /// + /// Initializes a new instance of the class. + /// + /// Stream instance as a source/destination to be read from/written to in a transfer. + public TransferLocation(Stream stream) + { + if (null == stream) + { + throw new ArgumentNullException("stream"); + } + + this.Stream = stream; + this.TransferLocationType = TransferLocationType.Stream; + } + + /// + /// Initializes a new instance of the class. + /// + /// Blob instance as a location in a transfer job. + /// It could be a source, a destination. + public TransferLocation(CloudBlob blob) + { + if (null == blob) + { + throw new ArgumentNullException("blob"); + } + + this.Blob = blob; + this.TransferLocationType = TransferLocationType.AzureBlob; + } + + /// + /// Initializes a new instance of the class. + /// + /// CloudFile instance as a location in a transfer job. + /// It could be a source, a destination. + public TransferLocation(CloudFile azureFile) + { + if (null == azureFile) + { + throw new ArgumentNullException("azureFile"); + } + + this.AzureFile = azureFile; + this.TransferLocationType = TransferLocationType.AzureFile; + } + + /// + /// Initializes a new instance of the class. + /// + /// Uri to the source in an asynchronously copying job. + public TransferLocation(Uri sourceUri) + { + if (null == sourceUri) + { + throw new ArgumentNullException("sourceUri"); + } + + this.SourceUri = sourceUri; + this.TransferLocationType = TransferLocationType.SourceUri; + } + + /// + /// Gets or sets access condition for this location. + /// This property only takes effact when the location is a blob or an azure file. + /// + public AccessCondition AccessCondition + { + get + { + return SerializableAccessCondition.GetAccessCondition(this.accessCondition); + } + + set + { + SerializableAccessCondition.SetAccessCondition(ref this.accessCondition, value); + } + } + + /// + /// Gets or sets request options when send request to this location. + /// Only a FileRequestOptions instance takes effact when the location is an azure file; + /// Only a BlobRequestOptions instance takes effact when the locaiton is a blob. + /// + public IRequestOptions RequestOptions + { + get + { + return SerializableRequestOptions.GetRequestOptions(this.requestOptions); + } + + set + { + SerializableRequestOptions.SetRequestOptions(ref this.requestOptions, value); + } + } + + /// + /// Gets the type for this location. + /// + public TransferLocationType TransferLocationType + { + get; + private set; + } + + /// + /// Gets path to the local file location. + /// + public string FilePath + { + get; + private set; + } + + /// + /// Gets an stream instance representing the location for this instance. + /// + public Stream Stream + { + get; + private set; + } + + /// + /// Gets Uri to the source location in asynchronously copying job. + /// + public Uri SourceUri + { + get; + private set; + } + + /// + /// Gets blob location in this instance. + /// + public CloudBlob Blob + { + get + { + return SerializableCloudBlob.GetBlob(this.blobSerializer); + } + + private set + { + SerializableCloudBlob.SetBlob(ref this.blobSerializer, value); + } + } + + /// + /// Gets azure file location in this instance. + /// + public CloudFile AzureFile + { + get + { + return SerializableCloudFile.GetFile(this.fileSerializer); + } + + private set + { + SerializableCloudFile.SetFile(ref this.fileSerializer, value); + } + } + + internal string ETag + { + get; + set; + } + + internal bool CheckedAccessCondition + { + get; + set; + } + + internal BlobRequestOptions BlobRequestOptions + { + get + { + return this.RequestOptions as BlobRequestOptions; + } + } + + internal FileRequestOptions FileRequestOptions + { + get + { + return this.RequestOptions as FileRequestOptions; + } + } + + internal string BlockIdPrefix + { + get; + set; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads", Justification="We need to distinct from local file with URI")] + public static implicit operator TransferLocation(string filePath) + { + return new TransferLocation(filePath); + } + + public static implicit operator TransferLocation(Stream stream) + { + return new TransferLocation(stream); + } + + public static implicit operator TransferLocation(CloudBlockBlob blob) + { + return new TransferLocation(blob); + } + + public static implicit operator TransferLocation(CloudPageBlob blob) + { + return new TransferLocation(blob); + } + + public static implicit operator TransferLocation(CloudFile azureFile) + { + return new TransferLocation(azureFile); + } + + public static implicit operator TransferLocation(Uri sourceUri) + { + return ToTransferLocation(sourceUri); + } + + public static TransferLocation ToTransferLocation(Uri sourceUri) + { + return new TransferLocation(sourceUri); + } + + /// + /// Serializes the object. + /// + /// Serialization info object. + /// Streaming context. + public void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new System.ArgumentNullException("info"); + } + + info.AddValue(TransferLocationTypeName, this.TransferLocationType); + + switch (this.TransferLocationType) + { + case TransferLocationType.FilePath: + info.AddValue(FilePathName, this.FilePath); + break; + case TransferLocationType.SourceUri: + info.AddValue(SourceUriName, this.SourceUri, typeof(Uri)); + break; + case TransferLocationType.AzureBlob: + info.AddValue(BlobName, this.blobSerializer, typeof(SerializableCloudBlob)); + break; + case TransferLocationType.AzureFile: + info.AddValue(AzureFileName, this.fileSerializer, typeof(SerializableCloudFile)); + break; + case TransferLocationType.Stream: + default: + throw new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + Resources.CannotDeserializeLocationType, + this.TransferLocationType)); + } + + info.AddValue(AccessConditionName, this.accessCondition, typeof(SerializableAccessCondition)); + info.AddValue(CheckedAccessConditionName, this.CheckedAccessCondition); + info.AddValue(RequestOptionsName, this.requestOptions, typeof(SerializableRequestOptions)); + info.AddValue(ETagName, this.ETag); + info.AddValue(BlockIDPrefixName, this.BlockIdPrefix); + } + + /// + /// Update credentials of blob or azure file location. + /// + /// Storage credentials to be updated in blob or azure file location. + public void UpdateCredentials(StorageCredentials credentials) + { + if (null != this.blobSerializer) + { + this.blobSerializer.UpdateStorageCredentials(credentials); + } + else if (null != this.fileSerializer) + { + this.fileSerializer.UpdateStorageCredentials(credentials); + } + } + + // + // Summary: + // Returns a string that represents the transfer location. + // + // Returns: + // A string that represents the transfer location. + public override string ToString() + { + switch(this.TransferLocationType) + { + case TransferLocationType.FilePath: + return this.FilePath; + + case TransferLocationType.AzureBlob: + return this.Blob.SnapshotQualifiedUri.ToString(); + + case TransferLocationType.AzureFile: + return this.AzureFile.Uri.ToString(); + + case TransferLocationType.SourceUri: + return this.SourceUri.ToString(); + + case TransferLocationType.Stream: + return this.Stream.ToString(); + + default: + throw new ArgumentException("TransferLocationType"); + } + } + + // Summary: + // Determines whether the specified transfer location is equal to the current transfer location. + // + // Parameters: + // obj: + // The transfer location to compare with the current transfer location. + // + // Returns: + // true if the specified transfer location is equal to the current transfer location; otherwise, false. + public override bool Equals(object obj) + { + TransferLocation location = obj as TransferLocation; + if (location == null || this.TransferLocationType != location.TransferLocationType) + return false; + + switch (this.TransferLocationType) + { + case TransferLocationType.AzureBlob: + case TransferLocationType.AzureFile: + case TransferLocationType.FilePath: + case TransferLocationType.SourceUri: + return this.ToString() == location.ToString(); + + case TransferLocationType.Stream: + default: + return false; + } + } + + // + // Summary: + // Returns the hash code for the transfer location. + // + // Returns: + // A 32-bit signed integer hash code. + public override int GetHashCode() + { + return this.ToString().GetHashCode(); + } + } +} diff --git a/lib/TransferJobs/TransferLocationType.cs b/lib/TransferJobs/TransferLocationType.cs new file mode 100644 index 00000000..12f2a25a --- /dev/null +++ b/lib/TransferJobs/TransferLocationType.cs @@ -0,0 +1,17 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + internal enum TransferLocationType + { + FilePath, + Stream, + AzureBlob, + AzureFile, + SourceUri + } +} diff --git a/lib/TransferJobs/TransferMethod.cs b/lib/TransferJobs/TransferMethod.cs new file mode 100644 index 00000000..f8d5b94a --- /dev/null +++ b/lib/TransferJobs/TransferMethod.cs @@ -0,0 +1,22 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + internal enum TransferMethod + { + /// + /// To read data from source to memory and then write the data in memory to destination. + /// + SyncCopy, + + /// + /// To send a start copy request to azure storage to let it do the copying, + /// and monitor the copying progress until the copy finished. + /// + AsyncCopy, + } +} diff --git a/lib/TransferManager.cs b/lib/TransferManager.cs new file mode 100644 index 00000000..f9df09e3 --- /dev/null +++ b/lib/TransferManager.cs @@ -0,0 +1,845 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation +// +//----------------------------------------------------------------------------- +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.Collections.Concurrent; + using System.IO; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.File; + using TransferKey = System.Tuple; + + /// + /// TransferManager class + /// + public static class TransferManager + { + /// + /// Transfer scheduler that schedules execution of transfer jobs + /// + private static TransferScheduler scheduler = new TransferScheduler(); + + /// + /// Transfer configurations associated with the transfer manager + /// + private static TransferConfigurations configurations = new TransferConfigurations(); + + /// + /// Stores all running transfers + /// + private static ConcurrentDictionary allTransfers = new ConcurrentDictionary(); + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "Performance")] + static TransferManager() + { + OperationContext.GlobalSendingRequest += (sender, args) => + { + string userAgent = Constants.UserAgent + ";" + Microsoft.WindowsAzure.Storage.Shared.Protocol.Constants.HeaderConstants.UserAgent; + + if (!string.IsNullOrEmpty(configurations.UserAgentSuffix)) + { + userAgent += ";" + configurations.UserAgentSuffix; + } + + args.Request.UserAgent = userAgent; + }; + } + + /// + /// Gets or sets the transfer configurations associated with the transfer manager + /// + public static TransferConfigurations Configurations + { + get + { + return configurations; + } + } + + /// + /// Upload a file to Azure Blob Storage. + /// + /// Path to the source file. + /// The that is the destination Azure blob. + /// A object that represents the asynchronous operation. + public static Task UploadAsync(string sourcePath, CloudBlob destBlob) + { + return UploadAsync(sourcePath, destBlob, null, null); + } + + /// + /// Upload a file to Azure Blob Storage. + /// + /// Path to the source file. + /// The that is the destination Azure blob. + /// An object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object that represents the asynchronous operation. + public static Task UploadAsync(string sourcePath, CloudBlob destBlob, UploadOptions options, TransferContext context) + { + return UploadAsync(sourcePath, destBlob, options, context, CancellationToken.None); + } + + /// + /// Upload a file to Azure Blob Storage. + /// + /// Path to the source file. + /// The that is the destination Azure blob. + /// An object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object that represents the asynchronous operation. + /// A object to observe while waiting for a task to complete. + public static Task UploadAsync(string sourcePath, CloudBlob destBlob, UploadOptions options, TransferContext context, CancellationToken cancellationToken) + { + TransferLocation sourceLocation = new TransferLocation(sourcePath); + TransferLocation destLocation = new TransferLocation(destBlob); + return UploadInternalAsync(sourceLocation, destLocation, options, context, cancellationToken); + } + + /// + /// Upload a file to Azure Blob Storage. + /// + /// A object providing the file content. + /// The that is the destination Azure blob. + /// A object that represents the asynchronous operation. + public static Task UploadAsync(Stream sourceStream, CloudBlob destBlob) + { + return UploadAsync(sourceStream, destBlob, null, null); + } + + /// + /// Upload a file to Azure Blob Storage. + /// + /// A object providing the file content. + /// The that is the destination Azure blob. + /// An object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object that represents the asynchronous operation. + public static Task UploadAsync(Stream sourceStream, CloudBlob destBlob, UploadOptions options, TransferContext context) + { + return UploadAsync(sourceStream, destBlob, options, context, CancellationToken.None); + } + + /// + /// Upload a file to Azure Blob Storage. + /// + /// A object providing the file content. + /// The that is the destination Azure blob. + /// An object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object that represents the asynchronous operation. + /// A object to observe while waiting for a task to complete. + public static Task UploadAsync(Stream sourceStream, CloudBlob destBlob, UploadOptions options, TransferContext context, CancellationToken cancellationToken) + { + TransferLocation sourceLocation = new TransferLocation(sourceStream); + TransferLocation destLocation = new TransferLocation(destBlob); + return UploadInternalAsync(sourceLocation, destLocation, options, context, cancellationToken); + } + + /// + /// Upload a file to Azure File Storage. + /// + /// Path to the source file. + /// The that is the destination Azure file. + /// A object that represents the asynchronous operation. + public static Task UploadAsync(string sourcePath, CloudFile destFile) + { + return UploadAsync(sourcePath, destFile, null, null); + } + + /// + /// Upload a file to Azure File Storage. + /// + /// Path to the source file. + /// The that is the destination Azure file. + /// An object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object that represents the asynchronous operation. + public static Task UploadAsync(string sourcePath, CloudFile destFile, UploadOptions options, TransferContext context) + { + return UploadAsync(sourcePath, destFile, options, context, CancellationToken.None); + } + + /// + /// Upload a file to Azure File Storage. + /// + /// Path to the source file. + /// The that is the destination Azure file. + /// An object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object to observe while waiting for a task to complete. + /// A object that represents the asynchronous operation. + public static Task UploadAsync(string sourcePath, CloudFile destFile, UploadOptions options, TransferContext context, CancellationToken cancellationToken) + { + TransferLocation sourceLocation = new TransferLocation(sourcePath); + TransferLocation destLocation = new TransferLocation(destFile); + return UploadInternalAsync(sourceLocation, destLocation, options, context, cancellationToken); + } + + /// + /// Upload a file to Azure File Storage. + /// + /// A object providing the file content. + /// The that is the destination Azure file. + /// A object that represents the asynchronous operation. + public static Task UploadAsync(Stream sourceStream, CloudFile destFile) + { + return UploadAsync(sourceStream, destFile, null, null); + } + + /// + /// Upload a file to Azure File Storage. + /// + /// A object providing the file content. + /// The that is the destination Azure file. + /// An object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object that represents the asynchronous operation. + public static Task UploadAsync(Stream sourceStream, CloudFile destFile, UploadOptions options, TransferContext context) + { + return UploadAsync(sourceStream, destFile, options, context, CancellationToken.None); + } + + /// + /// Upload a file to Azure File Storage. + /// + /// A object providing the file content. + /// The that is the destination Azure file. + /// An object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object to observe while waiting for a task to complete. + /// A object that represents the asynchronous operation. + public static Task UploadAsync(Stream sourceStream, CloudFile destFile, UploadOptions options, TransferContext context, CancellationToken cancellationToken) + { + TransferLocation sourceLocation = new TransferLocation(sourceStream); + TransferLocation destLocation = new TransferLocation(destFile); + return UploadInternalAsync(sourceLocation, destLocation, options, context, cancellationToken); + } + + /// + /// Download an Azure blob from Azure Blob Storage. + /// + /// The that is the source Azure blob. + /// Path to the destination file. + /// A object that represents the asynchronous operation. + public static Task DownloadAsync(CloudBlob sourceBlob, string destPath) + { + return DownloadAsync(sourceBlob, destPath, null, null); + } + + /// + /// Download an Azure blob from Azure Blob Storage. + /// + /// The that is the source Azure blob. + /// Path to the destination file. + /// A object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object that represents the asynchronous operation. + public static Task DownloadAsync(CloudBlob sourceBlob, string destPath, DownloadOptions options, TransferContext context) + { + return DownloadAsync(sourceBlob, destPath, options, context, CancellationToken.None); + } + + /// + /// Download an Azure blob from Azure Blob Storage. + /// + /// The that is the source Azure blob. + /// Path to the destination file. + /// A object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object to observe while waiting for a task to complete. + /// A object that represents the asynchronous operation. + public static Task DownloadAsync(CloudBlob sourceBlob, string destPath, DownloadOptions options, TransferContext context, CancellationToken cancellationToken) + { + TransferLocation sourceLocation = new TransferLocation(sourceBlob); + TransferLocation destLocation = new TransferLocation(destPath); + + if (options != null) + { + BlobRequestOptions requestOptions = Transfer_RequestOptions.DefaultBlobRequestOptions; + requestOptions.DisableContentMD5Validation = options.DisableContentMD5Validation; + sourceLocation.RequestOptions = requestOptions; + } + + return DownloadInternalAsync(sourceLocation, destLocation, options, context, cancellationToken); + } + + /// + /// Download an Azure blob from Azure Blob Storage. + /// + /// The that is the source Azure blob. + /// A object representing the destination stream. + /// A object that represents the asynchronous operation. + public static Task DownloadAsync(CloudBlob sourceBlob, Stream destStream) + { + return DownloadAsync(sourceBlob, destStream, null, null); + } + + /// + /// Download an Azure blob from Azure Blob Storage. + /// + /// The that is the source Azure blob. + /// A object representing the destination stream. + /// A object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object that represents the asynchronous operation. + public static Task DownloadAsync(CloudBlob sourceBlob, Stream destStream, DownloadOptions options, TransferContext context) + { + return DownloadAsync(sourceBlob, destStream, options, context, CancellationToken.None); + } + + /// + /// Download an Azure blob from Azure Blob Storage. + /// + /// The that is the source Azure blob. + /// A object representing the destination stream. + /// A object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object to observe while waiting for a task to complete. + /// A object that represents the asynchronous operation. + public static Task DownloadAsync(CloudBlob sourceBlob, Stream destStream, DownloadOptions options, TransferContext context, CancellationToken cancellationToken) + { + TransferLocation sourceLocation = new TransferLocation(sourceBlob); + TransferLocation destLocation = new TransferLocation(destStream); + + if (options != null) + { + BlobRequestOptions requestOptions = Transfer_RequestOptions.DefaultBlobRequestOptions; + requestOptions.DisableContentMD5Validation = options.DisableContentMD5Validation; + sourceLocation.RequestOptions = requestOptions; + } + + return DownloadInternalAsync(sourceLocation, destLocation, options, context, cancellationToken); + } + + /// + /// Download an Azure file from Azure File Storage. + /// + /// The that is the source Azure file. + /// Path to the destination file. + /// A object that represents the asynchronous operation. + public static Task DownloadAsync(CloudFile sourceFile, string destPath) + { + return DownloadAsync(sourceFile, destPath, null, null); + } + + /// + /// Download an Azure file from Azure File Storage. + /// + /// The that is the source Azure file. + /// Path to the destination file. + /// A object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object that represents the asynchronous operation. + public static Task DownloadAsync(CloudFile sourceFile, string destPath, DownloadOptions options, TransferContext context) + { + return DownloadAsync(sourceFile, destPath, options, context, CancellationToken.None); + } + + /// + /// Download an Azure file from Azure File Storage. + /// + /// The that is the source Azure file. + /// Path to the destination file. + /// A object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object to observe while waiting for a task to complete. + /// A object that represents the asynchronous operation. + public static Task DownloadAsync(CloudFile sourceFile, string destPath, DownloadOptions options, TransferContext context, CancellationToken cancellationToken) + { + TransferLocation sourceLocation = new TransferLocation(sourceFile); + TransferLocation destLocation = new TransferLocation(destPath); + + if (options != null) + { + FileRequestOptions requestOptions = Transfer_RequestOptions.DefaultFileRequestOptions; + requestOptions.DisableContentMD5Validation = options.DisableContentMD5Validation; + sourceLocation.RequestOptions = requestOptions; + } + + return DownloadInternalAsync(sourceLocation, destLocation, options, context, cancellationToken); + } + + /// + /// Download an Azure file from Azure File Storage. + /// + /// The that is the source Azure file. + /// A object representing the destination stream. + /// A object that represents the asynchronous operation. + public static Task DownloadAsync(CloudFile sourceFile, Stream destStream) + { + return DownloadAsync(sourceFile, destStream, null, null); + } + + /// + /// Download an Azure file from Azure File Storage. + /// + /// The that is the source Azure file. + /// A object representing the destination stream. + /// A object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object that represents the asynchronous operation. + public static Task DownloadAsync(CloudFile sourceFile, Stream destStream, DownloadOptions options, TransferContext context) + { + return DownloadAsync(sourceFile, destStream, options, context, CancellationToken.None); + } + + /// + /// Download an Azure file from Azure File Storage. + /// + /// The that is the source Azure file. + /// A object representing the destination stream. + /// A object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object to observe while waiting for a task to complete. + /// A object that represents the asynchronous operation. + public static Task DownloadAsync(CloudFile sourceFile, Stream destStream, DownloadOptions options, TransferContext context, CancellationToken cancellationToken) + { + TransferLocation sourceLocation = new TransferLocation(sourceFile); + TransferLocation destLocation = new TransferLocation(destStream); + + if (options != null) + { + FileRequestOptions requestOptions = Transfer_RequestOptions.DefaultFileRequestOptions; + requestOptions.DisableContentMD5Validation = options.DisableContentMD5Validation; + sourceLocation.RequestOptions = requestOptions; + } + + return DownloadInternalAsync(sourceLocation, destLocation, options, context, cancellationToken); + } + + /// + /// Copy content, properties and metadata of one Azure blob to another. + /// + /// The that is the source Azure blob. + /// The that is the destination Azure blob. + /// A flag indicating whether the copy is service-side asynchronous copy or not. + /// If this flag is set to true, service-side asychronous copy will be used; if this flag is set to false, + /// file is downloaded from source first, then uploaded to destination. + /// A object that represents the asynchronous operation. + public static Task CopyAsync(CloudBlob sourceBlob, CloudBlob destBlob, bool isServiceCopy) + { + return CopyAsync(sourceBlob, destBlob, isServiceCopy, null, null); + } + + /// + /// Copy content, properties and metadata of one Azure blob to another. + /// + /// The that is the source Azure blob. + /// The that is the destination Azure blob. + /// A flag indicating whether the copy is service-side asynchronous copy or not. + /// If this flag is set to true, service-side asychronous copy will be used; if this flag is set to false, + /// file is downloaded from source first, then uploaded to destination. + /// A object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object that represents the asynchronous operation. + public static Task CopyAsync(CloudBlob sourceBlob, CloudBlob destBlob, bool isServiceCopy, CopyOptions options, TransferContext context) + { + return CopyAsync(sourceBlob, destBlob, isServiceCopy, options, context, CancellationToken.None); + } + + /// + /// Copy content, properties and metadata of one Azure blob to another. + /// + /// The that is the source Azure blob. + /// The that is the destination Azure blob. + /// A flag indicating whether the copy is service-side asynchronous copy or not. + /// If this flag is set to true, service-side asychronous copy will be used; if this flag is set to false, + /// file is downloaded from source first, then uploaded to destination. + /// A object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object to observe while waiting for a task to complete. + /// A object that represents the asynchronous operation. + public static Task CopyAsync(CloudBlob sourceBlob, CloudBlob destBlob, bool isServiceCopy, CopyOptions options, TransferContext context, CancellationToken cancellationToken) + { + TransferLocation sourceLocation = new TransferLocation(sourceBlob); + TransferLocation destLocation = new TransferLocation(destBlob); + return CopyInternalAsync(sourceLocation, destLocation, isServiceCopy, options, context, cancellationToken); + } + + /// + /// Copy content, properties and metadata of an Azure blob to an Azure file. + /// + /// The that is the source Azure blob. + /// The that is the destination Azure file. + /// A flag indicating whether the copy is service-side asynchronous copy or not. + /// If this flag is set to true, service-side asychronous copy will be used; if this flag is set to false, + /// file is downloaded from source first, then uploaded to destination. + /// A object that represents the asynchronous operation. + public static Task CopyAsync(CloudBlob sourceBlob, CloudFile destFile, bool isServiceCopy) + { + return CopyAsync(sourceBlob, destFile, isServiceCopy, null, null); + } + + /// + /// Copy content, properties and metadata of an Azure blob to an Azure file. + /// + /// The that is the source Azure blob. + /// The that is the destination Azure file. + /// A flag indicating whether the copy is service-side asynchronous copy or not. + /// If this flag is set to true, service-side asychronous copy will be used; if this flag is set to false, + /// file is downloaded from source first, then uploaded to destination. + /// A object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object that represents the asynchronous operation. + public static Task CopyAsync(CloudBlob sourceBlob, CloudFile destFile, bool isServiceCopy, CopyOptions options, TransferContext context) + { + return CopyAsync(sourceBlob, destFile, isServiceCopy, options, context, CancellationToken.None); + } + + /// + /// Copy content, properties and metadata of an Azure blob to an Azure file. + /// + /// The that is the source Azure blob. + /// The that is the destination Azure file. + /// A flag indicating whether the copy is service-side asynchronous copy or not. + /// If this flag is set to true, service-side asychronous copy will be used; if this flag is set to false, + /// file is downloaded from source first, then uploaded to destination. + /// A object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object to observe while waiting for a task to complete. + /// A object that represents the asynchronous operation. + public static Task CopyAsync(CloudBlob sourceBlob, CloudFile destFile, bool isServiceCopy, CopyOptions options, TransferContext context, CancellationToken cancellationToken) + { + TransferLocation sourceLocation = new TransferLocation(sourceBlob); + TransferLocation destLocation = new TransferLocation(destFile); + return CopyInternalAsync(sourceLocation, destLocation, isServiceCopy, options, context, cancellationToken); + } + + /// + /// Copy content, properties and metadata of an Azure file to an Azure blob. + /// + /// The that is the source Azure file. + /// The that is the destination Azure blob. + /// A flag indicating whether the copy is service-side asynchronous copy or not. + /// If this flag is set to true, service-side asychronous copy will be used; if this flag is set to false, + /// file is downloaded from source first, then uploaded to destination. + /// A object that represents the asynchronous operation. + public static Task CopyAsync(CloudFile sourceFile, CloudBlob destBlob, bool isServiceCopy) + { + return CopyAsync(sourceFile, destBlob, isServiceCopy, null, null); + } + + /// + /// Copy content, properties and metadata of an Azure file to an Azure blob. + /// + /// The that is the source Azure file. + /// The that is the destination Azure blob. + /// A flag indicating whether the copy is service-side asynchronous copy or not. + /// If this flag is set to true, service-side asychronous copy will be used; if this flag is set to false, + /// file is downloaded from source first, then uploaded to destination. + /// A object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object that represents the asynchronous operation. + public static Task CopyAsync(CloudFile sourceFile, CloudBlob destBlob, bool isServiceCopy, CopyOptions options, TransferContext context) + { + return CopyAsync(sourceFile, destBlob, isServiceCopy, options, context, CancellationToken.None); + } + + /// + /// Copy content, properties and metadata of an Azure file to an Azure blob. + /// + /// The that is the source Azure file. + /// The that is the destination Azure blob. + /// A flag indicating whether the copy is service-side asynchronous copy or not. + /// If this flag is set to true, service-side asychronous copy will be used; if this flag is set to false, + /// file is downloaded from source first, then uploaded to destination. + /// A object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object to observe while waiting for a task to complete. + /// A object that represents the asynchronous operation. + public static Task CopyAsync(CloudFile sourceFile, CloudBlob destBlob, bool isServiceCopy, CopyOptions options, TransferContext context, CancellationToken cancellationToken) + { + TransferLocation sourceLocation = new TransferLocation(sourceFile); + TransferLocation destLocation = new TransferLocation(destBlob); + return CopyInternalAsync(sourceLocation, destLocation, isServiceCopy, options, context, cancellationToken); + } + + + /// + /// Copy content, properties and metadata of an Azure file to another. + /// + /// The that is the source Azure file. + /// The that is the destination Azure file. + /// A flag indicating whether the copy is service-side asynchronous copy or not. + /// If this flag is set to true, service-side asychronous copy will be used; if this flag is set to false, + /// file is downloaded from source first, then uploaded to destination. + /// A object that represents the asynchronous operation. + public static Task CopyAsync(CloudFile sourceFile, CloudFile destFile, bool isServiceCopy) + { + return CopyAsync(sourceFile, destFile, isServiceCopy, null, null); + } + + /// + /// Copy content, properties and metadata of an Azure file to another. + /// + /// The that is the source Azure file. + /// The that is the destination Azure file. + /// A flag indicating whether the copy is service-side asynchronous copy or not. + /// If this flag is set to true, service-side asychronous copy will be used; if this flag is set to false, + /// file is downloaded from source first, then uploaded to destination. + /// A object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object that represents the asynchronous operation. + public static Task CopyAsync(CloudFile sourceFile, CloudFile destFile, bool isServiceCopy, CopyOptions options, TransferContext context) + { + return CopyAsync(sourceFile, destFile, isServiceCopy, options, context, CancellationToken.None); + } + + /// + /// Copy content, properties and metadata of an Azure file to another. + /// + /// The that is the source Azure file. + /// The that is the destination Azure file. + /// A flag indicating whether the copy is service-side asynchronous copy or not. + /// If this flag is set to true, service-side asychronous copy will be used; if this flag is set to false, + /// file is downloaded from source first, then uploaded to destination. + /// A object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object to observe while waiting for a task to complete. + /// A object that represents the asynchronous operation. + public static Task CopyAsync(CloudFile sourceFile, CloudFile destFile, bool isServiceCopy, CopyOptions options, TransferContext context, CancellationToken cancellationToken) + { + TransferLocation sourceLocation = new TransferLocation(sourceFile); + TransferLocation destLocation = new TransferLocation(destFile); + return CopyInternalAsync(sourceLocation, destLocation, isServiceCopy, options, context, cancellationToken); + } + + /// + /// Copy file from an specified URI to an Azure blob. + /// + /// The of the source file. + /// The that is the destination Azure blob. + /// A flag indicating whether the copy is service-side asynchronous copy or not. + /// If this flag is set to true, service-side asychronous copy will be used; if this flag is set to false, + /// file is downloaded from source first, then uploaded to destination. + /// A object that represents the asynchronous operation. + /// Copying from an URI to Azure blob synchronously is not supported yet. + public static Task CopyAsync(Uri sourceUri, CloudBlob destBlob, bool isServiceCopy) + { + return CopyAsync(sourceUri, destBlob, isServiceCopy, null, null); + } + + /// + /// Copy file from an specified URI to an Azure blob. + /// + /// The of the source file. + /// The that is the destination Azure blob. + /// A flag indicating whether the copy is service-side asynchronous copy or not. + /// If this flag is set to true, service-side asychronous copy will be used; if this flag is set to false, + /// file is downloaded from source first, then uploaded to destination. + /// A object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object that represents the asynchronous operation. + /// Copying from an URI to Azure blob synchronously is not supported yet. + public static Task CopyAsync(Uri sourceUri, CloudBlob destBlob, bool isServiceCopy, CopyOptions options, TransferContext context) + { + return CopyAsync(sourceUri, destBlob, isServiceCopy, options, context, CancellationToken.None); + } + + /// + /// Copy file from an specified URI to an Azure blob. + /// + /// The of the source file. + /// The that is the destination Azure blob. + /// A flag indicating whether the copy is service-side asynchronous copy or not. + /// If this flag is set to true, service-side asychronous copy will be used; if this flag is set to false, + /// file is downloaded from source first, then uploaded to destination. + /// A object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object to observe while waiting for a task to complete. + /// A object that represents the asynchronous operation. + /// Copying from an URI to Azure blob synchronously is not supported yet. + public static Task CopyAsync(Uri sourceUri, CloudBlob destBlob, bool isServiceCopy, CopyOptions options, TransferContext context, CancellationToken cancellationToken) + { + if (!isServiceCopy) + { + throw new NotSupportedException(Resources.SyncCopyFromUriToAzureBlobNotSupportedException); + } + + TransferLocation sourceLocation = new TransferLocation(sourceUri); + TransferLocation destLocation = new TransferLocation(destBlob); + return CopyInternalAsync(sourceLocation, destLocation, isServiceCopy, options, context, cancellationToken); + } + + /// + /// Copy file from an specified URI to an Azure file. + /// + /// The of the source file. + /// The that is the destination Azure file. + /// A flag indicating whether the copy is service-side asynchronous copy or not. + /// If this flag is set to true, service-side asychronous copy will be used; if this flag is set to false, + /// file is downloaded from source first, then uploaded to destination. + /// A object that represents the asynchronous operation. + /// Copying from an URI to Azure file synchronously is not supported yet. + public static Task CopyAsync(Uri sourceUri, CloudFile destFile, bool isServiceCopy) + { + return CopyAsync(sourceUri, destFile, isServiceCopy, null, null); + } + + /// + /// Copy file from an specified URI to an Azure file. + /// + /// The of the source file. + /// The that is the destination Azure file. + /// A flag indicating whether the copy is service-side asynchronous copy or not. + /// If this flag is set to true, service-side asychronous copy will be used; if this flag is set to false, + /// file is downloaded from source first, then uploaded to destination. + /// A object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object that represents the asynchronous operation. + /// Copying from an URI to Azure file synchronously is not supported yet. + public static Task CopyAsync(Uri sourceUri, CloudFile destFile, bool isServiceCopy, CopyOptions options, TransferContext context) + { + return CopyAsync(sourceUri, destFile, isServiceCopy, options, context, CancellationToken.None); + } + + /// + /// Copy file from an specified URI to an Azure file. + /// + /// The of the source file. + /// The that is the destination Azure file. + /// A flag indicating whether the copy is service-side asynchronous copy or not. + /// If this flag is set to true, service-side asychronous copy will be used; if this flag is set to false, + /// file is downloaded from source first, then uploaded to destination. + /// A object that specifies additional options for the operation. + /// A object that represents the context for the current operation. + /// A object to observe while waiting for a task to complete. + /// A object that represents the asynchronous operation. + /// Copying from an URI to Azure file synchronously is not supported yet. + public static Task CopyAsync(Uri sourceUri, CloudFile destFile, bool isServiceCopy, CopyOptions options, TransferContext context, CancellationToken cancellationToken) + { + if (!isServiceCopy) + { + throw new NotSupportedException(Resources.SyncCopyFromUriToAzureFileNotSupportedException); + } + + TransferLocation sourceLocation = new TransferLocation(sourceUri); + TransferLocation destLocation = new TransferLocation(destFile); + return CopyInternalAsync(sourceLocation, destLocation, isServiceCopy, options, context, cancellationToken); + } + + private static Task UploadInternalAsync(TransferLocation sourceLocation, TransferLocation destLocation, UploadOptions options, TransferContext context, CancellationToken cancellationToken) + { + if (options != null) + { + destLocation.AccessCondition = options.DestinationAccessCondition; + } + + Transfer transfer = CreateSingleObjectTransfer(sourceLocation, destLocation, TransferMethod.SyncCopy, context); + if (options != null) + { + transfer.ContentType = options.ContentType; + } + + return DoTransfer(transfer, cancellationToken); + } + + private static Task DownloadInternalAsync(TransferLocation sourceLocation, TransferLocation destLocation, DownloadOptions options, TransferContext context, CancellationToken cancellationToken) + { + if (options != null) + { + sourceLocation.AccessCondition = options.SourceAccessCondition; + } + + Transfer transfer = CreateSingleObjectTransfer(sourceLocation, destLocation, TransferMethod.SyncCopy, context); + return DoTransfer(transfer, cancellationToken); + } + + private static Task CopyInternalAsync(TransferLocation sourceLocation, TransferLocation destLocation, bool isServiceCopy, CopyOptions options, TransferContext context, CancellationToken cancellationToken) + { + if (options != null) + { + sourceLocation.AccessCondition = options.SourceAccessCondition; + destLocation.AccessCondition = options.DestinationAccessCondition; + } + + Transfer transfer = CreateSingleObjectTransfer(sourceLocation, destLocation, isServiceCopy ? TransferMethod.AsyncCopy : TransferMethod.SyncCopy, context); + return DoTransfer(transfer, cancellationToken); + } + + private static async Task DoTransfer(Transfer transfer, CancellationToken cancellationToken) + { + if (!TryAddTransfer(transfer)) + { + throw new TransferException(TransferErrorCode.TransferAlreadyExists, Resources.TransferAlreadyExists); + } + + try + { + await transfer.ExecuteAsync(scheduler, cancellationToken); + } + finally + { + RemoveTransfer(transfer); + } + } + + private static Transfer CreateSingleObjectTransfer(TransferLocation sourceLocation, TransferLocation destLocation, TransferMethod transferMethod, TransferContext transferContext) + { + Transfer transfer = GetTransfer(sourceLocation, destLocation, transferMethod, transferContext); + if (transfer == null) + { + transfer = new SingleObjectTransfer(sourceLocation, destLocation, transferMethod); + if (transferContext != null) + { + transferContext.Checkpoint.AddTransfer(transfer); + } + } + + if (transferContext != null) + { + transfer.ProgressTracker.Parent = transferContext.OverallProgressTracker; + transfer.Context = transferContext; + } + + return transfer; + } + + private static Transfer GetTransfer(TransferLocation sourceLocation, TransferLocation destLocation, TransferMethod transferMethod, TransferContext transferContext) + { + Transfer transfer = null; + if (transferContext != null) + { + transfer = transferContext.Checkpoint.GetTransfer(sourceLocation, destLocation, transferMethod); + if (transfer != null) + { + // update transfer location information + UpdateTransferLocation(transfer.Source, sourceLocation); + UpdateTransferLocation(transfer.Destination, destLocation); + } + } + + return transfer; + } + + private static bool TryAddTransfer(Transfer transfer) + { + return allTransfers.TryAdd(new TransferKey(transfer.Source, transfer.Destination), transfer); + } + + private static void RemoveTransfer(Transfer transfer) + { + Transfer unused = null; + allTransfers.TryRemove(new TransferKey(transfer.Source, transfer.Destination), out unused); + } + + private static void UpdateTransferLocation(TransferLocation targetLocation, TransferLocation location) + { + // update storage credentials + if (targetLocation.TransferLocationType == TransferLocationType.AzureBlob) + { + targetLocation.UpdateCredentials(location.Blob.ServiceClient.Credentials); + } + else if (targetLocation.TransferLocationType == TransferLocationType.AzureFile) + { + targetLocation.UpdateCredentials(location.AzureFile.ServiceClient.Credentials); + } + } + } +} diff --git a/lib/TransferOptions/CopyOptions.cs b/lib/TransferOptions/CopyOptions.cs new file mode 100644 index 00000000..f088fd9d --- /dev/null +++ b/lib/TransferOptions/CopyOptions.cs @@ -0,0 +1,23 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + /// + /// Represents a set of options that may be specified for copy operation + /// + public sealed class CopyOptions + { + /// + /// Gets or sets an object that represents the access conditions for the source object. If null, no condition is used. + /// + public AccessCondition SourceAccessCondition { get; set; } + + /// + /// Gets or sets an object that represents the access conditions for the destination object. If null, no condition is used. + /// + public AccessCondition DestinationAccessCondition { get; set; } + } +} diff --git a/lib/TransferOptions/DownloadOptions.cs b/lib/TransferOptions/DownloadOptions.cs new file mode 100644 index 00000000..74e5146b --- /dev/null +++ b/lib/TransferOptions/DownloadOptions.cs @@ -0,0 +1,25 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + /// + /// Represents a set of options that may be specified for download operation + /// + public sealed class DownloadOptions + { + /// + /// Gets or sets an object that represents the access conditions for the source object. If null, no condition is used. + /// + public AccessCondition SourceAccessCondition { get; set; } + + /// + /// Gets or sets a flag that indicates whether to validate content MD5 or not when reading data from the source object. + /// If set to true, source object content MD5 will be validated; otherwise, source object content MD5 will not be validated. + /// If not specified, it defaults to false. + /// + public bool DisableContentMD5Validation { get; set; } + } +} diff --git a/lib/TransferOptions/UploadOptions.cs b/lib/TransferOptions/UploadOptions.cs new file mode 100644 index 00000000..ef481a5b --- /dev/null +++ b/lib/TransferOptions/UploadOptions.cs @@ -0,0 +1,23 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + /// + /// Represents a set of options that may be specified for upload operation + /// + public sealed class UploadOptions + { + /// + /// Gets or sets an object that represents the access conditions for the destination object. If null, no condition is used. + /// + public AccessCondition DestinationAccessCondition { get; set; } + + /// + /// Gets or sets a string that indicates the content-type of the destination Azure blob or Azure file. + /// + public string ContentType { get; set; } + } +} diff --git a/lib/TransferProgress.cs b/lib/TransferProgress.cs new file mode 100644 index 00000000..77ccd046 --- /dev/null +++ b/lib/TransferProgress.cs @@ -0,0 +1,49 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation +// +//----------------------------------------------------------------------------- +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + /// + /// Transfer progress + /// + public sealed class TransferProgress + { + /// + /// Gets the number of bytes that have been transferred. + /// + public long BytesTransferred + { + get; + internal set; + } + + /// + /// Gets the number of files that have been transferred. + /// + public long NumberOfFilesTransferred + { + get; + internal set; + } + + /// + /// Gets the number of files that are skipped to be transferred. + /// + public long NumberOfFilesSkipped + { + get; + internal set; + } + + /// + /// Gets the number of files that are failed to be transferred. + /// + public long NumberOfFilesFailed + { + get; + internal set; + } + } +} diff --git a/lib/TransferScheduler.cs b/lib/TransferScheduler.cs new file mode 100644 index 00000000..bac76c27 --- /dev/null +++ b/lib/TransferScheduler.cs @@ -0,0 +1,424 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation +// +//----------------------------------------------------------------------------- +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.WindowsAzure.Storage.DataMovement.TransferControllers; + using System.Diagnostics; + + /// + /// TransferScheduler class, used for transferring Microsoft Azure + /// Storage objects. + /// + internal sealed class TransferScheduler : IDisposable + { + /// + /// Main collection of transfer controllers. + /// + private BlockingCollection controllerQueue; + + /// + /// Internal queue for the main controllers collection. + /// + private ConcurrentQueue internalControllerQueue; + + /// + /// A buffer from which we select a transfer controller and add it into + /// active tasks when the bucket of active tasks is not full. + /// + private ConcurrentDictionary activeControllerItems = + new ConcurrentDictionary(); + + /// + /// CancellationToken source. + /// + private CancellationTokenSource cancellationTokenSource = + new CancellationTokenSource(); + + /// + /// Transfer options that this manager will pass to transfer controllers. + /// + private TransferConfigurations transferOptions; + + /// + /// Wait handle event for completion. + /// + private ManualResetEventSlim controllerResetEvent = + new ManualResetEventSlim(); + + /// + /// A pool of memory buffer objects, used to limit total consumed memory. + /// + private MemoryManager memoryManager; + + /// + /// Random object to generate random numbers. + /// + private Random randomGenerator; + + /// + /// Used to lock disposing to avoid race condition between different disposing and other method calls. + /// + private object disposeLock = new object(); + + private SemaphoreSlim scheduleSemaphore; + + /// + /// Indicate whether the instance has been disposed. + /// + private bool isDisposed = false; + + /// + /// Initializes a new instance of the + /// class. + /// + public TransferScheduler() + : this(null) + { + } + + /// + /// Initializes a new instance of the + /// class. + /// + /// BlobTransfer options. + public TransferScheduler(TransferConfigurations options) + { + // If no options specified create a default one. + this.transferOptions = options ?? new TransferConfigurations(); + + this.internalControllerQueue = new ConcurrentQueue(); + this.controllerQueue = new BlockingCollection( + this.internalControllerQueue); + this.memoryManager = new MemoryManager( + this.transferOptions.MaximumCacheSize, + this.transferOptions.BlockSize); + + this.randomGenerator = new Random(); + + this.scheduleSemaphore = new SemaphoreSlim( + this.transferOptions.ParallelOperations, + this.transferOptions.ParallelOperations); + + this.StartSchedule(); + } + + /// + /// Finalizes an instance of the + /// class. + /// + ~TransferScheduler() + { + this.Dispose(false); + } + + /// + /// Gets the transfer options that this manager will pass to + /// transfer controllers. + /// + internal TransferConfigurations TransferOptions + { + get + { + return this.transferOptions; + } + } + + internal CancellationTokenSource CancellationTokenSource + { + get + { + return this.cancellationTokenSource; + } + } + + internal MemoryManager MemoryManager + { + get + { + return this.memoryManager; + } + } + + /// + /// Public dispose method to release all resources owned. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Execute a transfer job asynchronously. + /// + /// Transfer job to be executed. + /// Token used to notify the job that it should stop. + public Task ExecuteJobAsync( + TransferJob job, + CancellationToken cancellationToken) + { + if (null == job) + { + throw new ArgumentNullException("job"); + } + + lock (this.disposeLock) + { + this.CheckDisposed(); + + return ExecuteJobInternalAsync(job, cancellationToken); + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Instances will be disposed in other place.")] + private async Task ExecuteJobInternalAsync( + TransferJob job, + CancellationToken cancellationToken) + { + Debug.Assert( + job.Status == TransferJobStatus.NotStarted || + job.Status == TransferJobStatus.Monitor || + job.Status == TransferJobStatus.Transfer); + + TransferControllerBase controller = null; + switch (job.Transfer.TransferMethod) + { + case TransferMethod.SyncCopy: + controller = new SyncTransferController(this, job, cancellationToken); + break; + + case TransferMethod.AsyncCopy: + controller = AsyncCopyController.CreateAsyncCopyController(this, job, cancellationToken); + break; + } + + Utils.CheckCancellation(this.cancellationTokenSource); + this.controllerQueue.Add(controller, this.cancellationTokenSource.Token); + + try + { + await controller.TaskCompletionSource.Task; + } + catch(StorageException sex) + { + throw new TransferException(TransferErrorCode.Unknown, Resources.UncategorizedException, sex); + } + finally + { + controller.Dispose(); + } + } + + private void FillInQueue( + ConcurrentDictionary activeItems, + BlockingCollection collection, + CancellationToken token) + { + while (!token.IsCancellationRequested + && activeItems.Count < this.transferOptions.ParallelOperations) + { + if (activeItems.Count >= this.transferOptions.ParallelOperations) + { + return; + } + + ITransferController transferItem = null; + + try + { + if (!collection.TryTake(out transferItem) + || null == transferItem) + { + return; + } + } + catch (ObjectDisposedException) + { + return; + } + + activeItems.TryAdd(transferItem, null); + } + } + + /// + /// Blocks until the queue is empty and all transfers have been + /// completed. + /// + private void WaitForCompletion() + { + this.controllerResetEvent.Wait(); + } + + /// + /// Cancels any remaining queued work. + /// + private void CancelWork() + { + this.cancellationTokenSource.Cancel(); + this.controllerQueue.CompleteAdding(); + + // Move following to Cancel method. + // there might be running "work" when the transfer is cancelled. + // wait until all running "work" is done. + SpinWait sw = new SpinWait(); + while (this.scheduleSemaphore.CurrentCount != this.transferOptions.ParallelOperations) + { + sw.SpinOnce(); + } + + this.controllerResetEvent.Set(); + } + + /// + /// Private dispose method to release managed/unmanaged objects. + /// If disposing is true clean up managed resources as well as + /// unmanaged resources. + /// If disposing is false only clean up unmanaged resources. + /// + /// Indicates whether or not to dispose + /// managed resources. + private void Dispose(bool disposing) + { + if (!this.isDisposed) + { + lock (this.disposeLock) + { + // We got the lock, isDisposed is true, means that the disposing has been finished. + if (this.isDisposed) + { + return; + } + + this.isDisposed = true; + + this.CancelWork(); + this.WaitForCompletion(); + + if (disposing) + { + if (null != this.controllerQueue) + { + this.controllerQueue.Dispose(); + this.controllerQueue = null; + } + + if (null != this.cancellationTokenSource) + { + this.cancellationTokenSource.Dispose(); + this.cancellationTokenSource = null; + } + + if (null != this.controllerResetEvent) + { + this.controllerResetEvent.Dispose(); + this.controllerResetEvent = null; + } + + if (null != this.scheduleSemaphore) + { + this.scheduleSemaphore.Dispose(); + this.scheduleSemaphore = null; + } + + this.memoryManager = null; + } + } + } + else + { + this.WaitForCompletion(); + } + } + + private void CheckDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException("TransferScheduler"); + } + } + + private void StartSchedule() + { + Task.Run(() => + { + SpinWait sw = new SpinWait(); + while (!this.cancellationTokenSource.Token.IsCancellationRequested && + (!this.controllerQueue.IsCompleted || this.activeControllerItems.Any())) + { + FillInQueue( + this.activeControllerItems, + this.controllerQueue, + this.cancellationTokenSource.Token); + + if (!this.cancellationTokenSource.Token.IsCancellationRequested) + { + // If we don't have the requested amount of active tasks + // running, get a task item from any active transfer item + // that has work available. + if (!this.DoWorkFrom(this.activeControllerItems)) + { + sw.SpinOnce(); + } + else + { + sw.Reset(); + continue; + } + } + } + }); + } + + private void FinishedWorkItem( + ITransferController transferController) + { + object dummy; + this.activeControllerItems.TryRemove(transferController, out dummy); + } + + private bool DoWorkFrom( + ConcurrentDictionary activeItems) + { + // Filter items with work only. + List> activeItemsWithWork = + new List>( + activeItems.Where(item => item.Key.HasWork && !item.Key.IsFinished)); + + if (0 != activeItemsWithWork.Count) + { + // Select random item and get work delegate. + int idx = this.randomGenerator.Next(activeItemsWithWork.Count); + ITransferController transferController = activeItemsWithWork[idx].Key; + + DoControllerWork(transferController); + + return true; + } + + return false; + } + + private async void DoControllerWork(ITransferController controller) + { + this.scheduleSemaphore.Wait(); + bool finished = await controller.DoWorkAsync(); + this.scheduleSemaphore.Release(); + + if (finished) + { + this.FinishedWorkItem(controller); + } + } + } +} diff --git a/lib/TransferStatusHelpers/Attributes.cs b/lib/TransferStatusHelpers/Attributes.cs new file mode 100644 index 00000000..29c6b0e4 --- /dev/null +++ b/lib/TransferStatusHelpers/Attributes.cs @@ -0,0 +1,58 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + internal class Attributes + { + /// + /// Gets or sets the cache-control value stored for blob/azure file. + /// + public string CacheControl { get; set; } + + /// + /// Gets or sets the content-disposition value stored for blob/azure file. + /// + public string ContentDisposition { get; set; } + + /// + /// Gets or sets the content-encoding value stored for blob/azure file. + /// + public string ContentEncoding { get; set; } + + /// + /// Gets or sets the content-language value stored for blob/azure file. + /// + public string ContentLanguage { get; set; } + + /// + /// Gets or sets the content-MD5 value stored for blob/azure file. + /// + public string ContentMD5 { get; set; } + + /// + /// Gets or sets the content-type value stored for blob/azure file. + /// + public string ContentType { get; set; } + + /// + /// Gets or sets the user-defined metadata for blob/azure file. + /// + public IDictionary Metadata { get; set; } + + /// + /// Gets or sets a value to indicate whether to overwrite all attribute on destination, + /// or keep its original value if it's not set. + /// + public bool OverWriteAll { get; set; } + } +} diff --git a/lib/TransferStatusHelpers/ReadDataState.cs b/lib/TransferStatusHelpers/ReadDataState.cs new file mode 100644 index 00000000..b384cce3 --- /dev/null +++ b/lib/TransferStatusHelpers/ReadDataState.cs @@ -0,0 +1,61 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System.IO; + + /// + /// Keep the state of reading a single block from the input stream. + /// + internal class ReadDataState : TransferDataState + { + /// + /// Gets or sets the memory stream used to encapsulate the memory + /// buffer for passing the methods such as PutBlock, WritePages, + /// DownloadToStream and DownloadRangeToStream, as these methods + /// requires a stream and doesn't allow for a byte array as input. + /// + public MemoryStream MemoryStream + { + get; + set; + } + + /// + /// Gets or sets the memory manager that controls global memory + /// allocation. + /// + public MemoryManager MemoryManager + { + get; + set; + } + + /// + /// Private dispose method to release managed/unmanaged objects. + /// If disposing = true clean up managed resources as well as unmanaged resources. + /// If disposing = false only clean up unmanaged resources. + /// + /// Indicates whether or not to dispose managed resources. + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (null != this.MemoryStream) + { + this.MemoryStream.Dispose(); + this.MemoryStream = null; + } + + if (null != this.MemoryBuffer) + { + this.MemoryManager.ReleaseBuffer(this.MemoryBuffer); + this.MemoryManager = null; + } + } + } + } +} diff --git a/lib/TransferStatusHelpers/SharedTransferData.cs b/lib/TransferStatusHelpers/SharedTransferData.cs new file mode 100644 index 00000000..2c3b4eba --- /dev/null +++ b/lib/TransferStatusHelpers/SharedTransferData.cs @@ -0,0 +1,45 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.Collections.Concurrent; + + internal class SharedTransferData + { + /// + /// Gets or sets length of source. + /// + public long TotalLength { get; set; } + + /// + /// Gets or sets the job instance representing the transfer. + /// + public TransferJob TransferJob { get; set; } + + /// + /// Gest or sets list of available transfer data from source. + /// + public ConcurrentDictionary AvailableData { get; set; } + + /// + /// Gets or sets a value indicating whether should disable validation of content md5. + /// The reader should get this value from source's RequestOptions, + /// the writer should do or not do validation on content md5 according to this value. + /// + public bool DisableContentMD5Validation { get; set; } + + /// + /// Gets or sets string which representing source location. + /// + public string SourceLocation { get; set; } + + /// + /// Gets or sets attribute for blob/azure file. + /// + public Attributes Attributes { get; set; } + } +} diff --git a/lib/TransferStatusHelpers/TransferData.cs b/lib/TransferStatusHelpers/TransferData.cs new file mode 100644 index 00000000..d9102356 --- /dev/null +++ b/lib/TransferStatusHelpers/TransferData.cs @@ -0,0 +1,44 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System.IO; + + internal class TransferData : TransferDataState + { + private MemoryManager memoryManager; + + public TransferData(MemoryManager memoryManager) + { + this.memoryManager = memoryManager; + } + + public Stream Stream + { + get; + set; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (null != this.Stream) + { + this.Stream.Close(); + this.Stream = null; + } + + if (null != this.MemoryBuffer) + { + this.memoryManager.ReleaseBuffer(this.MemoryBuffer); + this.MemoryBuffer = null; + } + } + } + } +} diff --git a/lib/TransferStatusHelpers/TransferDataState.cs b/lib/TransferStatusHelpers/TransferDataState.cs new file mode 100644 index 00000000..39a45291 --- /dev/null +++ b/lib/TransferStatusHelpers/TransferDataState.cs @@ -0,0 +1,69 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + + /// + /// Calculate and show transfer speed. + /// + internal abstract class TransferDataState : IDisposable + { + /// + /// Gets or sets a handle to the memory buffer to ensure the + /// memory buffer remains in memory during the entire operation. + /// + public byte[] MemoryBuffer + { + get; + set; + } + + /// + /// Gets or sets the starting offset of this part of data. + /// + public long StartOffset + { + get; + set; + } + + /// + /// Gets or sets the length of this part of data. + /// + public int Length + { + get; + set; + } + + /// + /// Gets or sets how many bytes have been read. + /// + public int BytesRead + { + get; + set; + } + + /// + /// Public dispose method to release all resources owned. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Private dispose method to release managed/unmanaged objects. + /// If disposing = true clean up managed resources as well as unmanaged resources. + /// If disposing = false only clean up unmanaged resources. + /// + /// Indicates whether or not to dispose managed resources. + protected abstract void Dispose(bool disposing); + } +} diff --git a/lib/TransferStatusHelpers/TransferDownloadBuffer.cs b/lib/TransferStatusHelpers/TransferDownloadBuffer.cs new file mode 100644 index 00000000..0da5a318 --- /dev/null +++ b/lib/TransferStatusHelpers/TransferDownloadBuffer.cs @@ -0,0 +1,67 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System.Threading; + + class TransferDownloadBuffer + { + private int finishedLength = 0; + + private int processed = 0; + + public TransferDownloadBuffer(long startOffset, int expectedLength, byte[] buffer) + { + this.Length = expectedLength; + this.StartOffset = startOffset; + this.MemoryBuffer = buffer; + } + + public int Length + { + get; + private set; + } + + public long StartOffset + { + get; + private set; + } + + public byte[] MemoryBuffer + { + get; + private set; + } + + public bool Finished + { + get + { + return this.finishedLength == this.Length; + } + } + + /// + /// Mark this buffer as processed. The return value indicates whether the buffer + /// is marked as processed by invocation of this method. This method returns true + /// exactly once. The caller is supposed to invoke this method before processing + /// the buffer and proceed only if this method returns true. + /// + /// Whether this instance is marked as processed by invocation of this method. + public bool MarkAsProcessed() + { + return 0 == Interlocked.CompareExchange(ref this.processed, 1, 0); + } + + public void ReadFinish(int length) + { + Interlocked.Add(ref this.finishedLength, length); + } + } +} diff --git a/lib/TransferStatusHelpers/TransferDownloadStream.cs b/lib/TransferStatusHelpers/TransferDownloadStream.cs new file mode 100644 index 00000000..23b8cce0 --- /dev/null +++ b/lib/TransferStatusHelpers/TransferDownloadStream.cs @@ -0,0 +1,262 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + + class TransferDownloadStream : Stream + { + TransferDownloadBuffer firstBuffer; + MemoryStream firstStream; + int firstOffset; + + TransferDownloadBuffer secondBuffer; + MemoryStream secondStream; + int secondOffset; + + bool onSecondStream = false; + + MemoryManager memoryManager; + + public TransferDownloadStream(MemoryManager memoryManager, TransferDownloadBuffer buffer, int offset, int count) + :this(memoryManager, buffer, offset, count, null, 0, 0) + { + } + + public TransferDownloadStream( + MemoryManager memoryManager, + TransferDownloadBuffer firstBuffer, + int firstOffset, + int firstCount, + TransferDownloadBuffer secondBuffer, + int secondOffset, + int secondCount) + { + this.memoryManager = memoryManager; + this.firstBuffer = firstBuffer; + this.firstOffset = firstOffset; + this.firstStream = new MemoryStream(this.firstBuffer.MemoryBuffer, firstOffset, firstCount); + + if (null != secondBuffer) + { + this.secondBuffer = secondBuffer; + this.secondOffset = secondOffset; + this.secondStream = new MemoryStream(this.secondBuffer.MemoryBuffer, secondOffset, secondCount); + } + } + + public override bool CanRead + { + get + { + return false; + } + } + + public override bool CanWrite + { + get + { + return true; + } + } + + public override bool CanSeek + { + get + { + return true; + } + } + + public bool ReserveBuffer + { + get; + set; + } + + public override long Length + { + get + { + if (null == this.secondStream) + { + return this.firstStream.Length; + } + + return this.firstStream.Length + this.secondStream.Length; + } + } + + public override long Position + { + get + { + if (!this.onSecondStream) + { + return this.firstStream.Position; + } + else + { + Debug.Assert(null != this.secondStream, "Second stream should exist when position is on the second stream"); + return this.firstStream.Length + this.secondStream.Position; + } + } + + set + { + long position = value; + + if (position < this.firstStream.Length) + { + this.onSecondStream = false; + this.firstStream.Position = position; + } + else + { + position -= this.firstStream.Length; + this.onSecondStream = true; + this.secondStream.Position = position; + } + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + long position = 0; + + switch (origin) + { + case SeekOrigin.End: + position = this.Length + offset; + break; + case SeekOrigin.Current: + position = this.Position + offset; + break; + default: + position = offset; + break; + } + + this.Position = position; + return position; + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + // do nothing + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + int length = count; + int firstLength = 0; + if (!this.onSecondStream) + { + firstLength = Math.Min(length, (int)(this.firstStream.Length - this.firstStream.Position)); + this.firstStream.Write(buffer, offset, firstLength); + length -= firstLength; + if (0 == length) + { + return; + } + else + { + if (null == this.secondStream) + { + throw new NotSupportedException(Resources.StreamNotExpandable); + } + + this.onSecondStream = true; + } + } + + Debug.Assert(null != this.secondStream, "Position is on the second stream, it should not be null"); + + this.secondStream.Write(buffer, offset + firstLength, length); + } + + public void SetAllZero() + { + Array.Clear(this.firstBuffer.MemoryBuffer, this.firstOffset, (int)this.firstStream.Length); + + if (null != this.secondBuffer) + { + Array.Clear(this.secondBuffer.MemoryBuffer, this.secondOffset, (int)this.secondStream.Length); + } + } + + public void FinishWrite() + { + this.firstBuffer.ReadFinish((int)this.firstStream.Length); + + if (null != this.secondBuffer) + { + this.secondBuffer.ReadFinish((int)this.secondStream.Length); + } + } + + public IEnumerable GetBuffers() + { + yield return this.firstBuffer; + + if (null != this.secondBuffer) + { + yield return this.secondBuffer; + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + + if (null != this.firstStream) + { + this.firstStream.Dispose(); + this.firstStream = null; + } + + if (null != this.secondStream) + { + this.secondStream.Dispose(); + this.secondStream = null; + } + + if (!this.ReserveBuffer) + { + if (null != this.firstBuffer) + { + this.memoryManager.ReleaseBuffer(this.firstBuffer.MemoryBuffer); + this.firstBuffer = null; + } + + if (null != this.secondBuffer) + { + this.memoryManager.ReleaseBuffer(this.secondBuffer.MemoryBuffer); + this.secondBuffer = null; + } + } + } + } + } +} diff --git a/lib/TransferStatusHelpers/TransferProgressTracker.cs b/lib/TransferStatusHelpers/TransferProgressTracker.cs new file mode 100644 index 00000000..817204d0 --- /dev/null +++ b/lib/TransferStatusHelpers/TransferProgressTracker.cs @@ -0,0 +1,277 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.Runtime.Serialization; + using System.Threading; + + /// + /// Calculate transfer progress. + /// + [Serializable] + internal class TransferProgressTracker : ISerializable + { + private const string BytesTransferredName = "BytesTransferred"; + private const string FilesTransferredName = "FilesTransferred"; + private const string FilesSkippedName = "FilesSkipped"; + private const string FilesFailedName = "FilesFailed"; + + /// + /// Stores the number of bytes that have been transferred. + /// + private long bytesTransferred; + + /// + /// Stores the number of files that have been transferred. + /// + private long numberOfFilesTransferred; + + /// + /// Stores the number of files that are failed to be transferred. + /// + private long numberOfFilesSkipped; + + /// + /// Stores the number of files that are skipped. + /// + private long numberOfFilesFailed; + + /// + /// A flag indicating whether the progress handler is being invoked + /// + private int invokingProgressHandler; + + /// + /// Initializes a new instance of the class. + /// + public TransferProgressTracker() + { + this.bytesTransferred = 0; + this.numberOfFilesTransferred = 0; + this.numberOfFilesSkipped = 0; + this.numberOfFilesFailed = 0; + } + + /// + /// Initializes a new instance of the class. + /// + /// Serialization information. + /// Streaming context. + protected TransferProgressTracker(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new System.ArgumentNullException("info"); + } + + this.bytesTransferred = info.GetInt64(BytesTransferredName); + this.numberOfFilesTransferred = info.GetInt64(FilesTransferredName); + this.numberOfFilesSkipped = info.GetInt64(FilesSkippedName); + this.numberOfFilesFailed = info.GetInt64(FilesFailedName); + } + + /// + /// Initializes a new instance of the class. + /// + private TransferProgressTracker(TransferProgressTracker other) + { + this.bytesTransferred = other.bytesTransferred; + this.numberOfFilesTransferred = other.numberOfFilesTransferred; + this.numberOfFilesSkipped = other.numberOfFilesSkipped; + this.numberOfFilesFailed = other.numberOfFilesFailed; + } + + /// + /// Gets or sets the parent progress tracker + /// + public TransferProgressTracker Parent + { + get; + set; + } + + /// + /// Gets or sets the progress handler + /// + public IProgress ProgressHandler + { + get; + set; + } + + /// + /// Gets the number of bytes that have been transferred. + /// + public long BytesTransferred + { + get + { + return Interlocked.Read(ref this.bytesTransferred); + } + } + + /// + /// Gets the number of files that have been transferred. + /// + public long NumberOfFilesTransferred + { + get + { + return Interlocked.Read(ref this.numberOfFilesTransferred); + } + + } + + /// + /// Gets the number of files that are skipped to be transferred. + /// + public long NumberOfFilesSkipped + { + get + { + return Interlocked.Read(ref this.numberOfFilesSkipped); + } + } + + /// + /// Gets the number of files that are failed to be transferred. + /// + public long NumberOfFilesFailed + { + get + { + return Interlocked.Read(ref this.numberOfFilesFailed); + } + } + + /// + /// Updates the current status by indicating the bytes transferred. + /// + /// Indicating by how much the bytes transferred increased. + public void AddBytesTransferred(long bytesToIncrease) + { + if (bytesToIncrease != 0) + { + Interlocked.Add(ref this.bytesTransferred, bytesToIncrease); + + if (this.Parent != null) + { + this.Parent.AddBytesTransferred(bytesToIncrease); + } + } + + this.InvokeProgressHandler(); + } + + /// + /// Updates the number of files that have been transferred. + /// + /// Indicating by how much the number of file that have been transferred increased. + public void AddNumberOfFilesTransferred(long numberOfFilesToIncrease) + { + if (numberOfFilesToIncrease != 0) + { + Interlocked.Add(ref this.numberOfFilesTransferred, numberOfFilesToIncrease); + + if (this.Parent != null) + { + this.Parent.AddNumberOfFilesTransferred(numberOfFilesToIncrease); + } + } + + this.InvokeProgressHandler(); + } + + /// + /// Updates the number of files that are skipped. + /// + /// Indicating by how much the number of file that are skipped increased. + public void AddNumberOfFilesSkipped(long numberOfFilesToIncrease) + { + if (numberOfFilesToIncrease != 0) + { + Interlocked.Add(ref this.numberOfFilesSkipped, numberOfFilesToIncrease); + + if (this.Parent != null) + { + this.Parent.AddNumberOfFilesSkipped(numberOfFilesToIncrease); + } + } + + this.InvokeProgressHandler(); + } + + /// + /// Updates the number of files that are failed to be transferred. + /// + /// Indicating by how much the number of file that are failed to be transferred increased. + public void AddNumberOfFilesFailed(long numberOfFilesToIncrease) + { + if (numberOfFilesToIncrease != 0) + { + Interlocked.Add(ref this.numberOfFilesFailed, numberOfFilesToIncrease); + + if (this.Parent != null) + { + this.Parent.AddNumberOfFilesFailed(numberOfFilesToIncrease); + } + } + + this.InvokeProgressHandler(); + } + + /// + /// Gets a copy of this transfer progress tracker object. + /// + /// A copy of current TransferProgressTracker object + public TransferProgressTracker Copy() + { + return new TransferProgressTracker(this); + } + + /// + /// Serializes transfer progress. + /// + /// Serialization info object. + /// Streaming context. + public virtual void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new ArgumentNullException("info"); + } + + info.AddValue(BytesTransferredName, this.BytesTransferred); + info.AddValue(FilesTransferredName, this.NumberOfFilesTransferred); + info.AddValue(FilesSkippedName, this.NumberOfFilesSkipped); + info.AddValue(FilesFailedName, this.NumberOfFilesFailed); + } + + private void InvokeProgressHandler() + { + if (this.ProgressHandler != null) + { + if ( 0 == Interlocked.CompareExchange(ref this.invokingProgressHandler, 1, 0)) + { + lock (this.ProgressHandler) + { + Interlocked.Exchange(ref this.invokingProgressHandler, 0); + + this.ProgressHandler.Report( + new TransferProgress() + { + BytesTransferred = this.BytesTransferred, + NumberOfFilesTransferred = this.NumberOfFilesTransferred, + NumberOfFilesSkipped = this.NumberOfFilesSkipped, + NumberOfFilesFailed = this.NumberOfFilesFailed, + }); + } + } + } + } + } +} diff --git a/lib/Transfer_RequestOptions.cs b/lib/Transfer_RequestOptions.cs new file mode 100644 index 00000000..1a203cc5 --- /dev/null +++ b/lib/Transfer_RequestOptions.cs @@ -0,0 +1,305 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.Diagnostics; + using System.Net; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.File; + using Microsoft.WindowsAzure.Storage.RetryPolicies; + using Microsoft.WindowsAzure.Storage.Table; + + /// + /// Defines default RequestOptions for every type of transfer job. + /// + internal static class Transfer_RequestOptions + { + /// + /// Stores the default client retry count in x-ms error. + /// + private const int DefaultRetryCountXMsError = 10; + + /// + /// Stores the default client retry count in non x-ms error. + /// + private const int DefaultRetryCountOtherError = 3; + + /// + /// Stores the default maximum execution time across all potential retries. + /// + private static readonly TimeSpan DefaultMaximumExecutionTime = + TimeSpan.FromSeconds(900); + + /// + /// Stores the default server timeout. + /// + private static readonly TimeSpan DefaultServerTimeout = + TimeSpan.FromSeconds(300); + + /// + /// Stores the default back-off. + /// Increases exponentially used with ExponentialRetry: 3, 9, 21, 45, 93, 120, 120, 120, ... + /// + private static TimeSpan retryPoliciesDefaultBackoff = + TimeSpan.FromSeconds(3.0); + + /// + /// Gets the default . + /// + /// The default + public static BlobRequestOptions DefaultBlobRequestOptions + { + get + { + IRetryPolicy defaultRetryPolicy = new TransferRetryPolicy( + retryPoliciesDefaultBackoff, + DefaultRetryCountXMsError, + DefaultRetryCountOtherError); + + return new BlobRequestOptions() + { + MaximumExecutionTime = DefaultMaximumExecutionTime, + RetryPolicy = defaultRetryPolicy, + ServerTimeout = DefaultServerTimeout, + UseTransactionalMD5 = true + }; + } + } + + /// + /// Gets the default . + /// + /// The default + public static FileRequestOptions DefaultFileRequestOptions + { + get + { + IRetryPolicy defaultRetryPolicy = new TransferRetryPolicy( + retryPoliciesDefaultBackoff, + DefaultRetryCountXMsError, + DefaultRetryCountOtherError); + + return new FileRequestOptions() + { + MaximumExecutionTime = DefaultMaximumExecutionTime, + RetryPolicy = defaultRetryPolicy, + ServerTimeout = DefaultServerTimeout, + UseTransactionalMD5 = true + }; + } + } + + /// + /// Gets the default . + /// + /// The default + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "It will be called in TableDataMovement project.")] + public static TableRequestOptions DefaultTableRequestOptions + { + get + { + IRetryPolicy defaultRetryPolicy = new TransferRetryPolicy( + retryPoliciesDefaultBackoff, + DefaultRetryCountXMsError, + DefaultRetryCountOtherError); + + return new TableRequestOptions + { + MaximumExecutionTime = DefaultMaximumExecutionTime, + RetryPolicy = defaultRetryPolicy, + ServerTimeout = DefaultServerTimeout, + PayloadFormat = TablePayloadFormat.Json + }; + } + } + + /// + /// Define retry policy used in blob transfer. + /// + private class TransferRetryPolicy : IExtendedRetryPolicy + { + /// + /// Prefix of Azure Storage response keys. + /// + private const string XMsPrefix = "x-ms"; + + /// + /// Max retry count in non x-ms error. + /// + private int maxAttemptsOtherError; + + /// + /// ExponentialRetry retry policy object. + /// + private ExponentialRetry retryPolicy; + + /// + /// Indicate whether has met x-ms once or more. + /// + private bool gotXMsError = false; + + /// + /// Initializes a new instance of the class. + /// + /// Back-off in ExponentialRetry retry policy. + /// Max retry count when meets x-ms error. + /// Max retry count when meets non x-ms error. + public TransferRetryPolicy(TimeSpan deltaBackoff, int maxAttemptsXMsError, int maxAttemptsOtherError) + { + Debug.Assert( + maxAttemptsXMsError >= maxAttemptsOtherError, + "We should retry more times when meets x-ms errors than the other errors."); + + this.retryPolicy = new ExponentialRetry(deltaBackoff, maxAttemptsXMsError); + this.maxAttemptsOtherError = maxAttemptsOtherError; + } + + /// + /// Initializes a new instance of the class. + /// + /// ExponentialRetry object. + /// Max retry count when meets non x-ms error. + private TransferRetryPolicy(ExponentialRetry retryPolicy, int maxAttemptsInOtherError) + { + this.retryPolicy = retryPolicy; + this.maxAttemptsOtherError = maxAttemptsInOtherError; + } + + /// + /// Generates a new retry policy for the current request attempt. + /// + /// An IRetryPolicy object that represents the retry policy for the current request attempt. + public IRetryPolicy CreateInstance() + { + return new TransferRetryPolicy( + this.retryPolicy.CreateInstance() as ExponentialRetry, + this.maxAttemptsOtherError); + } + + /// + /// Determines whether the operation should be retried and the interval until the next retry. + /// + /// + /// A RetryContext object that indicates the number of retries, the results of the last request, + /// and whether the next retry should happen in the primary or secondary location, and specifies the location mode. + /// An OperationContext object for tracking the current operation. + /// + /// A RetryInfo object that indicates the location mode, + /// and whether the next retry should happen in the primary or secondary location. + /// If null, the operation will not be retried. + public RetryInfo Evaluate(RetryContext retryContext, OperationContext operationContext) + { + if (null == retryContext) + { + throw new ArgumentNullException("retryContext"); + } + + if (null == operationContext) + { + throw new ArgumentNullException("operationContext"); + } + + RetryInfo retryInfo = this.retryPolicy.Evaluate(retryContext, operationContext); + + if (null != retryInfo) + { + if (this.ShouldRetry(retryContext.CurrentRetryCount, retryContext.LastRequestResult.Exception)) + { + return retryInfo; + } + } + + return null; + } + + /// + /// Determines if the operation should be retried and how long to wait until the next retry. + /// + /// The number of retries for the given operation. + /// The status code for the last operation. + /// An Exception object that represents the last exception encountered. + /// The interval to wait until the next retry. + /// An OperationContext object for tracking the current operation. + /// True if the operation should be retried; otherwise, false. + public bool ShouldRetry( + int currentRetryCount, + int statusCode, + Exception lastException, + out TimeSpan retryInterval, + OperationContext operationContext) + { + if (!this.retryPolicy.ShouldRetry(currentRetryCount, statusCode, lastException, out retryInterval, operationContext)) + { + return false; + } + + return this.ShouldRetry(currentRetryCount, lastException); + } + + /// + /// Determines if the operation should be retried. + /// This function uses http header to determine whether the error is returned from Windows Azure. + /// If it's from Windows Azure (with x-ms in header), the request will retry 10 times at most. + /// Otherwise, the request will retry 3 times at most. + /// + /// The number of retries for the given operation. + /// An Exception object that represents the last exception encountered. + /// True if the operation should be retried; otherwise, false. + private bool ShouldRetry( + int currentRetryCount, + Exception lastException) + { + if (this.gotXMsError) + { + return true; + } + + StorageException storageException = lastException as StorageException; + + if (null != storageException) + { + WebException webException = storageException.InnerException as WebException; + + if (null != webException) + { + if (WebExceptionStatus.ConnectionClosed == webException.Status) + { + return true; + } + + HttpWebResponse response = webException.Response as HttpWebResponse; + + if (null != response) + { + if (null != response.Headers) + { + if (null != response.Headers.AllKeys) + { + for (int i = 0; i < response.Headers.AllKeys.Length; ++i) + { + if (response.Headers.AllKeys[i].StartsWith(XMsPrefix, StringComparison.OrdinalIgnoreCase)) + { + this.gotXMsError = true; + return true; + } + } + } + } + } + } + } + + if (currentRetryCount < this.maxAttemptsOtherError) + { + return true; + } + + return false; + } + } + } +} diff --git a/lib/Utils.cs b/lib/Utils.cs new file mode 100644 index 00000000..0b8e9f64 --- /dev/null +++ b/lib/Utils.cs @@ -0,0 +1,393 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace Microsoft.WindowsAzure.Storage.DataMovement +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Threading; + using Microsoft.WindowsAzure.Storage.Auth; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.File; + + /// + /// Class for various utils. + /// + internal static class Utils + { + private const int RequireBufferMaxRetryCount = 10; + + /// + /// Define the various possible size postfixes. + /// + private static readonly string[] SizeFormats = + { + Resources.ReadableSizeFormatBytes, + Resources.ReadableSizeFormatKiloBytes, + Resources.ReadableSizeFormatMegaBytes, + Resources.ReadableSizeFormatGigaBytes, + Resources.ReadableSizeFormatTeraBytes, + Resources.ReadableSizeFormatPetaBytes, + Resources.ReadableSizeFormatExaBytes + }; + + /// + /// Translate a size in bytes to human readable form. + /// + /// Size in bytes. + /// Human readable form string. + public static string BytesToHumanReadableSize(double size) + { + int order = 0; + + while (size >= 1024 && order + 1 < SizeFormats.Length) + { + ++order; + size /= 1024; + } + + return string.Format(CultureInfo.CurrentCulture, SizeFormats[order], size); + } + + public static void CheckCancellation(CancellationTokenSource cancellationTokenSource) + { + if (cancellationTokenSource.IsCancellationRequested) + { + throw new OperationCanceledException(Resources.BlobTransferCancelledException); + } + } + + /// + /// Generate an AccessCondition instance of IfMatchETag with customer condition. + /// For download/copy, if it succeeded at the first operation to fetching attribute with customer condition, + /// it means that the blob totally meet the condition. + /// Here, only need to keep LeaseId in the customer condition for the following operations. + /// + /// ETag string. + /// Condition customer input in TransferLocation. + /// To specify whether have already verified the custom access condition against the blob. + /// AccessCondition instance of IfMatchETag with customer condition's LeaseId. + public static AccessCondition GenerateIfMatchConditionWithCustomerCondition( + string etag, + AccessCondition customCondition, + bool checkedCustomAC = true) + { + if (!checkedCustomAC) + { + return customCondition; + } + + AccessCondition accessCondition = AccessCondition.GenerateIfMatchCondition(etag); + + if (null != customCondition) + { + accessCondition.LeaseId = customCondition.LeaseId; + } + + return accessCondition; + } + + public static bool DictionaryEquals( + this IDictionary firstDic, IDictionary secondDic) + { + if (firstDic == secondDic) + { + return true; + } + + if (firstDic == null || secondDic == null) + { + return false; + } + + if (firstDic.Count != secondDic.Count) + { + return false; + } + + foreach (var pair in firstDic) + { + string secondValue; + if (!secondDic.TryGetValue(pair.Key, out secondValue)) + { + return false; + } + + if (!string.Equals(pair.Value, secondValue, StringComparison.Ordinal)) + { + return false; + } + } + + return true; + } + + public static Attributes GenerateAttributes(CloudBlob blob) + { + return new Attributes() + { + CacheControl = blob.Properties.CacheControl, + ContentDisposition = blob.Properties.ContentDisposition, + ContentEncoding = blob.Properties.ContentEncoding, + ContentLanguage = blob.Properties.ContentLanguage, + ContentMD5 = blob.Properties.ContentMD5, + ContentType = blob.Properties.ContentType, + Metadata = blob.Metadata, + OverWriteAll = true + }; + } + + public static Attributes GenerateAttributes(CloudFile file) + { + return new Attributes() + { + CacheControl = file.Properties.CacheControl, + ContentDisposition = file.Properties.ContentDisposition, + ContentEncoding = file.Properties.ContentEncoding, + ContentLanguage = file.Properties.ContentLanguage, + ContentMD5 = file.Properties.ContentMD5, + ContentType = file.Properties.ContentType, + Metadata = file.Metadata, + OverWriteAll = true + }; + } + + public static void SetAttributes(CloudBlob blob, Attributes attributes) + { + if (attributes.OverWriteAll) + { + blob.Properties.CacheControl = attributes.CacheControl; + blob.Properties.ContentDisposition = attributes.ContentDisposition; + blob.Properties.ContentEncoding = attributes.ContentEncoding; + blob.Properties.ContentLanguage = attributes.ContentLanguage; + blob.Properties.ContentMD5 = attributes.ContentMD5; + blob.Properties.ContentType = attributes.ContentType; + + blob.Metadata.Clear(); + + foreach (var metadataPair in attributes.Metadata) + { + blob.Metadata.Add(metadataPair); + } + } + else + { + blob.Properties.ContentMD5 = attributes.ContentMD5; + if (null != attributes.ContentType) + { + blob.Properties.ContentType = attributes.ContentType; + } + } + } + + public static void SetAttributes(CloudFile file, Attributes attributes) + { + if (attributes.OverWriteAll) + { + file.Properties.CacheControl = attributes.CacheControl; + file.Properties.ContentDisposition = attributes.ContentDisposition; + file.Properties.ContentEncoding = attributes.ContentEncoding; + file.Properties.ContentLanguage = attributes.ContentLanguage; + file.Properties.ContentMD5 = attributes.ContentMD5; + file.Properties.ContentType = attributes.ContentType; + + file.Metadata.Clear(); + + foreach (var metadataPair in attributes.Metadata) + { + file.Metadata.Add(metadataPair); + } + } + else + { + file.Properties.ContentMD5 = attributes.ContentMD5; + + if (null != attributes.ContentType) + { + file.Properties.ContentType = attributes.ContentType; + } + } + } + + /// + /// Generate an AccessCondition instance with lease id customer condition. + /// For upload/copy, if it succeeded at the first operation to fetching destination attribute with customer condition, + /// it means that the blob totally meet the condition. + /// Here, only need to keep LeaseId in the customer condition for the following operations. + /// + /// Condition customer input in TransferLocation. + /// To specify whether have already verified the custom access condition against the blob. + /// AccessCondition instance with customer condition's LeaseId. + public static AccessCondition GenerateConditionWithCustomerCondition( + AccessCondition customCondition, + bool checkedCustomAC = true) + { + if (!checkedCustomAC) + { + return customCondition; + } + + if ((null != customCondition) + && !string.IsNullOrEmpty(customCondition.LeaseId)) + { + return AccessCondition.GenerateLeaseCondition(customCondition.LeaseId); + } + + return null; + } + + /// + /// Generate a BlobRequestOptions with custom BlobRequestOptions. + /// We have default MaximumExecutionTime, ServerTimeout and RetryPolicy. + /// If user doesn't set these properties, we should use the default ones. + /// Others, we should the custom ones. + /// + /// BlobRequestOptions customer input in TransferLocation. + /// BlobRequestOptions instance with custom BlobRequestOptions properties. + public static BlobRequestOptions GenerateBlobRequestOptions( + BlobRequestOptions customRequestOptions) + { + if (null == customRequestOptions) + { + return Transfer_RequestOptions.DefaultBlobRequestOptions; + } + else + { + BlobRequestOptions requestOptions = Transfer_RequestOptions.DefaultBlobRequestOptions; + + AssignToRequestOptions(requestOptions, customRequestOptions); + + if (null != customRequestOptions.UseTransactionalMD5) + { + requestOptions.UseTransactionalMD5 = customRequestOptions.UseTransactionalMD5; + } + + requestOptions.DisableContentMD5Validation = customRequestOptions.DisableContentMD5Validation; + return requestOptions; + } + } + + /// + /// Generate a FileRequestOptions with custom FileRequestOptions. + /// We have default MaximumExecutionTime, ServerTimeout and RetryPolicy. + /// If user doesn't set these properties, we should use the default ones. + /// Others, we should the custom ones. + /// + /// FileRequestOptions customer input in TransferLocation. + /// FileRequestOptions instance with custom FileRequestOptions properties. + public static FileRequestOptions GenerateFileRequestOptions( + FileRequestOptions customRequestOptions) + { + if (null == customRequestOptions) + { + return Transfer_RequestOptions.DefaultFileRequestOptions; + } + else + { + FileRequestOptions requestOptions = Transfer_RequestOptions.DefaultFileRequestOptions; + + AssignToRequestOptions(requestOptions, customRequestOptions); + + if (null != customRequestOptions.UseTransactionalMD5) + { + requestOptions.UseTransactionalMD5 = customRequestOptions.UseTransactionalMD5; + } + + requestOptions.DisableContentMD5Validation = customRequestOptions.DisableContentMD5Validation; + return requestOptions; + } + } + + /// + /// Generate an OperationContext from the the specified TransferContext. + /// + /// Transfer context + /// An object. + public static OperationContext GenerateOperationContext( + TransferContext transferContext) + { + if (transferContext == null) + { + return null; + } + + return new OperationContext() + { + ClientRequestID = transferContext.ClientRequestId, + LogLevel = transferContext.LogLevel, + }; + } + + public static CloudBlob GetBlobReference(Uri blobUri, StorageCredentials credentials, BlobType blobType) + { + switch (blobType) + { + case BlobType.BlockBlob: + return new CloudBlockBlob(blobUri, credentials); + case BlobType.PageBlob: + return new CloudPageBlob(blobUri, credentials); + case BlobType.AppendBlob: + return new CloudAppendBlob(blobUri, credentials); + default: + throw new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + Resources.NotSupportedBlobType, + blobType)); + } + } + + public static byte[] RequireBuffer(MemoryManager memoryManager, Action checkCancellation) + { + byte[] buffer; + buffer = memoryManager.RequireBuffer(); + + if (null == buffer) + { + int retryCount = 0; + int retryInterval = 100; + while ((retryCount < RequireBufferMaxRetryCount) + && (null == buffer)) + { + checkCancellation(); + retryInterval <<= 1; + Thread.Sleep(retryInterval); + buffer = memoryManager.RequireBuffer(); + ++retryCount; + } + } + + if (null == buffer) + { + throw new TransferException( + TransferErrorCode.FailToAllocateMemory, + Resources.FailedToAllocateMemoryException); + } + + return buffer; + } + + private static void AssignToRequestOptions(IRequestOptions targetRequestOptions, IRequestOptions customRequestOptions) + { + if (null != customRequestOptions.MaximumExecutionTime) + { + targetRequestOptions.MaximumExecutionTime = customRequestOptions.MaximumExecutionTime; + } + + if (null != customRequestOptions.RetryPolicy) + { + targetRequestOptions.RetryPolicy = customRequestOptions.RetryPolicy; + } + + if (null != customRequestOptions.ServerTimeout) + { + targetRequestOptions.ServerTimeout = customRequestOptions.ServerTimeout; + } + + targetRequestOptions.LocationMode = customRequestOptions.LocationMode; + } + } +} diff --git a/lib/packages.config b/lib/packages.config new file mode 100644 index 00000000..f48ace35 --- /dev/null +++ b/lib/packages.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/samples/DataMovementSamples/DataMovementSamples.sln b/samples/DataMovementSamples/DataMovementSamples.sln new file mode 100644 index 00000000..bc55af7b --- /dev/null +++ b/samples/DataMovementSamples/DataMovementSamples.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.21005.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataMovementSamples", "DataMovementSamples\DataMovementSamples.csproj", "{6004824E-4A84-463E-9094-451B253470CC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6004824E-4A84-463E-9094-451B253470CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6004824E-4A84-463E-9094-451B253470CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6004824E-4A84-463E-9094-451B253470CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6004824E-4A84-463E-9094-451B253470CC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/samples/DataMovementSamples/DataMovementSamples/App.config b/samples/DataMovementSamples/DataMovementSamples/App.config new file mode 100644 index 00000000..9d612be5 --- /dev/null +++ b/samples/DataMovementSamples/DataMovementSamples/App.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/samples/DataMovementSamples/DataMovementSamples/DataMovementSamples.csproj b/samples/DataMovementSamples/DataMovementSamples/DataMovementSamples.csproj new file mode 100644 index 00000000..877afa04 --- /dev/null +++ b/samples/DataMovementSamples/DataMovementSamples/DataMovementSamples.csproj @@ -0,0 +1,97 @@ + + + + + Debug + AnyCPU + {6004824E-4A84-463E-9094-451B253470CC} + Exe + Properties + DataMovementSamples + DataMovementSamples + v4.5 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Microsoft.Data.Edm.5.6.4\lib\net40\Microsoft.Data.Edm.dll + True + + + ..\packages\Microsoft.Data.OData.5.6.4\lib\net40\Microsoft.Data.OData.dll + True + + + ..\packages\Microsoft.Data.Services.Client.5.6.4\lib\net40\Microsoft.Data.Services.Client.dll + True + + + ..\packages\Microsoft.WindowsAzure.ConfigurationManager.1.8.0.0\lib\net35-full\Microsoft.WindowsAzure.Configuration.dll + True + + + ..\packages\WindowsAzure.Storage.5.0.0\lib\net40\Microsoft.WindowsAzure.Storage.dll + True + + + ..\packages\Microsoft.Azure.Storage.DataMovement.0.0.76\lib\net45\Microsoft.WindowsAzure.Storage.DataMovement.dll + True + + + ..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll + True + + + + + ..\packages\System.Spatial.5.6.4\lib\net40\System.Spatial.dll + True + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + + \ No newline at end of file diff --git a/samples/DataMovementSamples/DataMovementSamples/Properties/AssemblyInfo.cs b/samples/DataMovementSamples/DataMovementSamples/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..81f9e3f3 --- /dev/null +++ b/samples/DataMovementSamples/DataMovementSamples/Properties/AssemblyInfo.cs @@ -0,0 +1,42 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("DataMovementSamples")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("DataMovementSamples")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("d4c65175-d41e-4b9e-a898-ff45d07ee90c")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/samples/DataMovementSamples/DataMovementSamples/Samples.cs b/samples/DataMovementSamples/DataMovementSamples/Samples.cs new file mode 100644 index 00000000..896621ab --- /dev/null +++ b/samples/DataMovementSamples/DataMovementSamples/Samples.cs @@ -0,0 +1,258 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DataMovementSamples +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.DataMovement; + using Microsoft.WindowsAzure.Storage.File; + + public class Samples + { + public static void Main(string[] args) + { + Console.WriteLine("Data movement upload sample."); + UploadSample().Wait(); + + Console.WriteLine(); + Console.WriteLine("Data movement copy sample."); + CopySample().Wait(); + + Console.WriteLine(); + Console.WriteLine("Data movement download sample."); + DownloadSample().Wait(); + + Console.WriteLine(); + Console.WriteLine("Cleanup generated data."); + Cleanup(); + } + + /// + /// Container name used in this sample. + /// + private const string ContainerName = "samplecontainer"; + + /// + /// Share name used in this sample. + /// + private const string ShareName = "sampleshare"; + + /// + /// Upload a local picture to azure storage. + /// 1. Upload a local picture as a block blob. + /// 2. Set its content type to "image/png". + /// + private static async Task UploadSample() + { + string sourceFileName = "azure.png"; + string destinationBlobName = "azure_blockblob.png"; + + // Create the destination CloudBlob instance + CloudBlob destinationBlob = Util.GetCloudBlob(ContainerName, destinationBlobName, BlobType.BlockBlob); + + // Use UploadOptions to set ContentType of destination CloudBlob + UploadOptions options = new UploadOptions(); + options.ContentType = "image/png"; + + // Start the upload + await TransferManager.UploadAsync(sourceFileName, destinationBlob, options, null /* context */); + Console.WriteLine("File {0} is uploaded to {1} successfully.", sourceFileName, destinationBlob.Uri.ToString()); + } + + /// + /// Copy data between Azure storage. + /// 1. Copy a CloudBlob as a CloudFile. + /// 2. Cancel the transfer before it finishes with a CancellationToken + /// 3. Store the transfer checkpoint after transfer being cancelled + /// 4. Resume the transfer with the stored checkpoint + /// + private static async Task CopySample() + { + string sourceBlobName = "azure_blockblob.png"; + string destinationFileName = "azure_cloudfile.png"; + + // Create the source CloudBlob instance + CloudBlob sourceBlob = Util.GetCloudBlob(ContainerName, sourceBlobName, BlobType.BlockBlob); + + // Create the destination CloudFile instance + CloudFile destinationFile = Util.GetCloudFile(ShareName, destinationFileName); + + // Create CancellationTokenSource used to cancel the transfer + CancellationTokenSource cancellationSource = new CancellationTokenSource(); + + TransferCheckpoint checkpoint = null; + TransferContext context = new TransferContext(); + + // Cancel the transfer after there's any progress reported + Progress progress = new Progress( + (transferProgress) => { + if (!cancellationSource.IsCancellationRequested) + { + Console.WriteLine("Cancel the transfer."); + + // Cancel the transfer + cancellationSource.Cancel(); + + // Store the transfer checkpoint + checkpoint = context.LastCheckpoint; + } + }); + + context.ProgressHandler = progress; + + // Start the transfer + try + { + await TransferManager.CopyAsync(sourceBlob, destinationFile, false /* isServiceCopy */, null /* options */, context, cancellationSource.Token); + } + catch (TaskCanceledException e) + { + Console.WriteLine("The transfer is cancelled: {0}", e.Message); + } + + // Create a new TransferContext with the store checkpoint + TransferContext resumeContext = new TransferContext(checkpoint); + + // Resume transfer from the stored checkpoint + Console.WriteLine("Resume the cancelled transfer."); + await TransferManager.CopyAsync(sourceBlob, destinationFile, false /* isServiceCopy */, null /* options */, resumeContext); + Console.WriteLine("CloudBlob {0} is copied to {1} successfully.", sourceBlob.Uri.ToString(), destinationFile.Uri.ToString()); + } + + /// + /// Download data from Azure storage. + /// 1. Download a CloudBlob to an exsiting local file + /// 2. Query the user to overwrite the local file or not in the OverwriteCallback + /// 3. Download a CloudFile to local with content MD5 validation disabled + /// 4. Show the overall progress of both transfers + /// + private static async Task DownloadSample() + { + string sourceBlobName = "azure_blockblob.png"; + string sourceFileName = "azure_cloudfile.png"; + string destinationFileName1 = "azure.png"; + string destinationFileName2 = "azure_new.png"; + + // Create the source CloudBlob instance + CloudBlob sourceBlob = Util.GetCloudBlob(ContainerName, sourceBlobName, BlobType.BlockBlob); + + // Create the source CloudFile instance + CloudFile sourceFile = Util.GetCloudFile(ShareName, sourceFileName); + + // Create a TransferContext shared by both transfers + TransferContext sharedTransferContext = new TransferContext(); + + // Show overwrite prompt in console when OverwriteCallback is triggered + sharedTransferContext.OverwriteCallback = (source, destination) => + { + Console.WriteLine("{0} already exists. Do you want to overwrite it with {1}? (Y/N)", destination, source); + + while (true) + { + ConsoleKeyInfo keyInfo = Console.ReadKey(true); + char key = keyInfo.KeyChar; + + if (key == 'y' || key == 'Y') + { + Console.WriteLine("User choose to overwrite the destination."); + return true; + } + else if (key == 'n' || key == 'N') + { + Console.WriteLine("User choose NOT to overwrite the destination."); + return false; + } + + Console.WriteLine("Please press 'y' or 'n'."); + } + }; + + // Record the overall progress + ProgressRecorder recorder = new ProgressRecorder(); + sharedTransferContext.ProgressHandler = recorder; + + // Start the blob download + Task task1 = TransferManager.DownloadAsync(sourceBlob, destinationFileName1, null /* options */, sharedTransferContext); + + // Create a DownloadOptions to disable md5 check after data is downloaded. Otherwise, data movement + // library will check the md5 checksum stored in the ContentMD5 property of the source CloudFile/CloudBlob + // You can uncomment following codes, enable ContentMD5Validation and have a try. + // sourceFile.Properties.ContentMD5 = "WrongMD5"; + // sourceFile.SetProperties(); + DownloadOptions options = new DownloadOptions(); + options.DisableContentMD5Validation = true; + + // Start the download + Task task2 = TransferManager.DownloadAsync(sourceFile, destinationFileName2, options, sharedTransferContext); + + // Wait for both transfers to finish + try + { + await task1; + } + catch(TransferException e) + { + // Data movement library will throw a TransferException when user choose to not overwrite the existing destination + Console.WriteLine(e.Message); + } + + await task2; + + // Print out the final transfer state + Console.WriteLine("Final transfer state: {0}", recorder.ToString()); + } + + /// + /// Cleanup all data generated by this sample. + /// + private static void Cleanup() + { + Console.Write("Deleting container..."); + Util.DeleteContainer(ContainerName); + Console.WriteLine("Done"); + + Console.Write("Deleting share..."); + Util.DeleteShare(ShareName); + Console.WriteLine("Done"); + + Console.Write("Deleting local file..."); + File.Delete("azure_new.png"); + Console.WriteLine("Done"); + } + + /// + /// A helper class to record progress reported by data movement library in console. + /// + class ProgressRecorder : IProgress + { + private long latestBytesTransferred; + private long latestNumberOfFilesTransferred; + private long latestNumberOfFilesSkipped; + private long latestNumberOfFilesFailed; + + public void Report(TransferProgress progress) + { + this.latestBytesTransferred = progress.BytesTransferred; + this.latestNumberOfFilesTransferred = progress.NumberOfFilesTransferred; + this.latestNumberOfFilesSkipped = progress.NumberOfFilesSkipped; + this.latestNumberOfFilesFailed = progress.NumberOfFilesFailed; + } + + public override string ToString() + { + return string.Format("Transferred bytes: {0}; Transfered: {1}; Skipped: {2}, Failed: {3}", + this.latestBytesTransferred, + this.latestNumberOfFilesTransferred, + this.latestNumberOfFilesSkipped, + this.latestNumberOfFilesFailed); + } + } + } +} diff --git a/samples/DataMovementSamples/DataMovementSamples/Util.cs b/samples/DataMovementSamples/DataMovementSamples/Util.cs new file mode 100644 index 00000000..728e14b7 --- /dev/null +++ b/samples/DataMovementSamples/DataMovementSamples/Util.cs @@ -0,0 +1,131 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DataMovementSamples +{ + using System; + using Microsoft.WindowsAzure; + using Microsoft.WindowsAzure.Storage; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.File; + + /// + /// A helper class provides convenient operations against storage account configured in the App.config. + /// + public class Util + { + private static CloudStorageAccount storageAccount; + private static CloudBlobClient blobClient; + private static CloudFileClient fileClient; + + /// + /// Get a CloudBlob instance with the specified name and type in the given container. + /// + /// Container name. + /// Blob name. + /// Type of blob. + /// A CloudBlob instance with the specified name and type in the given container. + public static CloudBlob GetCloudBlob(string containerName, string blobName, BlobType blobType) + { + CloudBlobClient client = GetCloudBlobClient(); + CloudBlobContainer container = client.GetContainerReference(containerName); + container.CreateIfNotExists(); + + CloudBlob cloudBlob; + switch (blobType) + { + case BlobType.AppendBlob: + cloudBlob = container.GetAppendBlobReference(blobName); + break; + case BlobType.BlockBlob: + cloudBlob = container.GetBlockBlobReference(blobName); + break; + case BlobType.PageBlob: + cloudBlob = container.GetPageBlobReference(blobName); + break; + case BlobType.Unspecified: + default: + throw new ArgumentException(string.Format("Invalid blob type {0}", blobType.ToString()), "blobType"); + } + + return cloudBlob; + } + + /// + /// Get a CloudFile instance with the specified name in the given share. + /// + /// Share name. + /// File name. + /// A CloudFile instance with the specified name in the given share. + public static CloudFile GetCloudFile(string shareName, string fileName) + { + CloudFileClient client = GetCloudFileClient(); + CloudFileShare share = client.GetShareReference(shareName); + share.CreateIfNotExists(); + + CloudFileDirectory rootDirectory = share.GetRootDirectoryReference(); + return rootDirectory.GetFileReference(fileName); + } + + /// + /// Delete the share with the specified name if it exists. + /// + /// Name of share to delete. + public static void DeleteShare(string shareName) + { + CloudFileClient client = GetCloudFileClient(); + CloudFileShare share = client.GetShareReference(shareName); + share.DeleteIfExists(); + } + + /// + /// Delete the container with the specified name if it exists. + /// + /// Name of container to delete. + public static void DeleteContainer(string containerName) + { + CloudBlobClient client = GetCloudBlobClient(); + CloudBlobContainer container = client.GetContainerReference(containerName); + container.DeleteIfExists(); + } + + private static CloudBlobClient GetCloudBlobClient() + { + if (Util.blobClient == null) + { + Util.blobClient = GetStorageAccount().CreateCloudBlobClient(); + } + + return Util.blobClient; + } + + private static CloudFileClient GetCloudFileClient() + { + if (Util.fileClient == null) + { + Util.fileClient = GetStorageAccount().CreateCloudFileClient(); + } + + return Util.fileClient; + } + + private static string LoadConnectionStringFromConfigration() + { + // How to create a storage connection string: http://msdn.microsoft.com/en-us/library/azure/ee758697.aspx + return CloudConfigurationManager.GetSetting("StorageConnectionString"); + } + + private static CloudStorageAccount GetStorageAccount() + { + if (Util.storageAccount == null) + { + string connectionString = LoadConnectionStringFromConfigration(); + Util.storageAccount = CloudStorageAccount.Parse(connectionString); + } + + return Util.storageAccount; + } + } +} diff --git a/samples/DataMovementSamples/DataMovementSamples/azure.png b/samples/DataMovementSamples/DataMovementSamples/azure.png new file mode 100644 index 0000000000000000000000000000000000000000..f50ba5b757f89cd063d14fa195746640df6eaf6d GIT binary patch literal 6534 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 DqH}Tm literal 0 HcmV?d00001 diff --git a/samples/DataMovementSamples/DataMovementSamples/packages.config b/samples/DataMovementSamples/DataMovementSamples/packages.config new file mode 100644 index 00000000..3c3755c4 --- /dev/null +++ b/samples/DataMovementSamples/DataMovementSamples/packages.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/test/DMLibTest/Cases/AccessConditionTest.cs b/test/DMLibTest/Cases/AccessConditionTest.cs new file mode 100644 index 00000000..4c8addfb --- /dev/null +++ b/test/DMLibTest/Cases/AccessConditionTest.cs @@ -0,0 +1,132 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest.Cases +{ + using System; + using System.Net; + using DMLibTestCodeGen; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Microsoft.WindowsAzure.Storage; + using Microsoft.WindowsAzure.Storage.DataMovement; + using MS.Test.Common.MsTestLib; + + [MultiDirectionTestClass] + public class AccessConditionTest : DMLibTestBase + { + #region Additional test attributes + [ClassInitialize()] + public static void MyClassInitialize(TestContext testContext) + { + DMLibTestBase.BaseClassInitialize(testContext); + } + + [ClassCleanup()] + public static void MyClassCleanup() + { + DMLibTestBase.BaseClassCleanup(); + } + + [TestInitialize()] + public void MyTestInitialize() + { + base.BaseTestInitialize(); + } + + [TestCleanup()] + public void MyTestCleanup() + { + base.BaseTestCleanup(); + } + #endregion + + [TestCategory(Tag.Function)] + [DMLibTestMethodSet(DMLibTestMethodSet.CloudBlobSource)] + public void TestSourceAccessCondition() + { + this.TestAccessCondition(SourceOrDest.Source); + } + + [TestCategory(Tag.Function)] + [DMLibTestMethodSet(DMLibTestMethodSet.CloudBlobDest)] + public void TestDestAccessCondition() + { + this.TestAccessCondition(SourceOrDest.Dest); + } + + private void TestAccessCondition(SourceOrDest sourceOrDest) + { + string eTag = "notmatch"; + AccessCondition accessCondition = new AccessCondition() + { + IfMatchETag = eTag + }; + + DMLibDataInfo sourceDataInfo = new DMLibDataInfo(string.Empty); + DMLibDataHelper.AddOneFileInBytes(sourceDataInfo.RootNode, DMLibTestBase.FileName, 1024); + + DMLibDataInfo destDataInfo = new DMLibDataInfo(string.Empty); + DMLibDataHelper.AddOneFileInBytes(destDataInfo.RootNode, DMLibTestBase.FileName, 1024); + + var options = new TestExecutionOptions(); + + if (sourceOrDest == SourceOrDest.Dest) + { + options.DestTransferDataInfo = destDataInfo; + } + + options.TransferItemModifier = (fileNode, transferItem) => + { + dynamic transferOptions = DefaultTransferOptions; + + if (sourceOrDest == SourceOrDest.Source) + { + transferOptions.SourceAccessCondition = accessCondition; + } + else + { + transferOptions.DestinationAccessCondition = accessCondition; + } + + transferItem.Options = transferOptions; + }; + + var result = this.ExecuteTestCase(sourceDataInfo, options); + + if (sourceOrDest == SourceOrDest.Dest) + { + Test.Assert(DMLibDataHelper.Equals(destDataInfo, result.DataInfo), "Verify no file is transferred."); + } + else + { + if (DMLibTestContext.DestType != DMLibDataType.Stream) + { + Test.Assert(DMLibDataHelper.Equals(new DMLibDataInfo(string.Empty), result.DataInfo), "Verify no file is transferred."); + } + else + { + foreach(var fileNode in result.DataInfo.EnumerateFileNodes()) + { + Test.Assert(fileNode.SizeInByte == 0, "Verify file {0} is empty", fileNode.Name); + } + } + } + + // Verify TransferException + if (result.Exceptions.Count != 1) + { + Test.Error("There should be exactly one exceptions."); + return; + } + + Exception exception = result.Exceptions[0]; + VerificationHelper.VerifyTransferException(exception, TransferErrorCode.Unknown); + + // Verify innner StorageException + VerificationHelper.VerifyStorageException(exception.InnerException, (int)HttpStatusCode.PreconditionFailed, + "The condition specified using HTTP conditional header(s) is not met."); + } + } +} diff --git a/test/DMLibTest/Cases/AllTransferDirectionTest.cs b/test/DMLibTest/Cases/AllTransferDirectionTest.cs new file mode 100644 index 00000000..eb5c9d04 --- /dev/null +++ b/test/DMLibTest/Cases/AllTransferDirectionTest.cs @@ -0,0 +1,418 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace DMLibTest +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using DMLibTestCodeGen; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Microsoft.WindowsAzure.Storage.DataMovement; + using MS.Test.Common.MsTestLib; + + [TestClass] + public class AllTransferDirectionTest : DMLibTestBase + { + #region Additional test attributes + [ClassInitialize()] + public static void MyClassInitialize(TestContext testContext) + { + DMLibTestBase.BaseClassInitialize(testContext); + } + + [ClassCleanup()] + public static void MyClassCleanup() + { + DMLibTestBase.BaseClassCleanup(); + } + + [TestInitialize()] + public void MyTestInitialize() + { + base.BaseTestInitialize(); + } + + [TestCleanup()] + public void MyTestCleanup() + { + base.BaseTestCleanup(); + } + #endregion + + private Dictionary PrepareSourceData(long fileSizeInB) + { + var sourceFileNodes = new Dictionary(); + var sourceDataInfos = new Dictionary(); + + // Prepare source data info + foreach (DMLibTransferDirection direction in GetAllValidDirections()) + { + string fileName = GetTransferFileName(direction); + + DMLibDataInfo sourceDataInfo; + string sourceDataInfoKey; + if (direction.SourceType != DMLibDataType.URI) + { + sourceDataInfoKey = direction.SourceType.ToString(); + } + else + { + sourceDataInfoKey = GetTransferFileName(direction); + } + + if (sourceDataInfos.ContainsKey(sourceDataInfoKey)) + { + sourceDataInfo = sourceDataInfos[sourceDataInfoKey]; + } + else + { + sourceDataInfo = new DMLibDataInfo(string.Empty); + sourceDataInfos[sourceDataInfoKey] = sourceDataInfo; + } + + DMLibDataHelper.AddOneFileInBytes(sourceDataInfo.RootNode, fileName, fileSizeInB); + + FileNode sourceFileNode = sourceDataInfo.RootNode.GetFileNode(fileName); + sourceFileNodes.Add(fileName, sourceFileNode); + } + + // Generate source data + foreach (var pair in sourceDataInfos) + { + DMLibDataType sourceDataType; + if (Enum.TryParse(pair.Key, out sourceDataType)) + { + DataAdaptor sourceAdaptor = GetSourceAdaptor(sourceDataType); + sourceAdaptor.Cleanup(); + sourceAdaptor.CreateIfNotExists(); + + sourceAdaptor.GenerateData(pair.Value); + } + } + + // Generate source data for URI source separately since it's destination related + DataAdaptor uriSourceAdaptor = GetSourceAdaptor(DMLibDataType.URI); + uriSourceAdaptor.Cleanup(); + uriSourceAdaptor.CreateIfNotExists(); + + DMLibTestContext.SourceType = DMLibDataType.URI; + DMLibTestContext.IsAsync = true; + + DMLibDataType[] uriDestDataTypes = { DMLibDataType.CloudFile, DMLibDataType.BlockBlob, DMLibDataType.PageBlob, DMLibDataType.AppendBlob }; + foreach (DMLibDataType uriDestDataType in uriDestDataTypes) + { + DMLibTestContext.DestType = uriDestDataType; + string sourceDataInfoKey = GetTransferFileName(DMLibDataType.URI, uriDestDataType, true); + + uriSourceAdaptor.GenerateData(sourceDataInfos[sourceDataInfoKey]); + } + + // Clean up destination + foreach (DMLibDataType destDataType in DataTypes) + { + if (destDataType != DMLibDataType.URI) + { + DataAdaptor destAdaptor = GetDestAdaptor(destDataType); + destAdaptor.Cleanup(); + destAdaptor.CreateIfNotExists(); + } + } + + return sourceFileNodes; + } + + private List GetTransformItemsForAllDirections(Dictionary fileNodes) + { + List allItems = new List(); + foreach (DMLibTransferDirection direction in GetAllValidDirections()) + { + string fileName = GetTransferFileName(direction); + DataAdaptor sourceAdaptor = GetSourceAdaptor(direction.SourceType); + DataAdaptor destAdaptor = GetDestAdaptor(direction.DestType); + + FileNode fileNode = fileNodes[fileName]; + TransferItem item = new TransferItem() + { + SourceObject = sourceAdaptor.GetTransferObject(fileNode), + DestObject = destAdaptor.GetTransferObject(fileNode), + SourceType = direction.SourceType, + DestType = direction.DestType, + IsServiceCopy = direction.IsAsync, + }; + allItems.Add(item); + } + + return allItems; + } + + [TestMethod] + [TestCategory(Tag.Function)] + public void ResumeInAllDirections() + { + long fileSizeInByte = 10 * 1024 * 1024; + Dictionary sourceFileNodes = this.PrepareSourceData(fileSizeInByte); + List allItems = this.GetTransformItemsForAllDirections(sourceFileNodes); + + int fileCount = sourceFileNodes.Keys.Count; + + // Execution and store checkpoints + CancellationTokenSource tokenSource = new CancellationTokenSource(); + + var transferContext = new TransferContext(); + var progressChecker = new ProgressChecker(fileCount, fileSizeInByte * fileCount); + transferContext.ProgressHandler = progressChecker.GetProgressHandler(); + allItems.ForEach(item => + { + item.CancellationToken = tokenSource.Token; + item.TransferContext = transferContext; + }); + + var options = new TestExecutionOptions(); + options.DisableDestinationFetch = true; + + // Checkpoint names + const string PartialStarted = "PartialStarted", + AllStarted = "AllStarted", + AllStartedAndWait = "AllStartedAndWait", + BeforeCancel = "BeforeCancel", + AfterCancel = "AfterCancel"; + Dictionary checkpoints = new Dictionary(); + + TransferItem randomItem = allItems[random.Next(0, allItems.Count)]; + + randomItem.AfterStarted = () => + { + Test.Info("Store check point after transfer item: {0}.", randomItem.ToString()); + checkpoints.Add(PartialStarted, transferContext.LastCheckpoint); + }; + + options.AfterAllItemAdded = () => + { + progressChecker.DataTransferred.WaitOne(); + checkpoints.Add(AllStarted, transferContext.LastCheckpoint); + Thread.Sleep(1000); + checkpoints.Add(AllStartedAndWait, transferContext.LastCheckpoint); + Thread.Sleep(1000); + checkpoints.Add(BeforeCancel, transferContext.LastCheckpoint); + tokenSource.Cancel(); + checkpoints.Add(AfterCancel, transferContext.LastCheckpoint); + }; + + var result = this.RunTransferItems(allItems, options); + + // Resume with stored checkpoints in random order + var checkpointList = new List>(); + checkpointList.AddRange(checkpoints); + checkpointList.Shuffle(); + + foreach(var pair in checkpointList) + { + Test.Info("===Resume with checkpoint '{0}'===", pair.Key); + options = new TestExecutionOptions(); + options.DisableDestinationFetch = true; + + progressChecker.Reset(); + transferContext = new TransferContext(pair.Value) + { + ProgressHandler = progressChecker.GetProgressHandler(), + + // The checkpoint can be stored when DMLib doesn't check overwrite callback yet. + // So it will case an skip file error if the desination file already exists and + // We don't have overwrite callback here. + OverwriteCallback = DMLibInputHelper.GetDefaultOverwiteCallbackY() + }; + + List itemsToResume = allItems.Select(item => + { + TransferItem itemToResume = item.Clone(); + itemToResume.TransferContext = transferContext; + return itemToResume; + }).ToList(); + + result = this.RunTransferItems(itemsToResume, options); + + int resumeFailCount = 0; + foreach (DMLibDataType destDataType in DataTypes) + { + DataAdaptor destAdaptor = GetSourceAdaptor(destDataType); + DMLibDataInfo destDataInfo = destAdaptor.GetTransferDataInfo(string.Empty); + + foreach (FileNode destFileNode in destDataInfo.EnumerateFileNodes()) + { + string fileName = destFileNode.Name; + if (!fileName.Contains(DMLibDataType.Stream.ToString())) + { + FileNode sourceFileNode = sourceFileNodes[fileName]; + Test.Assert(DMLibDataHelper.Equals(sourceFileNode, destFileNode), "Verify transfer result."); + } + else + { + resumeFailCount++; + } + } + } + + Test.Assert(result.Exceptions.Count == resumeFailCount, "Verify resume failure count: expected {0}, actual {1}.", resumeFailCount, result.Exceptions.Count); + + foreach (var resumeException in result.Exceptions) + { + Test.Assert(resumeException is NotSupportedException, "Verify resume exception is NotSupportedException."); + } + } + } + + [TestMethod] + [TestCategory(Tag.BVT)] + public void TransferInAllDirections() + { + // Prepare source data + Dictionary sourceFileNodes = this.PrepareSourceData(10 * 1024 * 1024); + List allItems = this.GetTransformItemsForAllDirections(sourceFileNodes); + + // Execution + var result = this.RunTransferItems(allItems, new TestExecutionOptions()); + + // Verify all files are transfered successfully + Test.Assert(result.Exceptions.Count == 0, "Verify no exception occurs."); + foreach (DMLibDataType destDataType in DataTypes) + { + DataAdaptor destAdaptor = GetSourceAdaptor(destDataType); + DMLibDataInfo destDataInfo = destAdaptor.GetTransferDataInfo(string.Empty); + + foreach (FileNode destFileNode in destDataInfo.EnumerateFileNodes()) + { + FileNode sourceFileNode = sourceFileNodes[destFileNode.Name]; + Test.Assert(DMLibDataHelper.Equals(sourceFileNode, destFileNode), "Verify transfer result."); + } + } + } + + private static string GetTransferFileName(DMLibTransferDirection direction) + { + return GetTransferFileName(direction.SourceType, direction.DestType, direction.IsAsync); + } + + private static string GetTransferFileName(DMLibDataType sourceType, DMLibDataType destType, bool isAsync) + { + return sourceType.ToString() + destType.ToString() + (isAsync ? "async" : ""); + } + + private static IEnumerable GetAllValidDirections() + { + for (int sourceIndex = 0; sourceIndex < DataTypes.Length; ++sourceIndex) + { + for (int destIndex = 0; destIndex < DataTypes.Length; ++destIndex) + { + DMLibDataType sourceDataType = DataTypes[sourceIndex]; + DMLibDataType destDataType = DataTypes[destIndex]; + + if (validSyncDirections[sourceIndex][destIndex]) + { + yield return new DMLibTransferDirection() + { + SourceType = sourceDataType, + DestType = destDataType, + IsAsync = false, + }; + } + + if (validAsyncDirections[sourceIndex][destIndex]) + { + yield return new DMLibTransferDirection() + { + SourceType = sourceDataType, + DestType = destDataType, + IsAsync = true, + }; + } + } + } + } + + // [SourceType][DestType] + private static bool[][] validSyncDirections = + { + // stream, uri, local, xsmb, block, page, append + new bool[] {false, false, false, true, true, true, true}, // stream + new bool[] {false, false, false, false, false, false, false}, // uri + new bool[] {false, false, false, true, true, true, true}, // local + new bool[] {true, false, true, true, true, true, true}, // xsmb + new bool[] {true, false, true, true, true, false, false}, // block + new bool[] {true, false, true, true, false, true, false}, // page + new bool[] {true, false, true, true, false, false, true}, // append + }; + + // [SourceType][DestType] + private static bool[][] validAsyncDirections = + { + // stream, uri, local, xsmb, block, page, append + new bool[] {false, false, false, false, false, false, false}, // stream + new bool[] {false, false, false, true, true, true, true}, // uri + new bool[] {false, false, false, false, false, false, false}, // local + new bool[] {false, false, false, true, true, false, false}, // xsmb + new bool[] {false, false, false, true, true, false, false}, // block + new bool[] {false, false, false, true, false, true, false}, // page + new bool[] {false, false, false, true, false, false, true}, // append + }; + + private static DMLibDataType[] DataTypes = + { + DMLibDataType.Stream, + DMLibDataType.URI, + DMLibDataType.Local, + DMLibDataType.CloudFile, + DMLibDataType.BlockBlob, + DMLibDataType.PageBlob, + DMLibDataType.AppendBlob + }; + + private static int GetValidDirectionsIndex(DMLibDataType dataType) + { + switch (dataType) + { + case DMLibDataType.Stream: + return 0; + case DMLibDataType.URI: + return 1; + case DMLibDataType.Local: + return 2; + case DMLibDataType.CloudFile: + return 3; + case DMLibDataType.BlockBlob: + return 4; + case DMLibDataType.PageBlob: + return 5; + case DMLibDataType.AppendBlob: + return 6; + default: + throw new ArgumentException(string.Format("Invalid data type {0}", dataType), "dataType");; + } + } + } + + internal class DMLibTransferDirection + { + public DMLibDataType SourceType + { + get; + set; + } + + public DMLibDataType DestType + { + get; + set; + } + + public bool IsAsync + { + get; + set; + } + } +} diff --git a/test/DMLibTest/Cases/BVT.cs b/test/DMLibTest/Cases/BVT.cs new file mode 100644 index 00000000..989402f5 --- /dev/null +++ b/test/DMLibTest/Cases/BVT.cs @@ -0,0 +1,179 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using DMLibTestCodeGen; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.File; + using MS.Test.Common.MsTestLib; + + [MultiDirectionTestClass] + public class BVT : DMLibTestBase + { + #region Additional test attributes + [ClassInitialize()] + public static void MyClassInitialize(TestContext testContext) + { + DMLibTestBase.BaseClassInitialize(testContext); + BVT.UnicodeFileName = FileOp.NextString(random, random.Next(6, 10)); + Test.Info("Use file name {0} in BVT.", UnicodeFileName); + } + + [ClassCleanup()] + public static void MyClassCleanup() + { + DMLibTestBase.BaseClassCleanup(); + } + + [TestInitialize()] + public void MyTestInitialize() + { + base.BaseTestInitialize(); + } + + [TestCleanup()] + public void MyTestCleanup() + { + base.BaseTestCleanup(); + } + #endregion + + private static string UnicodeFileName; + + [TestCategory(Tag.BVT)] + [DMLibTestMethodSet(DMLibTestMethodSet.AllValidDirection)] + public void TransferDifferentSizeObject() + { + DMLibDataInfo sourceDataInfo = new DMLibDataInfo(string.Empty); + DMLibDataHelper.AddMultipleFilesNormalSize(sourceDataInfo.RootNode, BVT.UnicodeFileName); + + var options = new TestExecutionOptions(); + options.AfterDataPrepared = () => + { + if ((DMLibTestContext.SourceType == DMLibDataType.CloudFile || DMLibTestContext.SourceType == DMLibDataType.PageBlob) && + !DMLibTestContext.IsAsync) + { + string sparseFileName = "SparseFile"; + + DMLibDataHelper.AddOneFile(sourceDataInfo.RootNode, sparseFileName, 1); + FileNode sparseFileNode = sourceDataInfo.RootNode.GetFileNode(sparseFileName); + + if (DMLibTestContext.SourceType == DMLibDataType.CloudFile) + { + CloudFileDataAdaptor cloudFileDataAdaptor = SourceAdaptor as CloudFileDataAdaptor; + CloudFile sparseCloudFile = cloudFileDataAdaptor.GetCloudFileReference(sparseFileNode); + this.PrepareCloudFileWithDifferentSizeRange(sparseCloudFile); + sparseFileNode.MD5 = sparseCloudFile.Properties.ContentMD5; + sparseFileNode.Metadata = sparseCloudFile.Metadata; + } + else if (DMLibTestContext.SourceType == DMLibDataType.PageBlob) + { + CloudBlobDataAdaptor cloudBlobDataAdaptor = SourceAdaptor as CloudBlobDataAdaptor; + CloudPageBlob sparsePageBlob = cloudBlobDataAdaptor.GetCloudBlobReference(sparseFileNode) as CloudPageBlob; + this.PreparePageBlobWithDifferenSizePage(sparsePageBlob); + sparseFileNode.MD5 = sparsePageBlob.Properties.ContentMD5; + sparseFileNode.Metadata = sparsePageBlob.Metadata; + } + } + }; + + var result = this.ExecuteTestCase(sourceDataInfo, options); + + // For sync copy, recalculate md5 of destination by downloading the file to local. + if (IsCloudService(DMLibTestContext.DestType) && !DMLibTestContext.IsAsync) + { + DMLibDataHelper.SetCalculatedFileMD5(result.DataInfo, DestAdaptor); + } + + Test.Assert(result.Exceptions.Count == 0, "Verify no exception is thrown."); + Test.Assert(DMLibDataHelper.Equals(sourceDataInfo, result.DataInfo), "Verify transfer result."); + } + + private void PreparePageBlobWithDifferenSizePage(CloudPageBlob pageBlob) + { + List ranges = new List(); + List gaps = new List(); + + // Add one 4MB - 16MB page, align with 512 byte + ranges.Add(random.Next(4 * 2 * 1024, 16 * 2 * 1024) * 512); + + // Add one 512B page + ranges.Add(512); + + int remainingPageNumber = random.Next(10, 20); + + // Add ten - twenty 512B - 4MB page, align with 512 byte + for (int i = 0; i < remainingPageNumber; ++i) + { + ranges.Add(random.Next(1, 4 * 2 * 1024) * 512); + } + + // Add one 4M - 8M gap, align with 512 byte + gaps.Add(random.Next(4 * 2 * 1024, 8 * 2 * 1024) * 512); + + // Add 512B - 2048B gaps, align with 512 byte + for (int i = 1; i < ranges.Count - 1; ++i) + { + gaps.Add(random.Next(1, 5) * 512); + } + + ranges.Shuffle(); + gaps.Shuffle(); + + CloudBlobHelper.GeneratePageBlobWithRangedData(pageBlob, ranges, gaps); + } + + private void PrepareCloudFileWithDifferentSizeRange(CloudFile cloudFile) + { + List ranges = new List(); + List gaps = new List(); + + // Add one 4MB - 16MB range + ranges.Add(random.Next(4 * 1024 * 1024, 16 * 1024 * 1024)); + + // Add one 1B range + ranges.Add(1); + + int remainingPageNumber = random.Next(10, 20); + + // Add ten - twenty 1B - 4MB range + for (int i = 0; i < remainingPageNumber; ++i) + { + ranges.Add(random.Next(1, 4 * 1024 * 1024)); + } + + // Add one 4M - 8M gap + gaps.Add(random.Next(4 * 1024 * 1024, 8 * 1024 * 1024)); + + // Add 512B - 2048B gaps + for (int i = 1; i < ranges.Count - 1; ++i) + { + gaps.Add(random.Next(1, 512 * 4)); + } + + if (DMLibTestContext.DestType == DMLibDataType.PageBlob) + { + int totalSize = ranges.Sum() + gaps.Sum(); + int remainder = totalSize % 512; + + if (remainder != 0) + { + ranges[ranges.Count - 1] += 512 - remainder; + } + } + + ranges.Shuffle(); + gaps.Shuffle(); + + CloudFileHelper.GenerateCloudFileWithRangedData(cloudFile, ranges, gaps); + } + } +} diff --git a/test/DMLibTest/Cases/BigFileTest.cs b/test/DMLibTest/Cases/BigFileTest.cs new file mode 100644 index 00000000..8e39fc9b --- /dev/null +++ b/test/DMLibTest/Cases/BigFileTest.cs @@ -0,0 +1,57 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using DMLibTestCodeGen; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using MS.Test.Common.MsTestLib; + + [MultiDirectionTestClass] + public class BigFileTest : DMLibTestBase + { + #region Additional test attributes + [ClassInitialize()] + public static void MyClassInitialize(TestContext testContext) + { + DMLibTestBase.BaseClassInitialize(testContext); + } + + [ClassCleanup()] + public static void MyClassCleanup() + { + DMLibTestBase.BaseClassCleanup(); + } + + [TestInitialize()] + public void MyTestInitialize() + { + base.BaseTestInitialize(); + } + + [TestCleanup()] + public void MyTestCleanup() + { + base.BaseTestCleanup(); + } + #endregion + + [TestCategory(Tag.Function)] + [DMLibTestMethodSet(DMLibTestMethodSet.AllValidDirection)] + public void TransferBigSizeObject() + { + DMLibDataInfo sourceDataInfo = new DMLibDataInfo(string.Empty); + DMLibDataHelper.AddMultipleFilesBigSize(sourceDataInfo.RootNode, DMLibTestBase.FileName); + + var result = this.ExecuteTestCase(sourceDataInfo, new TestExecutionOptions()); + + Test.Assert(result.Exceptions.Count == 0, "Verify no exception is thrown."); + Test.Assert(DMLibDataHelper.Equals(sourceDataInfo, result.DataInfo), "Verify transfer result."); + } + } +} diff --git a/test/DMLibTest/Cases/CheckContentMD5Test.cs b/test/DMLibTest/Cases/CheckContentMD5Test.cs new file mode 100644 index 00000000..f3c08352 --- /dev/null +++ b/test/DMLibTest/Cases/CheckContentMD5Test.cs @@ -0,0 +1,93 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest.Cases +{ + using System; +using DMLibTestCodeGen; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.WindowsAzure.Storage.DataMovement; +using MS.Test.Common.MsTestLib; + + [MultiDirectionTestClass] + public class CheckContentMD5Test : DMLibTestBase + { + #region Additional test attributes + [ClassInitialize()] + public static void MyClassInitialize(TestContext testContext) + { + DMLibTestBase.BaseClassInitialize(testContext); + } + + [ClassCleanup()] + public static void MyClassCleanup() + { + DMLibTestBase.BaseClassCleanup(); + } + + [TestInitialize()] + public void MyTestInitialize() + { + base.BaseTestInitialize(); + } + + [TestCleanup()] + public void MyTestCleanup() + { + base.BaseTestCleanup(); + } + #endregion + + [TestCategory(Tag.Function)] + [DMLibTestMethodSet(DMLibTestMethodSet.LocalDest)] + public void TestCheckContentMD5() + { + long fileSize = 10 * 1024 * 1024; + string wrongMD5 = "wrongMD5"; + + string checkWrongMD5File = "checkWrongMD5File"; + string notCheckWrongMD5File = "notCheckWrongMD5File"; + string checkCorrectMD5File = "checkCorrectMD5File"; + string notCheckCorrectMD5File = "notCheckCorrectMD5File"; + + DMLibDataInfo sourceDataInfo = new DMLibDataInfo(string.Empty); + DMLibDataHelper.AddOneFileInBytes(sourceDataInfo.RootNode, checkWrongMD5File, fileSize); + FileNode tmpFileNode = sourceDataInfo.RootNode.GetFileNode(checkWrongMD5File); + tmpFileNode.MD5 = wrongMD5; + + DMLibDataHelper.AddOneFileInBytes(sourceDataInfo.RootNode, notCheckWrongMD5File, fileSize); + tmpFileNode = sourceDataInfo.RootNode.GetFileNode(notCheckWrongMD5File); + tmpFileNode.MD5 = wrongMD5; + + DMLibDataHelper.AddOneFileInBytes(sourceDataInfo.RootNode, checkCorrectMD5File, fileSize); + DMLibDataHelper.AddOneFileInBytes(sourceDataInfo.RootNode, notCheckCorrectMD5File, fileSize); + + var options = new TestExecutionOptions(); + options.TransferItemModifier = (fileNode, transferItem) => + { + string fileName = fileNode.Name; + DownloadOptions downloadOptions = new DownloadOptions(); + if (fileName.Equals(checkWrongMD5File) || fileName.Equals(checkCorrectMD5File)) + { + downloadOptions.DisableContentMD5Validation = false; + } + else if (fileName.Equals(notCheckWrongMD5File) || fileName.Equals(notCheckCorrectMD5File)) + { + downloadOptions.DisableContentMD5Validation = true; + } + + transferItem.Options = downloadOptions; + }; + + var result = this.ExecuteTestCase(sourceDataInfo, options); + + Test.Assert(result.Exceptions.Count == 1, "Verify there's one exception."); + Exception exception = result.Exceptions[0]; + + Test.Assert(exception is InvalidOperationException, "Verify it's an invalid operation exception."); + VerificationHelper.VerifyExceptionErrorMessage(exception, "The MD5 hash calculated from the downloaded data does not match the MD5 hash stored", checkWrongMD5File); + } + } +} diff --git a/test/DMLibTest/Cases/MetadataTest.cs b/test/DMLibTest/Cases/MetadataTest.cs new file mode 100644 index 00000000..247e4bcb --- /dev/null +++ b/test/DMLibTest/Cases/MetadataTest.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace DMLibTest +{ + using System.Collections.Generic; + using DMLibTestCodeGen; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using MS.Test.Common.MsTestLib; + + [MultiDirectionTestClass] + public class MetadataTest : DMLibTestBase + { + #region Additional test attributes + [ClassInitialize()] + public static void MyClassInitialize(TestContext testContext) + { + DMLibTestBase.BaseClassInitialize(testContext); + } + + [ClassCleanup()] + public static void MyClassCleanup() + { + DMLibTestBase.BaseClassCleanup(); + } + + [TestInitialize()] + public void MyTestInitialize() + { + base.BaseTestInitialize(); + } + + [TestCleanup()] + public void MyTestCleanup() + { + base.BaseTestCleanup(); + } + #endregion + + [TestCategory(Tag.Function)] + [DMLibTestMethodSet(DMLibTestMethodSet.Cloud2Cloud)] + public void TestMetadata() + { + Dictionary metadata = new Dictionary(); + metadata.Add(FileOp.NextCIdentifierString(random), FileOp.NextNormalString(random)); + metadata.Add(FileOp.NextCIdentifierString(random), FileOp.NextNormalString(random)); + + Test.Info("Metadata is ====================="); + foreach (var keyValue in metadata) + { + Test.Info("name:{0} value:{1}", keyValue.Key, keyValue.Value); + } + + DMLibDataInfo sourceDataInfo = new DMLibDataInfo(string.Empty); + FileNode fileNode = new FileNode(DMLibTestBase.FileName) + { + SizeInByte = DMLibTestBase.FileSizeInKB * 1024L, + Metadata = metadata + }; + sourceDataInfo.RootNode.AddFileNode(fileNode); + + var result = this.ExecuteTestCase(sourceDataInfo, new TestExecutionOptions()); + + Test.Assert(result.Exceptions.Count == 0, "Verify no exception is thrown."); + Test.Assert(DMLibDataHelper.Equals(sourceDataInfo, result.DataInfo), "Verify transfer result."); + } + } +} diff --git a/test/DMLibTest/Cases/OverwriteTest.cs b/test/DMLibTest/Cases/OverwriteTest.cs new file mode 100644 index 00000000..a81d2e68 --- /dev/null +++ b/test/DMLibTest/Cases/OverwriteTest.cs @@ -0,0 +1,115 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest.Cases +{ + using DMLibTestCodeGen; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Microsoft.WindowsAzure.Storage.DataMovement; + using MS.Test.Common.MsTestLib; + + [MultiDirectionTestClass] + public class OverwriteTest : DMLibTestBase + { + #region Additional test attributes + [ClassInitialize()] + public static void MyClassInitialize(TestContext testContext) + { + DMLibTestBase.BaseClassInitialize(testContext); + } + + [ClassCleanup()] + public static void MyClassCleanup() + { + DMLibTestBase.BaseClassCleanup(); + } + + [TestInitialize()] + public void MyTestInitialize() + { + base.BaseTestInitialize(); + } + + [TestCleanup()] + public void MyTestCleanup() + { + base.BaseTestCleanup(); + } + #endregion + + [TestCategory(Tag.Function)] + [DMLibTestMethodSet(DMLibTestMethodSet.AllValidDirection)] + public void OverwriteDestination() + { + string destExistYName = "destExistY"; + string destExistNName = "destExistN"; + string destNotExistYName = "destNotExistY"; + + DMLibDataInfo sourceDataInfo = new DMLibDataInfo(string.Empty); + DMLibDataHelper.AddOneFileInBytes(sourceDataInfo.RootNode, destExistYName, 1024); + DMLibDataHelper.AddOneFileInBytes(sourceDataInfo.RootNode, destExistNName, 1024); + DMLibDataHelper.AddOneFileInBytes(sourceDataInfo.RootNode, destNotExistYName, 1024); + + DMLibDataInfo destDataInfo = new DMLibDataInfo(string.Empty); + DMLibDataHelper.AddOneFileInBytes(destDataInfo.RootNode, destExistYName, 1024); + DMLibDataHelper.AddOneFileInBytes(destDataInfo.RootNode, destExistNName, 1024); + + var options = new TestExecutionOptions(); + if (DMLibTestContext.DestType != DMLibDataType.Stream) + { + options.DestTransferDataInfo = destDataInfo; + } + + options.TransferItemModifier = (fileNode, transferItem) => + { + string fileName = fileNode.Name; + TransferContext transferContext = new TransferContext(); + + if (fileName.Equals(destExistYName)) + { + transferContext.OverwriteCallback = DMLibInputHelper.GetDefaultOverwiteCallbackY(); + } + else if (fileName.Equals(destExistNName)) + { + transferContext.OverwriteCallback = DMLibInputHelper.GetDefaultOverwiteCallbackN(); + } + else if (fileName.Equals(destNotExistYName)) + { + transferContext.OverwriteCallback = DMLibInputHelper.GetDefaultOverwiteCallbackY(); + } + + transferItem.TransferContext = transferContext; + }; + + var result = this.ExecuteTestCase(sourceDataInfo, options); + + DMLibDataInfo expectedDataInfo = new DMLibDataInfo(string.Empty); + if (DMLibTestContext.DestType != DMLibDataType.Stream) + { + expectedDataInfo.RootNode.AddFileNode(sourceDataInfo.RootNode.GetFileNode(destExistYName)); + expectedDataInfo.RootNode.AddFileNode(destDataInfo.RootNode.GetFileNode(destExistNName)); + expectedDataInfo.RootNode.AddFileNode(sourceDataInfo.RootNode.GetFileNode(destNotExistYName)); + } + else + { + expectedDataInfo = sourceDataInfo; + } + + // Verify transfer result + Test.Assert(DMLibDataHelper.Equals(expectedDataInfo, result.DataInfo), "Verify transfer result."); + + // Verify exception + if (DMLibTestContext.DestType != DMLibDataType.Stream) + { + Test.Assert(result.Exceptions.Count == 1, "Verify there's only one exceptions."); + TransferException transferException = result.Exceptions[0] as TransferException; + Test.Assert(transferException != null, "Verify the exception is a TransferException"); + + VerificationHelper.VerifyTransferException(transferException, TransferErrorCode.NotOverwriteExistingDestination, + "Skiped file", destExistNName); + } + } + } +} diff --git a/test/DMLibTest/Cases/ProgressHandlerTest.cs b/test/DMLibTest/Cases/ProgressHandlerTest.cs new file mode 100644 index 00000000..90da1272 --- /dev/null +++ b/test/DMLibTest/Cases/ProgressHandlerTest.cs @@ -0,0 +1,66 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest.Cases +{ + using DMLibTestCodeGen; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Microsoft.WindowsAzure.Storage.DataMovement; + using MS.Test.Common.MsTestLib; + + [MultiDirectionTestClass] + public class ProgressHandlerTest : DMLibTestBase + { + #region Additional test attributes + [ClassInitialize()] + public static void MyClassInitialize(TestContext testContext) + { + DMLibTestBase.BaseClassInitialize(testContext); + } + + [ClassCleanup()] + public static void MyClassCleanup() + { + DMLibTestBase.BaseClassCleanup(); + } + + [TestInitialize()] + public void MyTestInitialize() + { + base.BaseTestInitialize(); + } + + [TestCleanup()] + public void MyTestCleanup() + { + base.BaseTestCleanup(); + } + #endregion + + [TestCategory(Tag.Function)] + [DMLibTestMethodSet(DMLibTestMethodSet.AllValidDirection)] + public void TestProgressHandlerTest() + { + long fileSize = 10 * 1024 * 1024; + + DMLibDataInfo sourceDataInfo = new DMLibDataInfo(string.Empty); + DMLibDataHelper.AddOneFileInBytes(sourceDataInfo.RootNode, DMLibTestBase.FileName, fileSize); + + var options = new TestExecutionOptions(); + options.TransferItemModifier = (fileNode, transferItem) => + { + TransferContext transferContext = new TransferContext(); + ProgressChecker progressChecker = new ProgressChecker(1, fileNode.SizeInByte); + transferContext.ProgressHandler = progressChecker.GetProgressHandler(); + transferItem.TransferContext = transferContext; + }; + + var result = this.ExecuteTestCase(sourceDataInfo, options); + + Test.Assert(result.Exceptions.Count == 0, "Verify no exception is thrown."); + Test.Assert(DMLibDataHelper.Equals(sourceDataInfo, result.DataInfo), "Verify transfer result."); + } + } +} diff --git a/test/DMLibTest/Cases/ResumeTest.cs b/test/DMLibTest/Cases/ResumeTest.cs new file mode 100644 index 00000000..bad569f3 --- /dev/null +++ b/test/DMLibTest/Cases/ResumeTest.cs @@ -0,0 +1,146 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace DMLibTest +{ + using System; + using System.Collections.Generic; + using System.Threading; + using DMLibTestCodeGen; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Microsoft.WindowsAzure.Storage; + using Microsoft.WindowsAzure.Storage.DataMovement; + using MS.Test.Common.MsTestLib; + + [MultiDirectionTestClass] + public class ResumeTest : DMLibTestBase + { + #region Additional test attributes + [ClassInitialize()] + public static void MyClassInitialize(TestContext testContext) + { + DMLibTestBase.BaseClassInitialize(testContext); + } + + [ClassCleanup()] + public static void MyClassCleanup() + { + DMLibTestBase.BaseClassCleanup(); + } + + [TestInitialize()] + public void MyTestInitialize() + { + base.BaseTestInitialize(); + } + + [TestCleanup()] + public void MyTestCleanup() + { + base.BaseTestCleanup(); + } + #endregion + + [TestCategory(Tag.Function)] + [DMLibTestMethodSet(DMLibTestMethodSet.AllSync)] + public void TestResume() + { + int fileSizeInKB = 100 * 1024; + DMLibDataInfo sourceDataInfo = new DMLibDataInfo(string.Empty); + DMLibDataHelper.AddOneFile(sourceDataInfo.RootNode, DMLibTestBase.FileName, fileSizeInKB); + + CancellationTokenSource tokenSource = new CancellationTokenSource(); + + TransferItem transferItem = null; + var options = new TestExecutionOptions(); + options.LimitSpeed = true; + var transferContext = new TransferContext(); + var progressChecker = new ProgressChecker(1, fileSizeInKB * 1024); + transferContext.ProgressHandler = progressChecker.GetProgressHandler(); + options.TransferItemModifier = (fileName, item) => + { + item.CancellationToken = tokenSource.Token; + item.TransferContext = transferContext; + transferItem = item; + }; + + TransferCheckpoint firstCheckpoint = null, secondCheckpoint = null; + options.AfterAllItemAdded = () => + { + // Wait until there are data transferred + progressChecker.DataTransferred.WaitOne(); + + // Store the first checkpoint + firstCheckpoint = transferContext.LastCheckpoint; + Thread.Sleep(1000); + + // Cancel the transfer and store the second checkpoint + tokenSource.Cancel(); + secondCheckpoint = transferContext.LastCheckpoint; + }; + + // Cancel and store checkpoint for resume + var result = this.ExecuteTestCase(sourceDataInfo, options); + + Test.Assert(result.Exceptions.Count == 1, "Verify job is cancelled"); + Exception exception = result.Exceptions[0]; + VerificationHelper.VerifyExceptionErrorMessage(exception, "A task was canceled."); + + TransferCheckpoint firstResumeCheckpoint = null, secondResumeCheckpoint = null; + + // DMLib doesn't support to resume transfer from a checkpoint which is inconsistent with + // the actual transfer progress when the destination is an append blob. + if (Helper.RandomBoolean() && DMLibTestContext.DestType != DMLibDataType.AppendBlob) + { + Test.Info("Resume with the first checkpoint first."); + firstResumeCheckpoint = firstCheckpoint; + secondResumeCheckpoint = secondCheckpoint; + } + else + { + Test.Info("Resume with the second checkpoint first."); + firstResumeCheckpoint = secondCheckpoint; + secondResumeCheckpoint = firstCheckpoint; + } + + // resume with firstResumeCheckpoint + TransferItem resumeItem = transferItem.Clone(); + progressChecker.Reset(); + TransferContext resumeContext = new TransferContext(firstResumeCheckpoint) + { + ProgressHandler = progressChecker.GetProgressHandler() + }; + resumeItem.TransferContext = resumeContext; + + result = this.RunTransferItems(new List() { resumeItem }, new TestExecutionOptions()); + + VerificationHelper.VerifySingleObjectResumeResult(result, sourceDataInfo); + + // resume with secondResumeCheckpoint + resumeItem = transferItem.Clone(); + progressChecker.Reset(); + resumeContext = new TransferContext(secondResumeCheckpoint) + { + ProgressHandler = progressChecker.GetProgressHandler() + }; + resumeItem.TransferContext = resumeContext; + + result = this.RunTransferItems(new List() { resumeItem }, new TestExecutionOptions()); + + if (DMLibTestContext.DestType != DMLibDataType.AppendBlob || DMLibTestContext.SourceType == DMLibDataType.Stream) + { + VerificationHelper.VerifySingleObjectResumeResult(result, sourceDataInfo); + } + else + { + Test.Assert(result.Exceptions.Count == 1, "Verify reumse fails when checkpoint is inconsistent with the actual progress when destination is append blob."); + exception = result.Exceptions[0]; + Test.Assert(exception is InvalidOperationException, "Verify reumse fails when checkpoint is inconsistent with the actual progress when destination is append blob."); + VerificationHelper.VerifyExceptionErrorMessage(exception, "Destination might be changed by other process or application."); + } + } + } +} diff --git a/test/DMLibTest/Cases/SetContentTypeTest.cs b/test/DMLibTest/Cases/SetContentTypeTest.cs new file mode 100644 index 00000000..4817ea72 --- /dev/null +++ b/test/DMLibTest/Cases/SetContentTypeTest.cs @@ -0,0 +1,68 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest.Cases +{ + using DMLibTestCodeGen; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Microsoft.WindowsAzure.Storage.DataMovement; + using MS.Test.Common.MsTestLib; + + [MultiDirectionTestClass] + public class SetContentTypeTest : DMLibTestBase + { + #region Additional test attributes + [ClassInitialize()] + public static void MyClassInitialize(TestContext testContext) + { + DMLibTestBase.BaseClassInitialize(testContext); + } + + [ClassCleanup()] + public static void MyClassCleanup() + { + DMLibTestBase.BaseClassCleanup(); + } + + [TestInitialize()] + public void MyTestInitialize() + { + base.BaseTestInitialize(); + } + + [TestCleanup()] + public void MyTestCleanup() + { + base.BaseTestCleanup(); + } + #endregion + + [TestCategory(Tag.Function)] + [DMLibTestMethodSet(DMLibTestMethodSet.LocalSource)] + public void TestSetContentType() + { + string contentType = "contenttype"; + DMLibDataInfo sourceDataInfo = new DMLibDataInfo(string.Empty); + DMLibDataHelper.AddOneFile(sourceDataInfo.RootNode, DMLibTestBase.FileName, 1024); + + var options = new TestExecutionOptions(); + options.TransferItemModifier = (fileNode, transferItem) => + { + UploadOptions uploadOptions = new UploadOptions(); + uploadOptions.ContentType = "contenttype"; + + transferItem.Options = uploadOptions; + }; + + var result = this.ExecuteTestCase(sourceDataInfo, options); + + Test.Assert(result.Exceptions.Count == 0, "Verify no exception is thrown."); + Test.Assert(DMLibDataHelper.Equals(sourceDataInfo, result.DataInfo), "Verify transfer result."); + + FileNode destFileNode = result.DataInfo.RootNode.GetFileNode(DMLibTestBase.FileName); + Test.Assert(contentType.Equals(destFileNode.ContentType), "Verify content type: {0}, expected {1}", destFileNode.ContentType, contentType); + } + } +} diff --git a/test/DMLibTest/Cases/UnsupportedDirectionTest.cs b/test/DMLibTest/Cases/UnsupportedDirectionTest.cs new file mode 100644 index 00000000..7d82d231 --- /dev/null +++ b/test/DMLibTest/Cases/UnsupportedDirectionTest.cs @@ -0,0 +1,91 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest.Cases +{ + using System; + using DMLibTestCodeGen; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using MS.Test.Common.MsTestLib; + + [MultiDirectionTestClass] + public class UnsupportedDirectionTest : DMLibTestBase + { + #region Additional test attributes + [ClassInitialize()] + public static void MyClassInitialize(TestContext testContext) + { + DMLibTestBase.BaseClassInitialize(testContext); + } + + [ClassCleanup()] + public static void MyClassCleanup() + { + DMLibTestBase.BaseClassCleanup(); + } + + [TestInitialize()] + public void MyTestInitialize() + { + base.BaseTestInitialize(); + } + + [TestCleanup()] + public void MyTestCleanup() + { + base.BaseTestCleanup(); + } + #endregion + + [TestCategory(Tag.Function)] + [DMLibTestMethod(DMLibDataType.BlockBlob, DMLibDataType.CloudBlob & ~DMLibDataType.BlockBlob)] + [DMLibTestMethod(DMLibDataType.AppendBlob, DMLibDataType.CloudBlob & ~DMLibDataType.AppendBlob)] + [DMLibTestMethod(DMLibDataType.PageBlob, DMLibDataType.CloudBlob & ~DMLibDataType.PageBlob)] + [DMLibTestMethod(DMLibDataType.BlockBlob, DMLibDataType.CloudBlob & ~DMLibDataType.BlockBlob, isAsync: true)] + [DMLibTestMethod(DMLibDataType.AppendBlob, DMLibDataType.CloudBlob & ~DMLibDataType.AppendBlob, isAsync: true)] + [DMLibTestMethod(DMLibDataType.PageBlob, DMLibDataType.CloudBlob & ~DMLibDataType.PageBlob, isAsync: true)] + [DMLibTestMethod(DMLibDataType.CloudFile, DMLibDataType.PageBlob, isAsync: true)] + [DMLibTestMethod(DMLibDataType.CloudFile, DMLibDataType.AppendBlob, isAsync: true)] + [DMLibTestMethod(DMLibDataType.URI, DMLibDataType.CloudFile)] + [DMLibTestMethod(DMLibDataType.URI, DMLibDataType.CloudBlob)] + public void TestUnsupportedDirection() + { + DMLibDataInfo sourceDataInfo = new DMLibDataInfo(string.Empty); + DMLibDataHelper.AddOneFileInBytes(sourceDataInfo.RootNode, DMLibTestBase.FileName, 1024); + + var result = this.ExecuteTestCase(sourceDataInfo, new TestExecutionOptions()); + + Test.Assert(result.Exceptions.Count == 1, "Verify no exception is thrown."); + + Exception exception = result.Exceptions[0]; + + if (DMLibTestContext.SourceType == DMLibDataType.URI) + { + Test.Assert(exception is NotSupportedException, "Verify exception is NotSupportedException."); + if (DMLibTestContext.DestType == DMLibDataType.CloudFile) + { + VerificationHelper.VerifyExceptionErrorMessage(exception, "Copying from uri to Azure File Storage synchronously is not supported"); + } + else + { + VerificationHelper.VerifyExceptionErrorMessage(exception, "Copying from uri to Azure Blob Storage synchronously is not supported"); + } + } + else if (DMLibTestBase.IsCloudBlob(DMLibTestContext.SourceType) && DMLibTestBase.IsCloudBlob(DMLibTestContext.DestType)) + { + Test.Assert(exception is InvalidOperationException, "Verify exception is InvalidOperationException."); + VerificationHelper.VerifyExceptionErrorMessage(exception, "Blob type of source and destination must be the same."); + } + else + { + Test.Assert(exception is InvalidOperationException, "Verify exception is InvalidOperationException."); + VerificationHelper.VerifyExceptionErrorMessage(exception, + string.Format("Copying from File Storage to {0} Blob Storage asynchronously is not supported.", MapBlobDataTypeToBlobType(DMLibTestContext.DestType))); + } + + Test.Assert(DMLibDataHelper.Equals(new DMLibDataInfo(string.Empty), result.DataInfo), "Verify no file is transfered."); + } + } +} diff --git a/test/DMLibTest/DMLibTest.csproj b/test/DMLibTest/DMLibTest.csproj new file mode 100644 index 00000000..8c671b24 --- /dev/null +++ b/test/DMLibTest/DMLibTest.csproj @@ -0,0 +1,209 @@ + + + + Debug + AnyCPU + {2A4656A4-F744-4653-A9D6-15112E9AB352} + Library + Properties + DMLibTest + DMLibTest + v4.5 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + ..\..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + false + + + true + + + ..\DMLibTestCodeGen\ + $(CodeGenPath)DMLibTestCodeGen.csproj + Generated + + + ..\..\tools\strongnamekeys\fake\windows.snk + + + + + False + ..\..\packages\Microsoft.Data.Edm.5.6.4\lib\net40\Microsoft.Data.Edm.dll + + + False + ..\..\packages\Microsoft.Data.OData.5.6.4\lib\net40\Microsoft.Data.OData.dll + + + False + ..\..\packages\Microsoft.Data.Services.Client.5.6.4\lib\net40\Microsoft.Data.Services.Client.dll + + + False + ..\..\packages\Microsoft.WindowsAzure.ConfigurationManager.1.8.0.0\lib\net35-full\Microsoft.WindowsAzure.Configuration.dll + + + False + ..\..\packages\WindowsAzure.Storage.5.0.0\lib\net40\Microsoft.WindowsAzure.Storage.dll + + + False + ..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll + + + + + False + ..\..\packages\System.Spatial.5.6.4\lib\net40\System.Spatial.dll + + + + + + + + + + + + + + + + + + + + + SharedAssemblyInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {b821e031-09cc-48f0-bdc6-2793228d4027} + DataMovement + + + {7018ee4e-d389-424e-a8dd-f9b4ffda5194} + DMLibTestCodeGen + + + {ac39b50f-dc27-4411-9ed4-a4a137190acb} + MsTestLib + + + + + PreserveNewest + + + + + + + + + + False + + + False + + + False + + + False + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/DMLibTest/Framework/AssemblyInitCleanup.cs b/test/DMLibTest/Framework/AssemblyInitCleanup.cs new file mode 100644 index 00000000..aac1f483 --- /dev/null +++ b/test/DMLibTest/Framework/AssemblyInitCleanup.cs @@ -0,0 +1,31 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using MS.Test.Common.MsTestLib; + + [TestClass] + public class AssemblyInitCleanup + { + [AssemblyInitialize] + public static void TestInit(TestContext testContext) + { + // init loggers and load test config data + String config = testContext.Properties["config"] as string; + Test.Init(config); + // set the assertfail delegate to report failure in VS + Test.AssertFail = new AssertFailDelegate(Assert.Fail); + } + + [AssemblyCleanup] + public static void TestCleanup() + { + Test.Close(); + } + } +} diff --git a/test/DMLibTest/Framework/BlobDataAdaptorBase.cs b/test/DMLibTest/Framework/BlobDataAdaptorBase.cs new file mode 100644 index 00000000..ff9955e9 --- /dev/null +++ b/test/DMLibTest/Framework/BlobDataAdaptorBase.cs @@ -0,0 +1,174 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using System.Text; + using Microsoft.WindowsAzure.Storage.Blob; + using BlobTypeConst = DMLibTest.BlobType; + + public abstract class BlobDataAdaptorBase : DataAdaptor where TDataInfo : IDataInfo + { + private string delimiter; + private readonly string defaultContainerName; + private string containerName; + + public override string StorageKey + { + get + { + return this.BlobHelper.Account.Credentials.ExportBase64EncodedKey(); + } + } + + public string ContainerName + { + get + { + return this.containerName; + } + + set + { + this.containerName = value; + } + } + + public CloudBlobHelper BlobHelper + { + get; + private set; + } + + protected TestAccount TestAccount + { + get; + private set; + } + + protected string TempFolder + { + get; + private set; + } + + protected virtual string BlobType + { + get; + private set; + } + + public BlobDataAdaptorBase(TestAccount testAccount, string containerName, string blobType, SourceOrDest sourceOrDest, string delimiter = "/") + { + if (BlobTypeConst.Block != blobType && BlobTypeConst.Page != blobType && BlobTypeConst.Append != blobType) + { + throw new ArgumentException("blobType"); + } + + this.TestAccount = testAccount; + this.BlobHelper = new CloudBlobHelper(testAccount.Account); + this.delimiter = delimiter; + this.containerName = containerName; + this.defaultContainerName = containerName; + this.TempFolder = Guid.NewGuid().ToString(); + this.BlobType = blobType; + this.SourceOrDest = sourceOrDest; + } + + public override string GetAddress(params string[] list) + { + return this.GetAddress(this.TestAccount.GetEndpointBaseUri(EndpointType.Blob), list); + } + + public override string GetSecondaryAddress(params string[] list) + { + return this.GetAddress(this.TestAccount.GetEndpointBaseUri(EndpointType.Blob, true), list); + } + + public override void CreateIfNotExists() + { + this.BlobHelper.CreateContainer(this.containerName); + } + + public override bool Exists() + { + return this.BlobHelper.Exists(this.containerName); + } + + private string GetAddress(string baseUri, params string[] list) + { + StringBuilder builder = new StringBuilder(); + builder.Append(baseUri + "/" + this.containerName + "/"); + + foreach (string token in list) + { + if (!string.IsNullOrEmpty(token)) + { + builder.Append(token); + builder.Append(this.delimiter); + } + } + + return builder.ToString(); + } + + public override void WaitForGEO() + { + CloudBlobContainer container = this.BlobHelper.GetGRSContainer(this.containerName); + Helper.WaitForTakingEffect(container.ServiceClient); + } + + public override void Cleanup() + { + this.BlobHelper.CleanupContainer(this.containerName); + } + + public override void DeleteLocation() + { + this.BlobHelper.DeleteContainer(this.containerName); + } + + public override void Reset() + { + if (this.Exists()) + { + this.BlobHelper.SetContainerAccessType(this.containerName, BlobContainerPublicAccessType.Off); + } + + this.containerName = this.defaultContainerName; + } + + public override string GenerateSAS(SharedAccessPermissions sap, int validatePeriod, string policySignedIdentifier = null) + { + if (null == policySignedIdentifier) + { + if (this.SourceOrDest == SourceOrDest.Dest) + { + this.BlobHelper.CreateContainer(this.containerName); + } + + return this.BlobHelper.GetSASofContainer(this.containerName, sap.ToBlobPermissions(), validatePeriod, false); + } + else + { + this.BlobHelper.CreateContainer(this.containerName); + return this.BlobHelper.GetSASofContainer(this.containerName, sap.ToBlobPermissions(), validatePeriod, true, policySignedIdentifier); + } + } + + public override void RevokeSAS() + { + this.BlobHelper.ClearSASPolicyofContainer(this.containerName); + } + + public override void MakePublic() + { + this.BlobHelper.SetContainerAccessType(this.containerName, BlobContainerPublicAccessType.Container); + + DMLibTestHelper.WaitForACLTakeEffect(); + } + } +} diff --git a/test/DMLibTest/Framework/CloudBlobDataAdaptor.cs b/test/DMLibTest/Framework/CloudBlobDataAdaptor.cs new file mode 100644 index 00000000..ba760bd3 --- /dev/null +++ b/test/DMLibTest/Framework/CloudBlobDataAdaptor.cs @@ -0,0 +1,243 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using System.Collections.Generic; + using System.IO; + using Microsoft.WindowsAzure.Storage; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.RetryPolicies; + using StorageBlob = Microsoft.WindowsAzure.Storage.Blob; + + internal class CloudBlobDataAdaptor : BlobDataAdaptorBase + { + public CloudBlobDataAdaptor(TestAccount testAccount, string containerName, string blobType, SourceOrDest sourceOrDest, string delimiter = "/") + : base(testAccount, containerName, blobType, sourceOrDest, delimiter) + { + } + + public override object GetTransferObject(FileNode fileNode) + { + return this.GetCloudBlobReference(fileNode); + } + + public override object GetTransferObject(DirNode dirNode) + { + return this.GetCloudBlobDirReference(dirNode); + } + + protected override void GenerateDataImp(DMLibDataInfo dataInfo) + { + this.BlobHelper.CreateContainer(this.ContainerName); + + using (TemporaryTestFolder localTemp = new TemporaryTestFolder(this.TempFolder)) + { + CloudBlobDirectory rootCloudBlobDir = this.BlobHelper.GetDirReference(this.ContainerName, dataInfo.RootPath); + this.GenerateDir(dataInfo.RootNode, rootCloudBlobDir); + } + } + + public override DMLibDataInfo GetTransferDataInfo(string rootDir) + { + CloudBlobDirectory blobDir = this.BlobHelper.QueryBlobDirectory(this.ContainerName, rootDir); + if (blobDir == null) + { + return null; + } + + DMLibDataInfo dataInfo = new DMLibDataInfo(rootDir); + + this.BuildDirNode(blobDir, dataInfo.RootNode); + return dataInfo; + } + + public string LeaseBlob(FileNode fileNode, TimeSpan? leaseTime) + { + var blob = this.GetCloudBlobReference(fileNode); + return blob.AcquireLease(leaseTime, null, options: HelperConst.DefaultBlobOptions); + } + + public void ReleaseLease(FileNode fileNode, string leaseId) + { + var blob = this.GetCloudBlobReference(fileNode); + blob.ReleaseLease(AccessCondition.GenerateLeaseCondition(leaseId), options: HelperConst.DefaultBlobOptions); + } + + public CloudBlob GetCloudBlobReference(FileNode fileNode) + { + var container = this.BlobHelper.BlobClient.GetContainerReference(this.ContainerName); + var blobName = fileNode.GetURLRelativePath(); + if (blobName.StartsWith("/")) + { + blobName = blobName.Substring(1, blobName.Length - 1); + } + + return CloudBlobHelper.GetCloudBlobReference(container, blobName, this.BlobType); + } + + public CloudBlobDirectory GetCloudBlobDirReference(DirNode dirNode) + { + var container = this.BlobHelper.BlobClient.GetContainerReference(this.ContainerName); + var dirName = dirNode.GetURLRelativePath(); + if (dirName.StartsWith("/")) + { + dirName = dirName.Substring(1, dirName.Length - 1); + } + + return container.GetDirectoryReference(dirName); + } + + private void GenerateDir(DirNode dirNode, CloudBlobDirectory cloudBlobDir) + { + DMLibDataHelper.CreateLocalDirIfNotExists(this.TempFolder); + + foreach (var subDir in dirNode.DirNodes) + { + CloudBlobDirectory subCloudBlobDir = cloudBlobDir.GetDirectoryReference(subDir.Name); + this.GenerateDir(subDir, subCloudBlobDir); + } + + List snapshotList = new List(); + + foreach (var file in dirNode.FileNodes) + { + CloudBlob cloudBlob = CloudBlobHelper.GetCloudBlobReference(cloudBlobDir, file.Name, this.BlobType); + this.GenerateFile(file, cloudBlob, snapshotList); + } + + foreach (var snapshot in snapshotList) + { + dirNode.AddFileNode(snapshot); + } + } + + private void CheckFileNode(FileNode fileNode) + { + if (fileNode.LastModifiedTime != null) + { + throw new InvalidOperationException("Can't set LastModifiedTime to cloud blob"); + } + + if (fileNode.FileAttr != null) + { + throw new InvalidOperationException("Can't set file attribute to cloud blob"); + } + } + + private void GenerateFile(FileNode fileNode, CloudBlob cloudBlob, List snapshotList) + { + this.CheckFileNode(fileNode); + + if ((StorageBlob.BlobType.PageBlob == cloudBlob.BlobType) && (fileNode.SizeInByte % 512 != 0)) + { + throw new InvalidOperationException(string.Format("Can only generate page blob which size is multiple of 512bytes. Expected size is {0}", fileNode.SizeInByte)); + } + + string tempFileName = Guid.NewGuid().ToString(); + string localFilePath = Path.Combine(this.TempFolder, tempFileName); + DMLibDataHelper.CreateLocalFile(fileNode, localFilePath); + + BlobRequestOptions storeMD5Options = new BlobRequestOptions() + { + RetryPolicy = new LinearRetry(TimeSpan.FromSeconds(90), 3), + StoreBlobContentMD5 = true, + }; + + cloudBlob.UploadFromFile(localFilePath, FileMode.Open, options: storeMD5Options); + + if (null != fileNode.MD5 || + null != fileNode.ContentType || + null != fileNode.CacheControl || + null != fileNode.ContentDisposition || + null != fileNode.ContentEncoding || + null != fileNode.ContentLanguage) + { + cloudBlob.Properties.ContentMD5 = fileNode.MD5; + cloudBlob.Properties.ContentType = fileNode.ContentType; + cloudBlob.Properties.CacheControl = fileNode.CacheControl; + cloudBlob.Properties.ContentDisposition = fileNode.ContentDisposition; + cloudBlob.Properties.ContentEncoding = fileNode.ContentEncoding; + cloudBlob.Properties.ContentLanguage = fileNode.ContentLanguage; + cloudBlob.SetProperties(options: HelperConst.DefaultBlobOptions); + } + + if (null != fileNode.Metadata && fileNode.Metadata.Count > 0) + { + cloudBlob.Metadata.Clear(); + foreach (var metaData in fileNode.Metadata) + { + cloudBlob.Metadata.Add(metaData); + } + + cloudBlob.SetMetadata(options: HelperConst.DefaultBlobOptions); + } + + cloudBlob.FetchAttributes(options: HelperConst.DefaultBlobOptions); + this.BuildFileNode(cloudBlob, fileNode); + + for (int i = 0; i < fileNode.SnapshotsCount; ++i) + { + CloudBlob snapshot = cloudBlob.Snapshot(); + snapshotList.Add(this.BuildSnapshotFileNode(snapshot, fileNode.Name)); + } + } + + private void BuildDirNode(CloudBlobDirectory cloudDir, DirNode dirNode) + { + foreach (IListBlobItem item in cloudDir.ListBlobs(false, BlobListingDetails.Metadata, HelperConst.DefaultBlobOptions)) + { + CloudBlob cloudBlob = item as CloudBlob; + CloudBlobDirectory subCloudDir = item as CloudBlobDirectory; + + if (cloudBlob != null) + { + if (CloudBlobHelper.MapStorageBlobTypeToBlobType(cloudBlob.BlobType) == this.BlobType) + { + FileNode fileNode = new FileNode(cloudBlob.GetShortName()); + this.BuildFileNode(cloudBlob, fileNode); + dirNode.AddFileNode(fileNode); + } + } + else if (subCloudDir != null) + { + var subDirName = subCloudDir.GetShortName(); + DirNode subDirNode = dirNode.GetDirNode(subDirName); + + // A blob directory could be listed more than once if it's across table servers. + if (subDirNode == null) + { + subDirNode = new DirNode(subDirName); + this.BuildDirNode(subCloudDir, subDirNode); + dirNode.AddDirNode(subDirNode); + } + } + } + } + + private FileNode BuildSnapshotFileNode(CloudBlob cloudBlob, string fileName) + { + FileNode fileNode = new FileNode(DMLibTestHelper.AppendSnapShotTimeToFileName(fileName, cloudBlob.SnapshotTime)); + this.BuildFileNode(cloudBlob, fileNode); + return fileNode; + } + + private void BuildFileNode(CloudBlob cloudBlob, FileNode fileNode) + { + fileNode.SizeInByte = cloudBlob.Properties.Length; + fileNode.MD5 = cloudBlob.Properties.ContentMD5; + fileNode.ContentType = cloudBlob.Properties.ContentType; + fileNode.CacheControl = cloudBlob.Properties.CacheControl; + fileNode.ContentDisposition = cloudBlob.Properties.ContentDisposition; + fileNode.ContentEncoding = cloudBlob.Properties.ContentEncoding; + fileNode.ContentLanguage = cloudBlob.Properties.ContentLanguage; + fileNode.Metadata = cloudBlob.Metadata; + + DateTimeOffset dateTimeOffset = (DateTimeOffset)cloudBlob.Properties.LastModified; + fileNode.LastModifiedTime = dateTimeOffset.UtcDateTime; + } + } +} diff --git a/test/DMLibTest/Framework/CloudFileDataAdaptor.cs b/test/DMLibTest/Framework/CloudFileDataAdaptor.cs new file mode 100644 index 00000000..b8cf82f2 --- /dev/null +++ b/test/DMLibTest/Framework/CloudFileDataAdaptor.cs @@ -0,0 +1,352 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using System.IO; + using System.Text; + using System.Text.RegularExpressions; + using Microsoft.WindowsAzure.Storage.File; + using Microsoft.WindowsAzure.Storage.RetryPolicies; + using MS.Test.Common.MsTestLib; + + internal class CloudFileDataAdaptor : DataAdaptor + { + private TestAccount testAccount; + private CloudFileHelper fileHelper; + private string tempFolder; + private readonly string defaultShareName; + private string shareName; + + public override string StorageKey + { + get + { + return this.fileHelper.Account.Credentials.ExportBase64EncodedKey(); + } + } + + public CloudFileDataAdaptor(TestAccount testAccount, string shareName, SourceOrDest sourceOrDest) + { + this.testAccount = testAccount; + this.fileHelper = new CloudFileHelper(testAccount.Account); + this.shareName = shareName; + this.defaultShareName = shareName; + this.tempFolder = Guid.NewGuid().ToString(); + this.SourceOrDest = sourceOrDest; + } + + public string ShareName + { + get + { + return this.shareName; + } + + set + { + this.shareName = value; + } + } + + public override object GetTransferObject(FileNode fileNode) + { + return this.GetCloudFileReference(fileNode); + } + + public override object GetTransferObject(DirNode dirNode) + { + return this.GetCloudFileDirReference(dirNode); + } + + public override string GetAddress(params string[] list) + { + StringBuilder builder = new StringBuilder(); + builder.Append(this.testAccount.GetEndpointBaseUri(EndpointType.File) + "/" + this.shareName + "/"); + + foreach (string token in list) + { + if (!string.IsNullOrEmpty(token)) + { + builder.Append(token); + builder.Append("/"); + } + } + + return builder.ToString(); + } + + public override string GetSecondaryAddress(params string[] list) + { + throw new NotSupportedException("GetSecondaryAddress is not supported in CloudFileDataAdaptor."); + } + + public override void CreateIfNotExists() + { + this.fileHelper.CreateShare(this.shareName); + } + + public override bool Exists() + { + return this.fileHelper.Exists(this.shareName); + } + + public override void WaitForGEO() + { + throw new NotSupportedException("WaitForGEO is not supported in CloudFileDataAdaptor."); + } + + public string MountFileShare() + { + this.fileHelper.CreateShare(this.shareName); + CloudFileShare share = this.fileHelper.FileClient.GetShareReference(this.shareName); + + string cmd = "net"; + string args = string.Format( + "use * {0} {1} /USER:{2}", + string.Format(@"\\{0}\{1}", share.Uri.Host, this.shareName), + this.fileHelper.Account.Credentials.ExportBase64EncodedKey(), + this.fileHelper.Account.Credentials.AccountName); + + string stdout, stderr; + int ret = TestHelper.RunCmd(cmd, args, out stdout, out stderr); + Test.Assert(0 == ret, "mounted to xsmb share successfully"); + Test.Info("stdout={0}, stderr={1}", stdout, stderr); + + Regex r = new Regex(@"Drive (\S+) is now connected to"); + Match m = r.Match(stdout); + if (m.Success) + { + return m.Groups[1].Value; + } + else + { + return null; + } + } + + public void UnmountFileShare(string deviceName) + { + string cmd = "net"; + string args = string.Format("use {0} /DELETE", deviceName); + string stdout, stderr; + int ret = TestHelper.RunCmd(cmd, args, out stdout, out stderr); + Test.Assert(0 == ret, "unmounted {0} successfully", deviceName); + Test.Info("stdout={0}, stderr={1}", stdout, stderr); + } + + public CloudFile GetCloudFileReference(FileNode fileNode) + { + var share = this.fileHelper.FileClient.GetShareReference(this.shareName); + string fileName = fileNode.GetURLRelativePath(); + if (fileName.StartsWith("/")) + { + fileName = fileName.Substring(1, fileName.Length - 1); + } + + return share.GetRootDirectoryReference().GetFileReference(fileName); + } + + public CloudFileDirectory GetCloudFileDirReference(DirNode dirNode) + { + var share = this.fileHelper.FileClient.GetShareReference(this.shareName); + string dirName = dirNode.GetURLRelativePath(); + if (dirName.StartsWith("/")) + { + dirName = dirName.Substring(1, dirName.Length - 1); + } + + return share.GetRootDirectoryReference().GetDirectoryReference(dirName); + } + + protected override void GenerateDataImp(DMLibDataInfo dataInfo) + { + fileHelper.CreateShare(this.shareName); + + using (TemporaryTestFolder localTemp = new TemporaryTestFolder(this.tempFolder)) + { + CloudFileDirectory rootCloudFileDir = this.fileHelper.GetDirReference(this.shareName, dataInfo.RootPath); + this.GenerateDir(dataInfo.RootNode, rootCloudFileDir, this.tempFolder); + } + } + + private void GenerateDir(DirNode dirNode, CloudFileDirectory cloudFileDir, string parentPath) + { + string dirPath = Path.Combine(parentPath, dirNode.Name); + DMLibDataHelper.CreateLocalDirIfNotExists(dirPath); + cloudFileDir.CreateIfNotExists(HelperConst.DefaultFileOptions); + + foreach (var subDir in dirNode.DirNodes) + { + CloudFileDirectory subCloudFileDir = cloudFileDir.GetDirectoryReference(subDir.Name); + this.GenerateDir(subDir, subCloudFileDir, dirPath); + } + + foreach (var file in dirNode.FileNodes) + { + CloudFile cloudFile = cloudFileDir.GetFileReference(file.Name); + this.GenerateFile(file, cloudFile, dirPath); + } + } + + private void CheckFileNode(FileNode fileNode) + { + if (fileNode.LastModifiedTime != null) + { + throw new InvalidOperationException("Can't set LastModifiedTime to cloud file"); + } + + if (fileNode.FileAttr != null) + { + throw new InvalidOperationException("Can't set file attribute to cloud file"); + } + } + + private void GenerateFile(FileNode fileNode, CloudFile cloudFile, string parentPath) + { + this.CheckFileNode(fileNode); + + string tempFileName = Guid.NewGuid().ToString(); + string localFilePath = Path.Combine(parentPath, tempFileName); + DMLibDataHelper.CreateLocalFile(fileNode, localFilePath); + + FileRequestOptions storeMD5Options = new FileRequestOptions() + { + RetryPolicy = new LinearRetry(TimeSpan.FromSeconds(90), 3), + StoreFileContentMD5 = true, + }; + + cloudFile.UploadFromFile(localFilePath, FileMode.Open, options: storeMD5Options); + + if (null != fileNode.MD5 || + null != fileNode.ContentType || + null != fileNode.CacheControl || + null != fileNode.ContentDisposition || + null != fileNode.ContentEncoding || + null != fileNode.ContentLanguage) + { + // set user defined MD5 to cloud file + cloudFile.Properties.ContentMD5 = fileNode.MD5; + cloudFile.Properties.ContentType = fileNode.ContentType; + cloudFile.Properties.CacheControl = fileNode.CacheControl; + cloudFile.Properties.ContentDisposition = fileNode.ContentDisposition; + cloudFile.Properties.ContentEncoding = fileNode.ContentEncoding; + cloudFile.Properties.ContentLanguage = fileNode.ContentLanguage; + cloudFile.SetProperties(options: HelperConst.DefaultFileOptions); + } + + if (null != fileNode.Metadata && fileNode.Metadata.Count > 0) + { + cloudFile.Metadata.Clear(); + foreach (var metaData in fileNode.Metadata) + { + cloudFile.Metadata.Add(metaData); + } + + cloudFile.SetMetadata(options: HelperConst.DefaultFileOptions); + } + + this.BuildFileNode(cloudFile, fileNode); + } + + public override DMLibDataInfo GetTransferDataInfo(string rootDir) + { + CloudFileDirectory fileDir = fileHelper.QueryFileDirectory(this.shareName, rootDir); + if (fileDir == null) + { + return null; + } + + DMLibDataInfo dataInfo = new DMLibDataInfo(rootDir); + + this.BuildDirNode(fileDir, dataInfo.RootNode); + return dataInfo; + } + + public override void Cleanup() + { + this.fileHelper.CleanupShare(this.shareName); + } + + public override void DeleteLocation() + { + this.fileHelper.DeleteShare(this.shareName); + } + + public override void MakePublic() + { + throw new NotSupportedException("MakePublic is not supported in CloudFileDataAdaptor."); + } + + public override void Reset() + { + this.shareName = defaultShareName; + } + + public override string GenerateSAS(SharedAccessPermissions sap, int validatePeriod, string policySignedIdentifier = null) + { + if (null == policySignedIdentifier) + { + if (this.SourceOrDest == SourceOrDest.Dest) + { + this.fileHelper.CreateShare(this.shareName); + } + + return this.fileHelper.GetSASofShare(this.shareName, sap.ToFilePermissions(), validatePeriod, false); + } + else + { + this.fileHelper.CreateShare(this.shareName); + return this.fileHelper.GetSASofShare(this.shareName, sap.ToFilePermissions(), validatePeriod, true, policySignedIdentifier); + } + } + + public override void RevokeSAS() + { + this.fileHelper.ClearSASPolicyofShare(this.shareName); + } + + private void BuildDirNode(CloudFileDirectory cloudDir, DirNode dirNode) + { + foreach (IListFileItem item in cloudDir.ListFilesAndDirectories(HelperConst.DefaultFileOptions)) + { + CloudFile cloudFile = item as CloudFile; + CloudFileDirectory subCloudDir = item as CloudFileDirectory; + + if (cloudFile != null) + { + // Cannot fetch attributes while listing, so do it for each cloud file. + cloudFile.FetchAttributes(options: HelperConst.DefaultFileOptions); + + FileNode fileNode = new FileNode(cloudFile.Name); + this.BuildFileNode(cloudFile, fileNode); + dirNode.AddFileNode(fileNode); + } + else if (subCloudDir != null) + { + DirNode subDirNode = new DirNode(subCloudDir.Name); + this.BuildDirNode(subCloudDir, subDirNode); + dirNode.AddDirNode(subDirNode); + } + } + } + + private void BuildFileNode(CloudFile cloudFile, FileNode fileNode) + { + fileNode.SizeInByte = cloudFile.Properties.Length; + fileNode.MD5 = cloudFile.Properties.ContentMD5; + fileNode.ContentType = cloudFile.Properties.ContentType; + fileNode.CacheControl = cloudFile.Properties.CacheControl; + fileNode.ContentDisposition = cloudFile.Properties.ContentDisposition; + fileNode.ContentEncoding = cloudFile.Properties.ContentEncoding; + fileNode.ContentLanguage = cloudFile.Properties.ContentLanguage; + fileNode.Metadata = cloudFile.Metadata; + + DateTimeOffset dateTimeOffset = (DateTimeOffset)cloudFile.Properties.LastModified; + fileNode.LastModifiedTime = dateTimeOffset.UtcDateTime; + } + } +} diff --git a/test/DMLibTest/Framework/CloudObjectExtensions.cs b/test/DMLibTest/Framework/CloudObjectExtensions.cs new file mode 100644 index 00000000..372f6da6 --- /dev/null +++ b/test/DMLibTest/Framework/CloudObjectExtensions.cs @@ -0,0 +1,79 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.File; + using Microsoft.WindowsAzure.Storage.RetryPolicies; + using Microsoft.WindowsAzure.Storage.Table; + + internal static class CloudObjectExtensions + { + public static string GetShortName(this CloudBlob cloudBlob) + { + CloudBlobDirectory parentDir = cloudBlob.Parent; + + if (null == parentDir) + { + // Root directory + return cloudBlob.Name; + } + + return GetShortNameFromUri(cloudBlob.Uri.ToString(), parentDir.Uri.ToString()); + } + + public static string GetShortName(this CloudBlobDirectory cloudBlobDirectory) + { + CloudBlobDirectory parentDir = cloudBlobDirectory.Parent; + + if (null == parentDir) + { + // Root directory + return String.Empty; + } + + return GetShortNameFromUri(cloudBlobDirectory.Uri.ToString(), parentDir.Uri.ToString()); + } + + private static string GetShortNameFromUri(string uri, string parentUri) + { + string delimiter = "/"; + + if (!parentUri.EndsWith(delimiter, StringComparison.Ordinal)) + { + parentUri += delimiter; + } + + string shortName = uri.Substring(parentUri.Length); + + if (shortName.EndsWith(delimiter, StringComparison.Ordinal)) + { + shortName = shortName.Substring(0, shortName.Length - delimiter.Length); + } + + return Uri.UnescapeDataString(shortName); + } + } + + internal static class HelperConst + { + public static BlobRequestOptions DefaultBlobOptions = new BlobRequestOptions + { + RetryPolicy = new LinearRetry(TimeSpan.FromSeconds(90), 3), + }; + + public static FileRequestOptions DefaultFileOptions = new FileRequestOptions + { + RetryPolicy = new LinearRetry(TimeSpan.FromSeconds(90), 3), + }; + + public static TableRequestOptions DefaultTableOptions = new TableRequestOptions + { + RetryPolicy = new LinearRetry(TimeSpan.FromSeconds(90), 3), + }; + } +} diff --git a/test/DMLibTest/Framework/CopyWrapper.cs b/test/DMLibTest/Framework/CopyWrapper.cs new file mode 100644 index 00000000..fda81eee --- /dev/null +++ b/test/DMLibTest/Framework/CopyWrapper.cs @@ -0,0 +1,43 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System.Threading; + using System.Threading.Tasks; + using Microsoft.WindowsAzure.Storage.DataMovement; + + internal class CopyWrapper : DMLibWrapper + { + public CopyWrapper() + { + } + + protected override Task DoTransferImp(TransferItem item) + { + return this.Copy(item.SourceObject, item.DestObject, item); + } + + private Task Copy(dynamic sourceObject, dynamic destObject, TransferItem item) + { + CopyOptions copyOptions = item.Options as CopyOptions; + TransferContext transferContext = item.TransferContext; + CancellationToken cancellationToken = item.CancellationToken; + + if (cancellationToken != null && cancellationToken != CancellationToken.None) + { + return TransferManager.CopyAsync(sourceObject, destObject, item.IsServiceCopy, copyOptions, transferContext, cancellationToken); + } + else if (transferContext != null || copyOptions != null) + { + return TransferManager.CopyAsync(sourceObject, destObject, item.IsServiceCopy, copyOptions, transferContext); + } + else + { + return TransferManager.CopyAsync(sourceObject, destObject, item.IsServiceCopy); + } + } + } +} diff --git a/test/DMLibTest/Framework/DMLibDataHelper.cs b/test/DMLibTest/Framework/DMLibDataHelper.cs new file mode 100644 index 00000000..c94693d5 --- /dev/null +++ b/test/DMLibTest/Framework/DMLibDataHelper.cs @@ -0,0 +1,472 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Text; + using DMLibTestCodeGen; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.File; + using MS.Test.Common.MsTestLib; + + internal static class DMLibDataHelper + { + public static void AddOneFile(DirNode dirNode, string fileName, long fileSizeInKB, FileAttributes? fa = null, DateTime? lmt = null) + { + AddOneFileInBytes(dirNode, fileName, 1024L * fileSizeInKB, fa, lmt); + } + + public static void AddOneFileInBytes(DirNode dirNode, string fileName, long fileSizeInB, FileAttributes? fa = null, DateTime? lmt = null) + { + FileNode fileNode = new FileNode(fileName) + { + SizeInByte = fileSizeInB, + FileAttr = fa, + LastModifiedTime = lmt, + }; + + dirNode.AddFileNode(fileNode); + } + + public static FileNode RemoveOneFile(DirNode dirNode, string fileName) + { + return dirNode.DeleteFileNode(fileName); + } + + public static DirNode RemoveOneDir(DirNode parentNode, string dirNodeToDelete) + { + return parentNode.DeleteDirNode(dirNodeToDelete); + } + + public static void AddMultipleFiles(DirNode dirNode, string filePrefix, int fileNumber, int fileSizeInKB, FileAttributes? fa = null, DateTime? lmt = null) + { + DMLibDataHelper.AddTree(dirNode, string.Empty, filePrefix, fileNumber, 0, fileSizeInKB, fa, lmt); + } + + public static void AddMultipleFilesNormalSize(DirNode dirNode, string filePrefix) + { + int[] fileSizes = new int[] { 0, 1, 4000, 4 * 1024, 10000 }; + AddMultipleFilesDifferentSize(dirNode, filePrefix, fileSizes); + } + + public static void AddMultipleFilesBigSize(DirNode dirNode, string filePrefix) + { + int[] fileSizes = new int[] { 32000, 64 * 1024 }; + AddMultipleFilesDifferentSize(dirNode, filePrefix, fileSizes); + } + + public static void AddMultipleFilesDifferentSize(DirNode dirNode, string filePrefix, int[] fileSizes) + { + for (int i = 0; i < fileSizes.Length; ++i) + { + FileNode fileNode = new FileNode(filePrefix + "_" + i) + { + SizeInByte = fileSizes[i] * 1024 + }; + + dirNode.AddFileNode(fileNode); + } + } + + public static void AddMultipleFilesTotalSize(DirNode dirNode, string filePrefix, int fileNumber, int totalSizeInKB, DateTime? lmt = null) + { + int fileSizeInKB = totalSizeInKB / fileNumber; + fileSizeInKB = fileSizeInKB == 0 ? 1 : fileSizeInKB; + DMLibDataHelper.AddMultipleFiles(dirNode, filePrefix, fileNumber, fileSizeInKB, lmt: lmt); + } + + public static void AddTree(DirNode dirNode, string dirPrefix, string filePrefix, int width, int depth, int fileSizeInKB, FileAttributes? fa = null, DateTime? lmt = null) + { + for (int i = 0; i < width; ++i) + { + string fileName = i == 0 ? filePrefix : filePrefix + "_" + i; + FileNode fileNode = new FileNode(fileName) + { + SizeInByte = 1024L * fileSizeInKB, + FileAttr = fa, + LastModifiedTime = lmt, + }; + + dirNode.AddFileNode(fileNode); + } + + if (depth > 0) + { + for (int i = 0; i < width; ++i) + { + string dirName = i == 0 ? dirPrefix : dirPrefix + "_" + i; + DirNode subDirNode = dirNode.GetDirNode(dirName); + if (subDirNode == null) + { + subDirNode = new DirNode(dirName); + dirNode.AddDirNode(subDirNode); + } + + DMLibDataHelper.AddTree(subDirNode, dirPrefix, filePrefix, width, depth - 1, fileSizeInKB, fa, lmt: lmt); + } + } + } + + public static void AddTreeTotalSize(DirNode dirNode, string dirPrefix, string filePrefix, int width, int depth, int totalSizeInKB, DateTime? lmt = null) + { + int fileNumber; + if (width <= 1) + { + fileNumber = (depth + 1) * width; + } + else + { + int widthPowDepth = width; + for (int i = 0; i < depth; ++i) + { + widthPowDepth *= width; + } + + fileNumber = width * (widthPowDepth - 1) / (width - 1); + } + + int fileSizeInKB = totalSizeInKB / fileNumber; + fileSizeInKB = fileSizeInKB == 0 ? 1 : fileSizeInKB; + + DMLibDataHelper.AddTree(dirNode, dirPrefix, filePrefix, width, depth, fileSizeInKB, lmt: lmt); + } + + public static void CreateLocalDirIfNotExists(string dirPath) + { + if (!String.Equals(string.Empty, dirPath) && !Directory.Exists(dirPath)) + { + Directory.CreateDirectory(dirPath); + } + } + + public static void CreateLocalFile(FileNode fileNode, string filePath) + { + Helper.GenerateFileInBytes(filePath, fileNode.SizeInByte); + fileNode.AbsolutePath = filePath; + + if (fileNode.LastModifiedTime != null) + { + // Set last modified time + FileInfo fileInfo = new FileInfo(filePath); + fileInfo.LastWriteTimeUtc = (DateTime)fileNode.LastModifiedTime; + } + + if (fileNode.FileAttr != null) + { + // remove default file attribute + FileOp.RemoveFileAttribute(filePath, FileAttributes.Archive); + + // Set file Attributes + FileOp.SetFileAttribute(filePath, (FileAttributes)fileNode.FileAttr); + Test.Info("{0} attr is {1}", filePath, File.GetAttributes(filePath).ToString()); + } + } + + public static FileNode GetFileNode(DirNode dirNode, params string[] tokens) + { + DirNode currentDirNode = dirNode; + + for (int i = 0; i < tokens.Length; ++i) + { + if (i == tokens.Length - 1) + { + FileNode fileNode = currentDirNode.GetFileNode(tokens[i]); + if (fileNode == null) + { + Test.Error("FileNode {0} doesn't exist.", tokens[i]); + return null; + } + + return fileNode; + } + else + { + currentDirNode = currentDirNode.GetDirNode(tokens[i]); + if (currentDirNode == null) + { + Test.Error("DirNode {0} doesn't exist.", tokens[i]); + return null; + } + } + } + + return null; + } + + public static void RemoveAllFileNodesExcept(DirNode rootNode, HashSet except) + { + List nodesToRemove = new List(); + foreach (FileNode fileNode in rootNode.EnumerateFileNodesRecursively()) + { + if (!except.Contains(fileNode)) + { + nodesToRemove.Add(fileNode); + } + } + + foreach(FileNode nodeToRemove in nodesToRemove) + { + nodeToRemove.Parent.DeleteFileNode(nodeToRemove.Name); + } + } + + public static string DetailedInfo(this DMLibDataInfo dataInfo) + { + StringBuilder builder = new StringBuilder(); + builder.AppendLine(string.Format("TransferDataInfo root: {0}", dataInfo.RootPath)); + + foreach (FileNode fileNode in dataInfo.EnumerateFileNodes()) + { + builder.AppendLine(fileNode.DetailedInfo()); + } + + return builder.ToString(); + } + + public static string DetailedInfo(this FileNode fileNode) + { + StringBuilder builder = new StringBuilder(); + builder.AppendFormat("FileNode {0}: MD5 ({1}), LMT ({2})", fileNode.GetURLRelativePath(), fileNode.MD5, fileNode.LastModifiedTime); + + return builder.ToString(); + } + + public static bool Equals(DMLibDataInfo infoA, DMLibDataInfo infoB) + { + bool result; + + bool aIsEmpty = infoA == null || infoA.RootNode.IsEmpty; + bool bIsEmpty = infoB == null || infoB.RootNode.IsEmpty; + + if (aIsEmpty && bIsEmpty) + { + result = true; + } + else if(aIsEmpty || bIsEmpty) + { + result = false; + } + else + { + result = Equals(infoA.RootNode, infoB.RootNode); + } + + if (!result) + { + Test.Info("-----Data Info A-----"); + MultiDirectionTestHelper.PrintTransferDataInfo(infoA); + + Test.Info("-----Data Info B-----"); + MultiDirectionTestHelper.PrintTransferDataInfo(infoB); + } + + return result; + } + + public static bool Equals(DirNode dirNodeA, DirNode dirNodeB) + { + // The same node + if (dirNodeA == dirNodeB) + { + return true; + } + + // Empty node equals to null + if ((dirNodeA == null || dirNodeA.IsEmpty) && + (dirNodeB == null || dirNodeB.IsEmpty)) + { + return true; + } + + // Compare two nodes + if (null != dirNodeA && null != dirNodeB) + { + if (dirNodeA.FileNodeCount != dirNodeB.FileNodeCount || + dirNodeA.NonEmptyDirNodeCount != dirNodeB.NonEmptyDirNodeCount) + { + return false; + } + + foreach(FileNode fileNodeA in dirNodeA.FileNodes) + { + FileNode fileNodeB = dirNodeB.GetFileNode(fileNodeA.Name); + + if (!DMLibDataHelper.Equals(fileNodeA, fileNodeB)) + { + return false; + } + } + + foreach(DirNode subDirNodeA in dirNodeA.DirNodes) + { + DirNode subDirNodeB = dirNodeB.GetDirNode(subDirNodeA.Name); + if (!DMLibDataHelper.Equals(subDirNodeA, subDirNodeB)) + { + return false; + } + } + + return true; + } + + return false; + } + + public static bool Equals(FileNode fileNodeA, FileNode fileNodeB) + { + if (fileNodeA == fileNodeB) + { + return true; + } + + if (null != fileNodeA && null != fileNodeB) + { + Test.Info(string.Format("Verify file: ({0},{1}); ({2},{3})", fileNodeA.Name, fileNodeA.MD5, fileNodeB.Name, fileNodeB.MD5)); + + if (!string.Equals(fileNodeA.Name, fileNodeB.Name, StringComparison.Ordinal) || + !PropertiesStringEquals(fileNodeA.MD5, fileNodeB.MD5) || + !PropertiesStringEquals(fileNodeA.CacheControl, fileNodeB.CacheControl) || + !PropertiesStringEquals(fileNodeA.ContentDisposition, fileNodeB.ContentDisposition) || + !PropertiesStringEquals(fileNodeA.ContentEncoding, fileNodeB.ContentEncoding) || + !PropertiesStringEquals(fileNodeA.ContentLanguage, fileNodeB.ContentLanguage)) + { + return false; + } + + if (!MetadataEquals(fileNodeA.Metadata, fileNodeB.Metadata)) + { + return false; + } + + foreach (var keyValuePair in fileNodeA.Metadata) + { + if (!fileNodeB.Metadata.Contains(keyValuePair)) + { + return false; + } + } + + return true; + } + + string name; + if (fileNodeA != null) + { + name = fileNodeA.Name; + } + else + { + name = fileNodeB.Name; + } + + Test.Info("Fail to verify file: {0}", name); + return false; + } + + private static bool MetadataEquals(IDictionary metadataA, IDictionary metadataB) + { + if (metadataA == metadataB) + { + return true; + } + + if (metadataA == null || metadataB == null) + { + return false; + } + + if (metadataA.Count != metadataB.Count) + { + return false; + } + + foreach (var keyValuePair in metadataB) + { + if (!metadataB.Contains(keyValuePair)) + { + return false; + } + } + + return true; + } + + private static bool PropertiesStringEquals(string valueA, string ValueB) + { + if (string.IsNullOrEmpty(valueA)) + { + if (string.IsNullOrEmpty(ValueB)) + { + return true; + } + + return false; + } + + return string.Equals(valueA, ValueB, StringComparison.Ordinal); + } + + public static string GetLocalRelativePath(this DataInfoNode node) + { + return Path.Combine(node.PathComponents.ToArray()); + } + + public static string GetURLRelativePath(this DataInfoNode node) + { + return String.Join("/", node.PathComponents); + } + + public static string GetSourceRelativePath(this DataInfoNode node) + { + if (DMLibTestContext.SourceType == DMLibDataType.Local) + { + return node.GetLocalRelativePath(); + } + else + { + return node.GetURLRelativePath(); + } + } + + public static string GetDestRelativePath(this DataInfoNode node) + { + if (DMLibTestContext.DestType == DMLibDataType.Local) + { + return node.GetLocalRelativePath(); + } + else + { + return node.GetURLRelativePath(); + } + } + + public static void SetCalculatedFileMD5(DMLibDataInfo dataInfo, DataAdaptor destAdaptor, bool disableMD5Check = false) + { + foreach (FileNode fileNode in dataInfo.EnumerateFileNodes()) + { + if (DMLibTestBase.IsCloudBlob(DMLibTestContext.DestType)) + { + CloudBlobDataAdaptor cloudBlobDataAdaptor = destAdaptor as CloudBlobDataAdaptor; + CloudBlob cloudBlob = cloudBlobDataAdaptor.GetCloudBlobReference(fileNode); + + fileNode.MD5 = CloudBlobHelper.CalculateMD5ByDownloading(cloudBlob, disableMD5Check); + } + else if (DMLibTestContext.DestType == DMLibDataType.CloudFile) + { + CloudFileDataAdaptor cloudFileDataAdaptor = destAdaptor as CloudFileDataAdaptor; + CloudFile cloudFile = cloudFileDataAdaptor.GetCloudFileReference(fileNode); + + fileNode.MD5 = CloudFileHelper.CalculateMD5ByDownloading(cloudFile, disableMD5Check); + } + + // No need to set md5 for local destination + } + } + } +} diff --git a/test/DMLibTest/Framework/DMLibDataInfo.cs b/test/DMLibTest/Framework/DMLibDataInfo.cs new file mode 100644 index 00000000..7b339c6a --- /dev/null +++ b/test/DMLibTest/Framework/DMLibDataInfo.cs @@ -0,0 +1,441 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Text; + + public class DMLibDataInfo : IDataInfo + { + public DMLibDataInfo(string rootPath) + { + this.RootPath = rootPath; + this.RootNode = new DirNode(string.Empty); + } + + public int FileCount + { + get + { + return this.RootNode.FileNodeCountRecursive; + } + } + + public string RootPath + { + get; + set; + } + + public DirNode RootNode + { + get; + set; + } + + public IEnumerable EnumerateFileNodes() + { + return this.RootNode.EnumerateFileNodesRecursively(); + } + + IDataInfo IDataInfo.Clone() + { + return this.Clone(); + } + + public DMLibDataInfo Clone() + { + return new DMLibDataInfo(this.RootPath) + { + RootNode = this.RootNode.Clone(), + }; + } + + public override string ToString() + { + return this.DetailedInfo(); + } + } + + public class DataInfoNode + { + public string Name + { + get; + set; + } + + public DirNode Parent + { + get; + set; + } + + public IEnumerable PathComponents + { + get + { + if (this.Parent != null) + { + foreach (string component in Parent.PathComponents) + { + yield return component; + } + + yield return this.Name; + } + } + } + } + + public class FileNode : DataInfoNode, IComparable + { + public FileNode(string name) + { + this.Name = name; + } + + public int SnapshotsCount + { + get; + set; + } + + public string MD5 + { + get; + set; + } + + public string CacheControl + { + get; + set; + } + + public string ContentDisposition + { + get; + set; + } + + public string ContentEncoding + { + get; + set; + } + + public string ContentLanguage + { + get; + set; + } + + public IDictionary Metadata + { + get; + set; + } + + public string ContentType + { + get; + set; + } + + public DateTime? LastModifiedTime + { + get; + set; + } + + public long SizeInByte + { + get; + set; + } + + public FileAttributes? FileAttr + { + get; + set; + } + + public string AbsolutePath + { + get; + set; + } + + public int CompareTo(FileNode other) + { + return string.Compare(this.Name, other.Name, StringComparison.OrdinalIgnoreCase); + } + + public FileNode Clone(string name = null) + { + // Clone metadata + Dictionary cloneMetaData = null; + if (this.Metadata != null) + { + cloneMetaData = new Dictionary(this.Metadata); + } + + return new FileNode(name ?? this.Name) + { + SnapshotsCount = this.SnapshotsCount, + CacheControl = this.CacheControl, + ContentDisposition = this.ContentDisposition, + ContentEncoding = this.ContentEncoding, + ContentLanguage = this.ContentLanguage, + ContentType = this.ContentType, + MD5 = this.MD5, + Metadata = cloneMetaData, + LastModifiedTime = this.LastModifiedTime, + SizeInByte = this.SizeInByte, + FileAttr = this.FileAttr, + AbsolutePath = this.AbsolutePath, + }; + } + } + + public class DirNode : DataInfoNode, IComparable + { + private Dictionary dirNodeMap; + private Dictionary fileNodeMap; + + public DirNode(string name) + { + this.Name = name; + this.dirNodeMap = new Dictionary(); + this.fileNodeMap = new Dictionary(); + } + + public int FileNodeCountRecursive + { + get + { + int totalCount = this.FileNodeCount; + foreach (DirNode subDirNode in this.DirNodes) + { + totalCount += subDirNode.FileNodeCountRecursive; + } + + return totalCount; + } + } + + public int FileNodeCount + { + get + { + return fileNodeMap.Count; + } + } + + public int DirNodeCount + { + get + { + return dirNodeMap.Count; + } + } + + public int NonEmptyDirNodeCount + { + get + { + int count = 0; + foreach(DirNode subDirNode in dirNodeMap.Values) + { + if (!subDirNode.IsEmpty) + { + count++; + } + } + + return count; + } + } + + public bool IsEmpty + { + get + { + if (this.FileNodeCount != 0) + { + return false; + } + + foreach(DirNode subDirNode in dirNodeMap.Values) + { + if (!subDirNode.IsEmpty) + { + return false; + } + } + + return true; + } + } + + public IEnumerable DirNodes + { + get + { + return dirNodeMap.Values; + } + } + + public IEnumerable FileNodes + { + get + { + return fileNodeMap.Values; + } + } + + public int CompareTo(DirNode other) + { + return string.Compare(this.Name, other.Name, StringComparison.OrdinalIgnoreCase); + } + + public FileNode GetFileNode(string name) + { + FileNode result = null; + if (this.fileNodeMap.TryGetValue(name, out result)) + { + return result; + } + + return null; + } + + public DirNode GetDirNode(string name) + { + DirNode result = null; + if (this.dirNodeMap.TryGetValue(name, out result)) + { + return result; + } + + return null; + } + + public void AddDirNode(DirNode dirNode) + { + dirNode.Parent = this; + this.dirNodeMap.Add(dirNode.Name, dirNode); + } + + public void AddFileNode(FileNode fileNode) + { + fileNode.Parent = this; + this.fileNodeMap.Add(fileNode.Name, fileNode); + } + + public FileNode DeleteFileNode(string name) + { + FileNode fn = null; + if (this.fileNodeMap.ContainsKey(name)) + { + fn = this.fileNodeMap[name]; + fn.Parent = null; + this.fileNodeMap.Remove(name); + } + + return fn; + } + + public DirNode DeleteDirNode(string name) + { + DirNode dn = null; + if (this.dirNodeMap.ContainsKey(name)) + { + dn = this.dirNodeMap[name]; + this.dirNodeMap.Remove(name); + } + + return dn; + } + + public DirNode Clone() + { + DirNode newDirNode = new DirNode(this.Name); + + foreach(FileNode fileNode in this.FileNodes) + { + newDirNode.AddFileNode(fileNode.Clone()); + } + + foreach(DirNode dirNode in this.DirNodes) + { + newDirNode.AddDirNode(dirNode.Clone()); + } + + return newDirNode; + } + + public IEnumerable EnumerateFileNodesRecursively() + { + foreach (var fileNode in this.FileNodes) + { + yield return fileNode; + } + + foreach (DirNode subDirNode in this.DirNodes) + { + foreach (var fileNode in subDirNode.EnumerateFileNodesRecursively()) + { + yield return fileNode; + } + } + } + + public IEnumerable EnumerateDirNodesRecursively() + { + foreach (DirNode subDirNode in this.DirNodes) + { + foreach (var dirNode in subDirNode.EnumerateDirNodesRecursively()) + { + yield return dirNode; + } + + yield return subDirNode; + } + } + + /// + /// for debug use, show DataInfo in tree format + /// + public void Display(int level) + { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < level; ++i) + sb.Append("--"); + sb.Append(this.Name); + Console.WriteLine(sb.ToString()); + + foreach (FileNode fn in fileNodeMap.Values) + { + StringBuilder fileNode = new StringBuilder(); + for (int i = 0; i < level + 1; ++i) + { + fileNode.Append("--"); + } + fileNode.Append(fn.Name); + Console.WriteLine(fileNode.ToString()); + } + + foreach (DirNode dn in dirNodeMap.Values) + { + dn.Display(level + 1); + } + } + } +} diff --git a/test/DMLibTest/Framework/DMLibInputHelper.cs b/test/DMLibTest/Framework/DMLibInputHelper.cs new file mode 100644 index 00000000..89e6a3d8 --- /dev/null +++ b/test/DMLibTest/Framework/DMLibInputHelper.cs @@ -0,0 +1,31 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using Microsoft.WindowsAzure.Storage.DataMovement; + using MS.Test.Common.MsTestLib; + + public static class DMLibInputHelper + { + public static OverwriteCallback GetDefaultOverwiteCallbackY() + { + return (sourcePath, destPath) => + { + Test.Info("Overwrite true: {0} -> {1}", sourcePath, destPath); + return true; + }; + } + + public static OverwriteCallback GetDefaultOverwiteCallbackN() + { + return (sourcePath, destPath) => + { + Test.Info("Overwrite false: {0} -> {1}", sourcePath, destPath); + return false; + }; + } + } +} diff --git a/test/DMLibTest/Framework/DMLibTestBase.cs b/test/DMLibTest/Framework/DMLibTestBase.cs new file mode 100644 index 00000000..fd519a87 --- /dev/null +++ b/test/DMLibTest/Framework/DMLibTestBase.cs @@ -0,0 +1,357 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using DMLibTestCodeGen; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Microsoft.WindowsAzure.Storage; + using Microsoft.WindowsAzure.Storage.DataMovement; + using MS.Test.Common.MsTestLib; + + public class DMLibTestBase : MultiDirectionTestBase + { + private static Dictionary sourceConnectionStrings = new Dictionary(); + private static Dictionary destConnectionStrings = new Dictionary(); + + public const string FolderName = "folder"; + public const string FileName = "testfile"; + public const string DirName = "testdir"; + + public static int FileSizeInKB + { + get; + set; + } + + public static void SetSourceConnectionString(string value, DMLibDataType dataType) + { + string key = DMLibTestBase.GetLocationKey(dataType); + sourceConnectionStrings[key] = value; + } + + public static void SetDestConnectionString(string value, DMLibDataType dataType) + { + string key = DMLibTestBase.GetLocationKey(dataType); + destConnectionStrings[key] = value; + } + + public static string GetSourceConnectionString(DMLibDataType dataType) + { + return GetConnectionString(SourceOrDest.Source, dataType); + } + + public static string GetDestConnectionString(DMLibDataType dataType) + { + return GetConnectionString(SourceOrDest.Dest, dataType); + } + + private static string GetConnectionString(SourceOrDest sourceOrDest, DMLibDataType dataType) + { + IDictionary connectionStrings = SourceOrDest.Source == sourceOrDest ? sourceConnectionStrings : destConnectionStrings; + string key = DMLibTestBase.GetLocationKey(dataType); + + string connectionString; + if (connectionStrings.TryGetValue(key, out connectionString)) + { + return connectionString; + } + + if (SourceOrDest.Dest == sourceOrDest) + { + return TestAccounts.Secondary.ConnectionString; + } + else + { + return TestAccounts.Primary.ConnectionString; + } + } + + public static new void BaseClassInitialize(TestContext testContext) + { + MultiDirectionTestBase.BaseClassInitialize(testContext); + FileSizeInKB = int.Parse(Test.Data.Get("FileSize")); + DMLibTestBase.InitializeDataAdaptor(); + } + + private static void InitializeDataAdaptor() + { + var srcBlobTestAccount = new TestAccount(GetSourceConnectionString(DMLibDataType.CloudBlob)); + var destBlobTestAccount = new TestAccount(GetDestConnectionString(DMLibDataType.CloudBlob)); + + var srcFileTestAccount = new TestAccount(GetSourceConnectionString(DMLibDataType.CloudFile)); + var destFileTestAccount = new TestAccount(GetDestConnectionString(DMLibDataType.CloudFile)); + + // Initialize data adaptor for normal location + SetSourceAdaptor(DMLibDataType.Local, new LocalDataAdaptor(DMLibTestBase.SourceRoot + DMLibTestHelper.RandomNameSuffix(), SourceOrDest.Source)); + SetSourceAdaptor(DMLibDataType.Stream, new LocalDataAdaptor(DMLibTestBase.SourceRoot + DMLibTestHelper.RandomNameSuffix(), SourceOrDest.Source, useStream: true)); + SetSourceAdaptor(DMLibDataType.URI, new URIBlobDataAdaptor(srcBlobTestAccount, DMLibTestBase.SourceRoot + DMLibTestHelper.RandomNameSuffix())); + SetSourceAdaptor(DMLibDataType.BlockBlob, new CloudBlobDataAdaptor(srcBlobTestAccount, DMLibTestBase.SourceRoot + DMLibTestHelper.RandomNameSuffix(), BlobType.Block, SourceOrDest.Source)); + SetSourceAdaptor(DMLibDataType.PageBlob, new CloudBlobDataAdaptor(srcBlobTestAccount, DMLibTestBase.SourceRoot + DMLibTestHelper.RandomNameSuffix(), BlobType.Page, SourceOrDest.Source)); + SetSourceAdaptor(DMLibDataType.AppendBlob, new CloudBlobDataAdaptor(srcBlobTestAccount, DMLibTestBase.SourceRoot + DMLibTestHelper.RandomNameSuffix(), BlobType.Append, SourceOrDest.Source)); + SetSourceAdaptor(DMLibDataType.CloudFile, new CloudFileDataAdaptor(srcFileTestAccount, DMLibTestBase.SourceRoot + DMLibTestHelper.RandomNameSuffix(), SourceOrDest.Source)); + + SetDestAdaptor(DMLibDataType.Local, new LocalDataAdaptor(DMLibTestBase.DestRoot + DMLibTestHelper.RandomNameSuffix(), SourceOrDest.Dest)); + SetDestAdaptor(DMLibDataType.Stream, new LocalDataAdaptor(DMLibTestBase.DestRoot + DMLibTestHelper.RandomNameSuffix(), SourceOrDest.Dest, useStream: true)); + SetDestAdaptor(DMLibDataType.BlockBlob, new CloudBlobDataAdaptor(destBlobTestAccount, DMLibTestBase.DestRoot + DMLibTestHelper.RandomNameSuffix(), BlobType.Block, SourceOrDest.Dest)); + SetDestAdaptor(DMLibDataType.PageBlob, new CloudBlobDataAdaptor(destBlobTestAccount, DMLibTestBase.DestRoot + DMLibTestHelper.RandomNameSuffix(), BlobType.Page, SourceOrDest.Dest)); + SetDestAdaptor(DMLibDataType.AppendBlob, new CloudBlobDataAdaptor(destBlobTestAccount, DMLibTestBase.DestRoot + DMLibTestHelper.RandomNameSuffix(), BlobType.Append, SourceOrDest.Dest)); + SetDestAdaptor(DMLibDataType.CloudFile, new CloudFileDataAdaptor(destFileTestAccount, DMLibTestBase.DestRoot + DMLibTestHelper.RandomNameSuffix(), SourceOrDest.Dest)); + } + + public TestResult ExecuteTestCase(DMLibDataInfo sourceDataInfo, TestExecutionOptions options) + { + this.CleanupData(); + SourceAdaptor.CreateIfNotExists(); + DestAdaptor.CreateIfNotExists(); + + if (sourceDataInfo != null) + { + SourceAdaptor.GenerateData(sourceDataInfo); + } + + if (options.DestTransferDataInfo != null) + { + DestAdaptor.GenerateData(options.DestTransferDataInfo); + } + + if (options.AfterDataPrepared != null) + { + options.AfterDataPrepared(); + } + + List allItems = new List(); + foreach(var fileNode in sourceDataInfo.EnumerateFileNodes()) + { + TransferItem item = new TransferItem() + { + SourceObject = SourceAdaptor.GetTransferObject(fileNode), + DestObject = DestAdaptor.GetTransferObject(fileNode), + SourceType = DMLibTestContext.SourceType, + DestType = DMLibTestContext.DestType, + IsServiceCopy = DMLibTestContext.IsAsync, + }; + + if (options.TransferItemModifier != null) + { + options.TransferItemModifier(fileNode, item); + } + + allItems.Add(item); + } + + return this.RunTransferItems(allItems, options); + } + + public TestResult RunTransferItems(IEnumerable items, TestExecutionOptions options) + { + List allTasks = new List(); + var testResult = new TestResult(); + + try + { + foreach (TransferItem item in items) + { + DMLibWrapper wrapper = GetDMLibWrapper(item.SourceType, item.DestType, DMLibTestContext.IsAsync); + + if (item.BeforeStarted != null) + { + item.BeforeStarted(); + } + + try + { + if (options.LimitSpeed) + { + OperationContext.GlobalSendingRequest += this.LimitSpeed; + TransferManager.Configurations.ParallelOperations = DMLibTestConstants.LimitedSpeedNC; + } + + allTasks.Add(wrapper.DoTransfer(item)); + } + catch (Exception e) + { + testResult.AddException(e); + } + + if (item.AfterStarted != null) + { + item.AfterStarted(); + } + } + + if (options.AfterAllItemAdded != null) + { + options.AfterAllItemAdded(); + } + + try + { + Task.WaitAll(allTasks.ToArray(), options.TimeoutInMs); + } + catch (Exception e) + { + AggregateException ae = e as AggregateException; + if (ae != null) + { + ae = ae.Flatten(); + foreach (var innerE in ae.InnerExceptions) + { + testResult.AddException(innerE); + } + } + else + { + testResult.AddException(e); + } + } + } + finally + { + if (options.LimitSpeed) + { + OperationContext.GlobalSendingRequest -= this.LimitSpeed; + TransferManager.Configurations.ParallelOperations = DMLibTestConstants.DefaultNC; + } + } + + Parallel.ForEach(items, currentItem => currentItem.CloseStreamIfNecessary()); + + if (!options.DisableDestinationFetch) + { + testResult.DataInfo = DestAdaptor.GetTransferDataInfo(string.Empty); + } + + foreach (var exception in testResult.Exceptions) + { + Test.Info("Exception from DMLib: {0}", exception.ToString()); + } + + return testResult; + } + + public DMLibWrapper GetDMLibWrapper(DMLibDataType sourceType, DMLibDataType destType, bool isServiceCopy) + { + if (DMLibTestBase.IsLocal(sourceType)) + { + return new UploadWrapper(); + } + else if (DMLibTestBase.IsLocal(destType)) + { + return new DownloadWrapper(); + } + else + { + return new CopyWrapper(); + } + } + + public static object DefaultTransferOptions + { + get + { + return DMLibTestBase.GetDefaultTransferOptions(DMLibTestContext.SourceType, DMLibTestContext.DestType); + } + } + + public static object GetDefaultTransferOptions(DMLibDataType sourceType, DMLibDataType destType) + { + if (DMLibTestBase.IsLocal(sourceType)) + { + return new UploadOptions(); + } + else if (DMLibTestBase.IsLocal(destType)) + { + return new DownloadOptions(); + } + else + { + return new CopyOptions(); + } + } + + public static string MapBlobDataTypeToBlobType(DMLibDataType blobDataType) + { + switch (blobDataType) + { + case DMLibDataType.BlockBlob: + return BlobType.Block; + case DMLibDataType.PageBlob: + return BlobType.Page; + case DMLibDataType.AppendBlob: + return BlobType.Append; + default: + throw new ArgumentException("blobDataType"); + } + } + + private void LimitSpeed(object sender, RequestEventArgs e) + { + Thread.Sleep(100); + } + + public DMLibDataInfo GenerateSourceDataInfo(FileNumOption fileNumOption, string folderName = "") + { + return this.GenerateSourceDataInfo(fileNumOption, DMLibTestBase.FileSizeInKB, folderName); + } + + public DMLibDataInfo GenerateSourceDataInfo(FileNumOption fileNumOption, int totalSizeInKB, string folderName = "") + { + DMLibDataInfo sourceDataInfo = new DMLibDataInfo(folderName); + + if (fileNumOption == FileNumOption.FileTree) + { + DMLibDataHelper.AddTreeTotalSize( + sourceDataInfo.RootNode, + DMLibTestBase.DirName, + DMLibTestBase.FileName, + DMLibTestConstants.RecursiveFolderWidth, + DMLibTestConstants.RecursiveFolderDepth, + totalSizeInKB); + } + else if (fileNumOption == FileNumOption.FlatFolder) + { + DMLibDataHelper.AddMultipleFilesTotalSize( + sourceDataInfo.RootNode, + DMLibTestBase.FileName, + DMLibTestConstants.FlatFileCount, + totalSizeInKB); + } + else if (fileNumOption == FileNumOption.OneFile) + { + DMLibDataHelper.AddOneFile(sourceDataInfo.RootNode, DMLibTestBase.FileName, totalSizeInKB); + } + + return sourceDataInfo; + } + + public enum FileNumOption + { + OneFile, + FlatFolder, + FileTree, + } + + public override bool IsCloudService(DMLibDataType dataType) + { + return DMLibDataType.Cloud.HasFlag(dataType); + } + + public static bool IsLocal(DMLibDataType dataType) + { + return dataType == DMLibDataType.Stream || dataType == DMLibDataType.Local; + } + + public static bool IsCloudBlob(DMLibDataType dataType) + { + return DMLibDataType.CloudBlob.HasFlag(dataType); + } + } +} diff --git a/test/DMLibTest/Framework/DMLibWrapper.cs b/test/DMLibTest/Framework/DMLibWrapper.cs new file mode 100644 index 00000000..2d4b650f --- /dev/null +++ b/test/DMLibTest/Framework/DMLibWrapper.cs @@ -0,0 +1,21 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System.Threading.Tasks; + using MS.Test.Common.MsTestLib; + + public abstract class DMLibWrapper + { + public Task DoTransfer(TransferItem item) + { + Test.Info("Do transfer: {0}", item.ToString()); + return this.DoTransferImp(item); + } + + protected abstract Task DoTransferImp(TransferItem item); + } +} diff --git a/test/DMLibTest/Framework/DataAdaptor.cs b/test/DMLibTest/Framework/DataAdaptor.cs new file mode 100644 index 00000000..72ef546f --- /dev/null +++ b/test/DMLibTest/Framework/DataAdaptor.cs @@ -0,0 +1,65 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + public abstract class DataAdaptor where TDataInfo : IDataInfo + { + public abstract string StorageKey + { + get; + } + + public SourceOrDest SourceOrDest + { + get; + protected set; + } + + public abstract string GetAddress(params string[] list); + + public abstract string GetSecondaryAddress(params string[] list); + + public abstract object GetTransferObject(FileNode fileNode); + + public abstract object GetTransferObject(DirNode dirNode); + + public abstract void CreateIfNotExists(); + + public abstract bool Exists(); + + public abstract void WaitForGEO(); + + public void GenerateData(TDataInfo dataInfo) + { + this.GenerateDataImp(dataInfo); + + if (SourceOrDest.Source == this.SourceOrDest) + { + MultiDirectionTestInfo.GeneratedSourceDataInfos.Add(dataInfo == null ? dataInfo : dataInfo.Clone()); + } + else + { + MultiDirectionTestInfo.GeneratedDestDataInfos.Add(dataInfo == null ? dataInfo : dataInfo.Clone()); + } + } + + public abstract TDataInfo GetTransferDataInfo(string rootDir); + + public abstract void Cleanup(); + + public abstract void DeleteLocation(); + + public abstract string GenerateSAS(SharedAccessPermissions sap, int validatePeriod, string policySignedIdentifier = null); + + public abstract void RevokeSAS(); + + public abstract void MakePublic(); + + public abstract void Reset(); + + protected abstract void GenerateDataImp(TDataInfo dataInfo); + } +} diff --git a/test/DMLibTest/Framework/DownloadWrapper.cs b/test/DMLibTest/Framework/DownloadWrapper.cs new file mode 100644 index 00000000..e10becdf --- /dev/null +++ b/test/DMLibTest/Framework/DownloadWrapper.cs @@ -0,0 +1,68 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System.IO; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.WindowsAzure.Storage.DataMovement; + + internal class DownloadWrapper : DMLibWrapper + { + public DownloadWrapper() + { + + } + + protected override Task DoTransferImp(TransferItem item) + { + return this.Download(item.SourceObject, item); + } + + private Task Download(dynamic sourceObject, TransferItem item) + { + DownloadOptions downloadOptions = item.Options as DownloadOptions; + TransferContext transferContext = item.TransferContext; + CancellationToken cancellationToken = item.CancellationToken; + string destPath = item.DestObject as string; + Stream destStream = item.DestObject as Stream; + + if (cancellationToken != null && cancellationToken != CancellationToken.None) + { + if (destPath != null) + { + return TransferManager.DownloadAsync(sourceObject, destPath, downloadOptions, transferContext, cancellationToken); + } + else + { + return TransferManager.DownloadAsync(sourceObject, destStream, downloadOptions, transferContext, cancellationToken); + } + } + else if (transferContext != null || downloadOptions != null) + { + if (destPath != null) + { + return TransferManager.DownloadAsync(sourceObject, destPath, downloadOptions, transferContext); + } + else + { + return TransferManager.DownloadAsync(sourceObject, destStream, downloadOptions, transferContext); + } + } + else + { + if (destPath != null) + { + return TransferManager.DownloadAsync(sourceObject, destPath); + } + else + { + return TransferManager.DownloadAsync(sourceObject, destStream); + } + } + } + } +} diff --git a/test/DMLibTest/Framework/IDataInfo.cs b/test/DMLibTest/Framework/IDataInfo.cs new file mode 100644 index 00000000..9d141b28 --- /dev/null +++ b/test/DMLibTest/Framework/IDataInfo.cs @@ -0,0 +1,14 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + public interface IDataInfo + { + string ToString(); + + IDataInfo Clone(); + } +} diff --git a/test/DMLibTest/Framework/LocalDataAdaptor.cs b/test/DMLibTest/Framework/LocalDataAdaptor.cs new file mode 100644 index 00000000..c24a3f76 --- /dev/null +++ b/test/DMLibTest/Framework/LocalDataAdaptor.cs @@ -0,0 +1,163 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal class LocalDataAdaptor : LocalDataAdaptorBase + { + private bool useStream; + + public LocalDataAdaptor(string basePath, SourceOrDest sourceOrDest, bool useStream = false) + : base(basePath, sourceOrDest) + { + this.useStream = useStream; + } + + public override object GetTransferObject(FileNode fileNode) + { + string filePath = Path.Combine(this.BasePath, fileNode.GetLocalRelativePath()); + + if (this.useStream) + { + if (SourceOrDest.Source == this.SourceOrDest) + { + return new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + } + else + { + return new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); + } + } + else + { + return filePath; + } + } + + public override object GetTransferObject(DirNode dirNode) + { + if (this.useStream) + { + throw new InvalidOperationException("Can't get directory transfer object in stream data adaptor."); + } + + return Path.Combine(this.BasePath, dirNode.GetLocalRelativePath()); + } + + protected override void GenerateDataImp(DMLibDataInfo dataInfo) + { + this.GenerateDir(dataInfo.RootNode, Path.Combine(this.BasePath, dataInfo.RootPath)); + } + + public override DMLibDataInfo GetTransferDataInfo(string rootDir) + { + DirectoryInfo rootDirInfo = new DirectoryInfo(Path.Combine(this.BasePath, rootDir)); + if (!rootDirInfo.Exists) + { + return null; + } + + DMLibDataInfo dataInfo = new DMLibDataInfo(rootDir); + this.BuildDirNode(rootDirInfo, dataInfo.RootNode); + + return dataInfo; + } + + private void GenerateDir(DirNode dirNode, string parentPath) + { + string dirPath = Path.Combine(parentPath, dirNode.Name); + DMLibDataHelper.CreateLocalDirIfNotExists(dirPath); + + foreach (var subDir in dirNode.DirNodes) + { + GenerateDir(subDir, dirPath); + } + + foreach (var file in dirNode.FileNodes) + { + GenerateFile(file, dirPath); + } + } + + private void CheckFileNode(FileNode fileNode) + { + if (fileNode.MD5 != null) + { + throw new InvalidOperationException("Can't set MD5 to local file"); + } + + if (fileNode.ContentType != null) + { + throw new InvalidOperationException("Can't set ContentType to local file"); + } + + if (fileNode.CacheControl != null) + { + throw new InvalidOperationException("Can't set CacheControl to local file"); + } + + if (fileNode.ContentDisposition != null) + { + throw new InvalidOperationException("Can't set ContentDisposition to local file"); + } + + if (fileNode.ContentEncoding != null) + { + throw new InvalidOperationException("Can't set ContentEncoding to local file"); + } + + if (fileNode.ContentLanguage != null) + { + throw new InvalidOperationException("Can't set ContentLanguage to local file"); + } + + if (fileNode.Metadata != null && fileNode.Metadata.Count > 0) + { + throw new InvalidOperationException("Can't set Metadata to local file"); + } + } + + private void GenerateFile(FileNode fileNode, string parentPath) + { + this.CheckFileNode(fileNode); + + string localFilePath = Path.Combine(parentPath, fileNode.Name); + DMLibDataHelper.CreateLocalFile(fileNode, localFilePath); + + FileInfo fileInfo = new FileInfo(localFilePath); + + this.BuildFileNode(fileInfo, fileNode); + } + + private void BuildDirNode(DirectoryInfo dirInfo, DirNode parent) + { + foreach (FileInfo fileInfo in dirInfo.GetFiles()) + { + FileNode fileNode = new FileNode(fileInfo.Name); + this.BuildFileNode(fileInfo, fileNode); + parent.AddFileNode(fileNode); + } + + foreach (DirectoryInfo subDirInfo in dirInfo.GetDirectories()) + { + DirNode subDirNode = new DirNode(subDirInfo.Name); + this.BuildDirNode(subDirInfo, subDirNode); + parent.AddDirNode(subDirNode); + } + } + + private void BuildFileNode(FileInfo fileInfo, FileNode fileNode) + { + fileNode.MD5 = Helper.GetFileContentMD5(fileInfo.FullName); + fileNode.LastModifiedTime = fileInfo.LastWriteTimeUtc; + fileNode.SizeInByte = fileInfo.Length; + fileNode.Metadata = new Dictionary(); + } + } +} diff --git a/test/DMLibTest/Framework/LocalDataAdaptorBase.cs b/test/DMLibTest/Framework/LocalDataAdaptorBase.cs new file mode 100644 index 00000000..38e06223 --- /dev/null +++ b/test/DMLibTest/Framework/LocalDataAdaptorBase.cs @@ -0,0 +1,93 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using System.IO; + + public abstract class LocalDataAdaptorBase : DataAdaptor where TDataInfo : IDataInfo + { + protected string BasePath + { + get; + private set; + } + + public override string StorageKey + { + get + { + throw new NotSupportedException("StorageKey is not supported in LocalDataAdaptorBase."); + } + } + + public LocalDataAdaptorBase(string basePath, SourceOrDest sourceOrDest) + { + // The folder pointed by basePath will be deleted when cleanup. + this.BasePath = basePath; + this.SourceOrDest = sourceOrDest; + } + + public override string GetAddress(params string[] list) + { + string address = Path.Combine(this.BasePath, Path.Combine(list)); + return address + Path.DirectorySeparatorChar; + } + + public override string GetSecondaryAddress(params string[] list) + { + throw new NotSupportedException("GetSecondaryAddress is not supported in LocalDataAdaptorBase."); + } + + public override void CreateIfNotExists() + { + if (!Directory.Exists(this.BasePath)) + { + Directory.CreateDirectory(this.BasePath); + } + } + + public override bool Exists() + { + return Directory.Exists(this.BasePath); + } + + public override void WaitForGEO() + { + throw new NotSupportedException("WaitForGEO is not supported in LocalDataAdaptorBase."); + } + + public override void Cleanup() + { + Helper.CleanupFolder(this.BasePath); + } + + public override void DeleteLocation() + { + Helper.DeleteFolder(this.BasePath); + } + + public override void MakePublic() + { + throw new NotSupportedException("MakePublic is not supported in LocalDataAdaptorBase."); + } + + public override void Reset() + { + // Nothing to reset + } + + public override string GenerateSAS(SharedAccessPermissions sap, int validatePeriod, string policySignedIdentifier = null) + { + throw new NotSupportedException("GenerateSAS is not supported in LocalDataAdaptorBase."); + } + + public override void RevokeSAS() + { + throw new NotSupportedException("RevokeSAS is not supported in LocalDataAdaptorBase."); + } + } +} diff --git a/test/DMLibTest/Framework/MultiDirectionTestBase.cs b/test/DMLibTest/Framework/MultiDirectionTestBase.cs new file mode 100644 index 00000000..096357dd --- /dev/null +++ b/test/DMLibTest/Framework/MultiDirectionTestBase.cs @@ -0,0 +1,297 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Text; + using System.Threading.Tasks; + using DMLibTestCodeGen; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using MS.Test.Common.MsTestLib; + + public enum StopDMLibType + { + None, + Kill, + TestHookCtrlC, + BreakNetwork + } + + public enum SourceOrDest + { + Source, + Dest, + } + + public abstract class MultiDirectionTestBase + where TDataInfo : IDataInfo + where TDataType : struct + { + public const string SourceRoot = "sourceroot"; + public const string DestRoot = "destroot"; + public const string SourceFolder = "sourcefolder"; + public const string DestFolder = "destfolder"; + + protected static Random random = new Random(); + + private static Dictionary> sourceAdaptors = new Dictionary>(); + private static Dictionary> destAdaptors = new Dictionary>(); + + private TestContext testContextInstance; + + /// + ///Gets or sets the test context which provides + ///information about and functionality for the current test run. + /// + public TestContext TestContext + { + get + { + return testContextInstance; + } + set + { + testContextInstance = value; + } + } + + public static string NetworkShare + { + get; + set; + } + + public static bool CleanupSource + { + get; + set; + } + + public static bool CleanupDestination + { + get; + set; + } + + public static DataAdaptor SourceAdaptor + { + get + { + return GetSourceAdaptor(MultiDirectionTestContext.SourceType); + } + } + + public static DataAdaptor DestAdaptor + { + get + { + return GetDestAdaptor(MultiDirectionTestContext.DestType); + } + } + + public static void BaseClassInitialize(TestContext testContext) + { + Test.Info("ClassInitialize"); + Test.FullClassName = testContext.FullyQualifiedTestClassName; + + MultiDirectionTestBase.CleanupSource = true; + MultiDirectionTestBase.CleanupDestination = true; + + NetworkShare = Test.Data.Get("NetworkFolder"); + } + + public static void BaseClassCleanup() + { + Test.Info("ClassCleanup"); + DeleteAllLocations(sourceAdaptors); + DeleteAllLocations(destAdaptors); + Test.Info("ClassCleanup done."); + } + + private static void DeleteAllLocations(Dictionary> adaptorDic) + { + Parallel.ForEach(adaptorDic, pair => + { + try + { + pair.Value.DeleteLocation(); + } + catch + { + Test.Warn("Fail to delete location for data adaptor: {0}", pair.Key); + } + }); + } + + public virtual void BaseTestInitialize() + { + Test.Start(TestContext.FullyQualifiedTestClassName, TestContext.TestName); + Test.Info("TestInitialize"); + + MultiDirectionTestInfo.Cleanup(); + } + + public virtual void BaseTestCleanup() + { + if (Test.ErrorCount > 0) + { + MultiDirectionTestInfo.Print(); + } + + Test.Info("TestCleanup"); + Test.End(TestContext.FullyQualifiedTestClassName, TestContext.TestName); + + try + { + this.CleanupData(); + MultiDirectionTestBase.SourceAdaptor.Reset(); + MultiDirectionTestBase.DestAdaptor.Reset(); + } + catch + { + // ignore exception + } + } + + public virtual void CleanupData() + { + this.CleanupData( + MultiDirectionTestBase.CleanupSource, + MultiDirectionTestBase.CleanupDestination); + } + + protected void CleanupData(bool cleanupSource, bool cleanupDestination) + { + if (cleanupSource) + { + MultiDirectionTestBase.SourceAdaptor.Cleanup(); + } + + if (cleanupDestination) + { + MultiDirectionTestBase.DestAdaptor.Cleanup(); + } + } + + protected static string GetLocationKey(TDataType dataType) + { + return dataType.ToString(); + } + + public static DataAdaptor GetSourceAdaptor(TDataType dataType) + { + string key = MultiDirectionTestBase.GetLocationKey(dataType); + + if (!sourceAdaptors.ContainsKey(key)) + { + throw new KeyNotFoundException( + string.Format("Can't find key of source data adaptor. DataType:{0}.", dataType.ToString())); + } + + return sourceAdaptors[key]; + } + + public static DataAdaptor GetDestAdaptor(TDataType dataType) + { + string key = MultiDirectionTestBase.GetLocationKey(dataType); + + if (!destAdaptors.ContainsKey(key)) + { + throw new KeyNotFoundException( + string.Format("Can't find key of destination data adaptor. DataType:{0}.", dataType.ToString())); + } + + return destAdaptors[key]; + } + + protected static void SetSourceAdaptor(TDataType dataType, DataAdaptor adaptor) + { + string key = MultiDirectionTestBase.GetLocationKey(dataType); + sourceAdaptors[key] = adaptor; + } + + protected static void SetDestAdaptor(TDataType dataType, DataAdaptor adaptor) + { + string key = MultiDirectionTestBase.GetLocationKey(dataType); + destAdaptors[key] = adaptor; + } + + public abstract bool IsCloudService(TDataType dataType); + + public static CredentialType GetRandomCredentialType() + { + int credentialCount = Enum.GetNames(typeof(CredentialType)).Length; + int randomNum = MultiDirectionTestBase.random.Next(0, credentialCount); + + CredentialType result; + switch (randomNum) + { + case 0: + result = CredentialType.None; + break; + case 1: + result = CredentialType.Public; + break; + case 2: + result = CredentialType.Key; + break; + case 3: + result = CredentialType.SAS; + break; + default: + result = CredentialType.EmbeddedSAS; + break; + } + + Test.Info("Random credential type: {0}", result.ToString()); + return result; + } + + protected static string GetRelativePath(string basePath, string fullPath) + { + string normalizedBasePath = MultiDirectionTestBase.NormalizePath(basePath); + string normalizedFullPath = MultiDirectionTestBase.NormalizePath(fullPath); + + int index = normalizedFullPath.IndexOf(normalizedBasePath); + + if (index < 0) + { + return null; + } + + return normalizedFullPath.Substring(index + normalizedBasePath.Length); + } + + protected static string NormalizePath(string path) + { + if (path.StartsWith("\"") && path.EndsWith("\"")) + { + path = path.Substring(1, path.Length - 2); + } + + try + { + var uri = new Uri(path); + return uri.GetComponents(UriComponents.Path, UriFormat.Unescaped); + } + catch (UriFormatException) + { + return path; + } + } + } + + public enum CredentialType + { + None = 0, + Public, + Key, + SAS, + EmbeddedSAS, + } +} diff --git a/test/DMLibTest/Framework/MultiDirectionTestHelper.cs b/test/DMLibTest/Framework/MultiDirectionTestHelper.cs new file mode 100644 index 00000000..89e6832e --- /dev/null +++ b/test/DMLibTest/Framework/MultiDirectionTestHelper.cs @@ -0,0 +1,107 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using System.IO; + using System.Linq; + using System.Threading; + using DMLibTestCodeGen; + using Microsoft.WindowsAzure.Storage; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.File; + using MS.Test.Common.MsTestLib; + + public static class MultiDirectionTestHelper + { + public static void WaitUntilFileCreated(FileNode fileNode, DataAdaptor dataAdaptor, DMLibDataType dataType, int timeoutInSec = 300) + { + Func checkFileCreated = null; + + if (dataType == DMLibDataType.Local) + { + string filePath = dataAdaptor.GetAddress() + fileNode.GetLocalRelativePath(); + checkFileCreated = () => + { + return File.Exists(filePath); + }; + } + else if (dataType == DMLibDataType.PageBlob || + dataType == DMLibDataType.AppendBlob) + { + CloudBlobDataAdaptor blobAdaptor = dataAdaptor as CloudBlobDataAdaptor; + + checkFileCreated = () => + { + CloudBlob cloudBlob = blobAdaptor.GetCloudBlobReference(fileNode); + return cloudBlob.Exists(options: HelperConst.DefaultBlobOptions); + }; + } + else if (dataType == DMLibDataType.BlockBlob) + { + CloudBlobDataAdaptor blobAdaptor = dataAdaptor as CloudBlobDataAdaptor; + + checkFileCreated = () => + { + CloudBlockBlob blockBlob = blobAdaptor.GetCloudBlobReference(fileNode) as CloudBlockBlob; + try + { + return blockBlob.DownloadBlockList(BlockListingFilter.All, options: HelperConst.DefaultBlobOptions).Any(); + } + catch (StorageException) + { + return false; + } + }; + } + else if (dataType == DMLibDataType.CloudFile) + { + CloudFileDataAdaptor fileAdaptor = dataAdaptor as CloudFileDataAdaptor; + + checkFileCreated = () => + { + CloudFile cloudFile = fileAdaptor.GetCloudFileReference(fileNode); + return cloudFile.Exists(options: HelperConst.DefaultFileOptions); + }; + } + else + { + Test.Error("Unexpected data type: {0}", DMLibTestContext.SourceType); + } + + MultiDirectionTestHelper.WaitUntil(checkFileCreated, timeoutInSec); + } + + private static void WaitUntil(Func condition, int timeoutInSec) + { + DateTime nowTime = DateTime.Now; + DateTime timeOut = nowTime.AddSeconds(timeoutInSec); + while (timeOut > DateTime.Now) + { + if (condition()) + { + return; + } + + Thread.Sleep(100); + } + + Test.Error("WaitUntil: condition doesn't meet within timeout {0} second(s).", timeoutInSec); + } + + public static void PrintTransferDataInfo(IDataInfo dataInfo) + { + if (null == dataInfo) + { + Test.Info("TransferDataInfo is null"); + } + else + { + Test.Info(dataInfo.ToString()); + } + } + } +} diff --git a/test/DMLibTest/Framework/MultiDirectionTestInfo.cs b/test/DMLibTest/Framework/MultiDirectionTestInfo.cs new file mode 100644 index 00000000..bec3b9dc --- /dev/null +++ b/test/DMLibTest/Framework/MultiDirectionTestInfo.cs @@ -0,0 +1,37 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System.Collections.Generic; + using MS.Test.Common.MsTestLib; + + public class MultiDirectionTestInfo + { + public static List GeneratedSourceDataInfos = new List(); + public static List GeneratedDestDataInfos = new List(); + + public static void Cleanup() + { + MultiDirectionTestInfo.GeneratedSourceDataInfos.Clear(); + MultiDirectionTestInfo.GeneratedDestDataInfos.Clear(); + } + + public static void Print() + { + Test.Info("-----Source Data-----"); + foreach (var sourceDataInfo in MultiDirectionTestInfo.GeneratedSourceDataInfos) + { + MultiDirectionTestHelper.PrintTransferDataInfo(sourceDataInfo); + } + + Test.Info("-----Dest Data-----"); + foreach (var destDataInfo in MultiDirectionTestInfo.GeneratedDestDataInfos) + { + MultiDirectionTestHelper.PrintTransferDataInfo(destDataInfo); + } + } + } +} diff --git a/test/DMLibTest/Framework/ProgressChecker.cs b/test/DMLibTest/Framework/ProgressChecker.cs new file mode 100644 index 00000000..aec055df --- /dev/null +++ b/test/DMLibTest/Framework/ProgressChecker.cs @@ -0,0 +1,101 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; +using System.Threading; +using Microsoft.WindowsAzure.Storage.DataMovement; +using MS.Test.Common.MsTestLib; + + public class ProgressChecker : IProgress + { + private ProgressValue transferedNumber = new ProgressValue(); + private ProgressValue failedNumber = new ProgressValue(); + private ProgressValue skippedNumber = new ProgressValue(); + private ProgressValue transferedBytes = new ProgressValue(); + private long totalNumber = 0; + private long totalBytes = 0; + private ManualResetEvent dataTransferred; + + public ProgressChecker(long totalNumber, long totalBytes) : this(totalNumber, totalBytes, totalNumber, 0, 0, totalBytes) + { + } + + public ProgressChecker(long totalNumber, long totalBytes, long transferedNumber, long failedNumber, long skippedNumber, long transferedBytes) + { + this.totalNumber = totalNumber; + this.totalBytes = totalBytes; + this.transferedNumber.MaxValue = transferedNumber; + this.failedNumber.MaxValue = failedNumber; + this.skippedNumber.MaxValue = skippedNumber; + this.transferedBytes.MaxValue = transferedBytes; + this.dataTransferred = new ManualResetEvent(false); + } + + public IProgress GetProgressHandler() + { + return this; + } + + public void Report(TransferProgress progress) + { + this.dataTransferred.Set(); + Test.Info("Check progress: {0}", progress.BytesTransferred); + this.CheckIncrease(this.transferedBytes, progress.BytesTransferred, "BytesTransferred"); + this.CheckIncrease(this.transferedNumber, progress.NumberOfFilesTransferred, "NumberOfFilesTransferred"); + this.CheckIncrease(this.failedNumber, progress.NumberOfFilesFailed, "NumberOfFilesFailed"); + this.CheckIncrease(this.skippedNumber, progress.NumberOfFilesSkipped, "NumberOfFilesSkipped"); + } + + public void Reset() + { + this.transferedNumber.PreviousValue = 0; + this.failedNumber.PreviousValue = 0; + this.skippedNumber.PreviousValue = 0; + this.transferedBytes.PreviousValue = 0; + this.dataTransferred.Reset(); + } + + public WaitHandle DataTransferred + { + get + { + return this.dataTransferred; + } + } + + private void CheckEqual(T expectedValue, T currentValue, string valueName) where T : IComparable + { + if (currentValue.CompareTo(expectedValue) != 0) + { + Test.Error("Wrong {0} value: {1}, expected value: {2}", valueName, currentValue, expectedValue); + } + } + + private void CheckIncrease(ProgressValue progressValue, T currentValue, string valueName) where T : IComparable + { + if (currentValue.CompareTo(progressValue.PreviousValue) < 0 || + currentValue.CompareTo(progressValue.MaxValue) > 0) + { + Test.Error("Wrong {0} value: {1}, previous value: {2}, max value {3}", valueName, currentValue, progressValue.PreviousValue, progressValue.MaxValue); + } + + progressValue.PreviousValue = currentValue; + } + } + + class ProgressValue where T : IComparable + { + public ProgressValue() + { + this.MaxValue = default(T); + this.PreviousValue = default(T); + } + + public T MaxValue; + public T PreviousValue; + } +} diff --git a/test/DMLibTest/Framework/SharedAccessPermissions.cs b/test/DMLibTest/Framework/SharedAccessPermissions.cs new file mode 100644 index 00000000..43bbb125 --- /dev/null +++ b/test/DMLibTest/Framework/SharedAccessPermissions.cs @@ -0,0 +1,53 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.File; + using Microsoft.WindowsAzure.Storage.Table; + + [Flags] + public enum SharedAccessPermissions + { + None = 0, + Read = 1, + Write = 2, + Delete = 4, + List = 8, + Query = 16, + Add = 32, + Update = 64, + } + + public static class SharedAccessPermissionsExtensions + { + public const SharedAccessPermissions LeastPermissionDest = SharedAccessPermissions.Write | SharedAccessPermissions.Read; + public const SharedAccessPermissions LeastPermissionSource = SharedAccessPermissions.List | SharedAccessPermissions.Read; + public const SharedAccessPermissions LeastPermissionSourceList = SharedAccessPermissions.List; + + public static SharedAccessBlobPermissions ToBlobPermissions(this SharedAccessPermissions sap) + { + return (SharedAccessBlobPermissions)Enum.Parse(typeof(SharedAccessBlobPermissions), sap.ToString()); + } + + public static SharedAccessFilePermissions ToFilePermissions(this SharedAccessPermissions sap) + { + return (SharedAccessFilePermissions)Enum.Parse(typeof(SharedAccessFilePermissions), sap.ToString()); + } + + public static SharedAccessTablePermissions ToTablePermissions(this SharedAccessPermissions sap) + { + return (SharedAccessTablePermissions)Enum.Parse(typeof(SharedAccessTablePermissions), sap.ToString()); + } + + public static SharedAccessPermissions ToCommonPermissions(this Enum specificPermissions) + { + return (SharedAccessPermissions)Enum.Parse(typeof(SharedAccessPermissions), specificPermissions.ToString()); + } + } + +} diff --git a/test/DMLibTest/Framework/TestExecutionOptions.cs b/test/DMLibTest/Framework/TestExecutionOptions.cs new file mode 100644 index 00000000..f344ed5a --- /dev/null +++ b/test/DMLibTest/Framework/TestExecutionOptions.cs @@ -0,0 +1,52 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + + public class TestExecutionOptions where TDataInfo : IDataInfo + { + public const int DefaultTimeoutInMs = 20 * 60 * 1000; // 20 min + + public TestExecutionOptions() + { + this.TimeoutInMs = DefaultTimeoutInMs; + this.DestTransferDataInfo = default(TDataInfo); + this.DisableDestinationFetch = false; + this.LimitSpeed = false; + } + + public int TimeoutInMs + { + get; + set; + } + + public TDataInfo DestTransferDataInfo + { + get; + set; + } + + public bool DisableDestinationFetch + { + get; + set; + } + + public bool LimitSpeed + { + get; + set; + } + + public Action TransferItemModifier; + + public Action AfterDataPrepared; + + public Action AfterAllItemAdded; + } +} diff --git a/test/DMLibTest/Framework/TestResult.cs b/test/DMLibTest/Framework/TestResult.cs new file mode 100644 index 00000000..fe63f3c1 --- /dev/null +++ b/test/DMLibTest/Framework/TestResult.cs @@ -0,0 +1,41 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using System.Collections; + using System.Collections.Generic; + + public class TestResult where TDataInfo : IDataInfo + { + private List exceptions; + + public TestResult() + { + this.exceptions = new List(); + this.DataInfo = default(TDataInfo); + } + + public TDataInfo DataInfo + { + set; + get; + } + + public List Exceptions + { + get + { + return this.exceptions; + } + } + + public void AddException(Exception e) + { + this.exceptions.Add(e); + } + } +} diff --git a/test/DMLibTest/Framework/TransferItem.cs b/test/DMLibTest/Framework/TransferItem.cs new file mode 100644 index 00000000..fd405215 --- /dev/null +++ b/test/DMLibTest/Framework/TransferItem.cs @@ -0,0 +1,168 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ + +namespace DMLibTest +{ + using System; + using System.Globalization; + using System.IO; + using System.Threading; + using DMLibTestCodeGen; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.DataMovement; + using Microsoft.WindowsAzure.Storage.File; + + public class TransferItem + { + public object SourceObject + { + get; + set; + } + + public object DestObject + { + get; + set; + } + + public DMLibDataType SourceType + { + get; + set; + } + + public DMLibDataType DestType + { + get; + set; + } + + public bool IsServiceCopy + { + get; + set; + } + + public object Options + { + get; + set; + } + + public TransferContext TransferContext + { + get; + set; + } + + public CancellationToken CancellationToken + { + get; + set; + } + + public Action BeforeStarted + { + get; + set; + } + + public Action AfterStarted + { + get; + set; + } + + public void CloseStreamIfNecessary() + { + Stream sourceStream = this.SourceObject as Stream; + Stream destStream = this.DestObject as Stream; + + if (sourceStream != null) + { + sourceStream.Close(); + } + + if (destStream != null) + { + destStream.Close(); + } + } + + public TransferItem Clone() + { + TransferItem newTransferItem = new TransferItem() + { + SourceObject = NewLocationObject(this.SourceObject), + DestObject = NewLocationObject(this.DestObject), + SourceType = this.SourceType, + DestType = this.DestType, + IsServiceCopy = this.IsServiceCopy, + Options = this.Options, + }; + + return newTransferItem; + } + + private static object NewLocationObject(object locationObject) + { + if (locationObject is CloudBlob) + { + CloudBlob cloudBlob = locationObject as CloudBlob; + if (cloudBlob is CloudPageBlob) + { + return new CloudPageBlob(cloudBlob.SnapshotQualifiedUri, cloudBlob.ServiceClient.Credentials); + } + else if (cloudBlob is CloudBlockBlob) + { + return new CloudBlockBlob(cloudBlob.SnapshotQualifiedUri, cloudBlob.ServiceClient.Credentials); + } + else if (cloudBlob is CloudAppendBlob) + { + return new CloudAppendBlob(cloudBlob.SnapshotQualifiedUri, cloudBlob.ServiceClient.Credentials); + } + else + { + throw new ArgumentException(string.Format("Unsupported blob type: {0}", cloudBlob.BlobType), "locationObject"); + } + } + else if (locationObject is CloudFile) + { + CloudFile cloudFile = locationObject as CloudFile; + CloudFile newCloudFile = new CloudFile(cloudFile.Uri, cloudFile.ServiceClient.Credentials); + return newCloudFile; + } + else + { + return locationObject; + } + } + + public override string ToString() + { + return string.Format( + CultureInfo.InvariantCulture, + "{0} -> {1}", + this.GetDataObjectString(this.SourceObject), + this.GetDataObjectString(this.DestObject)); + } + + private string GetDataObjectString(object dataObject) + { + if (dataObject is CloudBlob) + { + return (dataObject as CloudBlob).SnapshotQualifiedUri.ToString(); + } + else if (dataObject is CloudFile) + { + return (dataObject as CloudFile).Uri.ToString(); + } + + return dataObject.ToString(); + } + } +} diff --git a/test/DMLibTest/Framework/URIBlobDataAdaptor.cs b/test/DMLibTest/Framework/URIBlobDataAdaptor.cs new file mode 100644 index 00000000..a7e9f516 --- /dev/null +++ b/test/DMLibTest/Framework/URIBlobDataAdaptor.cs @@ -0,0 +1,55 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using DMLibTestCodeGen; + using BlobTypeConst = DMLibTest.BlobType; + + internal class URIBlobDataAdaptor : CloudBlobDataAdaptor + { + public URIBlobDataAdaptor(TestAccount testAccount, string containerName) + : base (testAccount, containerName, BlobTypeConst.Block, SourceOrDest.Source) + { + base.MakePublic(); + } + + public override void Reset() + { + // Do nothing, keep the container public + } + + public override object GetTransferObject(FileNode fileNode) + { + return base.GetCloudBlobReference(fileNode).Uri; + } + + public override object GetTransferObject(DirNode dirNode) + { + throw new InvalidOperationException("Can't get directory transfer object in URI data adaptor."); + } + + protected override string BlobType + { + get + { + DMLibDataType destDataType = DMLibTestContext.DestType; + if (destDataType == DMLibDataType.PageBlob) + { + return BlobTypeConst.Page; + } + else if (destDataType == DMLibDataType.AppendBlob) + { + return BlobTypeConst.Append; + } + else + { + return BlobTypeConst.Block; + } + } + } + } +} diff --git a/test/DMLibTest/Framework/UploadWrapper.cs b/test/DMLibTest/Framework/UploadWrapper.cs new file mode 100644 index 00000000..525a9d62 --- /dev/null +++ b/test/DMLibTest/Framework/UploadWrapper.cs @@ -0,0 +1,69 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System.IO; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.DataMovement; + using Microsoft.WindowsAzure.Storage.File; + + internal class UploadWrapper : DMLibWrapper + { + public UploadWrapper() + { + } + + protected override Task DoTransferImp(TransferItem item) + { + return this.Upload(item.DestObject, item); + } + + private Task Upload(dynamic destObject, TransferItem item) + { + UploadOptions uploadOptions = item.Options as UploadOptions; + TransferContext transferContext = item.TransferContext; + CancellationToken cancellationToken = item.CancellationToken; + string sourcePath = item.SourceObject as string; + Stream sourceStream = item.SourceObject as Stream; + + if (cancellationToken != null && cancellationToken != CancellationToken.None) + { + if (sourcePath != null) + { + return TransferManager.UploadAsync(sourcePath, destObject, uploadOptions, transferContext, cancellationToken); + } + else + { + return TransferManager.UploadAsync(sourceStream, destObject, uploadOptions, transferContext, cancellationToken); + } + } + else if (transferContext != null || uploadOptions != null) + { + if (sourcePath != null) + { + return TransferManager.UploadAsync(sourcePath, destObject, uploadOptions, transferContext); + } + else + { + return TransferManager.UploadAsync(sourceStream, destObject, uploadOptions, transferContext); + } + } + else + { + if (sourcePath != null) + { + return TransferManager.UploadAsync(sourcePath, destObject); + } + else + { + return TransferManager.UploadAsync(sourceStream, destObject); + } + } + } + } +} diff --git a/test/DMLibTest/Framework/VerificationHelper.cs b/test/DMLibTest/Framework/VerificationHelper.cs new file mode 100644 index 00000000..263ef9e4 --- /dev/null +++ b/test/DMLibTest/Framework/VerificationHelper.cs @@ -0,0 +1,67 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using DMLibTestCodeGen; + using Microsoft.WindowsAzure.Storage; + using Microsoft.WindowsAzure.Storage.DataMovement; + using MS.Test.Common.MsTestLib; + + public static class VerificationHelper + { + public static void VerifySingleObjectResumeResult(TestResult result, DMLibDataInfo expectedDataInfo) + { + if (DMLibTestContext.SourceType != DMLibDataType.Stream && DMLibTestContext.DestType != DMLibDataType.Stream) + { + Test.Assert(result.Exceptions.Count == 0, "Verify no exception is thrown."); + Test.Assert(DMLibDataHelper.Equals(expectedDataInfo, result.DataInfo), "Verify transfer result."); + } + else + { + Test.Assert(result.Exceptions.Count == 1, "Verify stream resume is not supported"); + Exception exception = result.Exceptions[0]; + Test.Assert(exception is NotSupportedException, "Verify stream resume is not supported"); + } + } + + public static void VerifyTransferException(Exception exception, TransferErrorCode expectedErrorCode, params string[] expectedMessages) + { + TransferException transferException = exception as TransferException; + if (transferException == null) + { + Test.Error("Verify exception is a transfer exception."); + return; + } + + Test.Assert(transferException.ErrorCode == expectedErrorCode, "Verify error code: {0}, expected: {1}", transferException.ErrorCode, expectedErrorCode); + VerificationHelper.VerifyExceptionErrorMessage(exception, expectedMessages); + } + + public static void VerifyStorageException(Exception exception, int expectedHttpStatusCode, params string[] expectedMessages) + { + StorageException storageException = exception as StorageException; + if (storageException == null) + { + Test.Error("Verify exception is a storage exception."); + return; + } + + Test.Assert(storageException.RequestInformation.HttpStatusCode == expectedHttpStatusCode, "Verify http status code: {0}, expected: {1}", storageException.RequestInformation.HttpStatusCode, expectedHttpStatusCode); + VerificationHelper.VerifyExceptionErrorMessage(exception, expectedMessages); + } + + public static void VerifyExceptionErrorMessage(Exception exception, params string[] expectedMessages) + { + Test.Info("Error message: {0}", exception.Message); + + foreach (string expectedMessage in expectedMessages) + { + Test.Assert(exception.Message.Contains(expectedMessage), "Verify exception message contains {0}", expectedMessage); + } + } + } +} diff --git a/test/DMLibTest/Properties/AssemblyInfo.cs b/test/DMLibTest/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..684f96e6 --- /dev/null +++ b/test/DMLibTest/Properties/AssemblyInfo.cs @@ -0,0 +1,14 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("DMLibTest")] +[assembly: AssemblyDescription("")] diff --git a/test/DMLibTest/TestData.xml b/test/DMLibTest/TestData.xml new file mode 100644 index 00000000..c70b7e18 --- /dev/null +++ b/test/DMLibTest/TestData.xml @@ -0,0 +1,22 @@ + + + true + false + DMLibTest.log + true + true + true + true + DevFabric + DefaultEndpointsProtocol=https;AccountName=testaccount1;AccountKey=FjUfNl1KiJttbXlsdkMzBTC7WagvrRM9/g6UPBuy0ypCpAbYTL6/KA+dI/7gyoWvLFYmah3IviUP1jykOHHOlA==;BlobEndpoint=http://127.0.0.1:10000/testaccount1;QueueEndpoint=http://127.0.0.1:10001/testaccount1;TableEndpoint=http://127.0.0.1:10002/testaccount1;FileEndpoint=http://127.0.0.1:10004/testaccount1 + DefaultEndpointsProtocol=https;AccountName=dmtestaccount1;AccountKey=FjUfNl1KiJttbXlsdkMzBTC7WagvrRM9/g6UPBuy0ypCpAbYTL6/KA+dI/7gyoWvLFYmah3IviUP1jykOHHOlA==;BlobEndpoint=http://127.0.0.1:10000/dmtestaccount1;QueueEndpoint=http://127.0.0.1:10001/dmtestaccount1;TableEndpoint=http://127.0.0.1:10002/dmtestaccount1;FileEndpoint=http://127.0.0.1:10004/dmtestaccount1 + testfile + 1024 + testfolder + \\127.0.0.1\Azcopy + journal + 200 + 20 + 3 + 3 + diff --git a/test/DMLibTest/Util/DMLibTestConstants.cs b/test/DMLibTest/Util/DMLibTestConstants.cs new file mode 100644 index 00000000..fac23d1a --- /dev/null +++ b/test/DMLibTest/Util/DMLibTestConstants.cs @@ -0,0 +1,111 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using Microsoft.WindowsAzure.Storage.DataMovement; + using MS.Test.Common.MsTestLib; + + public static class Tag + { + public const string BVT = "bvt"; + public const string Function = "function"; + public const string Stress = "stress"; + public const string Performance = "perf"; + } + + public static class Protocol + { + public const string Http = "http"; + + public static string Https + { + get + { + if (DMLibTestHelper.DisableHttps()) + { + return "http"; + } + + return "https"; + } + } + } + + public static class BlobType + { + public const string Page = "page"; + public const string Block = "block"; + public const string Append = "append"; + } + + public static class DMLibTestConstants + { + public const string ConnStr = "StorageConnectionString"; + public const string ConnStr2 = "StorageConnectionString2"; + public static readonly int DefaultNC = TransferManager.Configurations.ParallelOperations; + public static readonly int LimitedSpeedNC = 4; + + private static Random random = new Random(); + + public static int FlatFileCount + { + get + { + int flatFileCount; + try + { + flatFileCount = int.Parse(Test.Data.Get("FlatFileCount")); + } + catch + { + flatFileCount = 20; + } + + Test.Verbose("Flat file count: {0}", flatFileCount); + return flatFileCount; + } + } + + public static int RecursiveFolderWidth + { + get + { + int recursiveFolderWidth; + try + { + recursiveFolderWidth = int.Parse(Test.Data.Get("RecursiveFolderWidth")); + } + catch + { + recursiveFolderWidth = random.Next(3, 5); + } + + Test.Verbose("Recursive folder width: {0}", recursiveFolderWidth); + return recursiveFolderWidth; + } + } + + public static int RecursiveFolderDepth + { + get + { + int recursiveFolderDepth; + try + { + recursiveFolderDepth = int.Parse(Test.Data.Get("RecursiveFolderDepth")); + } + catch + { + recursiveFolderDepth = random.Next(3, 5); + } + + Test.Verbose("Recursive folder depth: {0}", recursiveFolderDepth); + return recursiveFolderDepth; + } + } + } +} diff --git a/test/DMLibTest/Util/DMLibTestHelper.cs b/test/DMLibTest/Util/DMLibTestHelper.cs new file mode 100644 index 00000000..b9da5696 --- /dev/null +++ b/test/DMLibTest/Util/DMLibTestHelper.cs @@ -0,0 +1,411 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Threading; + using Microsoft.WindowsAzure.Storage; + using MS.Test.Common.MsTestLib; + using StorageBlob = Microsoft.WindowsAzure.Storage.Blob; + + public enum FileSizeUnit + { + B, + KB, + MB, + GB, + } + + public enum TestAgainst + { + PublicAzure, + TestTenant, + DevFabric + } + + public class SummaryInterval + { + /// + /// Min value which is inclusive. + /// + private int minValue; + + /// + /// Max value which is inclusive. + /// + private int maxValue; + + public SummaryInterval(int minValue, int maxValue) + { + this.minValue = minValue; + this.maxValue = maxValue; + } + + public int MinValue + { + get + { + return this.minValue; + } + } + + public int MaxValue + { + get + { + return this.maxValue; + } + } + + public bool InsideInterval(int value) + { + return value >= this.minValue && value <= this.maxValue; + } + } + + public static class DMLibTestHelper + { + private static Random random = new Random(); + + private static readonly char[] validSuffixChars = "abcdefghijkjlmnopqrstuvwxyz".ToCharArray(); + + public static void KeepFilesWhenCaseFail(params string[] filesToKeep) + { + if (Test.ErrorCount > 0) + { + const string debugFilePrefix = "debug_file_"; + string folderName = Guid.NewGuid().ToString(); + Directory.CreateDirectory(folderName); + + Test.Info("Move files to folder {0} for debug.", folderName); + + for (int i = 0; i < filesToKeep.Length; ++i) + { + string debugFileName = debugFilePrefix + i; + string debugFilePath = Path.Combine(folderName, debugFileName); + File.Move(filesToKeep[i], debugFilePath); + + Test.Info("{0} ---> {1}", filesToKeep[i], debugFileName); + } + } + } + + public static string RandomContainerName() + { + return Test.Data.Get("containerName") + RandomNameSuffix(); + } + + public static string RandomNameSuffix() + { + return FileOp.NextString(random, 6, validSuffixChars); + } + + public static bool WaitForProcessExit(Process p, int timeoutInSecond) + { + bool exit = p.WaitForExit(timeoutInSecond * 1000); + if (!exit) + { + Test.Assert(false, "Process {0} should exit in {1} s.", p.ProcessName, timeoutInSecond); + p.Kill(); + return false; + } + + return true; + } + + public static string RandomizeCase(string value) + { + return ConvertRandomCharsToUpperCase(value.ToLower()); + } + + public static void UploadFromByteArray(this StorageBlob.CloudBlob cloudBlob, byte[] randomData) + { + if (StorageBlob.BlobType.BlockBlob == cloudBlob.BlobType) + { + (cloudBlob as StorageBlob.CloudBlockBlob).UploadFromByteArray(randomData, 0, randomData.Length); + } + else if (StorageBlob.BlobType.PageBlob == cloudBlob.BlobType) + { + (cloudBlob as StorageBlob.CloudPageBlob).UploadFromByteArray(randomData, 0, randomData.Length); + } + else if (StorageBlob.BlobType.AppendBlob == cloudBlob.BlobType) + { + (cloudBlob as StorageBlob.CloudAppendBlob).UploadFromByteArray(randomData, 0, randomData.Length); + } + else + { + throw new InvalidOperationException(string.Format("Invalid blob type: {0}", cloudBlob.BlobType)); + } + } + + public static void UploadFromFile(this StorageBlob.CloudBlob cloudBlob, + string path, + FileMode mode, + AccessCondition accessCondition = null, + StorageBlob.BlobRequestOptions options = null, + OperationContext operationContext = null) + { + if (StorageBlob.BlobType.BlockBlob == cloudBlob.BlobType) + { + (cloudBlob as StorageBlob.CloudBlockBlob).UploadFromFile(path, mode, accessCondition, options, operationContext); + } + else if (StorageBlob.BlobType.PageBlob == cloudBlob.BlobType) + { + (cloudBlob as StorageBlob.CloudPageBlob).UploadFromFile(path, mode, accessCondition, options, operationContext); + } + else if (StorageBlob.BlobType.AppendBlob == cloudBlob.BlobType) + { + (cloudBlob as StorageBlob.CloudAppendBlob).UploadFromFile(path, mode, accessCondition, options, operationContext); + } + else + { + throw new InvalidOperationException(string.Format("Invalid blob type: {0}", cloudBlob.BlobType)); + } + } + + public static void UploadFromStream(this StorageBlob.CloudBlob cloudBlob, Stream source) + { + if (StorageBlob.BlobType.BlockBlob == cloudBlob.BlobType) + { + (cloudBlob as StorageBlob.CloudBlockBlob).UploadFromStream(source); + } + else if (StorageBlob.BlobType.PageBlob == cloudBlob.BlobType) + { + (cloudBlob as StorageBlob.CloudPageBlob).UploadFromStream(source); + } + else if (StorageBlob.BlobType.AppendBlob == cloudBlob.BlobType) + { + (cloudBlob as StorageBlob.CloudAppendBlob).UploadFromStream(source); + } + else + { + throw new InvalidOperationException(string.Format("Invalid blob type: {0}", cloudBlob.BlobType)); + } + } + + public static void WaitForACLTakeEffect() + { + if (DMLibTestHelper.GetTestAgainst() != TestAgainst.DevFabric) + { + Test.Info("Waiting for 30s to ensure the ACL take effect on server side..."); + Thread.Sleep(30 * 1000); + } + } + + public static string RandomProtocol() + { + if (0 == new Random().Next(2)) + { + return Protocol.Http; + } + + return Protocol.Https; + } + + public static bool DisableHttps() + { + if (DMLibTestHelper.GetTestAgainst() == TestAgainst.TestTenant) + { + return true; + } + + return false; + } + + public static TestAgainst GetTestAgainst() + { + string testAgainst = string.Empty; + try + { + testAgainst = Test.Data.Get("TestAgainst"); + } + catch + { + } + + if (String.Compare(testAgainst, "publicazure", true) == 0) + { + return TestAgainst.PublicAzure; + } + else if (String.Compare(testAgainst, "testtenant", true) == 0) + { + return TestAgainst.TestTenant; + } + else if (String.Compare(testAgainst, "devfabric", true) == 0) + { + return TestAgainst.DevFabric; + } + + // Use dev fabric by default + return TestAgainst.DevFabric; + } + + public static List GetFileAttributesFromParameter(string s) + { + List Lfa = new List(); + + if (null == s) + { + return Lfa; + } + + foreach (char c in s) + { + switch (c) + { + case 'R': + if (!Lfa.Contains(FileAttributes.ReadOnly)) + Lfa.Add(FileAttributes.ReadOnly); + break; + case 'A': + if (!Lfa.Contains(FileAttributes.Archive)) + Lfa.Add(FileAttributes.Archive); + break; + case 'S': + if (!Lfa.Contains(FileAttributes.System)) + Lfa.Add(FileAttributes.System); + break; + case 'H': + if (!Lfa.Contains(FileAttributes.Hidden)) + Lfa.Add(FileAttributes.Hidden); + break; + case 'C': + if (!Lfa.Contains(FileAttributes.Compressed)) + Lfa.Add(FileAttributes.Compressed); + break; + case 'N': + if (!Lfa.Contains(FileAttributes.Normal)) + Lfa.Add(FileAttributes.Normal); + break; + case 'E': + if (!Lfa.Contains(FileAttributes.Encrypted)) + Lfa.Add(FileAttributes.Encrypted); + break; + case 'T': + if (!Lfa.Contains(FileAttributes.Temporary)) + Lfa.Add(FileAttributes.Temporary); + break; + case 'O': + if (!Lfa.Contains(FileAttributes.Offline)) + Lfa.Add(FileAttributes.Offline); + break; + case 'I': + if (!Lfa.Contains(FileAttributes.NotContentIndexed)) + Lfa.Add(FileAttributes.NotContentIndexed); + break; + default: + break; + } + } + return Lfa; + } + + public static List GenerateFileWithAttributes( + string folder, + string filePrefix, + int number, + List includeAttributes, + List excludeAttributes, + int fileSizeInUnit = 1, + FileSizeUnit unit = FileSizeUnit.KB) + { + List fileNames = new List(number); + + for (int i = 0; i < number; i++) + { + string fileName = filePrefix + i.ToString(); + string filePath = Path.Combine(folder, fileName); + fileNames.Add(fileName); + + DMLibTestHelper.PrepareLocalFile(filePath, fileSizeInUnit, unit); + + if (includeAttributes != null) + foreach (FileAttributes fa in includeAttributes) + FileOp.SetFileAttribute(filePath, fa); + if (excludeAttributes != null) + foreach (FileAttributes fa in excludeAttributes) + FileOp.RemoveFileAttribute(filePath, fa); + } + + return fileNames; + } + + public static void PrepareLocalFile(string filePath, long fileSizeInUnit, FileSizeUnit fileSizeUnit) + { + if (FileSizeUnit.B == fileSizeUnit) + { + Helper.GenerateFileInBytes(filePath, fileSizeInUnit); + } + else if (FileSizeUnit.KB == fileSizeUnit) + { + Helper.GenerateFileInKB(filePath, fileSizeInUnit); + } + else if (FileSizeUnit.MB == fileSizeUnit) + { + Helper.GenerateFileInMB(filePath, fileSizeInUnit); + } + else + { + Helper.GenerateFileInGB(filePath, fileSizeInUnit); + } + } + + public static bool ContainsIgnoreCase(string baseString, string subString) + { + return (baseString.IndexOf(subString, StringComparison.OrdinalIgnoreCase) >= 0); + } + + private static string ConvertRandomCharsToUpperCase(string input) + { + Random rnd = new Random(); + char[] array = input.ToCharArray(); + + for (int i = 0; i < array.Length; ++i) + { + if (Char.IsLower(array[i]) && rnd.Next() % 2 != 0) + { + array[i] = Char.ToUpper(array[i]); + } + } + + return new string(array); + } + + /// + /// Append snapshot time to a file name. + /// + /// Original file name. + /// Snapshot time to append. + /// A file name with appended snapshot time. + public static string AppendSnapShotTimeToFileName(string fileName, DateTimeOffset? snapshotTime) + { + string resultName = fileName; + + if (snapshotTime.HasValue) + { + string pathAndFileNameNoExt = Path.ChangeExtension(fileName, null); + string extension = Path.GetExtension(fileName); + string timeStamp = string.Format( + CultureInfo.InvariantCulture, + "{0:yyyy-MM-dd HHmmss fff}", + snapshotTime.Value); + + resultName = string.Format( + CultureInfo.InvariantCulture, + "{0} ({1}){2}", + pathAndFileNameNoExt, + timeStamp, + extension); + } + + return resultName; + } + } +} diff --git a/test/DMLibTest/Util/Helpers.cs b/test/DMLibTest/Util/Helpers.cs new file mode 100644 index 00000000..84a32be2 --- /dev/null +++ b/test/DMLibTest/Util/Helpers.cs @@ -0,0 +1,3668 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.IO.Compression; + using System.Linq; + using System.Runtime.InteropServices; + using System.Security.Cryptography; + using System.Text; + using System.Text.RegularExpressions; + using System.Threading; + using System.Threading.Tasks; + + using Microsoft.WindowsAzure.Storage; + using Microsoft.WindowsAzure.Storage.Auth; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.File; + using Microsoft.WindowsAzure.Storage.RetryPolicies; + using Microsoft.WindowsAzure.Storage.Table; + using MS.Test.Common.MsTestLib; + using StorageBlobType = Microsoft.WindowsAzure.Storage.Blob.BlobType; + + /// + /// this is a static helper class + /// + public static class Helper + { + private static Random random = new Random(); + + public static void CopyLocalDirectory(string sourceDir, string destDir, bool recursive) + { + if (!Directory.Exists(destDir)) + { + Directory.CreateDirectory(destDir); + } + + foreach (var subDir in Directory.GetDirectories(sourceDir, "*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)) + { + Directory.CreateDirectory(ConvertSourceToDestPath(sourceDir, destDir, subDir)); + } + + foreach (var file in Directory.GetFiles(sourceDir, "*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)) + { + File.Copy(file, ConvertSourceToDestPath(sourceDir, destDir, file)); + } + } + + private static string ConvertSourceToDestPath(string sourceRoot, string destRoot, string path) + { + int index = path.IndexOf(sourceRoot, StringComparison.OrdinalIgnoreCase); + string relativePath = path.Substring(index + sourceRoot.Length); + + if (relativePath.StartsWith(Path.DirectorySeparatorChar.ToString())) + { + relativePath = relativePath.Substring(1); + } + + return Path.Combine(destRoot, relativePath); + } + + /// + /// list blobs in a container, return blob name list and content MD5 list + /// + /// + /// + /// + public static bool ListBlobs(string connectionString, string containerName, out List blobNames, out List blobMD5s) + { + CloudBlobClient BlobClient = CloudStorageAccount.Parse(connectionString).CreateCloudBlobClient(); + BlobClient.DefaultRequestOptions.RetryPolicy = new LinearRetry(TimeSpan.Zero, 3); + + blobNames = new List(); + blobMD5s = new List(); + + try + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + IEnumerable blobs = container.ListBlobs(null, true, BlobListingDetails.All); + if (blobs != null) + { + foreach (CloudBlob blob in blobs) + { + blob.FetchAttributes(); + blobNames.Add(blob.Name); + blobMD5s.Add(blob.Properties.ContentMD5); + } + } + return true; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return false; + } + throw; + } + } + + public static void WaitForTakingEffect(dynamic cloudStorageClient) + { + Test.Assert( + cloudStorageClient is CloudBlobClient || cloudStorageClient is CloudTableClient || cloudStorageClient is CloudFileClient, + "The argument should only be CloudStorageClient."); + + if (DMLibTestHelper.GetTestAgainst() != TestAgainst.PublicAzure) + { + return; + } + + DateTimeOffset? lastSyncTime = cloudStorageClient.GetServiceStats().GeoReplication.LastSyncTime; + DateTimeOffset currentTime = DateTimeOffset.UtcNow; + int maxWaitCount = 120; + + DateTimeOffset? newLastSyncTime = cloudStorageClient.GetServiceStats().GeoReplication.LastSyncTime; + + while ((maxWaitCount > 0) + && (!newLastSyncTime.HasValue || (lastSyncTime.HasValue && newLastSyncTime.Value <= lastSyncTime.Value) || newLastSyncTime.Value <= currentTime)) + { + --maxWaitCount; + Test.Info("Waiting......"); + Thread.Sleep(10000); + newLastSyncTime = cloudStorageClient.GetServiceStats().GeoReplication.LastSyncTime; + } + + if (maxWaitCount <= 0) + { + Test.Info("NOTE: Wait for taking effect timed out, cases may fail..."); + } + } + + public static bool RandomBoolean() + { + return random.Next(0, 2) % 2 == 0; + } + + public static string RandomBlobType() + { + int rnd = random.Next(0, 3); + if (rnd == 0) + { + return BlobType.Block; + } + else if (rnd == 1) + { + return BlobType.Page; + } + else + { + return BlobType.Append; + } + } + + public static void GenerateFileInBytes(string filename, long sizeB) + { + Random r = new Random(); + byte[] data; + using (FileStream stream = new FileStream(filename, FileMode.Create)) + { + var oneMBInBytes = 1024 * 1024; + var sizeInMB = sizeB / oneMBInBytes; + data = new byte[oneMBInBytes]; + for (int i = 0; i < sizeInMB; i++) + { + r.NextBytes(data); + stream.Write(data, 0, data.Length); + } + + var restSizeInB = sizeB % oneMBInBytes; + data = new byte[restSizeInB]; + r.NextBytes(data); + stream.Write(data, 0, data.Length); + } + } + + public static void GenerateFileInKB(string filename, long sizeKB) + { + byte[] data = new byte[sizeKB * 1024]; + Random r = new Random(); + r.NextBytes(data); + File.WriteAllBytes(filename, data); + return; + } + + //it takes around 74 seconds to generate a 5G file + public static void GenerateFileInMB(string filename, long sizeMB) + { + byte[] data = new byte[1024 * 1024]; + Random r = new Random(); + using (FileStream stream = new FileStream(filename, FileMode.Create)) + { + for (int i = 0; i < sizeMB; i++) + { + r.NextBytes(data); + stream.Write(data, 0, data.Length); + } + } + + return; + } + + // the buffer is too large, better to use GenerateMediumFile + public static void GenerateFileInGB(string filename, long sizeGB) + { + byte[] data = new byte[4 * 1024 * 1024]; + long chunkCount = 256 * sizeGB; + Random r = new Random(); + using (FileStream stream = new FileStream(filename, FileMode.Create)) + { + for (int i = 0; i < chunkCount; i++) + { + r.NextBytes(data); + stream.Write(data, 0, data.Length); + } + } + + return; + } + + public static void GenerateEmptyFile(string filename) + { + if (File.Exists(filename)) + { + Test.Info("GenerateEmptyFile: delte existing file"); + File.Delete(filename); + } + + using (FileStream file = File.Create(filename)) + { + } + } + + public static void AggregateFile(string filename, int times) + { + using (FileStream outputStream = new FileStream(filename, FileMode.Create)) + { + using (FileStream inputStream = new FileStream("abc.txt", FileMode.Open)) + { + for (int i = 0; i < times; i++) + { + inputStream.CopyTo(outputStream); + inputStream.Seek(0, SeekOrigin.Begin); + } + } + } + } + + public static void CompressFile(string filename, int times) + { + using (FileStream outputStream = new FileStream(filename, FileMode.Create)) + { + using (GZipStream compress = new GZipStream(outputStream, CompressionMode.Compress)) + { + + using (FileStream inputStream = new FileStream("abc.txt", FileMode.Open)) + { + for (int i = 0; i < times; i++) + { + inputStream.CopyTo(compress); + inputStream.Seek(0, SeekOrigin.Begin); + } + } + } + } + } + + public static void GenerateLargeFileinKB(string filename, long sizeinKB) + { + byte[] data4MB = new byte[4 * 1024 * 1024]; + byte[] dataMB = new byte[1024 * 1024]; + Random r = new Random(); + using (FileStream stream = new FileStream(filename, FileMode.Create)) + { + long sizeGB = sizeinKB / (1024 * 1024); + long sizeMB = sizeinKB % (1024 * 1024) / 1024; + long sizeKB = sizeinKB % 1024; + for (long i = 0; i < sizeGB * 256; i++) + { + r.NextBytes(data4MB); + stream.Write(data4MB, 0, data4MB.Length); + } + for (long i = 0; i < sizeMB; i++) + { + r.NextBytes(dataMB); + stream.Write(dataMB, 0, dataMB.Length); + } + if (sizeKB != 0) + { + byte[] dataKB = new byte[sizeKB * 1024]; + r.NextBytes(dataKB); + stream.Write(dataKB, 0, dataKB.Length); + } + } + + return; + } + + //this is only for small data + public static byte[] GetMD5(byte[] data) + { + MD5 md5 = MD5.Create(); + return md5.ComputeHash(data); + } + + public static void GenerateRandomTestFile(string filename, long sizeKB, bool createDirIfNotExist = false) + { + if (createDirIfNotExist) + { + string dir = Path.GetDirectoryName(filename); + if (!string.IsNullOrEmpty(dir)) + { + Directory.CreateDirectory(dir); + } + } + + byte[] data = new byte[sizeKB * 1024]; + Random r = new Random(); + r.NextBytes(data); + File.WriteAllBytes(filename, data); + } + + public static void DeleteFile(string filename) + { + if (File.Exists(filename)) + { + File.Delete(filename); + } + } + + public static void CleanupFolder(string foldername) + { + DirectoryInfo rootFolderInfo = new DirectoryInfo(foldername); + + if (!rootFolderInfo.Exists) + { + return; + } + + foreach (FileInfo fileInfo in rootFolderInfo.GetFiles()) + { + ForceDeleteFile(fileInfo.FullName); + } + + foreach (DirectoryInfo subFolderInfo in rootFolderInfo.GetDirectories()) + { + ForceDeleteFiles(subFolderInfo.FullName); + } + } + + public static void DeleteFolder(string foldername) + { + if (Directory.Exists(foldername)) + { + ForceDeleteFiles(foldername); + } + } + + private static void ForceDeleteFile(string filename) + { + try + { + File.Delete(filename); + } + catch + { + FileOp.SetFileAttribute(filename, FileAttributes.Normal); + File.Delete(filename); + } + } + + private static void ForceDeleteFiles(string foldername) + { + try + { + Directory.Delete(foldername, true); + } + catch (Exception) + { + RecursiveRemoveReadOnlyAttribute(foldername); + Directory.Delete(foldername, true); + } + } + + private static void RecursiveRemoveReadOnlyAttribute(string foldername) + { + foreach (string filename in Directory.GetFiles(foldername)) + { + FileOp.SetFileAttribute(filename, FileAttributes.Normal); + } + + foreach (string folder in Directory.GetDirectories(foldername)) + { + RecursiveRemoveReadOnlyAttribute(folder); + } + } + + public static void DeletePattern(string pathPattern) + { + DirectoryInfo folder = new DirectoryInfo("."); + foreach (FileInfo fi in folder.GetFiles(pathPattern, SearchOption.TopDirectoryOnly)) + { + fi.Delete(); + } + foreach (DirectoryInfo di in folder.GetDirectories(pathPattern, SearchOption.TopDirectoryOnly)) + { + di.Delete(true); + } + } + + public static void CreateNewFolder(string foldername) + { + if (Directory.Exists(foldername)) + { + Directory.Delete(foldername, true); + } + if (File.Exists(foldername)) + { + File.Delete(foldername); + } + + Directory.CreateDirectory(foldername); + } + + // for a 5G file, this can be done in 20 seconds + public static string GetFileMD5Hash(string filename) + { + using (FileStream fs = File.Open(filename, FileMode.Open)) + { + MD5 md5 = MD5.Create(); + byte[] md5Hash = md5.ComputeHash(fs); + + + StringBuilder sb = new StringBuilder(); + foreach (byte b in md5Hash) + { + sb.Append(b.ToString("x2").ToLower()); + } + + return sb.ToString(); + } + } + + public static string GetFileContentMD5(string filename) + { + using (FileStream fs = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + MD5 md5 = MD5.Create(); + byte[] md5Hash = md5.ComputeHash(fs); + + return Convert.ToBase64String(md5Hash); + } + } + + public static List GenerateFlatTestFolder(string fileNamePrefix, string parentDir, int fileSizeInKB = -1, bool doNotGenerateFile = false) + { + return GenerateFixedTestTree(fileNamePrefix, string.Empty, parentDir, DMLibTestConstants.FlatFileCount, 0, fileSizeInKB, doNotGenerateFile); + } + + public static List GenerateRecursiveTestFolder(string fileNamePrefix, string dirNamePrefix, string parentDir, int fileSizeInKB = -1, bool doNotGenerateFile = false) + { + return GenerateFixedTestTree(fileNamePrefix, dirNamePrefix, parentDir, DMLibTestConstants.RecursiveFolderWidth, DMLibTestConstants.RecursiveFolderDepth, fileSizeInKB, doNotGenerateFile); + } + + public static List GenerateFixedTestTree(string fileNamePrefix, string dirNamePrefix, string parentDir, int width, int depth, int fileSizeInKB = -1, bool doNotGenerateFile = false) + { + var fileList = new List(); + for (int i = 0; i < width; i++) + { + var fileName = parentDir + "\\" + fileNamePrefix + "_" + i; + fileList.Add(fileName); + if (!doNotGenerateFile) + { + GenerateRandomTestFile(fileName, fileSizeInKB < 0 ? i : fileSizeInKB); + } + } + + if (depth > 0) + { + for (int i = 0; i < width; i++) + { + string dirName = parentDir + "\\" + dirNamePrefix + "_" + i; + + if (!doNotGenerateFile) + { + Directory.CreateDirectory(dirName); + } + + fileList.AddRange(GenerateFixedTestTree(fileNamePrefix, dirNamePrefix, dirName, width, depth - 1, fileSizeInKB, doNotGenerateFile)); + } + } + + return fileList; + } + + public static List TraversalFolderInDepth(string folderName) + { + List files = new List(); + Stack dirStack = new Stack(); + dirStack.Push(folderName); + + while (dirStack.Count > 0) + { + string currentFolder = dirStack.Pop(); + + foreach (string file in Directory.EnumerateFiles(currentFolder)) + { + files.Add(file); + } + + Stack foldersUnderCurrent = new Stack(); + + foreach (string folder in Directory.EnumerateDirectories(currentFolder)) + { + foldersUnderCurrent.Push(folder); + } + + foreach (string folderPath in foldersUnderCurrent) + { + dirStack.Push(folderPath); + } + } + + return files; + } + + public static void CompareBlobAndFile(string filename, CloudBlob blob) + { + string tempblob = "tempblob"; + DeleteFile(tempblob); + try + { + if (!File.Exists(filename)) + Test.Error("The file {0} should exist", filename); + if (blob == null) + Test.Error("The blob {0} should exist", blob.Name); + using (FileStream fileStream = new FileStream(tempblob, FileMode.Create)) + { + BlobRequestOptions bro = new BlobRequestOptions(); + bro.RetryPolicy = new LinearRetry(new TimeSpan(0, 0, 30), 3); + bro.ServerTimeout = new TimeSpan(1, 30, 0); + bro.MaximumExecutionTime = new TimeSpan(1, 30, 0); + blob.DownloadToStream(fileStream, null, bro); + fileStream.Close(); + } + string MD51 = Helper.GetFileContentMD5(tempblob); + string MD52 = Helper.GetFileContentMD5(filename); + + if (MD51 != MD52) + Test.Error("{2}: {0} == {1}", MD51, MD52, filename); + DeleteFile(tempblob); + } + catch (Exception e) + { + Test.Error("Meet Excpetion when download and compare blob {0}, file{1}, Excpetion: {2}", blob.Name, filename, e.ToString()); + DeleteFile(tempblob); + return; + } + } + + public static bool CompareTwoFiles(string filename, string filename2) + { + FileInfo fi = new FileInfo(filename); + FileInfo fi2 = new FileInfo(filename2); + return CompareTwoFiles(fi, fi2); + } + + public static bool CompareTwoFiles(FileInfo fi, FileInfo fi2) + { + if (!fi.Exists || !fi2.Exists) + { + return false; + } + if (fi.Length != fi2.Length) + { + return false; + } + + long fileLength = fi.Length; + // 4M a chunk + const int ChunkSizeByte = 4 * 1024 * 1024; + using (FileStream fs = new FileStream(fi.FullName, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + using (FileStream fs2 = new FileStream(fi2.FullName, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + BinaryReader reader = new BinaryReader(fs); + BinaryReader reader2 = new BinaryReader(fs2); + + long comparedLength = 0; + do + { + byte[] bytes = reader.ReadBytes(ChunkSizeByte); + byte[] bytes2 = reader2.ReadBytes(ChunkSizeByte); + + MD5 md5 = MD5.Create(); + byte[] md5Hash = md5.ComputeHash(bytes); + byte[] md5Hash2 = md5.ComputeHash(bytes2); + + if (!md5Hash.SequenceEqual(md5Hash2)) + { + return false; + } + + comparedLength += bytes.Length; + } while (comparedLength < fileLength); + } + } + + return true; + } + + public static bool CompareTwoFolders(string foldername, string foldername2, bool recursive = true) + { + DirectoryInfo folder = new DirectoryInfo(foldername); + DirectoryInfo folder2 = new DirectoryInfo(foldername2); + + IEnumerable list = folder.GetFiles("*.*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); + IEnumerable list2 = folder2.GetFiles("*.*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); + + FileCompare fc = new FileCompare(); + + return list.SequenceEqual(list2, fc); + } + + public static bool CompareFolderWithBlob(string foldername, string containerName) + { + return true; + } + + public static bool CompareTwoBlobs(string containerName, string containerName2) + { + return false; //todo: implement + } + + public static string[] ListToGetRelativePaths(string folderName) + { + DirectoryInfo folder = new DirectoryInfo(folderName); + IEnumerable list = folder.GetFiles("*.*", SearchOption.AllDirectories); + List relativePaths = new List(); + + string absolutePath = folder.FullName + "\\"; + + foreach (FileInfo fileInfo in list) + { + relativePaths.Add(fileInfo.FullName.Substring(absolutePath.Length, fileInfo.FullName.Length - absolutePath.Length)); + } + + return relativePaths.ToArray(); + } + + public static void verifyFilesExistinBlobDirectory(int fileNumber, CloudBlobDirectory blobDirectory, string FileName, String blobType) + { + for (int i = 0; i < fileNumber; i++) + { + string blobName = FileName + "_" + i; + CloudBlob blob = blobDirectory.GetBlobReference(blobName); + if (null == blob || !blob.Exists()) + { + Test.Error("the file {0} in the blob virtual directory does not exist:", blobName); + } + } + } + + public static void VerifyFilesExistInFileDirectory(int fileNumber, CloudFileDirectory fileDirectory, string fileName) + { + for (int i = 0; i < fileNumber; i++) + { + CloudFile cloudFile = fileDirectory.GetFileReference(fileName + "_" + i); + if (null == cloudFile || !cloudFile.Exists()) + Test.Error("the file {0}_{1} in the directory does not exist:", fileName, i); + } + } + + /// + /// calculate folder size in Byte + /// + /// the folder path + /// the folder size in Byte + public static long CalculateFolderSizeInByte(string folder) + { + long folderSize = 0; + try + { + //Checks if the path is valid or not + if (!Directory.Exists(folder)) + return folderSize; + else + { + try + { + foreach (string file in Directory.GetFiles(folder)) + { + if (File.Exists(file)) + { + FileInfo finfo = new FileInfo(file); + folderSize += finfo.Length; + } + } + + foreach (string dir in Directory.GetDirectories(folder)) + folderSize += CalculateFolderSizeInByte(dir); + } + catch (NotSupportedException e) + { + Test.Error("Unable to calculate folder size: {0}", e.Message); + throw; + } + } + } + catch (UnauthorizedAccessException e) + { + Test.Error("Unable to calculate folder size: {0}", e.Message); + throw; + } + + return folderSize; + } + + /// + /// Count number of files in the folder + /// + /// the folder path + /// whether including subfolders recursively or not + /// number of files under the folder (and subfolders) + public static int GetFileCount(string folder, bool recursive) + { + int count = 0; + try + { + //Checks if the path is valid or not + if (Directory.Exists(folder)) + { + count += Directory.GetFiles(folder).Length; + + if (recursive) + { + foreach (string dir in Directory.GetDirectories(folder)) + count += GetFileCount(dir, true); + } + } + } + catch (NotSupportedException e) + { + Test.Error("Exception thrown when accessing folder: {0}", e.Message); + throw; + } + catch (UnauthorizedAccessException e) + { + Test.Error("Exception thrown when accessing folder: {0}", e.Message); + throw; + } + + return count; + } + + public static Process StartProcess(string cmd, string args) + { + Test.Info("Running: {0} {1}", cmd, args); + ProcessStartInfo psi = new ProcessStartInfo(cmd, args); + psi.CreateNoWindow = false; + psi.UseShellExecute = false; + Process p = Process.Start(psi); + return p; + } + + public static bool WaitUntilFileCreated(string fileName, int timeoutInSeconds, bool checkContent = true) + { + int i = 0; + while (i < timeoutInSeconds) + { + FileInfo f = new FileInfo(fileName); + + // wait for file size > 0 + if (f.Exists) + { + if (!checkContent || f.Length > 0) + { + return true; + } + } + + Test.Info("waiting file '{0}' to be created...", fileName); + Thread.Sleep(1000); + i++; + } + + return false; + } + + [DllImport("kernel32.dll", CallingConvention = CallingConvention.StdCall)] + static extern bool GenerateConsoleCtrlEvent(ConsoleCtrlEvent sigevent, int dwProcessGroupId); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + public static extern bool SetConsoleCtrlHandler(HandlerRoutine Handler, bool Add); + public delegate bool HandlerRoutine(ConsoleCtrlEvent CtrlType); + + // An enumerated type for the control messages + // sent to the handler routine. + public enum ConsoleCtrlEvent + { + CTRL_C_EVENT = 0, + CTRL_BREAK_EVENT, + CTRL_CLOSE_EVENT, + CTRL_LOGOFF_EVENT = 5, + CTRL_SHUTDOWN_EVENT + } + + public static Process StartProcess(string cmd, string args, out StreamReader stdout, out StreamReader stderr, out StreamWriter stdin) + { + Test.Logger.Verbose("Running: {0} {1}", cmd, args); + ProcessStartInfo psi = new ProcessStartInfo(cmd, args); + psi.CreateNoWindow = true; + psi.WindowStyle = ProcessWindowStyle.Hidden; + psi.UseShellExecute = false; + psi.RedirectStandardError = true; + psi.RedirectStandardOutput = true; + psi.RedirectStandardInput = true; + Process p = Process.Start(psi); + stdout = p.StandardOutput; + stderr = p.StandardError; + stdin = p.StandardInput; + return p; + } + + public static Process StartProcess(string cmd, string args, out StringBuilder stdout, out StringBuilder stderr, out StreamWriter stdin, Dictionary faultInjectionPoints = null) + { + Test.Logger.Verbose("Running: {0} {1}", cmd, args); + ProcessStartInfo psi = new ProcessStartInfo(cmd, args); + psi.CreateNoWindow = true; + psi.WindowStyle = ProcessWindowStyle.Hidden; + psi.UseShellExecute = false; + psi.RedirectStandardError = true; + psi.RedirectStandardOutput = true; + psi.RedirectStandardInput = true; + if (null != faultInjectionPoints) + { + foreach (var kv in faultInjectionPoints) + { + Test.Info("Envrionment {0}:{1}", kv.Key, kv.Value); + psi.EnvironmentVariables.Add(kv.Key, kv.Value); + } + } + + Process p = Process.Start(psi); + + StringBuilder outString = new StringBuilder(); + p.OutputDataReceived += (sendingProcess, outLine) => + { + if (!String.IsNullOrEmpty(outLine.Data)) + { + outString.Append(outLine.Data + "\n"); + } + }; + + StringBuilder errString = new StringBuilder(); + p.ErrorDataReceived += (sendingProcess, outLine) => + { + if (!String.IsNullOrEmpty(outLine.Data)) + { + errString.Append(outLine.Data + "\n"); + } + }; + p.BeginOutputReadLine(); + p.BeginErrorReadLine(); + + stdout = outString; + stderr = errString; + stdin = p.StandardInput; + + return p; + } + + public static void PrintBlockBlobBlocks(CloudBlockBlob cloudBlob, bool printDetailBlock = true) + { + IEnumerable blocks = cloudBlob.DownloadBlockList(); + + Test.Info("There are {0} blocks in blob {1}: ", blocks.Count(), cloudBlob.Name); + + if (printDetailBlock) + { + foreach (var block in blocks) + { + Test.Info("BlockId:{0}, Length:{1}", block.Name, block.Length); + } + } + } + + public static void PrintPageBlobRanges(CloudPageBlob cloudBlob, bool printDetailPage = true) + { + //Write out the page ranges for the page blob. + IEnumerable ranges = cloudBlob.GetPageRanges(options: HelperConst.DefaultBlobOptions); + + Test.Info("There are {0} pages range in blob {1}: ", ranges.Count(), cloudBlob.Name); + if (printDetailPage) + { + PrintRanges(ranges); + } + } + + public static void PrintCloudFileRanges(CloudFile cloudFile, bool printDetailRange = true) + { + IEnumerable ranges = cloudFile.ListRanges(options: HelperConst.DefaultFileOptions); + + Test.Info("There are {0} ranges in cloud file {1}: ", ranges.Count(), cloudFile.Name); + if (printDetailRange) + { + PrintRanges(ranges); + } + } + + private static void PrintRanges(IEnumerable ranges) + { + foreach (var range in ranges) + { + Test.Info(" [{0}-{1}]: {2} ", range.StartOffset, range.EndOffset, range.EndOffset - range.StartOffset + 1); + } + } + + public static char GenerateRandomDelimiter() + { + List notavailList = new List(new char[] { 't', 'e', 's', 'f', 'o', 'l', 'd', 'r', 'i', '\\', '?' }); + Random rnd = new Random(); + int random; + do + { + random = rnd.Next(0x20, 0xFF); + } + while (char.GetUnicodeCategory((char)random) == UnicodeCategory.Control || notavailList.Contains((char)random)); + + return (char)random; + } + + public static string GetAccountNameFromConnectString(string connectString) + { + Dictionary dict = connectString.Split(';') + .Select(s => s.Split('=')) + .ToDictionary(key => key[0].Trim(), value => value[1].Trim()); + + return dict["AccountName"]; + } + + public static string GetBlobDirectoryUri(string blobEndpoint, string containerName, string dirName) + { + string containerUri = string.Format("{0}/{1}", blobEndpoint, containerName); + var containerRef = new CloudBlobContainer(new Uri(containerUri)); + return containerRef.GetDirectoryReference(dirName).Uri.ToString(); + } + + public static string GetXsmbDirectoryUri(string fileEndpoint, string shareName, string dirName) + { + string shareUri = string.Format("{0}/{1}", fileEndpoint, shareName); + var shareRef = new CloudFileShare(new Uri(shareUri), new StorageCredentials()); + return shareRef.GetRootDirectoryReference().GetDirectoryReference(dirName).Uri.ToString(); + } + + public static string AppendSlash(string input) + { + if (input.EndsWith("/")) + { + return input; + } + else + { + return input + "/"; + } + } + + public static bool IsNotFoundException(StorageException e) + { + if (null != e.RequestInformation && + 404 == e.RequestInformation.HttpStatusCode) + { + Test.Info("Server returns 404 error: {0}", e.ToString()); + return true; + } + + return false; + } + + public static void GenerateSparseCloudObject( + List ranges, + List gaps, + Action createObject, + Action writeUnit) + { + if (ranges.Count != gaps.Count + 1) + { + Test.Error("Invalid input for SparseCloudObject."); + } + + Test.Info("Ranges:"); + ranges.PrintAllElements(); + Test.Info("Gaps:"); + gaps.PrintAllElements(); + + int totalSize = ranges.Sum() + gaps.Sum(); + createObject(totalSize); + + int offset = 0; + for (int i = 0; i < ranges.Count; ++i) + { + int range = ranges[i]; + + Helper.WriteRange(offset, range, writeUnit); + + offset += range; + + if (i != ranges.Count - 1) + { + offset += gaps[i]; + } + } + } + + private static void WriteRange(int offset, int length, Action writeUnit) + { + int remainingLength = length; + int currentOffset = offset; + const int MaxLength = 4 * 1024 * 1024; + + while (remainingLength > 0) + { + int lengthToWrite = Math.Min(MaxLength, remainingLength); + + using (MemoryStream randomData = Helper.GetRandomData(lengthToWrite)) + { + writeUnit(currentOffset, randomData); + } + + currentOffset += lengthToWrite; + remainingLength -= lengthToWrite; + } + } + + public static MemoryStream GetRandomData(int size) + { + Random random = new Random(); + byte[] data = new byte[size]; + random.NextBytes(data); + return new MemoryStream(data); + } + + public static void Shuffle(this List list) + { + Random random = new Random(); + int currentPosition = list.Count; + while (currentPosition > 1) + { + currentPosition--; + int swapPosition = random.Next(currentPosition + 1); + var temp = list[swapPosition]; + list[swapPosition] = list[currentPosition]; + list[currentPosition] = temp; + } + } + + public static void PrintAllElements(this List list) + { + Test.Info("[{0}]", string.Join(",", list)); + } + + /// + /// return setting from testData.xml if exist, else return default value + /// + /// the name of the setting + /// the default Value of the setting + /// the setting value + public static string ParseSetting(string settingName, object defaultValue) + { + try + { + return Test.Data.Get(settingName); + } + catch + { + return defaultValue.ToString(); + } + } + } + + public class FileCompare : IEqualityComparer + { + public FileCompare() { } + + public bool Equals(FileInfo f1, FileInfo f2) + { + if (f1.Name != f2.Name) + { + Test.Verbose("file name {0}:{1} not equal {2}:{3}", f1.FullName, f1.Name, f2.FullName, f2.Name); + return false; + } + + if (f1.Length != f2.Length) + { + Test.Verbose("file length {0}:{1} not equal {2}:{3}", f1.FullName, f1.Length, f2.FullName, f2.Length); + return false; + } + + if (f1.Length < 200 * 1024 * 1024) + { + string f1MD5Hash = f1.MD5Hash(); + string f2MD5Hash = f2.MD5Hash(); + if (f1MD5Hash != f2MD5Hash) + { + Test.Verbose("file MD5 mismatch {0}:{1} not equal {2}:{3}", f1.FullName, f1MD5Hash, f2.FullName, f2MD5Hash); + return false; + } + } + else + { + if (!Helper.CompareTwoFiles(f1, f2)) + { + Test.Verbose("file MD5 mismatch {0} not equal {1}", f1.FullName, f2.FullName); + return false; + } + } + return true; + } + + public int GetHashCode(FileInfo fi) + { + string s = String.Format("{0}{1}", fi.Name, fi.Length); + return s.GetHashCode(); + } + } + + public static class FileOp + { + private const int NumberBase = 48; + private const int UpperCaseLetterBase = 65; + private const int Underline = 95; + private const int LowerCaseLetterBase = 97; + + public static HashSet AllSpecialChars { get; private set; } + + public static HashSet InvalidCharsInLocalAndCloudFileName { get; private set; } + + public static HashSet InvalidCharsInBlobName { get; private set; } + + public static List ValidSpecialCharsInLocalFileName { get; private set; } + + static FileOp() + { + AllSpecialChars = new HashSet { '$', '&', '+', ',', '/', ':', '=', '?', '@', ' ', '"', '<', '>', '#', '%', '{', '}', '|', '\\', '^', '~', '[', ']', '`', '*', '!', '(', ')', '-', '_', '\'', '.', ';' }; + InvalidCharsInLocalAndCloudFileName = new HashSet { '/', ':', '?', '"', '<', '>', '|', '\\', '*' }; + InvalidCharsInBlobName = new HashSet { '\\' }; + + ValidSpecialCharsInLocalFileName = new List(); + foreach (var ch in AllSpecialChars) + { + if (!InvalidCharsInLocalAndCloudFileName.Contains(ch)) + { + ValidSpecialCharsInLocalFileName.Add(ch); + } + } + } + + public static string MD5Hash(this FileInfo fi) + { + return Helper.GetFileMD5Hash(fi.FullName); + } + + public static string NextString(Random Randomint) + { + int length = Randomint.Next(1, 100); + return NextString(Randomint, length); + } + + public static string NextString(Random Randomint, int length) + { + if (length == 0) + { + return string.Empty; + } + + while (true) + { + var result = new String( + Enumerable.Repeat(0, length) + .Select(p => GetRandomVisiableChar(Randomint)) + .ToArray()); + result = result.Trim(); + if (result.Length == length && !string.IsNullOrWhiteSpace(result) && !result.EndsWith(".")) + { + return result; + } + } + } + + public static string NextCIdentifierString(Random random) + { + int length = random.Next(1, 255); + return NextCIdentifierString(random, length); + } + + public static string NextCIdentifierString(Random random, int length) + { + char[] charArray = + Enumerable.Repeat(0, length) + .Select(p => GetCIdentifierChar(random)) + .ToArray(); + + if (charArray[0] >= NumberBase && charArray[0] <= (NumberBase + 10)) + { + charArray[0] = '_'; + } + + return new string(charArray); + } + + public static string NextNormalString(Random random) + { + int length = random.Next(1, 255); + return NextNormalString(random, length); + } + + public static string NextNormalString(Random random, int length) + { + while (true) + { + var result = new String( + Enumerable.Repeat(0, length) + .Select(p => GetNormalChar(random)) + .ToArray()); + result = result.Trim(); + if (result.Length == length && !string.IsNullOrWhiteSpace(result) && !result.EndsWith(".")) + { + return result; + } + } + } + + public static char GetCIdentifierChar(Random random) + { + int i = random.Next(0, 63); + + if (i < 10) + { + return (char)(NumberBase + i); + } + + i = i - 10; + + if (i < 26) + { + return (char)(UpperCaseLetterBase + i); + } + + i = i - 26; + + if (i == 0) + { + return (char)(Underline); + } + + i--; + + return (char)(LowerCaseLetterBase + i); + } + + public static char GetNormalChar(Random random) + { + return (char)random.Next(0x20, 0x7E); + } + + public static string NextString(Random Randomint, int length, char[] ValidChars) + { + return new String( + Enumerable.Repeat(0, length) + .Select(p => GetRandomItem(Randomint, ValidChars)) + .ToArray()); + } + + public static string NextNonASCIIString(Random Randomint) + { + var builder = new StringBuilder(FileOp.NextString(Randomint)); + var countToInsert = Randomint.Next(1, 50); + for (int i = 0; i < countToInsert; i++) + { + char ch; + while (true) + { + ch = FileOp.GetRandomVisiableChar(Randomint); + if ((int)ch >= 0x80) + { + break; + } + } + + builder.Insert(Randomint.Next(0, builder.Length + 1), ch); + } + + return builder.ToString(); + } + + public static T GetRandomItem(Random Randomint, T[] items) + { + if (items.Length <= 0) + { + Test.Error("no candidate item"); + } + + int i = Randomint.Next(0, items.Length); + return items[i]; + } + + public static char GetRandomVisiableChar(Random Randomint) + { + double specialCharProbability = 0.05; + + if (Randomint.Next(0, 100) / 100.0 < specialCharProbability) + { + return ValidSpecialCharsInLocalFileName[Randomint.Next(0, ValidSpecialCharsInLocalFileName.Count)]; + } + else + { + while (true) + { + int i = Randomint.Next(0x20, 0xD7FF); + var ch = (char)i; + + // Control characters are all invalid to blob name. + // Characters U+200E, U+200F, U+202A, U+202B, U+202C, U+202D, U+202E are all invalid to URI. + if (char.GetUnicodeCategory(ch) != UnicodeCategory.Control && + !InvalidCharsInLocalAndCloudFileName.Contains(ch) && + i != 0x200e && i != 0x200f && + i != 0x202a && i != 0x202b && i != 0x202c && i != 0x202d && i != 0x202e) + { + return ch; + } + } + } + } + + public static string GetDriveMapping(char letter) + { + var sb = new StringBuilder(259); + if (QueryDosDevice(CreateDeviceName(letter), sb, sb.Capacity) == 0) + { + // Return empty string if the drive is not mapped + int err = Marshal.GetLastWin32Error(); + if (err == 2) return ""; + throw new System.ComponentModel.Win32Exception(); + } + return sb.ToString().Substring(4); + } + + private static string CreateDeviceName(char letter) + { + return new string(char.ToUpper(letter), 1) + ":"; + } + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern bool DefineDosDevice(int flags, string devname, string path); + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern int QueryDosDevice(string devname, StringBuilder buffer, int bufSize); + + public static void SetFileAttribute(string Filename, FileAttributes attribute) + { + FileAttributes fa = File.GetAttributes(Filename); + if ((fa & attribute) == attribute) + { + Test.Info("Attribute {0} is already in file {1}. Don't need to add again.", attribute.ToString(), Filename); + return; + } + + switch (attribute) + { + case FileAttributes.Encrypted: + string fullPath = GetFullPath(Filename); + File.Encrypt(fullPath); + break; + case FileAttributes.Normal: + RemoveFileAttribute(Filename, FileAttributes.Encrypted); + RemoveFileAttribute(Filename, FileAttributes.Compressed); + fa = FileAttributes.Normal; + File.SetAttributes(Filename, fa); + break; + case FileAttributes.Compressed: + compress(Filename); + break; + default: + fa = fa | attribute; + File.SetAttributes(Filename, fa); + break; + } + Test.Info("Attribute {0} is added to file {1}.", attribute.ToString(), Filename); + } + + private static string GetFullPath(string Filename) + { + string fullPath = Path.GetFullPath(Filename); + char driveLetter = fullPath.ToCharArray()[0]; + String actualPath = GetDriveMapping(driveLetter); + // WAES will map c:\user\tasks\workitems\{jobid} to f:\wd, + // and File.Encrypt will throw DirectoryNotFoundException + // Thus it is necessary to convert the file path to original one + if (Regex.IsMatch(actualPath, @"\w:\\", RegexOptions.IgnoreCase) == true) + { + fullPath = String.Format("{0}{1}", actualPath, fullPath.Substring(2)); + } + return fullPath; + } + + public static void RemoveFileAttribute(string Filename, FileAttributes attribute) + { + FileAttributes fa = File.GetAttributes(Filename); + if ((fa & attribute) != attribute) + { + Test.Info("Attribute {0} is NOT in file{1}. Don't need to remove.", attribute.ToString(), Filename); + return; + } + + switch (attribute) + { + case FileAttributes.Encrypted: + File.Decrypt(GetFullPath(Filename)); + break; + case FileAttributes.Normal: + fa = fa | FileAttributes.Archive; + File.SetAttributes(Filename, fa); + break; + case FileAttributes.Compressed: + uncompress(Filename); + break; + default: + fa = fa & ~attribute; + File.SetAttributes(Filename, fa); + break; + } + Test.Info("Attribute {0} is removed from file{1}.", attribute.ToString(), Filename); + } + + [DllImport("kernel32.dll")] + public static extern int DeviceIoControl(IntPtr hDevice, int + dwIoControlCode, ref short lpInBuffer, int nInBufferSize, IntPtr + lpOutBuffer, int nOutBufferSize, ref int lpBytesReturned, IntPtr + lpOverlapped); + + private static int FSCTL_SET_COMPRESSION = 0x9C040; + private static short COMPRESSION_FORMAT_DEFAULT = 1; + private static short COMPRESSION_FORMAT_NONE = 0; + +#pragma warning disable 612, 618 + public static void compress(string filename) + { + if ((File.GetAttributes(filename) & FileAttributes.Encrypted) == FileAttributes.Encrypted) + { + Test.Info("Decrypt File {0} to prepare for compress.", filename); + File.Decrypt(GetFullPath(filename)); + } + int lpBytesReturned = 0; + FileStream f = File.Open(filename, System.IO.FileMode.Open, + System.IO.FileAccess.ReadWrite, System.IO.FileShare.None); + int result = DeviceIoControl(f.Handle, FSCTL_SET_COMPRESSION, + ref COMPRESSION_FORMAT_DEFAULT, 2 /*sizeof(short)*/, IntPtr.Zero, 0, + ref lpBytesReturned, IntPtr.Zero); + f.Close(); + } + + public static void uncompress(string filename) + { + int lpBytesReturned = 0; + FileStream f = File.Open(filename, System.IO.FileMode.Open, + System.IO.FileAccess.ReadWrite, System.IO.FileShare.None); + int result = DeviceIoControl(f.Handle, FSCTL_SET_COMPRESSION, + ref COMPRESSION_FORMAT_NONE, 2 /*sizeof(short)*/, IntPtr.Zero, 0, + ref lpBytesReturned, IntPtr.Zero); + f.Close(); + } +#pragma warning restore 612, 618 + + } + + public class CloudFileHelper + { + public const string AllowedCharactersInShareName = "abcdefghijklmnopqrstuvwxyz0123456789-"; + public const string InvalidCharactersInDirOrFileName = "\"\\/:|<>*?"; + public const int MinShareNameLength = 3; + public const int MaxShareNameLength = 63; + public const int MinDirOrFileNameLength = 1; + public const int MaxDirOrFileNameLength = 255; + + public CloudStorageAccount Account + { + get; + private set; + } + + public CloudFileClient FileClient + { + get; + private set; + } + + public CloudFileHelper(CloudStorageAccount account) + { + this.Account = account; + this.FileClient = account.CreateCloudFileClient(); + this.FileClient.DefaultRequestOptions.RetryPolicy = new LinearRetry(TimeSpan.Zero, 3); + } + + public bool Exists(string shareName) + { + CloudFileShare share = this.FileClient.GetShareReference(shareName); + return share.Exists(); + } + + public bool CreateShare(string shareName) + { + CloudFileShare share = this.FileClient.GetShareReference(shareName); + return share.CreateIfNotExists(); + } + + public bool CleanupShare(string shareName) + { + return this.CleanupFileDirectory(shareName, string.Empty); + } + + public bool CleanupShareByRecreateIt(string shareName) + { + try + { + CloudFileShare share = FileClient.GetShareReference(shareName); + if (share == null || !share.Exists()) return false; + + FileRequestOptions fro = new FileRequestOptions(); + fro.RetryPolicy = new LinearRetry(new TimeSpan(0, 1, 0), 3); + + share.DeleteIfExists(null, fro); + + Test.Info("Share deleted."); + fro.RetryPolicy = new LinearRetry(new TimeSpan(0, 3, 0), 3); + + bool createSuccess = false; + int retry = 0; + while (!createSuccess && retry++ < 100) //wait up to 5 minutes + { + try + { + share.Create(fro); + createSuccess = true; + Test.Info("Share recreated."); + } + catch (StorageException e) + { + if (e.Message.Contains("(409)")) //conflict, the container is still in deleteing + { + Thread.Sleep(3000); + } + else + { + throw; + } + } + } + + return createSuccess; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return false; + } + + throw; + } + } + + public bool DeleteShare(string shareName) + { + CloudFileShare share = this.FileClient.GetShareReference(shareName); + return share.DeleteIfExists(); + } + + public bool DownloadFile(string shareName, string fileName, string filePath) + { + try + { + CloudFileShare share = FileClient.GetShareReference(shareName); + FileRequestOptions fro = new FileRequestOptions(); + fro.RetryPolicy = new LinearRetry(new TimeSpan(0, 0, 30), 3); + fro.ServerTimeout = new TimeSpan(1, 30, 0); + fro.MaximumExecutionTime = new TimeSpan(1, 30, 0); + + CloudFileDirectory root = share.GetRootDirectoryReference(); + CloudFile cloudFile = root.GetFileReference(fileName); + + using (FileStream fileStream = new FileStream(filePath, FileMode.Create)) + { + cloudFile.DownloadToStream(fileStream, null, fro); + fileStream.Close(); + } + + return true; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return false; + } + + throw; + } + } + + public bool UploadFile(string shareName, string fileName, string filePath, bool createParentIfNotExist = true) + { + CloudFileShare share = FileClient.GetShareReference(shareName); + try + { + FileRequestOptions options = new FileRequestOptions + { + RetryPolicy = new ExponentialRetry(TimeSpan.FromSeconds(60), 3), + }; + + if (createParentIfNotExist) + { + share.CreateIfNotExists(options); + string parentDirectoryPath = GetFileDirectoryName(fileName); + this.CreateFileDirectory(shareName, parentDirectoryPath); + } + } + catch (StorageException e) + { + Test.Error("UploadFile: receives StorageException when creating parent: {0}", e.ToString()); + return false; + } + catch (Exception e) + { + Test.Error("UploadFile: receives Exception when creating parent: {0}", e.ToString()); + return false; + } + + CloudFileDirectory root = share.GetRootDirectoryReference(); + CloudFile cloudFile = root.GetFileReference(fileName); + return UploadFile(cloudFile, filePath); + } + + public static bool UploadFile(CloudFile destFile, string sourceFile) + { + try + { + FileInfo fi = new FileInfo(sourceFile); + if (!fi.Exists) + { + return false; + } + + FileRequestOptions fro = new FileRequestOptions(); + fro.RetryPolicy = new LinearRetry(new TimeSpan(0, 0, 60), 5); + fro.ServerTimeout = new TimeSpan(1, 90, 0); + fro.MaximumExecutionTime = new TimeSpan(1, 90, 0); + + destFile.Create(fi.Length, null, fro); + + using (FileStream fileStream = new FileStream(sourceFile, FileMode.Open)) + { + destFile.UploadFromStream(fileStream, null, fro); + fileStream.Close(); + } + + // update content md5 + destFile.Properties.ContentMD5 = Helper.GetFileContentMD5(sourceFile); + destFile.SetProperties(null, fro); + + Test.Info("Local file {0} has been uploaded to xSMB successfully", sourceFile); + + return true; + } + catch (StorageException e) + { + Test.Error("UploadFile: receives StorageException: {0}", e.ToString()); + return false; + } + catch (Exception e) + { + Test.Error("UploadFile: receives Exception: {0}", e.ToString()); + return false; + } + } + + public static void GenerateCloudFileWithRangedData(CloudFile cloudFile, List ranges, List gaps) + { + Helper.GenerateSparseCloudObject( + ranges, + gaps, + createObject: (totalSize) => + { + cloudFile.Create(totalSize); + }, + writeUnit: (unitOffset, randomData) => + { + cloudFile.WriteRange(randomData, unitOffset, options: HelperConst.DefaultFileOptions); + }); + + Helper.PrintCloudFileRanges(cloudFile, true); + + // Set correct MD5 to cloud file + string md5 = CalculateMD5ByDownloading(cloudFile); + cloudFile.Properties.ContentMD5 = md5; + cloudFile.SetProperties(options: HelperConst.DefaultFileOptions); + } + + public CloudFile QueryFile(string shareName, string fileName) + { + try + { + CloudFileShare share = this.FileClient.GetShareReference(shareName); + CloudFileDirectory root = share.GetRootDirectoryReference(); + CloudFile file = root.GetFileReference(fileName); + + if (file.Exists()) + { + file.FetchAttributes(); + return file; + } + else + { + return null; + } + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return null; + } + + throw; + } + } + + public bool DeleteFile(string shareName, string fileName) + { + CloudFileShare share = FileClient.GetShareReference(shareName); + if (share.Exists()) + { + CloudFileDirectory root = share.GetRootDirectoryReference(); + CloudFile file = root.GetFileReference(fileName); + + return file.DeleteIfExists(); + } + + return false; + } + + public bool DeleteFileDirectory(string shareName, string fileDirectoryName) + { + CloudFileShare share = FileClient.GetShareReference(shareName); + if (!share.Exists()) + { + return false; + } + + // do not try to delete a root directory + if (string.IsNullOrEmpty(fileDirectoryName)) + { + return false; + } + + CloudFileDirectory root = share.GetRootDirectoryReference(); + CloudFileDirectory dir = root.GetDirectoryReference(fileDirectoryName); + + if (!dir.Exists()) + { + return false; + } + + DeleteFileDirectory(dir); + + return true; + } + + public CloudFileDirectory QueryFileDirectory(string shareName, string fileDirectoryName) + { + try + { + CloudFileShare share = FileClient.GetShareReference(shareName); + if (share == null || !share.Exists()) return null; + + CloudFileDirectory root = share.GetRootDirectoryReference(); + if (string.IsNullOrEmpty(fileDirectoryName)) + { + return root; + } + + CloudFileDirectory dir = root.GetDirectoryReference(fileDirectoryName); + + return dir.Exists() ? dir : null; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return null; + } + + throw; + } + } + + public bool CreateFileDirectory(string shareName, string fileDirectoryName) + { + CloudFileShare share = FileClient.GetShareReference(shareName); + if (share == null || !share.Exists()) + { + return false; + } + + if (string.IsNullOrEmpty(fileDirectoryName)) + { + return false; + } + + CloudFileDirectory parent = share.GetRootDirectoryReference(); + + string[] directoryTokens = fileDirectoryName.Split('/'); + foreach (string directoryToken in directoryTokens) + { + parent = CreateFileDirectoryIfNotExist(parent, directoryToken); + } + + return true; + } + + // create a directory under the specified parent directory. + public static CloudFileDirectory CreateFileDirectoryIfNotExist(CloudFileDirectory parent, string fileDirectoryName) + { + CloudFileDirectory dir = parent.GetDirectoryReference(fileDirectoryName); + dir.CreateIfNotExists(); + + return dir; + } + + // upload all files & dirs(including empty dir) in a local directory to an xsmb directory + public void UploadDirectory(string localDirName, string shareName, string fileDirName, bool recursive) + { + DirectoryInfo srcDir = new DirectoryInfo(localDirName); + CloudFileDirectory destDir = QueryFileDirectory(shareName, fileDirName); + if (null == destDir) + { + this.CreateFileDirectory(shareName, fileDirName); + destDir = QueryFileDirectory(shareName, fileDirName); + Test.Assert(null != destDir, "{0} should exist in file share {1}.", fileDirName, shareName); + } + + UploadDirectory(srcDir, destDir, recursive); + } + + public static void UploadDirectory(DirectoryInfo sourceDir, CloudFileDirectory destDir, bool recursive) + { + destDir.CreateIfNotExists(); + + Parallel.ForEach( + sourceDir.EnumerateFiles(), + fi => + { + string fileName = Path.GetFileName(fi.Name); + CloudFile file = destDir.GetFileReference(fileName); + + bool uploaded = UploadFile(file, fi.FullName); + if (!uploaded) + { + Test.Assert(false, "failed to upload file:{0}", fi.FullName); + } + }); + + if (recursive) + { + foreach (DirectoryInfo di in sourceDir.EnumerateDirectories()) + { + string subDirName = Path.GetFileName(di.Name); + CloudFileDirectory subDir = destDir.GetDirectoryReference(subDirName); + UploadDirectory(di, subDir, true); + } + } + } + + // compare an xsmb directory with a local directory. return true only if + // 1. all files under both dir are the same, and + // 2. all sub directories under both dir are the same + public bool CompareCloudFileDirAndLocalDir(string shareName, string fileDirName, string localDirName) + { + try + { + CloudFileDirectory dir = QueryFileDirectory(shareName, fileDirName); + if (null == dir) + { + return false; + } + + return CompareCloudFileDirAndLocalDir(dir, localDirName); + } + catch + { + return false; + } + } + + public static bool CompareCloudFileDirAndLocalDir(CloudFileDirectory dir, string localDirName) + { + if (!dir.Exists() || !Directory.Exists(localDirName)) + { + // return false if cloud dir or local dir not exist. + Test.Info("dir not exist. local dir={0}", localDirName); + return false; + } + + HashSet localSubFiles = new HashSet(); + foreach (string localSubFile in Directory.EnumerateFiles(localDirName)) + { + localSubFiles.Add(Path.GetFileName(localSubFile)); + } + + HashSet localSubDirs = new HashSet(); + foreach (string localSubDir in Directory.EnumerateDirectories(localDirName)) + { + localSubDirs.Add(Path.GetFileName(localSubDir)); + } + + foreach (IListFileItem item in dir.ListFilesAndDirectories()) + { + if (item is CloudFile) + { + CloudFile tmpFile = item as CloudFile; + + // TODO: tmpFile.RelativeName + string tmpFileName = Path.GetFileName(tmpFile.Name); + if (!localSubFiles.Remove(tmpFileName)) + { + Test.Info("file not found at local: {0}", tmpFile.Name); + return false; + } + + if (!CompareCloudFileAndLocalFile(tmpFile, Path.Combine(localDirName, tmpFileName))) + { + Test.Info("file content not consistent: {0}", tmpFile.Name); + return false; + } + } + else if (item is CloudFileDirectory) + { + CloudFileDirectory tmpDir = item as CloudFileDirectory; + string tmpDirName = tmpDir.Name; + if (!localSubDirs.Remove(tmpDirName)) + { + Test.Info("dir not found at local: {0}", tmpDir.Name); + return false; + } + + if (!CompareCloudFileDirAndLocalDir(tmpDir, Path.Combine(localDirName, tmpDirName))) + { + return false; + } + } + } + + return (localSubFiles.Count == 0 && localSubDirs.Count == 0); + } + + public bool CompareCloudFileAndLocalFile(string shareName, string fileName, string localFileName) + { + CloudFile file = QueryFile(shareName, fileName); + if (null == file) + { + return false; + } + + return CompareCloudFileAndLocalFile(file, localFileName); + } + + public static bool CompareCloudFileAndLocalFile(CloudFile file, string localFileName) + { + if (!file.Exists() || !File.Exists(localFileName)) + { + return false; + } + + file.FetchAttributes(); + return file.Properties.ContentMD5 == Helper.GetFileContentMD5(localFileName); + } + + public static string CalculateMD5ByDownloading(CloudFile cloudFile, bool disableMD5Check = false) + { + using (TemporaryTestFolder tempFolder = new TemporaryTestFolder(Guid.NewGuid().ToString())) + { + const string tempFileName = "tempFile"; + string tempFilePath = Path.Combine(tempFolder.Path, tempFileName); + var fileOptions = new FileRequestOptions(); + fileOptions.DisableContentMD5Validation = disableMD5Check; + fileOptions.RetryPolicy = HelperConst.DefaultFileOptions.RetryPolicy.CreateInstance(); + cloudFile.DownloadToFile(tempFilePath, FileMode.OpenOrCreate, options: fileOptions); + return Helper.GetFileContentMD5(tempFilePath); + } + } + + public CloudFile GetFileReference(string shareName, string cloudFileName) + { + CloudFileShare share = FileClient.GetShareReference(shareName); + CloudFileDirectory dir = share.GetRootDirectoryReference(); + return dir.GetFileReference(cloudFileName); + } + + public CloudFileDirectory GetDirReference(string shareName, string cloudDirName) + { + CloudFileShare share = FileClient.GetShareReference(shareName); + CloudFileDirectory dir = share.GetRootDirectoryReference(); + + if (cloudDirName == string.Empty) + { + return dir; + } + else + { + return dir.GetDirectoryReference(cloudDirName); + } + } + + // enumerate files under the specified cloud directory. + // Returns an enumerable collection of the full names(including dirName), for the files in the directory. + public IEnumerable EnumerateFiles(string shareName, string dirName, bool recursive) + { + CloudFileDirectory dir = QueryFileDirectory(shareName, dirName); + if (null == dir) + { + Test.Assert(false, "directory or share doesn't exist"); + } + + return EnumerateFiles(dir, recursive); + } + + // enumerate files under the specified cloud directory. + // Returns an enumerable collection of the full names(including dir name), for the files in the directory. + public static IEnumerable EnumerateFiles(CloudFileDirectory dir, bool recursive) + { + var folders = new List(); + foreach (IListFileItem item in dir.ListFilesAndDirectories()) + { + if (item is CloudFile) + { + CloudFile file = item as CloudFile; + string fileName = Path.GetFileName(file.Name); + string filePath = dir.Name + "/" + fileName; + yield return filePath; + } + else if (item is CloudFileDirectory) + { + if (recursive) + { + CloudFileDirectory subDir = item as CloudFileDirectory; + folders.Add(subDir); + } + } + } + + foreach (var folder in folders) + { + foreach (var filePath in EnumerateFiles(folder, recursive)) + { + yield return dir.Name + "/" + filePath; + } + } + } + + // enumerate directory under the specified cloud directory. + // Returns an enumerable collection of the full names(including dirName), for the directories in the directory + public IEnumerable EnumerateDirectories(string shareName, string dirName, bool recursive) + { + CloudFileDirectory dir = QueryFileDirectory(shareName, dirName); + if (null == dir) + { + Test.Assert(false, "directory or share doesn't exist"); + } + + return EnumerateDirectories(dir, recursive); + } + + // enumerate directory under the specified cloud directory. + // Returns an enumerable collection of the full names(including dir name), for the directories in the directory + public static IEnumerable EnumerateDirectories(CloudFileDirectory dir, bool recursive) + { + List dirs = new List(); + foreach (IListFileItem item in dir.ListFilesAndDirectories()) + { + if (item is CloudFileDirectory) + { + CloudFileDirectory subDir = item as CloudFileDirectory; + dirs.Add(dir.Name + "/" + subDir.Name); + + if (recursive) + { + foreach (string subSubDir in EnumerateDirectories(subDir, true)) + { + dirs.Add(dir.Name + "/" + subSubDir); + } + } + } + } + + return dirs; + } + + // convert xsmb file name to local file name by replacing "/" with DirectorySeparatorChar + public static string ConvertCloudFileNameToLocalFileName(string fileName) + { + if (Path.DirectorySeparatorChar == '/') + { + return fileName; + } + + return fileName.Replace('/', Path.DirectorySeparatorChar); + } + + // convert local file name to xsmb by replacing DirectorySeparatorChar with "/" + public static string ConvertLocalFileNameToCloudFileName(string fileName) + { + if (Path.DirectorySeparatorChar == '/') + { + return fileName; + } + + return fileName.Replace(Path.DirectorySeparatorChar, '/'); + } + + public static string GetFileDirectoryName(string fileName) + { + int index = fileName.LastIndexOf('/'); + + if (-1 == index) + { + return string.Empty; + } + + return fileName.Substring(0, index); + } + + public bool CleanupFileDirectory(string shareName, string fileDirectoryName) + { + CloudFileShare share = FileClient.GetShareReference(shareName); + if (!share.Exists()) + { + return false; + } + + CloudFileDirectory root = share.GetRootDirectoryReference(); + if (!string.IsNullOrEmpty(fileDirectoryName)) + { + root = root.GetDirectoryReference(fileDirectoryName); + } + else + { + if (root.ListFilesAndDirectories().Count() > 500) + return CleanupFileShareByRecreateIt(shareName); + } + + CleanupFileDirectory(root); + return true; + } + + public static void CleanupFileDirectory(CloudFileDirectory cloudDirectory) + { + foreach (IListFileItem item in cloudDirectory.ListFilesAndDirectories()) + { + if (item is CloudFile) + { + (item as CloudFile).Delete(); + } + + if (item is CloudFileDirectory) + { + DeleteFileDirectory(item as CloudFileDirectory); + } + } + } + + public bool CleanupFileShareByRecreateIt(string shareName) + { + CloudFileShare share = FileClient.GetShareReference(shareName); + if (!share.Exists()) + { + return true; + } + + try + { + share.Delete(); + + Test.Info("share deleted."); + + bool createSuccess = false; + int retry = 0; + while (!createSuccess && retry++ < 100) //wait up to 5 minutes + { + try + { + share.Create(); + createSuccess = true; + Test.Info("share recreated."); + } + catch (StorageException e) + { + if (e.Message.Contains("(409)")) //conflict, the share is still in deleteing + { + Thread.Sleep(3000); + } + else + { + throw; + } + } + } + return true; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return false; + } + throw; + } + } + + public static void DeleteFileDirectory(CloudFileDirectory cloudDirectory) + { + CleanupFileDirectory(cloudDirectory); + cloudDirectory.Delete(); + } + + /// + /// Get SAS of a share with specific permission and period. + /// + /// The name of the share. + /// The permission of the SAS. + /// How long the SAS will be valid before expire, in second + /// the SAS + public string GetSASofShare( + string shareName, + SharedAccessFilePermissions permissions, + int validatePeriod, + bool UseSavedPolicy = true, + string policySignedIdentifier = "PolicyIdentifier") + { + var share = this.FileClient.GetShareReference(shareName); + string sas = string.Empty; + var policy = new SharedAccessFilePolicy(); + policy.Permissions = permissions; + policy.SharedAccessExpiryTime = DateTimeOffset.Now.AddSeconds(validatePeriod); + if (UseSavedPolicy) + { + var sharePermissions = share.GetPermissions(); + sharePermissions.SharedAccessPolicies.Clear(); + sharePermissions.SharedAccessPolicies.Add(policySignedIdentifier, policy); + share.SetPermissions(sharePermissions); + sas = share.GetSharedAccessSignature(new SharedAccessFilePolicy(), policySignedIdentifier); + + DMLibTestHelper.WaitForACLTakeEffect(); + } + else + { + sas = share.GetSharedAccessSignature(policy); + } + + Test.Info("The SAS is {0}", sas); + return sas; + } + + /// + /// Clears the SAS policy set to a container, used to revoke the SAS. + /// + /// The name of the share. + public void ClearSASPolicyofShare(string shareName) + { + var share = this.FileClient.GetShareReference(shareName); + var bp = share.GetPermissions(); + bp.SharedAccessPolicies.Clear(); + share.SetPermissions(bp); + } + } + + + + /// + /// This class helps to do operations on cloud blobs + /// + public class CloudBlobHelper + { + public const string RootContainer = "$root"; + + private CloudStorageAccount account; + + /// + /// The storage account + /// + public CloudStorageAccount Account + { + get { return account; } + private set { account = value; } + } + + private CloudBlobClient blobClient; + /// + /// The blob client + /// + public CloudBlobClient BlobClient + { + get { return blobClient; } + set { blobClient = value; } + } + + /// + /// Construct the helper with the storage account + /// + /// + public CloudBlobHelper(CloudStorageAccount account) + { + Account = account; + BlobClient = account.CreateCloudBlobClient(); + BlobClient.DefaultRequestOptions.RetryPolicy = new LinearRetry(TimeSpan.Zero, 3); + } + + /// + /// Construct the helper with the storage account + /// + /// + public CloudBlobHelper(string ConnectionString) + { + Account = CloudStorageAccount.Parse(ConnectionString); + BlobClient = Account.CreateCloudBlobClient(); + BlobClient.DefaultRequestOptions.RetryPolicy = new LinearRetry(TimeSpan.Zero, 3); + } + + public bool Exists(string containerName) + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + return container.Exists(); + } + + /// + /// Create a container for blobs + /// + /// the name of the container + /// Return true on success, false if already exists, throw exception on error + public bool CreateContainer(string containerName) + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + return container.CreateIfNotExists(); + } + + /// + /// Delete the container for the blobs + /// + /// the name of container + /// Return true on success (or the container was deleted before), false if the container doesnot exist, throw exception on error + public bool DeleteContainer(string containerName) + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + return container.DeleteIfExists(); + } + + public CloudBlobContainer GetGRSContainer(string containerName) + { + return new CloudBlobContainer( + new Uri(string.Format("{0}/{1}", this.Account.BlobStorageUri.SecondaryUri.AbsoluteUri, containerName)), + this.Account.Credentials); + } + + public BlobContainerPermissions SetGRSContainerAccessType(string containerName, BlobContainerPublicAccessType accessType) + { + BlobContainerPermissions oldPermissions = this.SetContainerAccessType(containerName, accessType); + if (null == oldPermissions) + { + return null; + } + + CloudBlobContainer containerGRS = new CloudBlobContainer( + new Uri(string.Format("{0}/{1}", this.Account.BlobStorageUri.SecondaryUri.AbsoluteUri, containerName)), + this.blobClient.Credentials); + + Helper.WaitForTakingEffect(containerGRS.ServiceClient); + return oldPermissions; + } + + /// + /// Set the specific container to the accesstype + /// + /// container Name + /// the accesstype the contain will be set + /// the container 's permission before set, so can be set back when test case finish + public BlobContainerPermissions SetContainerAccessType(string containerName, BlobContainerPublicAccessType accesstype) + { + try + { + CloudBlobContainer container = blobClient.GetContainerReference(containerName); + container.CreateIfNotExists(); + BlobContainerPermissions oldPerm = container.GetPermissions(); + BlobContainerPermissions blobPermissions = new BlobContainerPermissions(); + blobPermissions.PublicAccess = accesstype; + container.SetPermissions(blobPermissions); + return oldPerm; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return null; + } + throw; + } + } + + public bool ListBlobs(string containerName, out List blobList) + { + return this.ListBlobs(containerName, BlobListingDetails.All, out blobList); + } + + + public bool ListBlobs(string containerName, BlobListingDetails listingDetails, out List blobList) + { + blobList = new List(); + try + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + IEnumerable blobs = container.ListBlobs(null, true, listingDetails); + if (blobs != null) + { + foreach (CloudBlob blob in blobs) + { + blobList.Add(blob); + } + } + return true; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return false; + } + throw; + } + } + + + /// + /// list blobs in a folder, TODO: implement this for batch operations on blobs + /// + /// + /// + /// + public bool ListBlobs(string containerName, string folderName, out List blobList) + { + blobList = new List(); + + try + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + CloudBlobDirectory blobDir = container.GetDirectoryReference(folderName); + IEnumerable blobs = blobDir.ListBlobs(true, BlobListingDetails.All); + if (blobs != null) + { + foreach (CloudBlob blob in blobs) + { + blobList.Add(blob); + } + } + return true; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return false; + } + throw; + } + } + + /// + /// Validate the uploaded tree which is created by Helper.GenerateFixedTestTree() + /// + /// the file prefix of the tree + /// the folder prefix of the tree + /// + /// + /// how many files in each folder + /// how many folder level to verify + /// the container which contain the uploaded tree + /// true means should verify the folder not exist. false means verify the folder exist. + /// true if verify pass, false mean verify fail + public bool ValidateFixedTestTree(string filename, string foldername, string sourceFolder, string destFolder, int size, int layer, string containerName, bool empty = false) + { + Test.Info("Verify the folder {0}...", sourceFolder); + for (int i = 0; i < size; i++) + { + string sourcefilename = sourceFolder + "\\" + filename + "_" + i; + string destblobname = destFolder + "\\" + filename + "_" + i; + CloudBlob blob = this.QueryBlob(containerName, destblobname); + if (!empty) + { + if (blob == null) + { + Test.Error("Blob {0} not exist.", destblobname); + return false; + } + string source_MD5 = Helper.GetFileContentMD5(sourcefilename); + string Dest_MD5 = blob.Properties.ContentMD5; + if (source_MD5 != Dest_MD5) + { + Test.Error("sourcefile:{0}: {1} == destblob:{2}:{3}", sourcefilename, source_MD5, destblobname, Dest_MD5); + return false; + } + } + else + { + if (blob != null && blob.Properties.Length != 0) + { + Test.Error("Blob {0} should not exist.", destblobname); + return false; + } + } + } + if (layer > 0) + { + for (int i = 0; i < size; i++) + { + if (!ValidateFixedTestTree(filename, foldername, sourceFolder + "\\" + foldername + "_" + i, destFolder + "\\" + foldername + "_" + i, size, layer - 1, containerName, empty)) + return false; + } + + } + + return true; + } + + /// + /// Validate the uploaded tree which is created by Helper.GenerateFixedTestTree() + /// + /// the file prefix of the tree + /// the folder prefix of the tree + /// current folder to validate + /// how many files in each folder + /// how many folder level to verify + /// the container which contain the uploaded tree + /// true means should verify the folder not exist. false means verify the folder exist. + /// true if verify pass, false mean verify fail + public bool ValidateFixedTestTree(string filename, string foldername, string currentFolder, int size, int layer, string containerName, bool empty = false) + { + Test.Info("Verify the folder {0}...", currentFolder); + return this.ValidateFixedTestTree(filename, foldername, currentFolder, currentFolder, size, layer, containerName, empty); + } + + /// + /// Get SAS of a container with specific permission and period + /// + /// the name of the container + /// the permission of the SAS + /// How long the SAS will be valid before expire, in second + /// the SAS + public string GetSASofContainer(string containerName, SharedAccessBlobPermissions SAB, int validatePeriod, bool UseSavedPolicy = true, string PolicySignedIdentifier = "PolicyIdentifier") + { + try + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + string SAS = string.Empty; + SharedAccessBlobPolicy sap = new SharedAccessBlobPolicy(); + sap.Permissions = SAB; + sap.SharedAccessExpiryTime = DateTimeOffset.Now.AddSeconds(validatePeriod); + if (UseSavedPolicy) + { + BlobContainerPermissions bp = container.GetPermissions(); + bp.SharedAccessPolicies.Clear(); + bp.SharedAccessPolicies.Add(PolicySignedIdentifier, sap); + container.SetPermissions(bp); + SAS = container.GetSharedAccessSignature(new SharedAccessBlobPolicy(), PolicySignedIdentifier); + + DMLibTestHelper.WaitForACLTakeEffect(); + } + else + { + SAS = container.GetSharedAccessSignature(sap); + } + Test.Info("The SAS is {0}", SAS); + return SAS; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return string.Empty; + } + throw; + } + } + + /// + /// Clear the SAS policy set to a container, used to revoke the SAS + /// + /// the name of the container + /// True for success + public bool ClearSASPolicyofContainer(string containerName) + { + try + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + BlobContainerPermissions bp = container.GetPermissions(); + bp.SharedAccessPolicies.Clear(); + container.SetPermissions(bp); + return true; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return false; + } + throw; + } + } + + + public bool CleanupContainer(string containerName) + { + string blobname = string.Empty; + try + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + if (!container.Exists()) + return true; + IEnumerable blobs = container.ListBlobs(null, true, BlobListingDetails.All); + if (blobs != null) + { + if (blobs.Count() > 500) + { + return CleanupContainerByRecreateIt(containerName); + } + foreach (CloudBlob blob in blobs) + { + blobname = blob.Name; + if (blob == null) continue; + if (!blob.Exists()) + { + try + { + blob.Delete(DeleteSnapshotsOption.IncludeSnapshots); + continue; + } + catch (Exception) + { + continue; + } + } + try + { + blob.Delete(DeleteSnapshotsOption.IncludeSnapshots); + } + catch (Exception) + { + blob.Delete(DeleteSnapshotsOption.None); + } + } + } + + Thread.Sleep(5 * 1000); + if (container.ListBlobs(null, true, BlobListingDetails.All).Any()) + { + Test.Warn("The container hasn't been cleaned actually."); + Test.Info("Trying to cleanup the container by recreating it..."); + return CleanupContainerByRecreateIt(containerName); + } + else + { + return true; + } + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e) || 409 == e.RequestInformation.HttpStatusCode) + { + if (!CleanupContainerByRecreateIt(containerName)) + return false; + } + throw; + } + } + + public bool CleanupContainerByRecreateIt(string containerName) + { + try + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + if (container == null || !container.Exists()) return false; + + BlobRequestOptions bro = new BlobRequestOptions(); + bro.RetryPolicy = new LinearRetry(new TimeSpan(0, 1, 0), 3); + + try + { + container.Delete(null, bro); + } + catch (StorageException e) + { + if (!Helper.IsNotFoundException(e)) + { + throw; + } + } + + Test.Info("container deleted."); + bro.RetryPolicy = new LinearRetry(new TimeSpan(0, 3, 0), 3); + + bool createSuccess = false; + int retry = 0; + while (!createSuccess && retry++ < 100) //wait up to 5 minutes + { + try + { + container.Create(bro); + createSuccess = true; + Test.Info("container recreated."); + } + catch (StorageException e) + { + if (e.Message.Contains("(409)")) //conflict, the container is still in deleteing + { + Thread.Sleep(3000); + } + else + { + throw; + } + } + } + return true; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return false; + } + throw; + } + } + + // upload all files & dirs(including empty dir) in a local directory to a blob directory + public void UploadDirectory(string localDirName, string containerName, string blobDirName, bool recursive, string blobType = BlobType.Block) + { + DirectoryInfo srcDir = new DirectoryInfo(localDirName); + CloudBlobDirectory destDir = QueryBlobDirectory(containerName, blobDirName); + Test.Assert(null != destDir, "dest blob directory exists"); + + UploadDirectory(srcDir, destDir, recursive, blobType); + } + + public void UploadDirectory(DirectoryInfo sourceDir, CloudBlobDirectory destDir, bool recursive, string blobType = BlobType.Block) + { + Parallel.ForEach( + sourceDir.EnumerateFiles(), + fi => + { + string fileName = Path.GetFileName(fi.Name); + CloudBlob blob = GetCloudBlobReference(destDir, fileName, blobType); + bool uploaded = UploadFileToBlob(destDir.Container.Name, blob.Name, blobType, fi.FullName); + if (!uploaded) + { + Test.Assert(false, "failed to upload file:{0}", fi.FullName); + } + }); + + if (recursive) + { + foreach (DirectoryInfo di in sourceDir.EnumerateDirectories()) + { + string subDirName = Path.GetFileName(di.Name); + CloudBlobDirectory subDir = destDir.GetDirectoryReference(subDirName); + UploadDirectory(di, subDir, true); + } + } + } + + // upload all files & dirs(including empty dir) in a local directory to a blob directory + public void UploadDirectoryIfNotExist(string localDirName, string containerName, string blobDirName, bool recursive, string blobType = BlobType.Block) + { + DirectoryInfo srcDir = new DirectoryInfo(localDirName); + CloudBlobDirectory destDir = QueryBlobDirectory(containerName, blobDirName); + + UploadDirectoryIfNotExist(srcDir, destDir, recursive, blobType); + } + + public void UploadDirectoryIfNotExist(DirectoryInfo sourceDir, CloudBlobDirectory destDir, bool recursive, string blobType = BlobType.Block) + { + Dictionary blobs = new Dictionary(); + + foreach (IListBlobItem blobItem in destDir.ListBlobs(true)) + { + CloudBlob blob = blobItem as CloudBlob; + + if (null != blob) + { + if (MapStorageBlobTypeToBlobType(blob.BlobType) == blobType) + { + blob.Delete(); + } + else + { + blobs.Add(blob.Name.Substring(destDir.Prefix.Length), blob); + } + } + } + + foreach (FileInfo fi in sourceDir.EnumerateFiles()) + { + string fileName = Path.GetFileName(fi.Name); + CloudBlob blob; + + if (blobs.TryGetValue(fileName, out blob) + && (Helper.GetFileContentMD5(fi.Name) == blob.Properties.ContentMD5)) + { + continue; + } + + blob = GetCloudBlobReference(destDir, fileName, blobType); + bool uploaded = UploadFileToBlob(destDir.Container.Name, blob.Name, blobType, fi.FullName); + if (!uploaded) + { + Test.Assert(false, "failed to upload file:{0}", fi.FullName); + } + } + + if (recursive) + { + foreach (DirectoryInfo di in sourceDir.EnumerateDirectories()) + { + string subDirName = Path.GetFileName(di.Name); + CloudBlobDirectory subDir = destDir.GetDirectoryReference(subDirName); + UploadDirectory(di, subDir, true); + } + } + } + + // compare blob directory with a local directory. return true only if + // 1. all files under both dir are the same, and + // 2. all sub directories under both dir are the same + public bool CompareCloudBlobDirAndLocalDir(string containerName, string blobDirName, string localDirName) + { + try + { + CloudBlobDirectory dir = QueryBlobDirectory(containerName, blobDirName); + if (null == dir) + { + return false; + } + + return CompareCloudBlobDirAndLocalDir(dir, localDirName); + } + catch + { + return false; + } + } + + public static bool CompareCloudBlobDirAndLocalDir(CloudBlobDirectory dir, string localDirName) + { + if (!Directory.Exists(localDirName)) + { + // return false if local dir not exist. + Test.Info("dir not exist. local dir={0}", localDirName); + return false; + } + + HashSet localSubFiles = new HashSet(); + foreach (string localSubFile in Directory.EnumerateFiles(localDirName)) + { + localSubFiles.Add(Path.GetFileName(localSubFile)); + } + + HashSet localSubDirs = new HashSet(); + foreach (string localSubDir in Directory.EnumerateDirectories(localDirName)) + { + localSubDirs.Add(Path.GetFileName(localSubDir)); + } + + foreach (IListBlobItem item in dir.ListBlobs()) + { + if (item is CloudBlob) + { + CloudBlob tmpBlob = item as CloudBlob; + + string tmpFileName = Path.GetFileName(tmpBlob.Name); + if (!localSubFiles.Remove(tmpFileName)) + { + Test.Info("file not found at local: {0}", tmpBlob.Name); + return false; + } + + if (!CompareCloudBlobAndLocalFile(tmpBlob, Path.Combine(localDirName, tmpFileName))) + { + Test.Info("file content not consistent: {0}", tmpBlob.Name); + return false; + } + } + else if (item is CloudBlobDirectory) + { + CloudBlobDirectory tmpDir = item as CloudBlobDirectory; + string tmpDirName = tmpDir.Prefix.TrimEnd(new char[] { '/' }); + tmpDirName = Path.GetFileName(tmpDirName); + + if (!localSubDirs.Remove(tmpDirName)) + { + Test.Info("dir not found at local: {0}", tmpDirName); + return false; + } + + if (!CompareCloudBlobDirAndLocalDir(tmpDir, Path.Combine(localDirName, tmpDirName))) + { + return false; + } + } + } + + return (localSubFiles.Count == 0 && localSubDirs.Count == 0); + } + + public bool CompareCloudBlobAndLocalFile(string containerName, string blobName, string localFileName) + { + CloudBlob blob = QueryBlob(containerName, blobName); + if (null == blob) + { + return false; + } + + return CompareCloudBlobAndLocalFile(blob, localFileName); + } + + public static bool CompareCloudBlobAndLocalFile(CloudBlob blob, string localFileName) + { + if (!blob.Exists() || !File.Exists(localFileName)) + { + return false; + } + + blob.FetchAttributes(); + return blob.Properties.ContentMD5 == Helper.GetFileContentMD5(localFileName); + } + + public static bool CompareCloudBlobAndCloudBlob(CloudBlob blobA, CloudBlob blobB) + { + if (blobA == null || blobB == null || !blobA.Exists() || !blobB.Exists()) + { + return false; + } + + blobA.FetchAttributes(); + blobB.FetchAttributes(); + return blobA.Properties.ContentMD5 == blobB.Properties.ContentMD5; + } + + public static string CalculateMD5ByDownloading(CloudBlob blob, bool disableMD5Check = false) + { + using (TemporaryTestFolder tempFolder = new TemporaryTestFolder(Guid.NewGuid().ToString())) + { + const string tempFileName = "tempFile"; + string tempFilePath = Path.Combine(tempFolder.Path, tempFileName); + var blobOptions = new BlobRequestOptions(); + blobOptions.DisableContentMD5Validation = disableMD5Check; + blobOptions.RetryPolicy = HelperConst.DefaultBlobOptions.RetryPolicy.CreateInstance(); + blob.DownloadToFile(tempFilePath, FileMode.OpenOrCreate, options: blobOptions); + return Helper.GetFileContentMD5(tempFilePath); + } + } + + /// + /// Query the blob + /// + /// + /// + /// + public CloudBlob QueryBlob(string containerName, string blobName) + { + try + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + CloudBlob blob = GetCloudBlobReference(container, blobName); + //since GetBlobReference method return no null value even if blob is not exist. + //use FetchAttributes method to confirm the existence of the blob + blob.FetchAttributes(); + + return blob; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return null; + } + throw; + } + } + + + public BlobProperties QueryBlobProperties(string containerName, string blobName) + { + try + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + CloudBlob blob = container.GetBlobReference(blobName); + if (blob == null) + { + return null; + } + blob.FetchAttributes(); + return blob.Properties; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return null; + } + throw; + } + } + + /// + /// Query the blob virtual directory + /// + /// + /// + /// + public CloudBlobDirectory QueryBlobDirectory(string containerName, string blobDirectoryName) + { + try + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + if (container == null || !container.Exists()) return null; + CloudBlobDirectory blobDirectory = container.GetDirectoryReference(blobDirectoryName); + return blobDirectory; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return null; + } + throw; + } + } + + public static void GeneratePageBlobWithRangedData(CloudPageBlob pageBlob, List ranges, List gaps) + { + Helper.GenerateSparseCloudObject( + ranges, + gaps, + createObject: (totalSize) => + { + pageBlob.Create(totalSize); + }, + writeUnit: (unitOffset, randomData) => + { + pageBlob.WritePages(randomData, unitOffset, options: HelperConst.DefaultBlobOptions); + }); + + Helper.PrintPageBlobRanges(pageBlob); + + // Set correct MD5 to page blob + string md5 = CalculateMD5ByDownloading(pageBlob); + pageBlob.Properties.ContentMD5 = md5; + pageBlob.SetProperties(options: HelperConst.DefaultBlobOptions); + } + + public static void GenerateBlockBlob(CloudBlockBlob blockBlob, List blockSizes) + { + int blockIndex = 0; + List blocksToCommit = new List(); + foreach (int blockSize in blockSizes) + { + byte[] blockIdInBytes = System.Text.Encoding.UTF8.GetBytes(blockIndex.ToString("D4")); + string blockId = Convert.ToBase64String(blockIdInBytes); + blocksToCommit.Add(blockId); + + using (MemoryStream randomData = Helper.GetRandomData(blockSize)) + { + blockBlob.PutBlock(blockId, randomData, null, options: HelperConst.DefaultBlobOptions); + } + + ++blockIndex; + } + + // Commit + blockBlob.PutBlockList(blocksToCommit, options: HelperConst.DefaultBlobOptions); + + Helper.PrintBlockBlobBlocks(blockBlob); + + // Set correct MD5 to block blob + string md5 = CloudBlobHelper.CalculateMD5ByDownloading(blockBlob); + blockBlob.Properties.ContentMD5 = md5; + blockBlob.SetProperties(options: HelperConst.DefaultBlobOptions); + } + + /// + /// Create or update a blob by its name + /// + /// the name of the container + /// the name of the blob + /// the content to the blob + /// Return true on success, false if unable to create, throw exception on error + public bool PutBlob(string containerName, string blobName, string content) + { + try + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + if (container == null || !container.Exists()) return false; + CloudBlob blob = GetCloudBlobReference(container, blobName); + + using (MemoryStream MStream = new MemoryStream(ASCIIEncoding.Default.GetBytes(content))) + { + blob.UploadFromStream(MStream); + } + + return true; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return false; + } + throw; + } + } + + /// + /// change an exist Blob MD5 hash + /// + /// the name of the container + /// the name of the blob + /// the MD5 hash to set, must be a base 64 string + /// Return true on success, false if unable to set + public bool SetMD5Hash(string containerName, string blobName, string MD5Hash) + { + try + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + CloudBlob blob = container.GetBlobReference(blobName); + blob.FetchAttributes(); + blob.Properties.ContentMD5 = MD5Hash; + blob.SetProperties(); + return true; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return false; + } + throw; + } + } + + /// + /// put block list. TODO: implement this for large files + /// + /// + /// + /// + /// + public bool PutBlockList(string containerName, string blobName, string[] blockIds) + { + try + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + if (container == null || !container.Exists()) return false; + CloudBlockBlob blob = container.GetBlockBlobReference(blobName); + + blob.PutBlockList(blockIds); + + return true; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return false; + } + throw; + } + } + + public static string MapStorageBlobTypeToBlobType(StorageBlobType storageBlobType) + { + switch (storageBlobType) + { + case StorageBlobType.BlockBlob: + return BlobType.Block; + case StorageBlobType.PageBlob: + return BlobType.Page; + case StorageBlobType.AppendBlob: + return BlobType.Append; + default: + throw new ArgumentException("storageBlobType"); + } + } + + public static StorageBlobType MapBlobTypeToStorageBlobType(string blobType) + { + switch (blobType) + { + case BlobType.Block: + return StorageBlobType.BlockBlob; + case BlobType.Page: + return StorageBlobType.PageBlob; + case BlobType.Append: + return StorageBlobType.AppendBlob; + default: + throw new ArgumentException("blobType"); + } + } + + public static CloudBlob GetCloudBlobReference(CloudBlobContainer container, string blobName, string blobType) + { + switch (blobType) + { + case BlobType.Block: + return container.GetBlockBlobReference(blobName); + + case BlobType.Page: + return container.GetPageBlobReference(blobName); + + case BlobType.Append: + return container.GetAppendBlobReference(blobName); + + default: + throw new ArgumentException("blobType"); + } + } + + public static CloudBlob GetCloudBlobReference(CloudBlobContainer container, string blobName) + { + CloudBlob cloudBlob = container.GetBlobReference(blobName); + cloudBlob.FetchAttributes(); + + return GetCloudBlobReference(container, blobName, MapStorageBlobTypeToBlobType(cloudBlob.Properties.BlobType)); + } + + public static CloudBlob GetCloudBlobReference(CloudBlobDirectory directory, string blobName, string blobType) + { + switch (blobType) + { + case BlobType.Block: + return directory.GetBlockBlobReference(blobName); + + case BlobType.Page: + return directory.GetPageBlobReference(blobName); + + case BlobType.Append: + return directory.GetAppendBlobReference(blobName); + + default: + throw new ArgumentException("blobType"); + } + } + + public CloudBlob GetBlobReference(string containerName, string blobName, string blobType = BlobType.Block) + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + return GetCloudBlobReference(container, blobName, blobType); + } + + public CloudBlobDirectory GetDirReference(string containerName, string dirName) + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + return container.GetDirectoryReference(dirName); + } + + /// + /// Download Blob text by the blob name + /// + /// the name of the container + /// + /// + /// + public bool GetBlob(string containerName, string blobName, out string content) + { + content = null; + + try + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + CloudBlob blob = container.GetBlobReference(blobName); + //content = blob.DownloadText(); + string tempfile = "temp.txt"; + using (FileStream fileStream = new FileStream(tempfile, FileMode.Create)) + { + blob.DownloadToStream(fileStream); + fileStream.Close(); + } + content = File.ReadAllText(tempfile); + File.Delete(tempfile); + + return true; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return false; + } + throw; + } + } + + /// + /// Delete a blob by its name + /// + /// the name of the container + /// the name of the blob + /// Return true on success, false if blob not found, throw exception on error + public bool DeleteBlob(string containerName, string blobName) + { + blobName = blobName.Replace("\\", "/"); + try + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + if (container.Exists()) + { + IEnumerable blobs = container.ListBlobs(blobName, true, BlobListingDetails.All); + foreach (CloudBlob blob in blobs) + { + if (blob.Name == blobName) + { + return blob.DeleteIfExists(); + } + } + } + return true; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return false; + } + throw; + } + } + + public bool DeleteBlobDirectory(string containerName, string blobDirectoryName, bool recursive) + { + try + { + if (blobDirectoryName == string.Empty) + return true; + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + CloudBlobDirectory blobDirectory = container.GetDirectoryReference(blobDirectoryName); + + const int MaxRetryCount = 10; + int retryCount = 0; + while (true) + { + bool hasBlobDeleted = false; + if (recursive) + { + foreach (CloudBlob blob in blobDirectory.ListBlobs(recursive, BlobListingDetails.All)) + { + blob.Delete(); + hasBlobDeleted = true; + } + } + else + { + foreach (CloudBlob blob in blobDirectory.ListBlobs(recursive)) + { + blob.Delete(); + hasBlobDeleted = true; + } + } + + retryCount++; + + if (!hasBlobDeleted) + { + // Return from the method until no blob is listed. + break; + } + else + { + if (retryCount > MaxRetryCount) + { + Test.Error("Cannot delete the blob directory within max retry count"); + return false; + } + + // Wait for some time, and then attempt to delete all listed blobs again. + Thread.Sleep(5 * 1000); + } + } + + return true; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return false; + } + throw; + } + } + + public bool UploadFileToBlockBlob(string containerName, string blobName, string filePath) + { + return UploadFileToBlob(containerName, blobName, BlobType.Block, filePath); + } + + public bool UploadFileToPageBlob(string containerName, string blobName, string filePath) + { + return UploadFileToBlob(containerName, blobName, BlobType.Page, filePath); + } + + public bool UploadFileToAppendBlob(string containerName, string blobName, string filePath) + { + return UploadFileToBlob(containerName, blobName, BlobType.Append, filePath); + } + + public bool UploadFileToBlob(string containerName, string blobName, string blobType, string filePath) + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + BlobRequestOptions options = new BlobRequestOptions + { + RetryPolicy = new ExponentialRetry(TimeSpan.FromSeconds(90), 3), + StoreBlobContentMD5 = true, + }; + + container.CreateIfNotExists(options); + + CloudBlob blob = GetCloudBlobReference(container, blobName, blobType); + blob.UploadFromFile(filePath, FileMode.Open, null, options, null); + Test.Info("block blob {0} has been uploaded successfully", blob.Name); + + return true; + } + + public bool DownloadFile(string containerName, string blobName, string filePath) + { + try + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + BlobRequestOptions bro = new BlobRequestOptions(); + bro.RetryPolicy = new LinearRetry(new TimeSpan(0, 0, 30), 3); + bro.ServerTimeout = new TimeSpan(1, 30, 0); + bro.MaximumExecutionTime = new TimeSpan(1, 30, 0); + CloudBlob blob = container.GetBlobReference(blobName); + + using (FileStream fileStream = new FileStream(filePath, FileMode.Create)) + { + blob.DownloadToStream(fileStream, null, bro); + fileStream.Close(); + } + + return true; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return false; + } + throw; + } + } + /// + /// Creates a snapshot of the blob + /// + /// the name of the container + /// the name of blob + /// blob snapshot + public CloudBlob CreateSnapshot(string containerName, string blobName) + { + try + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + CloudBlob blob = GetCloudBlobReference(container, blobName); + if (blob.Properties.BlobType == Microsoft.WindowsAzure.Storage.Blob.BlobType.BlockBlob) + { + CloudBlockBlob BBlock = blob as CloudBlockBlob; + return BBlock.Snapshot(); + } + else + { + CloudPageBlob BBlock = blob as CloudPageBlob; + return BBlock.Snapshot(); + } + + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return null; + } + throw; + } + } + + /// + /// delete snapshot of the blob (DO NOT delete blob) + /// + /// the name of the container + /// the name of blob + /// + public void DeleteSnapshotOnly(string containerName, string blobName) + { + try + { + CloudBlobContainer container = BlobClient.GetContainerReference(containerName); + CloudBlob blob = container.GetBlobReference(blobName); + + //Indicate that any snapshots should be deleted. + blob.Delete(DeleteSnapshotsOption.DeleteSnapshotsOnly); + return; + } + catch (StorageException e) + { + if (Helper.IsNotFoundException(e)) + { + return; + } + throw; + } + } + /// + /// return name of snapshot + /// + /// the name of blob + /// A blob snapshot + /// name of snapshot + public string GetNameOfSnapshot(string fileName, CloudBlob snapshot) + { + string fileNameNoExt = Path.GetFileNameWithoutExtension(fileName); + string extension = Path.GetExtension(fileName); + string timeStamp = string.Format("{0:yyyy-MM-dd HHmmss fff}", snapshot.SnapshotTime.Value); + return string.Format("{0} ({1}){2}", fileNameNoExt, timeStamp, extension); + } + } + internal class TemporaryTestFile : IDisposable + { + private const int DefaultSizeInKB = 1; + private bool disposed = false; + + public string Path + { + get; + private set; + } + + public int Size + { + get; + private set; + } + + public TemporaryTestFile(string path) + : this(path, DefaultSizeInKB) + { + } + + public TemporaryTestFile(string path, int sizeInKB) + { + Path = path; + Size = sizeInKB; + + if (File.Exists(path)) + { + Test.Assert(false, "file {0} already exist", path); + } + + Helper.GenerateRandomTestFile(Path, Size); + } + + ~TemporaryTestFile() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + try + { + Helper.DeleteFile(Path); + } + catch + { + } + + disposed = true; + } + } + } + + internal class TemporaryTestFolder : IDisposable + { + private bool disposed = false; + + public string Path + { + get; + private set; + } + + public TemporaryTestFolder(string path) + { + Path = path; + + if (Directory.Exists(path)) + { + Test.Assert(false, "folder {0} already exist", path); + } + + Helper.CreateNewFolder(path); + } + + ~TemporaryTestFolder() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + try + { + Helper.DeleteFolder(Path); + } + catch + { + } + + disposed = true; + } + } + } +} diff --git a/test/DMLibTest/Util/TestAccounts.cs b/test/DMLibTest/Util/TestAccounts.cs new file mode 100644 index 00000000..fd627be6 --- /dev/null +++ b/test/DMLibTest/Util/TestAccounts.cs @@ -0,0 +1,163 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTest +{ + using System; + using Microsoft.WindowsAzure.Storage; + using MS.Test.Common.MsTestLib; + + public static class TestAccounts + { + public static TestAccount Primary + { + get + { + return new TestAccount(AccountInConfig.Primary); + } + } + + public static TestAccount Secondary + { + get + { + return new TestAccount(AccountInConfig.Secondary); + } + } + } + + public class TestAccount + { + public TestAccount(AccountInConfig accountInConfig) + : this(GetConnectionString(accountInConfig)) + { + } + + public TestAccount(string connectionString) + { + this.ConnectionString = connectionString; + this.Account = CloudStorageAccount.Parse(connectionString); + } + + public CloudStorageAccount Account { get; private set; } + + public string ConnectionString { get; private set; } + + public string AccountName + { + get + { + return this.Account.Credentials.AccountName; + } + } + + public string StorageKey + { + get + { + return this.Account.Credentials.ExportBase64EncodedKey(); + } + } + + public string GetEndpointBaseUri(EndpointType endpoint, bool secondary = false) + { + return this.GetEndpointBaseUri(endpoint, DMLibTestHelper.RandomProtocol(), secondary); + } + + public string GetEndpointBaseUri(EndpointType endpoint, string protocol, bool secondary = false) + { + string url = string.Empty; + bool isHttps = (string.Compare(protocol, "https", StringComparison.InvariantCultureIgnoreCase) == 0); + if (DMLibTestHelper.GetTestAgainst() == TestAgainst.DevFabric) + { + int port; + string host; + if (endpoint == EndpointType.Blob) + { + port = isHttps ? 10100 : 10000; + host = this.Account.BlobEndpoint.Host; + } + else if (endpoint == EndpointType.Queue) + { + port = isHttps ? 10101 : 10001; + host = this.Account.QueueEndpoint.Host; + } + else if (endpoint == EndpointType.Table) + { + port = isHttps ? 10102 : 10002; + host = this.Account.TableEndpoint.Host; + } + else + { + port = isHttps ? 10104 : 10004; + host = this.Account.FileEndpoint.Host; + } + + url = string.Format(@"{0}://{1}:{2}/{3}", protocol, host, port, this.AccountName); + if (secondary) + { + Test.Error("DevFabric doesn't have secondary endpoint."); + } + } + else + { + Uri endpointUri; + if (endpoint == EndpointType.Blob) + { + endpointUri = secondary ? this.Account.BlobStorageUri.SecondaryUri : this.Account.BlobStorageUri.PrimaryUri; + } + else if (endpoint == EndpointType.Queue) + { + endpointUri = secondary ? this.Account.QueueStorageUri.SecondaryUri : this.Account.QueueStorageUri.PrimaryUri; + } + else if (endpoint == EndpointType.Table) + { + endpointUri = secondary ? this.Account.TableStorageUri.SecondaryUri : this.Account.TableStorageUri.PrimaryUri; + } + else + { + endpointUri = secondary ? this.Account.FileStorageUri.SecondaryUri : this.Account.FileStorageUri.PrimaryUri; + } + + url = endpointUri.AbsoluteUri.Replace(endpointUri.Scheme, protocol); + } + + if (url.EndsWith("/")) + { + url = url.Remove(url.Length - 1); + } + + return url; + } + + private static string GetConnectionString(AccountInConfig accountInConfig) + { + if (accountInConfig == AccountInConfig.Primary) + { + return Test.Data.Get(DMLibTestConstants.ConnStr); + } + else if (accountInConfig == AccountInConfig.Secondary) + { + return Test.Data.Get(DMLibTestConstants.ConnStr2); + } + + throw new ArgumentException(string.Format("Invalid accountInConfig value: {0}", accountInConfig), "accountInConfig"); + } + } + + public enum AccountInConfig + { + Primary, + Secondary, + } + + public enum EndpointType + { + Blob, + Queue, + Table, + File, + } +} diff --git a/test/DMLibTest/packages.config b/test/DMLibTest/packages.config new file mode 100644 index 00000000..f48ace35 --- /dev/null +++ b/test/DMLibTest/packages.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/test/DMLibTestCodeGen/App.config b/test/DMLibTestCodeGen/App.config new file mode 100644 index 00000000..8e156463 --- /dev/null +++ b/test/DMLibTestCodeGen/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/test/DMLibTestCodeGen/DMLibDataType.cs b/test/DMLibTestCodeGen/DMLibDataType.cs new file mode 100644 index 00000000..b1eaf2a5 --- /dev/null +++ b/test/DMLibTestCodeGen/DMLibDataType.cs @@ -0,0 +1,52 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTestCodeGen +{ + using System; + using System.Collections.Generic; + + [Flags] + public enum DMLibDataType : int + { + Unspecified = 0x0, + Stream = 0x01, + URI = 0x02, + Local = 0x04, + CloudFile = 0x08, + BlockBlob = 0x10, + PageBlob = 0x20, + AppendBlob = 0x40, + + CloudBlob = PageBlob | BlockBlob | AppendBlob, + Cloud = CloudBlob | CloudFile, + All = Local | Cloud, + } + + internal static class DMLibDataTypeExtentions + { + public static IEnumerable Extract(this DMLibDataType type) + { + DMLibDataType[] dataTypesToExtract = + { + DMLibDataType.Stream, + DMLibDataType.URI, + DMLibDataType.Local, + DMLibDataType.CloudFile, + DMLibDataType.BlockBlob, + DMLibDataType.PageBlob, + DMLibDataType.AppendBlob, + }; + + foreach (var dataType in dataTypesToExtract) + { + if (type.HasFlag(dataType)) + { + yield return dataType; + } + } + } + } +} diff --git a/test/DMLibTestCodeGen/DMLibDirectionFilter.cs b/test/DMLibTestCodeGen/DMLibDirectionFilter.cs new file mode 100644 index 00000000..33fa1569 --- /dev/null +++ b/test/DMLibTestCodeGen/DMLibDirectionFilter.cs @@ -0,0 +1,85 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTestCodeGen +{ + using System; + + internal class DMLibDirectionFilter : DirectionFilter + { + public bool? IsAsync + { + get; + set; + } + + public DMLibDataType SourceType + { + get; + set; + } + + public DMLibDataType DestType + { + get; + set; + } + + public DMLibDirectionFilter(string queryString = null) + { + this.IsAsync = null; + this.SourceType = DMLibDataType.Unspecified; + this.DestType = DMLibDataType.Unspecified; + + this.SetProperties(queryString); + } + + protected override void AddValueGenerators() + { + base.AddValueGenerators(); + + this.AddValueGenerator("IsAsync", ParseNullableBoolean); + this.AddValueGenerator("SourceType", ParseDMLibDataType); + this.AddValueGenerator("DestType", ParseDMLibDataType); + } + + private static object ParseNullableBoolean(string value) + { + return (bool?)Boolean.Parse(value); + } + + private static object ParseDMLibDataType(string value) + { + return Enum.Parse(typeof(DMLibDataType), value, true); + } + + public override bool Filter(TestMethodDirection direction) + { + DMLibTransferDirection DMLibDirection = direction as DMLibTransferDirection; + + if (DMLibDirection == null) + { + throw new ArgumentException("DMLibDirectionFilter is only applicable to DMLibTransferDirection.", "direction"); + } + + if (this.IsAsync != null && this.IsAsync != DMLibDirection.IsAsync) + { + return false; + } + + if (this.SourceType != DMLibDataType.Unspecified && !this.SourceType.HasFlag(DMLibDirection.SourceType)) + { + return false; + } + + if (this.DestType != DMLibDataType.Unspecified && !this.DestType.HasFlag(DMLibDirection.DestType)) + { + return false; + } + + return true; + } + } +} diff --git a/test/DMLibTestCodeGen/DMLibTestCodeGen.csproj b/test/DMLibTestCodeGen/DMLibTestCodeGen.csproj new file mode 100644 index 00000000..05e4efef --- /dev/null +++ b/test/DMLibTestCodeGen/DMLibTestCodeGen.csproj @@ -0,0 +1,79 @@ + + + + + Debug + AnyCPU + {7018EE4E-D389-424E-A8DD-F9B4FFDA5194} + Exe + Properties + DMLibTestCodeGen + DMLibTestCodeGen + v4.5 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + SharedAssemblyInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/DMLibTestCodeGen/DMLibTestContext.cs b/test/DMLibTestCodeGen/DMLibTestContext.cs new file mode 100644 index 00000000..16eb43b6 --- /dev/null +++ b/test/DMLibTestCodeGen/DMLibTestContext.cs @@ -0,0 +1,16 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTestCodeGen +{ + public class DMLibTestContext : MultiDirectionTestContext + { + public static bool IsAsync + { + get; + set; + } + } +} diff --git a/test/DMLibTestCodeGen/DMLibTestMethodSet.cs b/test/DMLibTestCodeGen/DMLibTestMethodSet.cs new file mode 100644 index 00000000..da8ff9ca --- /dev/null +++ b/test/DMLibTestCodeGen/DMLibTestMethodSet.cs @@ -0,0 +1,163 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTestCodeGen +{ + using System; + using System.Collections.Generic; + + public enum DMLibTestMethodSet + { + AllValidDirection, + Cloud2Cloud, + AllAsync, + AllSync, + CloudSource, + CloudBlobSource, + CloudFileSource, + LocalSource, + CloudDest, + CloudBlobDest, + CloudFileDest, + LocalDest, + } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] + public class DMLibTestMethodSetAttribute : MultiDirectionTestMethodSetAttribute + { + public static DMLibTestMethodSetAttribute AllValidDirectionSet; + + static DMLibTestMethodSetAttribute() + { + // All valid direction + AllValidDirectionSet = new DMLibTestMethodSetAttribute(); + // Sync copy + AllValidDirectionSet.AddTestMethodAttribute(new DMLibTestMethodAttribute(DMLibDataType.Local, DMLibDataType.Cloud)); + AllValidDirectionSet.AddTestMethodAttribute(new DMLibTestMethodAttribute(DMLibDataType.Stream, DMLibDataType.Cloud)); + AllValidDirectionSet.AddTestMethodAttribute(new DMLibTestMethodAttribute(DMLibDataType.Cloud, DMLibDataType.Local)); + AllValidDirectionSet.AddTestMethodAttribute(new DMLibTestMethodAttribute(DMLibDataType.Cloud, DMLibDataType.Stream)); + AllValidDirectionSet.AddTestMethodAttribute(new DMLibTestMethodAttribute(DMLibDataType.CloudFile, DMLibDataType.Cloud)); + AllValidDirectionSet.AddTestMethodAttribute(new DMLibTestMethodAttribute(DMLibDataType.Cloud, DMLibDataType.CloudFile)); + AllValidDirectionSet.AddTestMethodAttribute(new DMLibTestMethodAttribute(DMLibDataType.CloudBlob)); + + // Async copy + AllValidDirectionSet.AddTestMethodAttribute(new DMLibTestMethodAttribute(DMLibDataType.URI, DMLibDataType.Cloud, isAsync: true)); + AllValidDirectionSet.AddTestMethodAttribute(new DMLibTestMethodAttribute(DMLibDataType.CloudBlob, isAsync: true)); + AllValidDirectionSet.AddTestMethodAttribute(new DMLibTestMethodAttribute(DMLibDataType.Cloud, DMLibDataType.CloudFile, isAsync: true)); + AllValidDirectionSet.AddTestMethodAttribute(new DMLibTestMethodAttribute(DMLibDataType.CloudFile, DMLibDataType.BlockBlob, isAsync: true)); + } + + public DMLibTestMethodSetAttribute() + { + } + + /// + /// Create a new instance of containing specific + /// valid transfer directions from a query string. Query string format: + /// propertyName1=value1,propertyName2=value2... + /// e.g. + /// To specify all valid async copy directions to blob: + /// DestType=CloudBlob,IsAsync=true + /// + /// Query string + public DMLibTestMethodSetAttribute(string queryString) + { + this.AddTestMethodAttribute(AllValidDirectionSet); + + DMLibDirectionFilter directionFilter = new DMLibDirectionFilter(queryString); + this.AddDirectionFilter(directionFilter); + } + + public DMLibTestMethodSetAttribute(DMLibTestMethodSet directionSet) + { + switch (directionSet) + { + case DMLibTestMethodSet.AllValidDirection: + this.AddTestMethodAttribute(AllValidDirectionSet); + break; + case DMLibTestMethodSet.Cloud2Cloud: + this.AddTestMethodAttribute(AllValidDirectionSet); + this.AddDirectionFilter(new DMLibDirectionFilter() + { + SourceType = DMLibDataType.Cloud, + DestType = DMLibDataType.Cloud, + }); + break; + case DMLibTestMethodSet.AllAsync: + this.AddTestMethodAttribute(AllValidDirectionSet); + this.AddDirectionFilter(new DMLibDirectionFilter() + { + IsAsync = true, + }); + break; + case DMLibTestMethodSet.AllSync: + this.AddTestMethodAttribute(AllValidDirectionSet); + this.AddDirectionFilter(new DMLibDirectionFilter() + { + IsAsync = false, + }); + break; + case DMLibTestMethodSet.CloudSource: + this.AddTestMethodAttribute(AllValidDirectionSet); + this.AddDirectionFilter(new DMLibDirectionFilter() + { + SourceType = DMLibDataType.Cloud, + }); + break; + case DMLibTestMethodSet.CloudBlobSource: + this.AddTestMethodAttribute(AllValidDirectionSet); + this.AddDirectionFilter(new DMLibDirectionFilter() + { + SourceType = DMLibDataType.CloudBlob, + }); + break; + case DMLibTestMethodSet.CloudFileSource: + this.AddTestMethodAttribute(AllValidDirectionSet); + this.AddDirectionFilter(new DMLibDirectionFilter() + { + SourceType = DMLibDataType.CloudFile, + }); + break; + case DMLibTestMethodSet.LocalSource: + this.AddTestMethodAttribute(AllValidDirectionSet); + this.AddDirectionFilter(new DMLibDirectionFilter() + { + SourceType = DMLibDataType.Local, + }); + break; + case DMLibTestMethodSet.CloudDest: + this.AddTestMethodAttribute(AllValidDirectionSet); + this.AddDirectionFilter(new DMLibDirectionFilter() + { + DestType = DMLibDataType.Cloud, + }); + break; + case DMLibTestMethodSet.CloudBlobDest: + this.AddTestMethodAttribute(AllValidDirectionSet); + this.AddDirectionFilter(new DMLibDirectionFilter() + { + DestType = DMLibDataType.CloudBlob, + }); + break; + case DMLibTestMethodSet.CloudFileDest: + this.AddTestMethodAttribute(AllValidDirectionSet); + this.AddDirectionFilter(new DMLibDirectionFilter() + { + DestType = DMLibDataType.CloudFile, + }); + break; + case DMLibTestMethodSet.LocalDest: + this.AddTestMethodAttribute(AllValidDirectionSet); + this.AddDirectionFilter(new DMLibDirectionFilter() + { + DestType = DMLibDataType.Local, + }); + break; + default: + throw new ArgumentException(string.Format("Invalid MultiDirectionSet: {0}", directionSet.ToString()), "directionSet"); + } + } + } +} diff --git a/test/DMLibTestCodeGen/DMLibTestMetholdAttribute.cs b/test/DMLibTestCodeGen/DMLibTestMetholdAttribute.cs new file mode 100644 index 00000000..3fc9c257 --- /dev/null +++ b/test/DMLibTestCodeGen/DMLibTestMetholdAttribute.cs @@ -0,0 +1,89 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTestCodeGen +{ + using System; + using System.Collections.Generic; + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] + public class DMLibTestMethodAttribute : MultiDirectionTestMethodAttribute, ITestDirection + { + public bool IsAsync + { + get; + private set; + } + + public DMLibDataType SourceType + { + get; + private set; + } + + public DMLibDataType DestType + { + get; + private set; + } + + public DMLibTestMethodAttribute( + DMLibDataType dataType, + bool isAsync = false, + string[] tags = null) + : this( + dataType, + DMLibDataType.Unspecified, + isAsync, + tags) + { + } + + public DMLibTestMethodAttribute( + DMLibDataType sourceType, + DMLibDataType destType, + bool isAsync = false, + string[] tags = null) + : base(tags) + { + this.SourceType = sourceType; + this.DestType = destType; + this.IsAsync = isAsync; + } + + internal override IEnumerable ExtractDirections() + { + if (this.DestType == DMLibDataType.Unspecified) + { + foreach (DMLibDataType sourceType in this.SourceType.Extract()) + { + DMLibTransferDirection transferDirection = + new DMLibTransferDirection( + sourceType, + sourceType, + this.IsAsync, + new List(this.Tags)); + yield return transferDirection; + } + } + else + { + foreach (DMLibDataType sourceType in this.SourceType.Extract()) + { + foreach (DMLibDataType destType in this.DestType.Extract()) + { + DMLibTransferDirection transferDirection = + new DMLibTransferDirection( + sourceType, + destType, + this.IsAsync, + new List(this.Tags)); + yield return transferDirection; + } + } + } + } + } +} diff --git a/test/DMLibTestCodeGen/DMLibTransferDirection.cs b/test/DMLibTestCodeGen/DMLibTransferDirection.cs new file mode 100644 index 00000000..a6576df6 --- /dev/null +++ b/test/DMLibTestCodeGen/DMLibTransferDirection.cs @@ -0,0 +1,113 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTestCodeGen +{ + using System.CodeDom; + using System.Collections.Generic; + + internal class DMLibTransferDirection : TestMethodDirection, ITestDirection + { + public DMLibTransferDirection( + DMLibDataType sourceType, + DMLibDataType destType, + bool isAsync, + List tags) + : base(tags) + { + this.SourceType = sourceType; + this.DestType = destType; + this.IsAsync = isAsync; + } + + public bool IsAsync + { + get; + private set; + } + + public DMLibDataType SourceType + { + get; + private set; + } + + public DMLibDataType DestType + { + get; + private set; + } + + public override bool Equals(object obj) + { + DMLibTransferDirection other = obj as DMLibTransferDirection; + if (other == null) + { + return false; + } + + return this.SourceType == other.SourceType && + this.DestType == other.DestType && + this.IsAsync == other.IsAsync; + } + + public override int GetHashCode() + { + int factor = 31; + int hash = this.IsAsync ? 1 : 0; + hash = hash * factor + (int)this.SourceType; + hash = hash * factor + (int)this.DestType; + + return hash; + } + + public override string GetTestMethodNameSuffix() + { + // [SourceType]2[DestType][Async] + return string.Format("{0}2{1}{2}", + this.SourceType.ToString(), + this.DestType.ToString(), + this.IsAsync ? "Async" : string.Empty); + } + + protected override IEnumerable GetExtraTags() + { + yield return string.Format("{0}2{1}{2}", this.SourceType, this.DestType, this.IsAsync ? "Async" : string.Empty); + + if (this.IsAsync) + { + yield return MultiDirectionTag.Async; + } + } + + public override IEnumerable EnumerateUpdateContextStatements() + { + CodeFieldReferenceExpression sourceType = new CodeFieldReferenceExpression( + new CodeTypeReferenceExpression(typeof(DMLibDataType)), + this.SourceType.ToString()); + CodeFieldReferenceExpression destType = new CodeFieldReferenceExpression( + new CodeTypeReferenceExpression(typeof(DMLibDataType)), + this.DestType.ToString()); + + CodePropertyReferenceExpression sourceTypeProperty = new CodePropertyReferenceExpression( + new CodeTypeReferenceExpression(typeof(DMLibTestContext)), + "SourceType"); + + CodePropertyReferenceExpression destTypeProperty = new CodePropertyReferenceExpression( + new CodeTypeReferenceExpression(typeof(DMLibTestContext)), + "DestType"); + + CodePropertyReferenceExpression isAsyncProperty = new CodePropertyReferenceExpression( + new CodeTypeReferenceExpression(typeof(DMLibTestContext)), + "IsAsync"); + + yield return new CodeAssignStatement(sourceTypeProperty, sourceType); + + yield return new CodeAssignStatement(destTypeProperty, destType); + + yield return new CodeAssignStatement(isAsyncProperty, new CodePrimitiveExpression(this.IsAsync)); + } + } +} diff --git a/test/DMLibTestCodeGen/DirectionFilter.cs b/test/DMLibTestCodeGen/DirectionFilter.cs new file mode 100644 index 00000000..72d7b02f --- /dev/null +++ b/test/DMLibTestCodeGen/DirectionFilter.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTestCodeGen +{ + using System; + using System.Collections.Generic; + using System.Reflection; + + internal abstract class DirectionFilter + { + private IDictionary> valueGenerators = new Dictionary>(); + + protected void SetProperties(string queryString) + { + if (string.IsNullOrEmpty(queryString)) + { + return; + } + + this.AddValueGenerators(); + + string[] keyValuePairs = queryString.Split(new char[] { ',' }, StringSplitOptions.None); + + foreach (var keyValuePair in keyValuePairs) + { + string key; + string value; + if (this.TryParseKeyValuePair(keyValuePair, out key, out value)) + { + var valueGen = this.valueGenerators[key]; + object valueObject = valueGen(value); + + PropertyInfo prop = this.GetType().GetProperty(key); + prop.SetValue(this, valueObject); + } + else + { + throw new ArgumentException(string.Format("Invalid queryString: {0}", queryString), "queryString"); + } + } + } + + private bool TryParseKeyValuePair(string keyValuePair, out string key, out string value) + { + string[] keyValueArray = keyValuePair.Split(new char[] { '=' }, StringSplitOptions.None); + if (keyValueArray.Length != 2) + { + key = null; + value = null; + return false; + } + + key = keyValueArray[0].Trim(); + value = keyValueArray[1].Trim(); + return true; + } + + protected virtual void AddValueGenerators() + { + } + + protected void AddValueGenerator(string propertyName, Func valueGenerator) + { + this.valueGenerators.Add(propertyName, valueGenerator); + } + + public abstract bool Filter(TestMethodDirection direction); + } +} diff --git a/test/DMLibTestCodeGen/ITestDirection.cs b/test/DMLibTestCodeGen/ITestDirection.cs new file mode 100644 index 00000000..70c946bb --- /dev/null +++ b/test/DMLibTestCodeGen/ITestDirection.cs @@ -0,0 +1,20 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTestCodeGen +{ + public interface ITestDirection where TDataType : struct + { + TDataType SourceType + { + get; + } + + TDataType DestType + { + get; + } + } +} diff --git a/test/DMLibTestCodeGen/MultiDirectionTag.cs b/test/DMLibTestCodeGen/MultiDirectionTag.cs new file mode 100644 index 00000000..7f8bc9bd --- /dev/null +++ b/test/DMLibTestCodeGen/MultiDirectionTag.cs @@ -0,0 +1,13 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTestCodeGen +{ + public static class MultiDirectionTag + { + public const string MultiDirection = "multiDirection"; + public const string Async = "async"; + } +} diff --git a/test/DMLibTestCodeGen/MultiDirectionTestClass.cs b/test/DMLibTestCodeGen/MultiDirectionTestClass.cs new file mode 100644 index 00000000..57f76a15 --- /dev/null +++ b/test/DMLibTestCodeGen/MultiDirectionTestClass.cs @@ -0,0 +1,104 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTestCodeGen +{ + using System; + using System.Collections.Generic; + using System.Reflection; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + internal class MultiDirectionTestClass + { + public Type ClassType + { + private set; + get; + } + + public MethodInfo TestInit + { + private set; + get; + } + + public MethodInfo TestCleanup + { + private set; + get; + } + + public MethodInfo ClassInit + { + private set; + get; + } + + public MethodInfo ClassCleanup + { + private set; + get; + } + + public List MultiDirectionMethods + { + private set; + get; + } + + public MultiDirectionTestClass(Type type) + { + this.ClassType = type; + this.MultiDirectionMethods = new List(); + + this.ParseTestMethods(type); + } + + private void ParseTestMethods(Type type) + { + foreach (MethodInfo methodInfo in type.GetMethods()) + { + this.ParseTestMethod(methodInfo); + } + } + + private void ParseTestMethod(MethodInfo methodInfo) + { + bool isMultiDirectionMethod = false; + foreach (Attribute attribute in methodInfo.GetCustomAttributes(true)) + { + if (attribute is ClassInitializeAttribute) + { + this.ClassInit = methodInfo; + } + else if (attribute is ClassCleanupAttribute) + { + this.ClassCleanup = methodInfo; + } + else if (attribute is TestInitializeAttribute) + { + this.TestInit = methodInfo; + } + else if (attribute is TestCleanupAttribute) + { + this.TestCleanup = methodInfo; + } + else if (attribute is MultiDirectionTestMethodAttribute) + { + isMultiDirectionMethod = true; + } + else if (attribute is MultiDirectionTestMethodSetAttribute) + { + isMultiDirectionMethod = true; + } + } + + if (isMultiDirectionMethod) + { + this.MultiDirectionMethods.Add(new MultiDirectionTestMethod(methodInfo)); + } + } + } +} diff --git a/test/DMLibTestCodeGen/MultiDirectionTestClassAttribute.cs b/test/DMLibTestCodeGen/MultiDirectionTestClassAttribute.cs new file mode 100644 index 00000000..8592e8b6 --- /dev/null +++ b/test/DMLibTestCodeGen/MultiDirectionTestClassAttribute.cs @@ -0,0 +1,17 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTestCodeGen +{ + using System; + + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public class MultiDirectionTestClassAttribute : Attribute + { + public MultiDirectionTestClassAttribute() + { + } + } +} diff --git a/test/DMLibTestCodeGen/MultiDirectionTestContext.cs b/test/DMLibTestCodeGen/MultiDirectionTestContext.cs new file mode 100644 index 00000000..5d43004f --- /dev/null +++ b/test/DMLibTestCodeGen/MultiDirectionTestContext.cs @@ -0,0 +1,22 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTestCodeGen +{ + public class MultiDirectionTestContext where TDataType : struct + { + public static TDataType SourceType + { + get; + set; + } + + public static TDataType DestType + { + get; + set; + } + } +} diff --git a/test/DMLibTestCodeGen/MultiDirectionTestMethod.cs b/test/DMLibTestCodeGen/MultiDirectionTestMethod.cs new file mode 100644 index 00000000..eb9523f3 --- /dev/null +++ b/test/DMLibTestCodeGen/MultiDirectionTestMethod.cs @@ -0,0 +1,56 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTestCodeGen +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + + internal class MultiDirectionTestMethod + { + private HashSet transferDirections; + + public MethodInfo MethodInfoObj + { + get; + private set; + } + + public MultiDirectionTestMethod(MethodInfo methodInfo) + { + this.MethodInfoObj = methodInfo; + transferDirections = new HashSet(); + + foreach (Attribute attribute in methodInfo.GetCustomAttributes(true)) + { + MultiDirectionTestMethodAttribute multiDirectionAttr = attribute as MultiDirectionTestMethodAttribute; + if (null != multiDirectionAttr) + { + this.ParseMultiDirectionAttribute(multiDirectionAttr); + } + } + } + + public IEnumerable GetTransferDirections() + { + return this.transferDirections; + } + + private void ParseMultiDirectionAttribute(MultiDirectionTestMethodAttribute multiDirectionAttr) + { + foreach (var direction in multiDirectionAttr.ExtractDirections()) + { + if (this.transferDirections.Contains(direction) && direction.Tags.Any()) + { + this.transferDirections.Remove(direction); + } + + this.transferDirections.Add(direction); + } + } + } +} diff --git a/test/DMLibTestCodeGen/MultiDirectionTestMethodAttribute.cs b/test/DMLibTestCodeGen/MultiDirectionTestMethodAttribute.cs new file mode 100644 index 00000000..a5f979d4 --- /dev/null +++ b/test/DMLibTestCodeGen/MultiDirectionTestMethodAttribute.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTestCodeGen +{ + using System; + using System.Collections.Generic; + + public abstract class MultiDirectionTestMethodAttribute : Attribute + { + protected string[] Tags + { + get; + private set; + } + + protected MultiDirectionTestMethodAttribute(string[] tags = null) + { + this.Tags = tags ?? new string[0]; + } + + internal abstract IEnumerable ExtractDirections(); + } +} diff --git a/test/DMLibTestCodeGen/MultiDirectionTestMethodSetAttribute.cs b/test/DMLibTestCodeGen/MultiDirectionTestMethodSetAttribute.cs new file mode 100644 index 00000000..95a81c6e --- /dev/null +++ b/test/DMLibTestCodeGen/MultiDirectionTestMethodSetAttribute.cs @@ -0,0 +1,60 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTestCodeGen +{ + using System; + using System.Collections.Generic; + using System.Reflection; + + public abstract class MultiDirectionTestMethodSetAttribute : MultiDirectionTestMethodAttribute + { + private List testMethodAttributes = new List(); + + private List directionFilters = new List(); + + protected void AddTestMethodAttribute(MultiDirectionTestMethodAttribute testMethodAttribute) + { + if (testMethodAttribute == null) + { + throw new ArgumentNullException("testMethodAttribute"); + } + + this.testMethodAttributes.Add(testMethodAttribute); + } + + internal void AddDirectionFilter(DirectionFilter directionFilter) + { + this.directionFilters.Add(directionFilter); + } + + internal override IEnumerable ExtractDirections() + { + foreach(var attribute in this.testMethodAttributes) + { + foreach(var direction in attribute.ExtractDirections()) + { + if (this.Filter(direction)) + { + yield return direction; + } + } + } + } + + private bool Filter(TestMethodDirection direction) + { + foreach(var directionFilter in this.directionFilters) + { + if (!directionFilter.Filter(direction)) + { + return false; + } + } + + return true; + } + } +} diff --git a/test/DMLibTestCodeGen/Program.cs b/test/DMLibTestCodeGen/Program.cs new file mode 100644 index 00000000..1b892b7c --- /dev/null +++ b/test/DMLibTestCodeGen/Program.cs @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTestCodeGen +{ + using System; + using System.Reflection; + + public class Program + { + public static void Main(string[] args) + { + if (args == null || args.Length != 2) + { + PrintHelp(); + return; + } + + string dllName = args[0]; + string sourceFolder = args[1]; + + GenerateCode(dllName, sourceFolder); + } + + private static void GenerateCode(string dllName, string outputFolder) + { + SourceCodeGenerator codeGen = new SourceCodeGenerator(outputFolder); + + Assembly assembly = Assembly.LoadFrom(dllName); + + foreach (Type type in assembly.GetTypes()) + { + if (null != type.GetCustomAttribute(typeof(MultiDirectionTestClassAttribute))) + { + MultiDirectionTestClass testClass = new MultiDirectionTestClass(type); + codeGen.GenerateSourceCode(testClass); + } + } + } + + private static void PrintHelp() + { + Console.WriteLine("Usage: DMLibTestCodeGen.exe [InputDll] [OutputSourceFolder]"); + } + } +} diff --git a/test/DMLibTestCodeGen/Properties/AssemblyInfo.cs b/test/DMLibTestCodeGen/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..04676b2b --- /dev/null +++ b/test/DMLibTestCodeGen/Properties/AssemblyInfo.cs @@ -0,0 +1,14 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("DMLibTestCodeGen")] +[assembly: AssemblyDescription("")] diff --git a/test/DMLibTestCodeGen/SourceCodeGenerator.cs b/test/DMLibTestCodeGen/SourceCodeGenerator.cs new file mode 100644 index 00000000..93db7847 --- /dev/null +++ b/test/DMLibTestCodeGen/SourceCodeGenerator.cs @@ -0,0 +1,263 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTestCodeGen +{ + using System; + using System.CodeDom; + using System.CodeDom.Compiler; + using System.IO; + using Microsoft.CSharp; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + internal static class GodeGeneratorConst + { + public const string RootNameSpace = "DMLibTest.Generated"; + public const string ClassInitMethodName = "GeneratedClassInit"; + public const string ClassCleanupMethodName = "GeneratedClassCleanup"; + public const string TestInitMethodName = "GeneratedTestInit"; + public const string TestCleanupMethodName = "GeneratedTestCleanup"; + } + + internal class SourceCodeGenerator + { + private const string SourceFileExtention = ".cs"; + private const string GeneratedSuffix = "_Generated"; + + private string outputPath; + + public SourceCodeGenerator(string outputPath) + { + this.outputPath = outputPath; + } + + public void GenerateSourceCode(MultiDirectionTestClass testClass) + { + string sourceFileName = this.GetSourceFileName(testClass); + + if (testClass.MultiDirectionMethods.Count == 0) + { + Console.WriteLine("{0} has no multiple direction test case. Skip code generating...", testClass.ClassType.Name); + return; + } + + Console.WriteLine("Generating code for {0}", testClass.ClassType.Name); + + CodeCompileUnit compileUnit = new CodeCompileUnit(); + CodeNamespace rootNameSpace = new CodeNamespace(GodeGeneratorConst.RootNameSpace); + + this.AddImport(rootNameSpace); + + rootNameSpace.Types.Add(GetGeneratedClass(testClass)); + + compileUnit.Namespaces.Add(rootNameSpace); + + this.WriteCodeToFile(compileUnit, Path.Combine(this.outputPath, sourceFileName)); + } + + private void AddImport(CodeNamespace nameSpace) + { + nameSpace.Imports.Add(new CodeNamespaceImport("DMLibTestCodeGen")); + nameSpace.Imports.Add(new CodeNamespaceImport("Microsoft.VisualStudio.TestTools.UnitTesting")); + nameSpace.Imports.Add(new CodeNamespaceImport("MS.Test.Common.MsTestLib")); + nameSpace.Imports.Add(new CodeNamespaceImport("System")); + } + + private CodeTypeDeclaration GetGeneratedClass(MultiDirectionTestClass testClass) + { + CodeTypeDeclaration result = new CodeTypeDeclaration(this.GetGeneratedClassName(testClass)); + result.Attributes = MemberAttributes.Public; + result.BaseTypes.Add(testClass.ClassType); + + CodeAttributeDeclaration testClassAttribute = new CodeAttributeDeclaration( + new CodeTypeReference(typeof(TestClassAttribute))); + + result.CustomAttributes.Add(testClassAttribute); + + // Add initialize and cleanup method + result.Members.Add(this.GetInitCleanupMethod(typeof(ClassInitializeAttribute), testClass)); + result.Members.Add(this.GetInitCleanupMethod(typeof(ClassCleanupAttribute), testClass)); + + // No need to generate TestInitialize and TestCleanup Method. + // Generated class can inherit from base class. + + // Expand multiple direction test case + foreach (MultiDirectionTestMethod testMethod in testClass.MultiDirectionMethods) + { + this.AddGeneratedMethod(result, testMethod); + } + + return result; + } + + private CodeMemberMethod GetInitCleanupMethod(Type methodAttributeType, MultiDirectionTestClass testClass) + { + bool isStatic = false; + string generatedMetholdName = string.Empty; + string methodToInvokeName = string.Empty; + CodeParameterDeclarationExpression parameterDec = null; + + if (methodAttributeType == typeof(ClassInitializeAttribute)) + { + isStatic = true; + generatedMetholdName = GodeGeneratorConst.ClassInitMethodName; + methodToInvokeName = testClass.ClassInit.Name; + parameterDec = new CodeParameterDeclarationExpression(typeof(TestContext), "testContext"); + } + else if (methodAttributeType == typeof(ClassCleanupAttribute)) + { + isStatic = true; + generatedMetholdName = GodeGeneratorConst.ClassCleanupMethodName; + methodToInvokeName = testClass.ClassCleanup.Name; + } + else + { + throw new ArgumentException("methodAttributeType"); + } + + CodeMemberMethod result = new CodeMemberMethod(); + result.Name = generatedMetholdName; + + // Add parameter list if needed + if (parameterDec != null) + { + result.Parameters.Add(parameterDec); + } + + CodeExpression callBase = null; + if (isStatic) + { + result.Attributes = MemberAttributes.Public | MemberAttributes.Static; + callBase = new CodeTypeReferenceExpression(testClass.ClassType.FullName); + } + else + { + result.Attributes = MemberAttributes.Public | MemberAttributes.Final; + callBase = new CodeBaseReferenceExpression(); + } + + // Add methold attribute + CodeAttributeDeclaration methodAttribute = new CodeAttributeDeclaration( + new CodeTypeReference(methodAttributeType)); + result.CustomAttributes.Add(methodAttribute); + + // Add invoke statement + CodeMethodInvokeExpression invokeExp = null; + if (parameterDec != null) + { + CodeVariableReferenceExpression sourceParameter = new CodeVariableReferenceExpression(parameterDec.Name); + invokeExp = new CodeMethodInvokeExpression(callBase, methodToInvokeName, sourceParameter); + } + else + { + invokeExp = new CodeMethodInvokeExpression(callBase, methodToInvokeName); + } + + result.Statements.Add(invokeExp); + + return result; + } + + private void AddGeneratedMethod(CodeTypeDeclaration generatedClass, MultiDirectionTestMethod testMethod) + { + foreach (var transferDirection in testMethod.GetTransferDirections()) + { + string generatedMethodName = this.GetGeneratedMethodName(testMethod, transferDirection); + + CodeMemberMethod generatedMethod = new CodeMemberMethod(); + generatedMethod.Name = generatedMethodName; + generatedMethod.Attributes = MemberAttributes.Public | MemberAttributes.Final; + + // Add TestCategoryAttribute to the generated method + this.AddTestCategoryAttributes(generatedMethod, testMethod); + this.AddTestCategoryAttribute(generatedMethod, MultiDirectionTag.MultiDirection); + foreach (var tag in transferDirection.GetTags()) + { + this.AddTestCategoryAttribute(generatedMethod, tag); + } + + CodeAttributeDeclaration testMethodAttribute = new CodeAttributeDeclaration( + new CodeTypeReference(typeof(TestMethodAttribute))); + + generatedMethod.CustomAttributes.Add(testMethodAttribute); + + foreach (var statement in transferDirection.EnumerateUpdateContextStatements()) + { + generatedMethod.Statements.Add(statement); + } + + CodeMethodReferenceExpression callee = new CodeMethodReferenceExpression( + new CodeBaseReferenceExpression(), testMethod.MethodInfoObj.Name); + CodeMethodInvokeExpression invokeExp = new CodeMethodInvokeExpression(callee); + generatedMethod.Statements.Add(invokeExp); + generatedClass.Members.Add(generatedMethod); + } + } + + private void AddTestCategoryAttributes(CodeMemberMethod method, MultiDirectionTestMethod testMethod) + { + foreach (var customAttribute in testMethod.MethodInfoObj.CustomAttributes) + { + if (customAttribute.AttributeType == typeof(TestCategoryAttribute)) + { + if (customAttribute.ConstructorArguments.Count != 1) + { + // Unrecognized attribute, skip + continue; + } + + this.AddTestCategoryAttribute( + method, + new CodeSnippetExpression(customAttribute.ConstructorArguments[0].ToString())); + } + } + } + + private void AddTestCategoryAttribute(CodeMemberMethod method, string tagName) + { + this.AddTestCategoryAttribute(method, new CodePrimitiveExpression(tagName)); + } + + private void AddTestCategoryAttribute(CodeMemberMethod method, CodeExpression expression) + { + CodeAttributeArgument testCategoryTag = new CodeAttributeArgument(expression); + + CodeAttributeDeclaration testCategoryAttribute = new CodeAttributeDeclaration( + new CodeTypeReference(typeof(TestCategoryAttribute)), + testCategoryTag); + + method.CustomAttributes.Add(testCategoryAttribute); + } + + private string GetSourceFileName(MultiDirectionTestClass testClass) + { + return this.GetGeneratedClassName(testClass) + SourceFileExtention; + } + + private string GetGeneratedClassName(MultiDirectionTestClass testClass) + { + return testClass.ClassType.Name + GeneratedSuffix; + } + + private string GetGeneratedMethodName(MultiDirectionTestMethod testMethod, TestMethodDirection transferDirection) + { + // [MethodName]_[DirectionSuffix] + return String.Format("{0}_{1}", testMethod.MethodInfoObj.Name, transferDirection.GetTestMethodNameSuffix()); + } + + private void WriteCodeToFile(CodeCompileUnit compileUnit, string sourceFileName) + { + CSharpCodeProvider provider = new CSharpCodeProvider(); + + using (StreamWriter sw = new StreamWriter(sourceFileName, false)) + { + using (IndentedTextWriter tw = new IndentedTextWriter(sw, " ")) + { + provider.GenerateCodeFromCompileUnit(compileUnit, tw, new CodeGeneratorOptions()); + } + } + } + } +} diff --git a/test/DMLibTestCodeGen/TestMethodDirection.cs b/test/DMLibTestCodeGen/TestMethodDirection.cs new file mode 100644 index 00000000..1ecfce0e --- /dev/null +++ b/test/DMLibTestCodeGen/TestMethodDirection.cs @@ -0,0 +1,50 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace DMLibTestCodeGen +{ + using System.CodeDom; + using System.Collections.Generic; + + internal abstract class TestMethodDirection + { + public List Tags + { + get; + private set; + } + + public TestMethodDirection(List tags) + { + if (null != tags) + { + this.Tags = new List(tags); + } + else + { + this.Tags = new List(); + } + } + + public abstract string GetTestMethodNameSuffix(); + + protected abstract IEnumerable GetExtraTags(); + + public IEnumerable GetTags() + { + foreach(var tag in Tags) + { + yield return tag; + } + + foreach (var extraTag in this.GetExtraTags()) + { + yield return extraTag; + } + } + + public abstract IEnumerable EnumerateUpdateContextStatements(); + } +} diff --git a/test/MsTestLib/ClassConfig.cs b/test/MsTestLib/ClassConfig.cs new file mode 100644 index 00000000..b4c481e9 --- /dev/null +++ b/test/MsTestLib/ClassConfig.cs @@ -0,0 +1,55 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MS.Test.Common.MsTestLib +{ + + public class ClassConfig + { + public ClassConfig() + { + classParams = new Dictionary(); + classMethods = new Dictionary(); + } + + private Dictionary classParams; + + public Dictionary ClassParams + { + get { return classParams; } + set { classParams = value; } + } + + private Dictionary classMethods; + + public MethodConfig this[string methodName] + { + get + { + if (classMethods.ContainsKey(methodName)) + { + return classMethods[methodName]; + } + else + { + return null; + } + } + + set + { + classMethods[methodName] = value; + } + + } + + } + +} diff --git a/test/MsTestLib/ConsoleLogger.cs b/test/MsTestLib/ConsoleLogger.cs new file mode 100644 index 00000000..8bd4a603 --- /dev/null +++ b/test/MsTestLib/ConsoleLogger.cs @@ -0,0 +1,163 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MS.Test.Common.MsTestLib +{ + public class ConsoleLogger : ILogger + { + + private const ConsoleColor ERROR_FG_COLOR = ConsoleColor.Red; + private const ConsoleColor INFO_FG_COLOR = ConsoleColor.White; + private const ConsoleColor WARN_FG_COLOR = ConsoleColor.Green; + private const ConsoleColor NOTE_FG_COLOR = ConsoleColor.DarkYellow; + + private ConsoleColor m_prevFGColor; + + public ConsoleLogger() + { + m_prevFGColor = Console.ForegroundColor; + } + + /// + /// + /// Writes an error log + /// + /// Format message string + /// exception object + /// Objects that need to be serialized in the message + /// + public void WriteError(string msg, params object[] objToLog) + { + DateTime dt = DateTime.Now; + StringBuilder sBuilder = new StringBuilder("[ERROR][" + dt.ToString() + "." + dt.Millisecond + "]"); + sBuilder.Append( MessageBuilder.FormatString( msg, objToLog ) ); + Console.ForegroundColor = ERROR_FG_COLOR; + Console.WriteLine( sBuilder.ToString() ); + Console.ForegroundColor = m_prevFGColor; + } + + /// + /// + /// Writes a warn log + /// + /// Format message string + /// Objects that need to be serialized in the message + /// + public void WriteWarning(string msg, params object[] objToLog) + { + DateTime dt = DateTime.Now; + StringBuilder sBuilder = new StringBuilder("[WARN][" + dt.ToString() + "." + dt.Millisecond + "]"); + sBuilder.Append( MessageBuilder.FormatString( msg, objToLog ) ); + Console.ForegroundColor = WARN_FG_COLOR; + Console.WriteLine( sBuilder.ToString() ); + Console.ForegroundColor = m_prevFGColor; + } + + /// + /// + /// Writes an info log + /// + /// Format message string + /// Objects that need to be serialized in the message + /// + public void WriteInfo(string msg, params object[] objToLog) + { + DateTime dt = DateTime.Now; + StringBuilder sBuilder = new StringBuilder( "[INFO][" + dt.ToString()+"."+ dt.Millisecond+ "]" ); + sBuilder.Append( MessageBuilder.FormatString( msg, objToLog) ); + Console.ForegroundColor = INFO_FG_COLOR; + Console.WriteLine( sBuilder.ToString() ); + Console.ForegroundColor = m_prevFGColor; + } + + /// + /// + /// Writes a verbose log + /// + /// Format message string + /// Objects that need to be serialized in the message + /// + public void WriteVerbose(string msg, params object[] objToLog) + { + DateTime dt = DateTime.Now; + StringBuilder sBuilder = new StringBuilder("[VERB][" + dt.ToString() + "." + dt.Millisecond + "]"); + sBuilder.Append( MessageBuilder.FormatString( msg, objToLog) ); + Console.ForegroundColor = INFO_FG_COLOR; + Console.WriteLine( sBuilder.ToString() ); + Console.ForegroundColor = m_prevFGColor; + } + + + /// + /// + /// Starts a test (as a child of the current context) + /// + /// Test id + /// + public void StartTest(string testId) + { + StringBuilder sBuilder = new StringBuilder("[START] Test: "); + sBuilder.Append( testId ); + + Console.ForegroundColor = NOTE_FG_COLOR; + Console.WriteLine( sBuilder.ToString() ); + Console.ForegroundColor = m_prevFGColor; + } + + /// + /// + /// Ends the specified test with the specified test result. + /// + /// Test id + /// Result of the Test + /// + public void EndTest(string testId, TestResult result ) + { + Console.ForegroundColor = NOTE_FG_COLOR; + + if (result == TestResult.FAIL || result == TestResult.SKIP) + { + Console.ForegroundColor = ERROR_FG_COLOR; + } + + StringBuilder sBuilder = new StringBuilder("[END] Test: "); + sBuilder.Append( testId ); + sBuilder.Append( " RESULT: " ); + sBuilder.Append( result.ToString() ); + + Console.WriteLine( sBuilder.ToString() ); + + Console.ForegroundColor = m_prevFGColor; + return; + } + + /// + /// + /// Returns "this" object + /// + /// SimpleConsoleLogger object + /// + public object GetLogger() + { + return this; + } + + /// + /// + /// Releases any resource held + /// + /// + public void Close() + { + //Do nothing + } + } +} + diff --git a/test/MsTestLib/Exceptions.cs b/test/MsTestLib/Exceptions.cs new file mode 100644 index 00000000..1cbe1437 --- /dev/null +++ b/test/MsTestLib/Exceptions.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MS.Test.Common.MsTestLib +{ + public class TestPauseException : Exception + { + public TestPauseException() + { + } + + public TestPauseException(string message) + : base(message) + { + } + + public TestPauseException(string message, Exception innerException) + : base(message, innerException) + { + } + + } +} diff --git a/test/MsTestLib/FileLogger.cs b/test/MsTestLib/FileLogger.cs new file mode 100644 index 00000000..f956c5af --- /dev/null +++ b/test/MsTestLib/FileLogger.cs @@ -0,0 +1,192 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MS.Test.Common.MsTestLib +{ + public class FileLogger : ILogger + { + + private System.IO.StreamWriter m_file; + + /// + /// + /// Creates a new instance of this class + /// + /// + /// + public FileLogger() + { + string fileName = Environment.UserName + "_" + Environment.MachineName + " " + DateTime.Now.ToString().Replace('/', '-').Replace(':', '_') + ".txt"; + m_file = new System.IO.StreamWriter(fileName.ToString(), true); + } + + /// + /// + /// Creates a new instance of this class + /// + /// + /// File to which logs should be appended + /// + public FileLogger(string fileName) + : this(fileName, true) + { + + } + + /// + /// + /// Creates a new instance of this class + /// + /// + /// File to which logs should be written/appended + /// denotes whether the file is to be appended or over-written + /// + public FileLogger(string fileName, bool append) + { + // Open the file and assign to member variable + m_file = new System.IO.StreamWriter(fileName, append); + } + + /// + /// + /// Writes an error log + /// + /// Format message string + /// exception object + /// Objects that need to be serialized in the message + /// + public void WriteError( + string msg, + params object[] objToLog) + { + DateTime dt = DateTime.Now; + StringBuilder sBuilder = new StringBuilder("[ERROR][" + dt.ToLongTimeString() + "." + dt.Millisecond + "]"); + sBuilder.Append(MessageBuilder.FormatString(msg, objToLog)); + m_file.WriteLine(sBuilder.ToString()); + m_file.Flush(); + } + + /// + /// + /// Writes a warn log + /// + /// Format message string + /// Objects that need to be serialized in the message + /// + public void WriteWarning( + string msg, + params object[] objToLog) + { + DateTime dt = DateTime.Now; + StringBuilder sBuilder = new StringBuilder("[WARN][" + dt.ToLongTimeString() + "." + dt.Millisecond + "]"); + sBuilder.Append(MessageBuilder.FormatString(msg, objToLog)); + m_file.WriteLine(sBuilder.ToString()); + m_file.Flush(); + } + + /// + /// + /// Writes an info log + /// + /// Format message string + /// Objects that need to be serialized in the message + /// + public void WriteInfo( + string msg, + params object[] objToLog) + { + DateTime dt = DateTime.Now; + StringBuilder sBuilder = new StringBuilder("[INFO][" + dt.ToLongTimeString() + "." + dt.Millisecond + "]"); + sBuilder.Append(MessageBuilder.FormatString(msg, objToLog)); + m_file.WriteLine(sBuilder.ToString()); + m_file.Flush(); + } + + /// + /// + /// Writes a verbose log + /// + /// Format message string + /// Objects that need to be serialized in the message + /// + public void WriteVerbose( + string msg, + params object[] objToLog) + { + DateTime dt = DateTime.Now; + StringBuilder sBuilder = new StringBuilder("[VERB][" + dt.ToLongTimeString() + "." + dt.Millisecond + "]"); + sBuilder.Append(MessageBuilder.FormatString(msg, objToLog)); + m_file.WriteLine(sBuilder.ToString()); + m_file.Flush(); + } + + + /// + /// + /// Starts a test (as a child of the current context) + /// + /// Test id + /// + public void StartTest( + string testId) + { + StringBuilder sBuilder = new StringBuilder("[START] Test: "); + sBuilder.Append(testId); + m_file.WriteLine(sBuilder.ToString()); + m_file.Flush(); + } + + /// + /// + /// Ends the specified test with the specified test result + /// + /// Test id + /// Result of the Test + /// + public void EndTest( + string testId, + TestResult result) + { + StringBuilder sBuilder = new StringBuilder("[END] Test: "); + sBuilder.Append(testId); + sBuilder.Append(" RESULT: "); + sBuilder.Append(result.ToString()); + m_file.WriteLine(sBuilder.ToString()); + m_file.Flush(); + return; + } + + + /// + /// + /// Returns "this" object + /// + /// SimpleFileLogger object + /// + public object GetLogger() + { + return this; + } + + /// + /// + /// Closes the log file + /// + /// + public void Close() + { + if (m_file != null) + { + m_file.Flush(); + m_file.Close(); + } + } + } +} diff --git a/test/MsTestLib/ILogger.cs b/test/MsTestLib/ILogger.cs new file mode 100644 index 00000000..6aa5f028 --- /dev/null +++ b/test/MsTestLib/ILogger.cs @@ -0,0 +1,52 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MS.Test.Common.MsTestLib +{ + public interface ILogger + { + void WriteError( + string msg, + params object[] objToLog); + + void WriteWarning( + string msg, + params object[] objToLog); + + void WriteInfo( + string msg, + params object[] objToLog); + + void WriteVerbose( + string msg, + params object[] objToLog); + + void StartTest( + string testId); + + void EndTest( + string testId, + TestResult result); + + object GetLogger(); + + void Close(); + + } + + public enum TestResult + { + PASS, + FAIL, + SKIP + } + + +} diff --git a/test/MsTestLib/MessageBuilder.cs b/test/MsTestLib/MessageBuilder.cs new file mode 100644 index 00000000..dc52094e --- /dev/null +++ b/test/MsTestLib/MessageBuilder.cs @@ -0,0 +1,85 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MS.Test.Common.MsTestLib +{ + public class MessageBuilder + { + /// + /// + /// Uses String.Format method for formatting. Incase of any Exceptions due + /// to null arguments or incorrect message format, it formats the message in + /// an internal standard format. For example: + /// MSG: my-message + /// Obj-1: objToLog[1] + /// Obj-2: objToLog[2] + /// ... + /// + /// + /// Objects that need to be serialized in the message + /// + /// + public static string FormatString(string msgFormat, params object[] objToLog) + { + if ((string.IsNullOrEmpty(msgFormat) == false) + && (msgFormat.IndexOf('{') != -1) + && (msgFormat.IndexOf('}') != -1)) + { + try + { + return String.Format(msgFormat, objToLog); + } + catch + { + //ignore exception + } + } + + string prefix = string.Empty; + if (objToLog != null && objToLog.Length > 1) + { + prefix = " "; + } + + StringBuilder sBuilder = new StringBuilder(prefix); + sBuilder.Append(msgFormat); + sBuilder.Append(SerializeObjects(objToLog)); + return sBuilder.ToString(); + } + + private static string SerializeObjects(object[] objToLog) + { + StringBuilder sBuilder = new StringBuilder(); + if (objToLog != null) + { + for (int i = 0; i < objToLog.Length; i++) + { + if (objToLog != null) + { + try + { + sBuilder.Append("\n"); + sBuilder.Append(" Obj-"); + sBuilder.Append(i); + sBuilder.Append(" : "); + sBuilder.Append(objToLog[i]); + } + catch + { + //Ignore any serialization exceptions + + } + } + } + } + return sBuilder.ToString(); + } + } +} diff --git a/test/MsTestLib/MethodConfig.cs b/test/MsTestLib/MethodConfig.cs new file mode 100644 index 00000000..5ae37bf8 --- /dev/null +++ b/test/MsTestLib/MethodConfig.cs @@ -0,0 +1,44 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MS.Test.Common.MsTestLib +{ + + public class MethodConfig + { + public MethodConfig() + { + methodParams = new Dictionary(); + } + + private Dictionary methodParams; + + public Dictionary MethodParams + { + get { return methodParams; } + set { methodParams = value; } + } + + public string this[string key] + { + get + { + return methodParams[key]; + } + + set + { + methodParams[key] = value; + } + } + } + + +} diff --git a/test/MsTestLib/MsTestLib.csproj b/test/MsTestLib/MsTestLib.csproj new file mode 100644 index 00000000..2ef0e25a --- /dev/null +++ b/test/MsTestLib/MsTestLib.csproj @@ -0,0 +1,73 @@ + + + + + Debug + AnyCPU + {AC39B50F-DC27-4411-9ED4-A4A137190ACB} + Library + MS.Test.Common.MsTestLib + MsTestLib + Properties + v4.5 + 512 + ..\ + true + + + + false + + + true + + + ..\..\tools\strongnamekeys\fake\windows.snk + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + ManagedMinimumRules.ruleset + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + ManagedMinimumRules.ruleset + + + + ..\..\..\..\imports\VisualStudio\VS10RTM\MsTest\Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/MsTestLib/Test.cs b/test/MsTestLib/Test.cs new file mode 100644 index 00000000..5d8225ef --- /dev/null +++ b/test/MsTestLib/Test.cs @@ -0,0 +1,139 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace MS.Test.Common.MsTestLib +{ + public static class Test + { + public static string TestDataFile; + public static TestConfig Data; + public static TestLogger Logger; + public static int TestCount = 0; + public static int FailCount = 0; + public static int SkipCount = 0; + + public static string FullClassName = string.Empty; + public static string MethodName = string.Empty; + + public static int ErrorCount = 0; + public static int SkipErrorCount = 0; + + public static List FailedCases = null; + public static List SkippedCases = null; + + public static void Init() + { + Init(TestDataFile); + } + + public static void Init(string testDataFile) + { + Data = new TestConfig(testDataFile); + Logger = new TestLogger(Data); + FailedCases = new List(); + SkippedCases = new List(); + } + + public static void Close() + { + Logger.Close(); + } + + public static void Info( + string msg, + params object[] objToLog) + { + Logger.Info(msg, objToLog); + } + + public static void Warn( + string msg, + params object[] objToLog) + { + Logger.Warning(msg, objToLog); + } + + public static void Verbose( + string msg, + params object[] objToLog) + { + Logger.Verbose(msg, objToLog); + } + + public static void Error( + string msg, + params object[] objToLog) + { + ErrorCount++; + Logger.Error(msg, objToLog); + } + + public static void SkipError( + string msg, + params object[] objToLog) + { + SkipErrorCount++; + Logger.Error(msg, objToLog); + } + + public static void Assert(bool condition, + string msg, + params object[] objToLog) + { + if (condition) + { + Verbose("[Assert Pass] " + msg, objToLog); + } + else + { + Error("[Assert Fail] " + msg, objToLog); + } + } + + public static void Start(string testClass, string testMethod) + { + TestCount++; + ErrorCount = 0; + SkipErrorCount = 0; + Logger.StartTest(testClass + "." + testMethod); + Test.FullClassName = testClass; + Test.MethodName = testMethod; + } + + public static void End(string testClass, string testMethod) + { + if (ErrorCount == 0 && SkipErrorCount == 0) + { + Logger.EndTest(testClass + "." + testMethod, TestResult.PASS); + } + else if (SkipErrorCount > 0) + { + SkipCount++; + Logger.EndTest(testClass + "." + testMethod, TestResult.SKIP); + AssertFail(string.Format("The case is skipped since Test init fail. Please check the detailed case log.")); + SkippedCases.Add(String.Format("{0}.{1}", testClass, testMethod)); + } + else + { + FailCount++; + Logger.EndTest(testClass + "." + testMethod, TestResult.FAIL); + AssertFail(string.Format("There " + (ErrorCount > 1 ? "are {0} errors" : "is {0} error") + " so the case fails. Please check the detailed case log.", ErrorCount)); + FailedCases.Add(String.Format("{0}.{1}", testClass, testMethod)); + } + + } + + public static AssertFailDelegate AssertFail; + + } + + public delegate void AssertFailDelegate(string msg); +} diff --git a/test/MsTestLib/TestConfig.cs b/test/MsTestLib/TestConfig.cs new file mode 100644 index 00000000..d1081f99 --- /dev/null +++ b/test/MsTestLib/TestConfig.cs @@ -0,0 +1,181 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml; +using System.IO; + +namespace MS.Test.Common.MsTestLib +{ + public class TestConfig + { + private string DefaultConfigFileName = "TestData.xml"; + + public TestConfig(string configFile) + { + testParams = new Dictionary(); + testClasses = new Dictionary(); + + //Initialze: read default config file TestData.xml and then read configFile file + if(string.IsNullOrEmpty(configFile)) + { + configFile = "TestData.xml"; + } + if (File.Exists(DefaultConfigFileName)) + { + ReadConfig(DefaultConfigFileName); //read default config file: TestData.xml + } + if (File.Exists(configFile)) + { + ReadConfig(configFile); //read configFile file: e.g MyTestData.xml, configuration in this file will cover settings in TestData.xml + } + else + { + throw new FileNotFoundException(String.Format("{0} not found", configFile)); + } + } + private void ReadConfig(string configFile) + { + if (string.IsNullOrEmpty(configFile)) + { + throw new ArgumentNullException(); //illegal use + } + XmlDocument config = new XmlDocument(); + try + { + config.Load(configFile); + } + catch (FileNotFoundException) + { + string errorMsg = string.Format("{0} file not found", configFile); + throw new FileNotFoundException(errorMsg); + } + catch (Exception) + { + throw; + } + XmlNode root = config.SelectSingleNode("TestConfig"); + if (root != null) + { + foreach (XmlNode node in root.ChildNodes) + { + XmlElement eleNode = node as XmlElement; + if (eleNode == null) + { + continue; + } + + if (string.Compare(eleNode.Name.ToLower(), "testclass") == 0 && eleNode.Attributes["name"] != null) + { + ClassConfig classConfig = this[eleNode.Attributes["name"].Value]; + if(classConfig == null) + classConfig = new ClassConfig(); + foreach (XmlNode subnode in eleNode.ChildNodes) + { + XmlElement eleSubnode = subnode as XmlElement; + if (eleSubnode == null) + { + continue; + } + + if (string.Compare(eleSubnode.Name.ToLower(), "testmethod") == 0 && eleSubnode.Attributes["name"] != null) + { + MethodConfig methodConfig = classConfig[eleSubnode.Attributes["name"].Value]; + if (methodConfig == null) + methodConfig = new MethodConfig(); + foreach (XmlNode methodParamNode in eleSubnode.ChildNodes) + { + XmlElement eleMethodParamNode = methodParamNode as XmlElement; + if (eleMethodParamNode == null) + { + continue; + } + methodConfig[eleMethodParamNode.Name] = eleMethodParamNode.InnerText; + + } + classConfig[eleSubnode.Attributes["name"].Value] = methodConfig; + continue; + } + + classConfig.ClassParams[eleSubnode.Name] = eleSubnode.InnerText; + + } + this[eleNode.Attributes["name"].Value] = classConfig; + continue; + + } + + TestParams[eleNode.Name] = eleNode.InnerText; + + } + } + } + + private Dictionary testParams = null; + + public Dictionary TestParams + { + get { return testParams; } + set { testParams = value; } + } + + private Dictionary testClasses; + + public ClassConfig this[string className] + { + get + { + if (testClasses.ContainsKey(className)) + { + return testClasses[className]; + } + else + { + return null; + } + } + + set + { + testClasses[className] = value; + } + } + + public string Get(string paramName) + { + //first search the method params + if (this[Test.FullClassName] != null) + { + if (this[Test.FullClassName][Test.MethodName] != null) + { + if (this[Test.FullClassName][Test.MethodName].MethodParams.ContainsKey(paramName)) + { + return this[Test.FullClassName][Test.MethodName].MethodParams[paramName].Trim(); + } + } + + if (this[Test.FullClassName].ClassParams.ContainsKey(paramName)) + { + return this[Test.FullClassName].ClassParams[paramName].Trim(); + } + } + + if (TestParams.ContainsKey(paramName)) + { + return TestParams[paramName].Trim(); + } + + throw new ArgumentException("The test param does not exist.", paramName); + + //return null; + + } + + } + +} diff --git a/test/MsTestLib/TestHelper.cs b/test/MsTestLib/TestHelper.cs new file mode 100644 index 00000000..50305539 --- /dev/null +++ b/test/MsTestLib/TestHelper.cs @@ -0,0 +1,258 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Diagnostics; +using System.Text.RegularExpressions; +using System.Threading; + +namespace MS.Test.Common.MsTestLib +{ + public class TestHelper + { + // default time out for runcmd + // AzCopy will retry for 15 min when server error happens + // Set command timeout to 20 min in case azcopy is terminated during retry and cause no error output + public const int CommandTimeoutInSec = 1200; + public const int CommandTimeoutInMs = CommandTimeoutInSec * 1000; + public const int WaitForKillTimeoutInMs = 30 * 1000; + + public static int RunCmd(string cmd, string args, string input = null) + { + return RunCmd(cmd, args, CommandTimeoutInMs, input); + } + + public static int RunCmd(string cmd, string args, out string stdout, out string stderr, string input = null) + { + return RunCmd(cmd, args, out stdout, out stderr, CommandTimeoutInMs, input); + } + + public static int RunCmd(string cmd, string args, int timeout, string input = null) + { + string stdout, stderr; + return RunCmd(cmd, args, out stdout, out stderr, timeout, input); + } + + public static int RunCmd(string cmd, string args, out string stdout, out string stderr, int timeout, string input = null) + { + Test.Logger.Verbose("Running: {0} {1}", cmd, args); + ProcessStartInfo psi = new ProcessStartInfo(cmd, args); + psi.CreateNoWindow = true; + psi.WindowStyle = ProcessWindowStyle.Hidden; + psi.UseShellExecute = false; + psi.RedirectStandardError = true; + psi.RedirectStandardOutput = true; + if (string.IsNullOrEmpty(input)) + { + psi.RedirectStandardInput = false; + } + else + { + psi.RedirectStandardInput = true; + } + + Process p = Process.Start(psi); + // To avoid deadlock between Process.WaitForExit and Process output redirection buffer filled up, we need to async read output before calling Process.WaitForExit + StringBuilder outputBuffer = new StringBuilder(); + var outputBufferLock = new object(); + p.OutputDataReceived += (sendingProcess, outLine) => + { + if (!String.IsNullOrEmpty(outLine.Data)) + { + lock (outputBufferLock) + { + outputBuffer.Append(outLine.Data + "\n"); + } + } + }; + StringBuilder errorBuffer = new StringBuilder(); + var errorBufferLock = new object(); + p.ErrorDataReceived += (sendingProcess, outLine) => + { + if (!String.IsNullOrEmpty(outLine.Data)) + { + lock (errorBufferLock) + { + errorBuffer.Append(outLine.Data + "\n"); + } + } + }; + + if (!string.IsNullOrEmpty(input)) + { + var writer = p.StandardInput; + writer.AutoFlush = true; + writer.WriteLine(input); + writer.Close(); + } + + p.BeginOutputReadLine(); + p.BeginErrorReadLine(); + if (p.WaitForExit(timeout)) + { + GetStdOutAndStdErr(p, outputBuffer, errorBuffer, out stdout, out stderr); + return p.ExitCode; + } + else + { + Test.Logger.Verbose("--Command timed out!"); + TestHelper.KillProcess(p); + GetStdOutAndStdErr(p, outputBuffer, errorBuffer, out stdout, out stderr); + return int.MinValue; + } + } + + private static void GetStdOutAndStdErr(Process p, StringBuilder outputBuffer, StringBuilder errorBuffer, out string stdout, out string stderr) + { + // Call this overload of WaitForExit to make sure all stdout/stderr strings are flushed. + p.WaitForExit(); + + stdout = outputBuffer.ToString(); + stderr = errorBuffer.ToString(); + + Test.Logger.Verbose("Stdout: {0}", stdout); + if (!string.IsNullOrEmpty(stderr) + && !string.Equals(stdout, stderr, StringComparison.InvariantCultureIgnoreCase)) + { + Test.Logger.Verbose("Stderr: {0}", stderr); + } + } + + public delegate bool RunningCondition(object arg); + /// + /// run cmd and specify the running condition. If running condition is not met, process will be terminated. + /// + public static int RunCmd(string cmd, string args, out string stdout, out string stderr, RunningCondition rc, object rcArg, string input = null) + { + Test.Logger.Verbose("Running: {0} {1}", cmd, args); + ProcessStartInfo psi = new ProcessStartInfo(cmd, args); + psi.CreateNoWindow = true; + psi.WindowStyle = ProcessWindowStyle.Hidden; + psi.UseShellExecute = false; + psi.RedirectStandardError = true; + psi.RedirectStandardOutput = true; + if (string.IsNullOrEmpty(input)) + { + psi.RedirectStandardInput = false; + } + else + { + psi.RedirectStandardInput = true; + } + + Process p = Process.Start(psi); + // To avoid deadlock between Process.WaitForExit and Process output redirection buffer filled up, we need to async read output before calling Process.WaitForExit + StringBuilder outputBuffer = new StringBuilder(); + p.OutputDataReceived += (sendingProcess, outLine) => + { + if (!String.IsNullOrEmpty(outLine.Data)) + { + outputBuffer.Append(outLine.Data + "\n"); + } + }; + StringBuilder errorBuffer = new StringBuilder(); + p.ErrorDataReceived += (sendingProcess, outLine) => + { + if (!String.IsNullOrEmpty(outLine.Data)) + { + errorBuffer.Append(outLine.Data + "\n"); + } + }; + + if (!string.IsNullOrEmpty(input)) + { + var writer = p.StandardInput; + writer.AutoFlush = true; + writer.WriteLine(input); + writer.Close(); + } + + p.BeginOutputReadLine(); + p.BeginErrorReadLine(); + DateTime nowTime = DateTime.Now; + DateTime timeOut = nowTime.AddMilliseconds(CommandTimeoutInMs); + + bool isTimedOut = false; + + while (rc(rcArg)) + { + if (p.HasExited) + { + // process has existed + break; + } + else if (timeOut < DateTime.Now) + { + //time out + isTimedOut = true; + break; + } + else + { + //continue to wait + Thread.Sleep(100); + } + } + stdout = outputBuffer.ToString(); + stderr = errorBuffer.ToString(); + if (p.HasExited) + { + Test.Logger.Verbose("Stdout: {0}", stdout); + if (!string.IsNullOrEmpty(stderr) + && !string.Equals(stdout, stderr, StringComparison.InvariantCultureIgnoreCase)) + Test.Logger.Verbose("Stderr: {0}", stderr); + return p.ExitCode; + } + else + { + if (isTimedOut) + { + Test.Logger.Verbose("--Command timed out!"); + } + + TestHelper.KillProcess(p); + + Test.Logger.Verbose("Stdout: {0}", stdout); + if (!string.IsNullOrEmpty(stderr) + && !string.Equals(stdout, stderr, StringComparison.InvariantCultureIgnoreCase)) + Test.Logger.Verbose("Stderr: {0}", stderr); + return int.MinValue; + } + } + + public static bool StringMatch(string source, string pattern, RegexOptions? regexOptions = null) + { + Regex r = null; + if (regexOptions.HasValue) + { + r = new Regex(pattern, regexOptions.Value); + } + else + { + r = new Regex(pattern); + } + + Match m = r.Match(source); + return m.Success; + } + + public static void KillProcess(Process process) + { + try + { + process.Kill(); + bool exit = process.WaitForExit(WaitForKillTimeoutInMs); + Test.Assert(exit, "Process {0} should exit after being killed", process.Id); + } + catch (InvalidOperationException e) + { + Test.Info("InvalidOperationException caught while trying to kill process {0}: {1}", process.Id, e.ToString()); + } + } + } +} diff --git a/test/MsTestLib/TestLogger.cs b/test/MsTestLib/TestLogger.cs new file mode 100644 index 00000000..eede97a3 --- /dev/null +++ b/test/MsTestLib/TestLogger.cs @@ -0,0 +1,148 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +//------------------------------------------------------------------------------ +namespace MS.Test.Common.MsTestLib +{ + using System; + using System.Collections.Generic; + + /// + /// the wrapper for the loggers + /// + public class TestLogger + { + public List Loggers; + private Object loggersLock = new Object(); + + public TestLogger() + { + Loggers = new List(); + } + + public TestLogger(TestConfig testConfig) + { + Loggers = new List(); + Init(testConfig); + } + + public bool LogVerbose = false; + public bool LogInfo = true; + public bool LogWarning = false; + public bool LogError = true; + + public void Init(TestConfig testConfig) + { + bool consoleLogger = false; + bool fileLogger = true; + bool.TryParse(testConfig.TestParams["consolelogger"], out consoleLogger); + bool.TryParse(testConfig.TestParams["filelogger"], out fileLogger); + + string logfileName = testConfig.TestParams["logfilename"]; + + if (consoleLogger) + { + Loggers.Add(new ConsoleLogger()); + } + + if (fileLogger) + { + string fileNameString = logfileName + ".txt"; + Loggers.Add(new FileLogger(fileNameString)); + } + + bool.TryParse(testConfig.TestParams["loginfo"], out LogInfo); + bool.TryParse(testConfig.TestParams["logverbose"], out LogVerbose); + bool.TryParse(testConfig.TestParams["logerror"], out LogError); + bool.TryParse(testConfig.TestParams["logwarning"], out LogWarning); + } + + public void Error( + string msg, + params object[] objToLog) + { + this.ForEachLogger((logger) => + { + if (LogError) + { + logger.WriteError(msg, objToLog); + } + }); + } + + public void Info( + string msg, + params object[] objToLog) + { + this.ForEachLogger((logger) => + { + if (LogInfo) + { + logger.WriteInfo(msg, objToLog); + } + }); + } + + public void Warning( + string msg, + params object[] objToLog) + { + this.ForEachLogger((logger) => + { + if (LogWarning) + { + logger.WriteWarning(msg, objToLog); + } + }); + } + + public void Verbose( + string msg, + params object[] objToLog) + { + this.ForEachLogger((logger) => + { + if (LogVerbose) + { + logger.WriteVerbose(msg, objToLog); + } + }); + } + + public void StartTest(string testId) + { + this.ForEachLogger((logger) => + { + logger.StartTest(testId); + }); + } + + public void EndTest(string testId, TestResult testResult) + { + this.ForEachLogger((logger) => + { + logger.EndTest(testId, testResult); + }); + } + + public void Close() + { + this.ForEachLogger((logger) => + { + logger.Close(); + }); + } + + private void ForEachLogger(Action action) + { + lock (this.loggersLock) + { + foreach (ILogger logger in Loggers) + { + action(logger); + } + } + } + } +} diff --git a/tools/AssemblyInfo/SharedAssemblyInfo.cs b/tools/AssemblyInfo/SharedAssemblyInfo.cs new file mode 100644 index 00000000..02c8f72a --- /dev/null +++ b/tools/AssemblyInfo/SharedAssemblyInfo.cs @@ -0,0 +1,32 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation +// +// +// Assembly global configuration. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; +using System.Resources; +using System.Runtime.InteropServices; + +[assembly: AssemblyVersion("0.1.0.0")] +[assembly: AssemblyFileVersion("0.1.0.2")] + +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("Microsoft Azure Storage")] +[assembly: AssemblyCopyright("Copyright © 2015 Microsoft Corp.")] +[assembly: AssemblyTrademark("Microsoft ® is a registered trademark of Microsoft Corporation.")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +[assembly: NeutralResourcesLanguageAttribute("en-US")] + +[assembly: CLSCompliant(false)] + diff --git a/tools/analysis/fxcop/azure-storage-dm.ruleset b/tools/analysis/fxcop/azure-storage-dm.ruleset new file mode 100644 index 00000000..332a0a32 --- /dev/null +++ b/tools/analysis/fxcop/azure-storage-dm.ruleset @@ -0,0 +1,363 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/apidoc/.gitignore b/tools/apidoc/.gitignore new file mode 100644 index 00000000..1d39e8b1 --- /dev/null +++ b/tools/apidoc/.gitignore @@ -0,0 +1 @@ +Help/ \ No newline at end of file diff --git a/tools/apidoc/dmlib.shfbproj b/tools/apidoc/dmlib.shfbproj new file mode 100644 index 00000000..de501509 --- /dev/null +++ b/tools/apidoc/dmlib.shfbproj @@ -0,0 +1,62 @@ + + + + + Debug + AnyCPU + 2.0 + {c33e44be-7d4f-428f-bed5-2d5b03ac5d90} + 1.9.9.0 + + Documentation + Documentation + Documentation + + .NET Framework 4.5 + .\Help\ + Documentation + en-US + OnlyWarningsAndErrors + shfb.log + Website + False + True + False + False + True + 2 + False + C# + Blank + False + VS2013 + False + Guid + Microsoft Azure Storage Data Movement Library + AboveNamespaces + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/nupkg/Microsoft.Azure.Storage.DataMovement.nuspec b/tools/nupkg/Microsoft.Azure.Storage.DataMovement.nuspec new file mode 100644 index 00000000..ad3261d6 --- /dev/null +++ b/tools/nupkg/Microsoft.Azure.Storage.DataMovement.nuspec @@ -0,0 +1,38 @@ + + + + Microsoft.Azure.Storage.DataMovement + 0.0.2 + Microsoft Azure Storage Data Movement Library + Microsoft + Microsoft + http://go.microsoft.com/fwlink/?LinkId=331471 + http://go.microsoft.com/fwlink/?LinkId=235168 + http://go.microsoft.com/fwlink/?LinkID=288890 + true + Microsoft Azure Storage DataMovement Library offers a set of APIs extending the existing Azure Storage .Net Client Library to help customer transfer Azure Blob and File Storage with high-performance, scalability and reliability. + For this release, see notes - https://github.com/Azure/azure-storage-net-data-movement/blob/master/README.md and https://github.com/Azure/azure-storage-net-data-movement/blob/master/changelog.txt + Microsoft Azure Storage team's blog - http://blogs.msdn.com/b/windowsazurestorage/ + A client library designed for high-performance and scalable uploading, downloading and copying data to and from Microsoft Azure Blob and File Storage + Microsoft, Azure, Storage, Blob, File, DataMovement, Upload, Download, Copy, High-Performance, Scalable, Reliable, windowsazureofficial + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/nupkg/buildNupkg.cmd b/tools/nupkg/buildNupkg.cmd new file mode 100644 index 00000000..5c4eff7a --- /dev/null +++ b/tools/nupkg/buildNupkg.cmd @@ -0,0 +1,10 @@ +pushd %~dp0 +rmdir /s /q .\lib +mkdir .\lib\net45 +pushd ..\.. +del /q /f *.nupkg +copy .\lib\bin\Release\Microsoft.WindowsAzure.Storage.DataMovement.dll .\tools\nupkg\lib\net45 +.\.nuget\nuget.exe pack .\tools\nupkg\Microsoft.Azure.Storage.DataMovement.nuspec +popd +rmdir /s /q .\lib +popd \ No newline at end of file diff --git a/tools/scripts/InjectBuildNumber.ps1 b/tools/scripts/InjectBuildNumber.ps1 new file mode 100644 index 00000000..622ccfdc --- /dev/null +++ b/tools/scripts/InjectBuildNumber.ps1 @@ -0,0 +1,30 @@ +Function UpdateVersionInFile +{ + Param ([string]$path, [string]$prefix, [string]$suffix, [int]$verNum) + + if ($env:BUILD_NUMBER) + { + + $lines = Get-Content $path -Encoding UTF8 + + $new_lines = $lines | %{ + if ($_.StartsWith($prefix)) + { + $num = $_.Substring($prefix.Length, $_.Length - $prefix.Length - $suffix.Length) + $num_p = $num.Split('.') + $new_num = [System.String]::Join('.', $num_p[0 .. ($verNum-2)] + $env:BUILD_NUMBER) + return $prefix + $new_num + $suffix + } + else + { + return $_ + } + } + + Set-Content -Path $path -Value $new_lines -Encoding UTF8 + } +} + +UpdateVersionInFile ((Split-Path -Parent $PSCommandPath) + '\..\nupkg\Microsoft.Azure.Storage.DataMovement.nuspec') ' ' '' 3 + +UpdateVersionInFile ((Split-Path -Parent $PSCommandPath) + '\..\AssemblyInfo\SharedAssemblyInfo.cs') '[assembly: AssemblyFileVersion("' '")]' 4 \ No newline at end of file diff --git a/tools/strongnamekeys/fake/windows.snk b/tools/strongnamekeys/fake/windows.snk new file mode 100644 index 0000000000000000000000000000000000000000..695f1b38774e839e5b90059bfb7f32df1dff4223 GIT binary patch literal 160 zcmV;R0AK$ABme*efB*oL000060ssI2Bme+XQ$aBR1ONa50098C{E+7Ye`kjtcRG*W zi8#m|)B?I?xgZ^2Sw5D;l4TxtPwG;3)3^j?qDHjEteSTF{rM+4WI`v zCD?tsZ^;k+S&r1&HRMb=j738S=;J$tCKNrc$@P|lZ