割と大変

AzCopyだとjnlファイルが生成されて並列に実行できないので、Powerhsellだけでダウンロード・アップロードするスクリプト書きました。

  • AzCopy禁止
  • Azure Powershell禁止
  • 追加DLL禁止
  • Invoke-WebRequest禁止

という条件だったので一筋縄ではいきませんでした。Invoke-WebRequestはIEのセキュリティポリシーに左右されるのでWindowsServerの立ち上げ時とかで使えないた使えません。

スクリプト

azput.ps1

param($Source, $Dest, $DestKey)

$r = [regex]"https://(.*).blob.core.windows.net/(.*)";

if(!$r.IsMatch($Dest)){
    echo "$Dest is not azure blob url"
    exit 1
}

$match = $r.Match($Dest)
$accountname = $match.Groups[1];
$path = $match.Groups[2];

$md5 = New-Object System.Security.Cryptography.MD5Cng

$Length = (Get-Item $Source).Length
$bytes = New-Object System.Byte[] $Length
$limit = 4 * 1024 * 1024; # 4MB
$stream = New-Object System.IO.FileStream @($Source, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read)
$ids = @()
for($i,$length = 0, $Length; $length -gt 0; $($i++; $length-=$limit;)){
    $position = $i * $limit
    $len = @{ $true = $limit ; $false = $Length }[$length -ge $limit]
    $bytes = New-Object System.Byte[] $len
    $stream.Read($bytes, 0, $len)

    $id = [Convert]::ToBase64String($i.ToString())
    $ids += $id
    $md5hash = [Convert]::ToBase64String($md5.ComputeHash($bytes))
    $headers = @{ "x-ms-date" = (Get-Date).ToUniversalTime().ToString("R"); "x-ms-version" = "2014-02-14"; "Content-Length" = $bytes.Length; "Content-MD5" = $md5hash  }
    $signatureStr = "PUT`n`n`n$len`n$md5hash`n`n`n`n`n`n`n`nx-ms-date:$($headers["x-ms-date"])`nx-ms-version:$($headers["x-ms-version"])`n/$accountname/$path`nblockid:$id`ncomp:block";
    $hmacsha = New-Object System.Security.Cryptography.HMACSHA256
    $hmacsha.Key = [Convert]::FromBase64String($DestKey)
    $signature = $hmacsha.ComputeHash([Text.Encoding]::Default.GetBytes($signatureStr))
    $signature = [Convert]::ToBase64String($signature)
    $authorization = "SharedKey $($accountname):$signature"
    $headers.Add("Authorization",$authorization)

    Invoke-RestMethod -Method Put -Uri "$Dest`?comp=block&blockid=$id" -Headers $headers -Body $bytes
}
$stream.Close()

$xml = ""
foreach($id in $ids){
    $xml += "$id"
}
$xml += ""
$stream = New-Object System.IO.FileStream @($Source, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read)
$md5hash = [Convert]::ToBase64String($md5.ComputeHash($stream))
$stream.Close()
$length = $xml.Length
$headers = @{ "x-ms-date" = (Get-Date).ToUniversalTime().ToString("R"); "x-ms-version" = "2014-02-14"; "x-ms-blob-content-md5"=$md5hash; "Content-Length" = $xml.Length }
$signatureStr = "PUT`n`n`n$length`n`n`n`n`n`n`n`n`nx-ms-blob-content-md5:$($headers["x-ms-blob-content-md5"])`nx-ms-date:$($headers["x-ms-date"])`nx-ms-version:$($headers["x-ms-version"])`n/$accountname/$path`ncomp:blocklist"
$hmacsha = New-Object System.Security.Cryptography.HMACSHA256
$hmacsha.Key = [Convert]::FromBase64String($DestKey)
$signature = $hmacsha.ComputeHash([Text.Encoding]::Default.GetBytes($signatureStr))
$signature = [Convert]::ToBase64String($signature)
$authorization = "SharedKey $($accountname):$signature"
$headers.Add("Authorization",$authorization)

Invoke-RestMethod -Method Put -Uri "$Dest`?comp=blocklist" -Headers $headers -Body $xml

azget.ps1

param($Source, $SourceKey, $Dest)

$r = [regex]"https://(.*).blob.core.windows.net/(.*)";

if(!$r.IsMatch($Source)){
    echo "$Source is not azure blob url"
    exit 1
}

$match = $r.Match($Source)
$accountname = $match.Groups[1];
$path = $match.Groups[2];

$headers = @{ "x-ms-date" = (Get-Date).ToUniversalTime().ToString("R"); "x-ms-version" = "2014-02-14";  }
$signatureStr = "GET`n`n`n`n`n`n`n`n`n`n`n`nx-ms-date:$($headers["x-ms-date"])`nx-ms-version:$($headers["x-ms-version"])`n/$accountname/$path";
$hmacsha = New-Object System.Security.Cryptography.HMACSHA256
$hmacsha.Key = [Convert]::FromBase64String($SourceKey)
$signature = $hmacsha.ComputeHash([Text.Encoding]::Default.GetBytes($signatureStr))
$signature = [Convert]::ToBase64String($signature)

$client = New-Object System.Net.WebClient
$request = [System.Net.WebRequest]::Create($Source)
$request.Headers.Add("x-ms-date",$headers["x-ms-date"])
$request.Headers.Add("x-ms-version",$headers["x-ms-version"])
$request.Headers.Add("Authorization", "SharedKey " + $accountname +  ":" + $signature)
$response = $request.GetResponse()
$stream = New-Object System.IO.FileStream @($Dest, [System.IO.FileMode]::Create, [System.IO.FileAccess]::ReadWrite)
$response.GetResponseStream().CopyTo($stream);

$md5hash = $response.Headers["Content-MD5"]

if($md5hash.Length -ne 0){
    $md5 = New-Object System.Security.Cryptography.MD5Cng
    $stream.Seek(0, "Begin")
    $hash = [Convert]::ToBase64String($md5.ComputeHash($stream))
    if($md5hash -ne $hash){
        $stream.Close();
        echo "md5 hash error"
        exit 1
    }
}

$stream.Close()

ポイント

ポイントはシグネチャの生成と、4MBのブロックにデータをわけるとこでしょうか。MD5使わない場合、レスポンスヘッダ取得する必要がないので、azgetはWebClientでなくInvoke-RestMethodが利用でき、もうちょっと短くなります。
アップロードする際に、Commit色々できることはあるようですが、今回は一番簡単なパターン。元あったファイルを置き換えます。

とりあえず、ここまで。

CATEGORIES